Git Product home page Git Product logo

trogon's People

Contributors

aradhya-tripathi avatar darrenburns avatar davep avatar figsoda avatar kianmeng avatar simonw avatar willmcgugan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

trogon's Issues

Feature request: customize command name and help text

These are currently hard-coded here:

trogon/trogon/trogon.py

Lines 288 to 294 in b9ddd90

if isinstance(app, click.Group):
app.command(name="tui", help="Open Textual TUI.")(wrapped_tui)
else:
new_group = click.Group()
new_group.add_command(app)
new_group.command(name="tui", help="Open Textual TUI.")(wrapped_tui)
return new_group

I'd like the option to customize them, for example like this:

@tui(name="ui", help="Interactive UI for this tool")
@click.group()
def cli():
    ...

Unpackaged scripts: after TUI interaction, trogon attempts to launch the final command in working dir, not the script dir

Possibly a misunderstanding on my behalf, but anyway:

I tried the nogroup_demo.py verbatim, running it from a higher directory:

❯ cd /Users/alexhunsley/Documents/dev/
❯ mkdir test_dir
❯ cat > test_dir/nogroup_demo.py   <then I pasted in the nogroup_demo src>
❯ python test_dir/nogroup_demo.py tui

<I did config in TUI interace, then hit CTRL-R>

Running python nogroup_demo.py add --category work -v --labels important sada                                                                                                                                                                                    

python: can't open file '/Users/alexhunsley/Documents/dev/test_dir/nogroup_demo.py': 
    [Errno 2] No such file or directory

So it's missing out the test_dir/ bit before the nogroup_demo.py.

Trogon: 0.4.0

M1 MacBook Pro (16-inch, 2021)
Mac OS: 13.3.1 (a) 
Python: 3.10.1
Click: 8.1.3

Thanks for making Trogon btw!

Sub-commands not detected if they don't take an argument

Sub-commands that don't take arguments don't show up in trogon. For example, with the following code:

import click

from trogon import tui


@tui()
@click.group()
def cli():
    return

@cli.command()
@click.argument("test")
@click.pass_context
def test1():
    return

@cli.command()
@click.pass_context
def test2():
    return

if __name__ == "__main__":
    cli(obj={})

Running it with python test.py tui will result in the test1 command showing up in the UI but not the test2 command. Adding a click.argument decorator to the function makes test2 show up.

(BTW as a side note awesome and neat looking project. I just love rich and textual already and you keep adding awesome stuff, so thanks.)

Error: cannot compare instances of `NoneType`

Hello!

When trying to add a TUI to my program, see jeertmans/manim-slides#249, the TUI crashes on two subcommands: present and wizard. The crash occurs when loading the subcommand's documentation, not actually running it.
FYI, both commands launch some GUI with PySide6.

I have attached the full error traceback below. As it is quite long and not very easy to understand, I was hoping to get some help here :-)

Thanks!

───────────────────── Traceback (most recent call last) ──────────────────────╮
│ /home/eertmans/.cache/pypoetry/virtualenvs/manim-slides-M-PeeMP--py3.10/lib/ │
│ python3.10/site-packages/trogon/trogon.py:171 in update_command_data         │
│                                                                              │
│   168 │   @on(CommandForm.Changed)                                           │
│   169 │   def update_command_data(self, event: CommandForm.Changed) -> None: │
│   170 │   │   self.command_data = event.command_data                         │
│ ❱ 171 │   │   self._update_execution_string_preview(                         │
│   172 │   │   │   self.selected_command_schema, self.command_data            │
│   173 │   │   )                                                              │
│   174                                                                        │
│                                                                              │
│ ╭───────── locals ─────────╮                                                 │
│ │ event = Changed()        │                                                 │
│ │  self = CommandBuilder() │                                                 │
│ ╰──────────────────────────╯                                                 │
│                                                                              │
│ /home/eertmans/.cache/pypoetry/virtualenvs/manim-slides-M-PeeMP--py3.10/lib/ │
│ python3.10/site-packages/trogon/trogon.py:193 in                             │
│ _update_execution_string_preview                                             │
│                                                                              │
│   190 │   │   │   │   "command-name-syntax"                                  │
│   191 │   │   │   )                                                          │
│   192 │   │   │   prefix = Text(f"{self.click_app_name} ", command_name_synt │
│ ❱ 193 │   │   │   new_value = command_data.to_cli_string(include_root_comman │
│   194 │   │   │   highlighted_new_value = Text.assemble(prefix, self.highlig │
│   195 │   │   │   prompt_style = self.get_component_rich_style("prompt")     │
│   196 │   │   │   preview_string = Text.assemble(("$ ", prompt_style), highl │
│                                                                              │
│ ╭───────────────────────────────── locals ─────────────────────────────────╮ │
│ │              command_data = UserCommandData(                             │ │
│ │                             │   name='root',                             │ │
│ │                             │   options=[                                │ │
│ │                             │   │   UserOptionData(                      │ │
│ │                             │   │   │   name=[                           │ │
│ │                             │   │   │   │   '--notify-outdated-version'  │ │
│ │                             │   │   │   ],                               │ │
│ │                             │   │   │   value=(True,),                   │ │
│ │                             │   │   │   option_schema=OptionSchema(      │ │
│ │                             │   │   │   │   name=[                       │ │
│ │                             │   │   │   │   │                            │ │

│ │     Discontinued because GitHub limits the length. See attached file.    │ │
[error.txt](https://github.com/Textualize/trogon/files/12407498/error.txt)


│ │                       │   │   │   },                                     │ │
│ │                       │   │   │   parent=None,                           │ │
│ │                       │   │   │   is_group=True                          │ │
│ │                       │   │   ),                                         │ │
│ │                       │   │   is_group=False                             │ │
│ │                       │   )                                              │ │
│ │                       )                                                  │ │
│ │          value_data = [('None',)]                                        │ │
│ │ values_are_defaults = True                                               │ │
│ │     values_supplied = True                                               │ │
│ ╰──────────────────────────────────────────────────────────────────────────╯ │
╰──────────────────────────────────────────────────────────────────────────────╯
TypeError: '<' not supported between instances of 'NoneType' and 'NoneType'

error.txt

Enhancement request: TUI should display help for arguments (not just parameters)

I'm adding Trogon support to a script that uses both arguments and parameters. The click help displays help text for the arguments, e.g.:

python src/airflow_log_search.py mlops-pod-logs --help
╭─ Arguments ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ *    dag                 TEXT              Airflow DAG ID, e.g. recs_sku_coview [default: None] [required]                   │
│ *    task_id             TEXT              Airflow task ID, e.g. predict_batch [default: None] [required]                    │
│      execution_date      [EXECUTION_DATE]  DAG execution date, e.g. 2023-07-06T19:52:45.488702+00:00 [default: None]         │
│      try_number          [TRY_NUMBER]      Task try number, e.g. 1 [default: 1]                                              │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

However, the TUI for the same script does not display the help text for arguments, only for parameters:
image

Doesn't support multiple positional arguments - nargs=-1

As far as I can tell Trogon supports this:

@click.option(
    "-c",
    "--column",
    type=str,
    multiple=True,
    help="Column definitions for the table",
)

This creates an input field with a "+ value" button for adding multiple rows.

But it doesn't support this:

@click.argument(
    "file_or_dir",
    nargs=-1,
    required=True,
    type=click.Path(file_okay=True, dir_okay=True, allow_dash=True),
)

Where nargs=-1 indicates a positional argument can be provided multiple times.

These examples are for sqlite-utils insert-files - which looks like this in Trogon:

CleanShot 2023-05-21 at 08 40 24@2x

file_or_dir there should have a + value button in the same way that -c / --column does.

This is the --help for that command:

Usage: sqlite-utils insert-files [OPTIONS] PATH TABLE FILE_OR_DIR...

  Insert one or more files using BLOB columns in the specified table

  Example:

      sqlite-utils insert-files pics.db images *.gif \
          -c name:name \
          -c content:content \
          -c content_hash:sha256 \
          -c created:ctime_iso \
          -c modified:mtime_iso \
          -c size:size \
          --pk name

Note how FILE_OR_DIR... in the summary has that ... indicating this can be passed multiple times.

Trogon doesn't work with Textual 0.54.0

When using Textual 0.54.0, the right-side panel with the inputs for building up a command with arguments and options no longer loads.

Explicitly pinning to textual<0.54.0 returns things to working order.

Reproduction here

PermissionError: [Errno 13] Permission denied when trying to run a command

Somehow I run into "Permission denied" when trying to run any command from tui (running from cli is ok). I get the following trace:

$ python3 demo.py tui
Running demo.py add
Traceback (most recent call last):
  File "/home/user/util/demo.py", line 89, in <module>
    cli(obj={})
  File "/usr/lib/python3/dist-packages/click/core.py", line 1128, in __call__
    return self.main(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/click/core.py", line 1053, in main
    rv = self.invoke(ctx)
  File "/usr/lib/python3/dist-packages/click/core.py", line 1659, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/lib/python3/dist-packages/click/core.py", line 1395, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/lib/python3/dist-packages/click/core.py", line 754, in invoke
    return __callback(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/click/decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/home/lexxpluss/.local/lib/python3.10/site-packages/trogon/trogon.py", line 292, in wrapped_tui
    Trogon(app, app_name=name, click_context=ctx).run()
  File "/home/lexxpluss/.local/lib/python3.10/site-packages/trogon/trogon.py", line 260, in run
    os.execvp(program_name, arguments)
  File "/usr/lib/python3.10/os.py", line 574, in execvp
    _execvpe(file, args)
  File "/usr/lib/python3.10/os.py", line 615, in _execvpe
    raise saved_exc
  File "/usr/lib/python3.10/os.py", line 607, in _execvpe
    exec_func(fullname, *argrest)
PermissionError: [Errno 13] Permission denied

Any ideas would be helpful?

Support for Poetry without poetry shell

If I have a CLI with a script defined in a Poetry pyproject.toml like this

[tool.poetry.scripts]
my-cli= "my_cli.main:app"

and I start the TUI with poetry run my-cli tui, then poetry run won't be prepended to the command my-cli ... and it will fail to run. Would it be possible to support this use case?

Thanks!

Make `click` optional; support manual definition of schemas.

Today, Trogon only supports click, and click-specific code is exists throughout Trogon. Frankly, Trogon is too amazing to be specific to click.

In addition to the reverse-engineering ("introspection") method of building a Trogon TUI from an existing click CLI, Trogon should offer an interface for any library to integrate Trogon by manually building and providing the command-schemas to Trogon. This way, Trogon could function without click, and any CLI app would have the ability to build and display a Trogon TUI. This would also lessen the burden on Trogon development, since it would not be necessary for Trogon devs to build "introspector" logic for each desired library, but would instead give devs of those libraries a path to D.I.Y. Trogon integration.

Instead of Trogon devs working to reverse-engineer and support this library and others, the developers of each library should have a path to integrate Trogon themselves.

Related issue: #5

trogon tui fallback

I'd like to use trogon when it's available, and fail gracefully when it is not. It seems this should be sufficient

try:
    from trogon import tui
except ImportError:
    tui = PassThroughDecorator()

@tui()
@click.group(...)
def cli():
    ...

but I've been unable to come up with a suitable PassThroughDecorator?

FileNotFoundError when running command via Trogon

When running a command via Trogon, a FileNotFoundError exception is raised. The UI is accessible, arguments and options are configurable, and the CLI application (click) works when running directly.

My application is running in a venv virtual environment. Trogon and dependencies are installed in the venv. Click was already in use in my project prior to adding Trogon, but it appears all the relevant dependencies meet versioning requirements.

Requirement already satisfied: trogon in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (0.2.1)
Requirement already satisfied: click>=8.0.0 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from trogon) (8.1.2)
Requirement already satisfied: textual>=0.26.0 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from trogon) (0.26.0)
Requirement already satisfied: colorama in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from click>=8.0.0->trogon) (0.4.4)
Requirement already satisfied: importlib-metadata>=4.11.3 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from textual>=0.26.0->trogon) (6.6.0)
Requirement already satisfied: markdown-it-py[linkify,plugins]<3.0.0,>=2.1.0 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from textual>=0.26.0->trogon) (2.2.0)
Requirement already satisfied: rich>=13.3.3 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from textual>=0.26.0->trogon) (13.3.5)
Requirement already satisfied: typing-extensions<5.0.0,>=4.4.0 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from textual>=0.26.0->trogon) (4.5.0)
Requirement already satisfied: zipp>=0.5 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from importlib-metadata>=4.11.3->textual>=0.26.0->trogon) (3.15.0)
Requirement already satisfied: mdurl~=0.1 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from markdown-it-py[linkify,plugins]<3.0.0,>=2.1.0->textual>=0.26.0->trogon) (0.1.2)
Requirement already satisfied: linkify-it-py<3,>=1 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from markdown-it-py[linkify,plugins]<3.0.0,>=2.1.0->textual>=0.26.0->trogon) (2.0.2)
Requirement already satisfied: mdit-py-plugins in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from markdown-it-py[linkify,plugins]<3.0.0,>=2.1.0->textual>=0.26.0->trogon) (0.3.5)
Requirement already satisfied: pygments<3.0.0,>=2.13.0 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from rich>=13.3.3->textual>=0.26.0->trogon) (2.15.1)
Requirement already satisfied: uc-micro-py in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from linkify-it-py<3,>=1->markdown-it-py[linkify,plugins]<3.0.0,>=2.1.0->textual>=0.26.0->trogon) (1.0.2)

Not certain if this is an issue with my system configuration, or a bug. Installed via pip and imported as shown in the README.

import click
from trogon import tui
from app.cli import run, create_template, version


@tui()
@click.group()
def main() -> None:
    pass


main.add_command(run)
main.add_command(create_template)
main.add_command(version)


if __name__ == "__main__":
    main()
(venv) PS C:\Users\<username>\Desktop\VS Codium\app> python -m app tui
Running python -m app run
Traceback (most recent call last):
  File "C:\Users\<username>\AppData\Local\Programs\Python\Python39\lib\runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Users\<username>\AppData\Local\Programs\Python\Python39\lib\runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "C:\Users\<username>\Desktop\VS Codium\app\app\__main__.py", line 18, in <module>
    main()
  File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\click\core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\click\core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\click\core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\click\core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\click\core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\click\decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\trogon\trogon.py", line 286, in wrapped_tui
    Trogon(app, app_name=name, click_context=ctx).run()
  File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\trogon\trogon.py", line 254, in run
    os.execvp(self.app_name, [self.app_name, *self.post_run_command])
  File "C:\Users\<username>\AppData\Local\Programs\Python\Python39\lib\os.py", line 574, in execvp
    _execvpe(file, args)
  File "C:\Users\<username>\AppData\Local\Programs\Python\Python39\lib\os.py", line 616, in _execvpe
    raise last_exc
  File "C:\Users\<username>\AppData\Local\Programs\Python\Python39\lib\os.py", line 607, in _execvpe
    exec_func(fullname, *argrest)
FileNotFoundError: [Errno 2] No such file or directory

Additional Completion Options for Files/Folders

First of all, I'd like to say how much I like this tool short of some funky output I get in windows (addressed with another issue). The paradigm of keyboard friendly gui for someone like me who struggles with RSI combined with mouse functionality is great, and the ease to add it to my app is amazing.

I am using this tool with typer, and I have limited knowledge of click and what capabilities it has. Type has the ability to using type annotations to define an input as a folder or a file:

somefile: Annotated[typer.FileText, typer.Argument(mode="r")]

See the docs here.

It would be great if trogon could read these types and provide an icon for brining up a file/folder selector and if some autocomplete could be included (I love how Powershell in Windows Terminal will either complete a path or give me a list of options that fit).

FileNotFoundError during trivial example

When executing my small test script scripts/trogon_test.py

@tui()
@click.command()
@click.option(
    "--plot_opt_metrics/--no-plot_opt_metrics",
    type=click.BOOL,
    default=False,
    help="Whether to plot and display the loss, defects, and Lagrangian multipliers of the constrained optimization.",
)
def main(
    plot_opt_metrics: bool,
):
    """Create and run a dynamic experimental design."""
    print("printing the only option: ", plot_opt_metrics)


if __name__ == "__main__":
    main()

in the command line using

poetry run python scripts/trogon_test.py tui

it displays the desired TUI, however, instantly crashes when hitten Crtl + R for running it:

poetry run python scripts/trogon_test.py tui
Running trogon_test.py main
Traceback (most recent call last):
  File "scripts/trogon_test.py", line 52, in <module>
    main()
  File "/home/redacted/.venv/lib/python3.8/site-packages/click/core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
  File "/home/redacted/.venv/lib/python3.8/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
  File "/home/redacted/.venv/lib/python3.8/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/redacted/.venv/lib/python3.8/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/redacted/.venv/lib/python3.8/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
  File "/home/redacted/.venv/lib/python3.8/site-packages/click/decorators.py", line 33, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/home/redacted/.venv/lib/python3.8/site-packages/trogon/trogon.py", line 296, in wrapped_tui
    Trogon(app, app_name=name, command_name=command, click_context=ctx).run()
  File "/home/redacted/.venv/lib/python3.8/site-packages/trogon/trogon.py", line 264, in run
    os.execvp(program_name, arguments)
  File "/home/muf2rng/.pyenv/versions/3.8.17/lib/python3.8/os.py", line 568, in execvp
    _execvpe(file, args)
  File "/home/muf2rng/.pyenv/versions/3.8.17/lib/python3.8/os.py", line 610, in _execvpe
    raise last_exc
  File "/home/muf2rng/.pyenv/versions/3.8.17/lib/python3.8/os.py", line 601, in _execvpe
    exec_func(fullname, *argrest)
FileNotFoundError: [Errno 2] No such file or directory

Using typer?

If I got it correctly, typer is also based up on click. Might it be possible to use trogon with typer in mind?

⛓️ Allow chaining subcommands

Trogon is a really awesome project, thank you very much!

AFAICS, it is currently only possible to run individual subcommands, right?

Many of my CLIs depend on chaining subcommands together.

Would be awesome if trogon supported that, similar to how arguments with multiple allowed invocations already work.

Increasing versatility of trogon by making it standalone tool

I'm looking for a tool and this is almost what I wanted except that its not a stand-alone utility, but rather an ad-on for people already using click.

Can I please suggest providing an example of how to use this tool independently? Concretely, the user provides a dictionary of keys with potential values for each and defaults and description etc, and boom, we have a TUI.

I think this is already what's happening internally in trogon where it is scraping the output of click to design the TUI, so in this case, we are providing that directly.

Thank you.

click tuple with diffrent data types

If I try to use click.Tuple with 2 different data types i.e. type=click.Tuple([float, str]), default=(0.0, 'test') The TUI breaks with this error: TypeError: '<' not supported between instances of 'float' and 'str'

It works fine if no default is provided or the default is set to None. It also works if both tuple values are str.
Not sure if that is a known issue or if it is on my side.

Ability to paste existing commands

Similar to #12 it would be interesting to have the ability to paste a command into the tui and switch to the subcommand page and have everything filled in so that it can be tweaked or so that the specific documentation for a flag could be quickly understood

Trogon script execution fails with FileNotFoundError: [Errno 2] No such file or directory

Running the nogroup_demo example fails with FileNotFoundError: [Errno 2] No such file or directory

The cwd and the program_name arguments to the os.execvp call seem to be correct, however the run still fails. The cli version works fine though.

I added a debug log just before the call to execvp listing the following
prog_name = nogroup_demo.py, args = ['nogroup_demo.py', 'add', '--category', 'work', 'foo'] app_name = nogroup_demo.py cwd = /Users/arunabhaghosh/dev/import_migrate_data

---- Details--------------------------

Running nogroup_demo.py add --category work foo
prog_name = nogroup_demo.py, args = ['nogroup_demo.py', 'add', '--category', 'work', 'foo'] app_name = nogroup_demo.py cwd = /Users/arunabhaghosh/dev/import_migrate_data
Traceback (most recent call last):
File "/Users/arunabhaghosh/dev/import_migrate_data/nogroup_demo.py", line 52, in
add()
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/click/core.py", line 1157, in call
return self.main(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/click/core.py", line 1078, in main
rv = self.invoke(ctx)
^^^^^^^^^^^^^^^^
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/click/core.py", line 1688, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/click/core.py", line 1434, in invoke
return ctx.invoke(self.callback, **ctx.params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/click/core.py", line 783, in invoke
return __callback(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/click/decorators.py", line 33, in new_func
return f(get_current_context(), *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/trogon/trogon.py", line 297, in wrapped_tui
Trogon(app, app_name=name, command_name=command, click_context=ctx).run()
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/trogon/trogon.py", line 265, in run
os.execvp(program_name, arguments)
File "", line 574, in execvp
File "", line 616, in _execvpe
File "", line 607, in _execvpe
FileNotFoundError: [Errno 2] No such file or directory

Argument with a default not working when using groups

I get some strange behaviour when using arguments with defaults on command groups. Here is a minimal example:

import click
from trogon import tui

@tui()
@click.group()
def cli():
    pass

@cli.command()
@click.argument("name", default="Bob")
def hello(name):
    """Pring hello."""
    print(f"Hello world and {name}")

if __name__ == "__main__":
    cli()

When running python cli.py tui and selecting the hello command, the command displayed at the bottom is:

cli.py '('"'"'Bob'"'"',)' hello Bob
image

When trying to run the command using ctrl+r, I get the following stacktrace:

Traceback (most recent call last):
  File "/some-dir/test-trogon/cli.py", line 16, in <module>
    cli()
  File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/click/decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/trogon/trogon.py", line 286, in wrapped_tui
    Trogon(app, app_name=name, click_context=ctx).run()
  File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/trogon/trogon.py", line 252, in run
    f"Running [b cyan]{self.app_name} {' '.join(shlex.quote(s) for s in self.post_run_command)}[/]"
                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/trogon/trogon.py", line 252, in <genexpr>
    f"Running [b cyan]{self.app_name} {' '.join(shlex.quote(s) for s in self.post_run_command)}[/]"
                                                ^^^^^^^^^^^^^^
  File "/somewhere/[email protected]/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/shlex.py", line 329, in quote
    if _find_unsafe(s) is None:
       ^^^^^^^^^^^^^^^
TypeError: expected string or bytes-like object, got 'tuple'

Note that it works if I remove the default on the argument or if I get rid of the group:

@tui()
@click.command(name="world")
@click.argument("name", default="Bob")
def cli(name):
    """Pring hello."""
    print(f"Hello world and {name}")

if __name__ == "__main__":
    cli()

feature request: ability copy the running command

As stated in the readme, trogon is made for discoverability, but after trying out the command, we want to actually add it to our history and use it directly. This feature request is thus about adding a shortcut to copy the command to the clipboard.

Possible with Typer app not in a decorator?

This is quite possibly a stupid question, but is there a way to wrap this in a Trogon TUI? I instantiate my typer.Typer app like this, as shown in that project's README

app = typer.Typer()

Is there a way to wrap that in a Trogon TUI?

Support for Click Custom Multi Commands

Hi team! I love your work, really. I discovered this new tool and would like to integrate it in our internal stack. We are using Click for an internal CLI, and we are using it commands and sub-commands with that implementation:
https://click.palletsprojects.com/en/8.1.x/commands/#custom-multi-commands

Basically we have a folder structure:

mycli/
|_ commands/
   |_ cmd1.py
   |_ cmd2.py
   |_ cmd3.py
   |_ ...

Running mycli would list all the commands, and running mycli cmd1 would list all the cmd subcommands

If I implement Trogon from the docs, I would need to do mycli cmd1 tui to have a TUI for the mycli cmd1 subcommands, but I would love having a mycli tui command that would create a TUI for all commands and associated subcommands. Is there a way to do it with the current Trogon code, or this would need extra development?
Let me know if it's unclear

Taking `default_map` as source of default values into account

When I specify the default parameter in an option, trogon considers this value in the TUI. But when I pass a default_map as part of the context_settings (like shown in the click docs here and here) the option doesn't get prefilled.

This behaviour might be misleading because an existing default_map overwrites an options default value. Thus, the application actually runs with a different option value than trogon suggested.

Consider the following code:

import click
from trogon import tui


@tui(command="ui", help="Open terminal UI")
@click.group()
def cli():
    pass


@cli.command()
@click.option("--port", default=8000, show_default=True)
def runserver(port):
    click.echo(f"Serving on http://127.0.0.1:{port}/")


if __name__ == "__main__":
    cli(default_map={"runserver": {"port": 5000}})

The runserver sub-command actually runs with port 5000 because of the default_map. But trogon prefills the textbox in the TUI with port 8000, which is wrong.

using trogon on Windows with py3.8.10 crashes

I have a very simple CLI for which I am using trogon. Upon running command tui, I get the following error. The same code works as expected on WSL.

Version:
python: 3.8.10
click: 8.1.3
trogon: 0.4.0

(fastai) C:\git\public\nested-cli-tui-demo>hello tui   
Traceback (most recent call last):
  File "C:\Users\deven\Envs\fastai\Scripts\hello-script.py", line 33, in <module>
    sys.exit(load_entry_point('foo', 'console_scripts', 'hello')())
  File "C:\Users\deven\Envs\fastai\lib\site-packages\click\core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "C:\Users\deven\Envs\fastai\lib\site-packages\click\core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "C:\Users\deven\Envs\fastai\lib\site-packages\click\core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "C:\Users\deven\Envs\fastai\lib\site-packages\click\core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "C:\Users\deven\Envs\fastai\lib\site-packages\click\core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "C:\Users\deven\Envs\fastai\lib\site-packages\click\decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "C:\Users\deven\Envs\fastai\lib\site-packages\trogon\trogon.py", line 292, in wrapped_tui
    Trogon(app, app_name=name, click_context=ctx).run()
  File "C:\Users\deven\Envs\fastai\lib\site-packages\trogon\trogon.py", line 228, in __init__
    self.app_name = detect_run_string()
  File "C:\Users\deven\Envs\fastai\lib\site-packages\trogon\detect_run_string.py", line 39, in detect_run_string
    argv = get_orig_argv()
  File "C:\Users\deven\Envs\fastai\lib\site-packages\trogon\detect_run_string.py", line 17, in get_orig_argv
    ctypes.pythonapi.Py_GetArgcArgv(ctypes.byref(_argc), ctypes.byref(_argv))
  File "c:\python38\lib\ctypes\__init__.py", line 386, in __getattr__
    func = self.__getitem__(name)
  File "c:\python38\lib\ctypes\__init__.py", line 391, in __getitem__
    func = self._FuncPtr((name_or_ordinal, self))
AttributeError: function 'Py_GetArgcArgv' not found

Error: F1 - About crashes because `Package 'textual.widgets' has no class '__wrapped__'`

Hello!

When trying to add a TUI to my program, see jeertmans/manim-slides#249, the TUI crashes when pressing F1. The issue seems to also occur during tests, see logs.

I have attached the full error traceback below.

Thanks!

╭─────────────── Traceback (most recent call last) ────────────────╮
│ /home/eertmans/.cache/pypoetry/virtualenvs/manim-slides-M-PeeMP- │
│ -py3.10/lib/python3.10/site-packages/trogon/trogon.py:138 in     │
│ action_about                                                     │
│                                                                  │
│   135 │   │   self.app.exit()                                    │
│   136 │                                                          │
│   137 │   def action_about(self) -> None:                        │
│ ❱ 138 │   │   from .widgets.about import AboutDialog             │
│   139 │   │                                                      │
│   140 │   │   self.app.push_screen(AboutDialog())                │
│   141                                                            │
│                                                                  │
│ ╭──────── locals ─────────╮                                      │
│ │ self = CommandBuilder() │                                      │
│ ╰─────────────────────────╯                                      │
│                                                                  │
│ /home/eertmans/.cache/pypoetry/virtualenvs/manim-slides-M-PeeMP- │
│ -py3.10/lib/python3.10/site-packages/trogon/widgets/about.py:8   │
│ in <module>                                                      │
│                                                                  │
│     5 from textual.binding import Binding                        │
│     6 from textual.containers import Center, Vertical            │
│     7 from textual.screen import ModalScreen                     │
│ ❱   8 from textual.widgets import Button, Static                 │
│     9 from textual.widgets._button import ButtonVariant          │
│    10                                                            │
│    11                                                            │
│                                                                  │
│ ╭──────────────────────── locals ────────────────────────╮       │
│ │       Binding = <class 'textual.binding.Binding'>      │       │
│ │        Center = <class 'textual.containers.Center'>    │       │
│ │ ComposeResult = typing.Iterable[textual.widget.Widget] │       │
│ │   ModalScreen = <class 'textual.screen.ModalScreen'>   │       │
│ │          Text = <class 'rich.text.Text'>               │       │
│ │      TextType = typing.Union[str, ForwardRef('Text')]  │       │
│ │      Vertical = <class 'textual.containers.Vertical'>  │       │
│ ╰────────────────────────────────────────────────────────╯       │
│                                                                  │
│ in feature_imported:61                                           │
│                                                                  │
│ in feature_imported:137                                          │
│                                                                  │
│ in _mod_uses_pyside:148                                          │
│                                                                  │
│ /usr/lib/python3.10/inspect.py:1139 in getsource                 │
│                                                                  │
│   1136 │   The argument may be a module, class, method, function 
│   1137 │   or code object.  The source code is returned as a sin │
│   1138 │   OSError is raised if the source code cannot be retrie │
│ ❱ 1139 │   lines, lnum = getsourcelines(object)                  │
│   1140 │   return ''.join(lines)                                 │
│   1141                                                           │
│   1142 # --------------------------------------------------- cla │
│                                                                  │
│ ╭─────────────────────────── locals ───────────────────────────╮ │
│ │ object = <module 'textual.widgets' from                      │ │
│ │          '/home/eertmans/.cache/pypoetry/virtualenvs/manim-… │ │
│ ╰──────────────────────────────────────────────────────────────╯ │
│                                                                  │
│ /usr/lib/python3.10/inspect.py:1120 in getsourcelines            │
│                                                                  │
│   1117 │   corresponding to the object and the line number indic │
│   1118 │   original source file the first line of code was found │
│   1119 │   raised if the source code cannot be retrieved."""     │
│ ❱ 1120 │   object = unwrap(object)                               │
│   1121 │   lines, lnum = findsource(object)                      │
│   1122 │                                                         │
│   1123 │   if istraceback(object):                               │
│                                                                  │
│ ╭─────────────────────────── locals ───────────────────────────╮ │
│ │ object = <module 'textual.widgets' from                      │ │
│ │          '/home/eertmans/.cache/pypoetry/virtualenvs/manim-… │ │
│ ╰──────────────────────────────────────────────────────────────╯ │
│                                                                  │
│ /usr/lib/python3.10/inspect.py:639 in unwrap                     │
│                                                                  │
│    636 │   # ensure they aren't destroyed, which would allow the │
│    637 │   memo = {id(f): f}                                     │
│    638 │   recursion_limit = sys.getrecursionlimit()             │
│ ❱  639 │   while _is_wrapper(func):                              │
│    640 │   │   func = func.__wrapped__                           │
│    641 │   │   id_func = id(func)                                │
│    642 │   │   if (id_func in memo) or (len(memo) >= recursion_l │
│                                                                  │
│ ╭─────────────────────────── locals ───────────────────────────╮ │
│ │     _is_wrapper = <function unwrap.<locals>._is_wrapper at   │ │
│ │                   0x7f0d12e38ee0>                            │ │
│ │               f = <module 'textual.widgets' from             │ │
│ │                   '/home/eertmans/.cache/pypoetry/virtualen… │ │
│ │            func = <module 'textual.widgets' from             │ │
│ │                   '/home/eertmans/.cache/pypoetry/virtualen… │ │
│ │            memo = {                                          │ │
│ │                   │   139695144842192: <module               │ │
│ │                   'textual.widgets' from                     │ │
│ │                   '/home/eertmans/.cache/pypoetry/virtualen… │ │
│ │                   }                                          │ │
│ │ recursion_limit = 1000                                       │ │
│ │            stop = None                                       │ │
│ ╰──────────────────────────────────────────────────────────────╯ │
│                                                                  │
│ /usr/lib/python3.10/inspect.py:630 in _is_wrapper                │
│                                                                  │
│    627 │   """                                                   │
│    628 │   if stop is None:                                      │
│    629 │   │   def _is_wrapper(f):                               │
│ ❱  630 │   │   │   return hasattr(f, '__wrapped__')              │
│    631 │   else:                                                 │
│    632 │   │   def _is_wrapper(f):                               │
│    633 │   │   │   return hasattr(f, '__wrapped__') and not stop │
│                                                                  │
│ ╭─────────────────────────── locals ───────────────────────────╮ │
│ │ f = <module 'textual.widgets' from                           │ │
│ │     '/home/eertmans/.cache/pypoetry/virtualenvs/manim-slide… │ │
│ ╰──────────────────────────────────────────────────────────────╯ │
│                                                                  │
│ /home/eertmans/.cache/pypoetry/virtualenvs/manim-slides-M-PeeMP- │
│ -py3.10/lib/python3.10/site-packages/textual/widgets/__init__.py │
│ :96 in __getattr__                                               │
│                                                                  │
│    93 │   │   pass                                               │
│    94 │                                                          │
│    95 │   if widget_class not in __all__:                        │
│ ❱  96 │   │   raise ImportError(f"Package 'textual.widgets' has  │
│    97 │                                                          │
│    98 │   widget_module_path = f"._{camel_to_snake(widget_class) │
│    99 │   module = import_module(widget_module_path, package="te │
│                                                                  │
│ ╭─────────── locals ───────────╮                                 │
│ │ widget_class = '__wrapped__' │                                 │
│ ╰──────────────────────────────╯                                 │
╰──────────────────────────────────────────────────────────────────╯
ImportError: Package 'textual.widgets' has no class '__wrapped__'

[Not Issue] What typeface is used in the screenshots?

As the title notes, this isn't directly related to the project, but I love the typeface used I your screenshots. What is it? Tried auto-detecting it, but couldn't figure out what it was.

Awesome project by the way!

click and rich prompts interweave into system command prompt

System: Windows 10 (have not tried on Linux yet).
Terminal: Windows Terminal set to Command Prompt.

When using @click.option with a prompt, the command prompt appears to interweave with the running program.
Also occurs when using click.prompt() or rich's Prompt.ask() inside of the function definition.

Will cause the command prompt (such as "(venv) C:\py>") to be displayed throughout click prompts while program is running.

Sometimes will treat response to prompt as if trying to run a system command, sometimes will accept prompt input from the command prompt when the click prompt is not the active display.

As an example, the attached screenshot shows one run of a program through trogon, asking for 2 prompt inputs (a staff name, and an email address). The greenish-yellow squares show the command prompt appearing inside of the running trogon program.

No such problems occur when directly running the click program without trogon. No issues occur if provide the option values in trogon and/or bypass prompts inside program. Have tried on several different functions.

tui

EDIT:
Minimal viable product:

mvp.py:

import click

from trogon import tui

@tui()
@click.group()
def cli():
    pass

@click.command()
@click.option('--name', prompt="Name")
def minimal(name):
    """Prompt for a name."""
    pass

cli.add_command(minimal)

if __name__ == '__main__':
    cli()

setup.py:

from setuptools import setup, find_packages

setup(
    name='mvp',
    version='0.1',
    py_modules=['mvp'],
    install_requires=[
        'Click'
    ],
    packages=find_packages(),
    entry_points='''
        [console_scripts]
        mvp=mvp:cli
    ''',
)

Example application run:
image

Feature request: Default Enum choices from Typer

Note

I am aware that Trogon is currently geared toward Click, but having seen a bit of past discussion on Typer I wanted to raise this as a rough edge stopping me from using Trogon full bore. Thanks for making a great tool, and if I can provide more clarity please let me know!

Whereas Click recommends using click.Choice(["one", "two", ...]), Typer recommends using something like the following:

from typing_extensions import Annotated

class SomeChoiceType(str, Enum):
    ONE = "one"
    TWO = "two"

some_app = typer.Typer()

@some_app.command
def some_command(some_option: Annotated[SomeChoiceType] = SomeChoiceType.ONE):
    ...

This all works great in Typer land, and Trogon will start up okay. But once one navigates to the some_command command in the Trogon TUI, it currently spits out an exception like the following:

╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /path/to/python3.11/site-packages/textual/widget.py:3325                                         │
│ in _on_compose                                                                                   │
│                                                                                                  │
│   3322 │                                                                                         │
│   3323 │   async def _on_compose(self) -> None:                                                  │
│   3324 │   │   try:                                                                              │
│ ❱ 3325 │   │   │   widgets = [*self._nodes, *compose(self)]                                      │
│   3326 │   │   except TypeError as error:                                                        │
│   3327 │   │   │   raise TypeError(                                                              │
│   3328 │   │   │   │   f"{self!r} compose() method returned an invalid result; {error}"          │
│                                                                                                  │
│ /path/to/python3.11/site-packages/trogon/widgets/parameter_controls.py:163 in compose            │
│                                                                                                  │
│   160 │   │   │   │   │   │   for default_value, control_widget in zip(                          │
│   161 │   │   │   │   │   │   │   default_value_tuple, widget_group                              │
│   162 │   │   │   │   │   │   ):                                                                 │
│ ❱ 163 │   │   │   │   │   │   │   self._apply_default_value(control_widget, default_value)       │
│   164 │   │   │   │   │   │   │   yield control_widget                                           │
│   165 │   │   │   │   │   │   │   # Keep track of the first control we render, for easy focus    │
│   166 │   │   │   │   │   │   │   if first_focus_control is None:                                │
│                                                                                                  │
│ /path/to/python3.11/site-packages/trogon/widgets/parameter_controls.py:247                       |
|  in _apply_default_value                                                                         │
│                                                                                                  │
│   244 │   │   │   control_widget.value = str(default_value)                                      │
│   245 │   │   │   control_widget.placeholder = f"{default_value} (default)"                      │
│   246 │   │   elif isinstance(control_widget, Select):                                           │
│ ❱ 247 │   │   │   control_widget.value = str(default_value)                                      │
│   248 │   │   │   control_widget.prompt = f"{default_value} (default)"                           │
│   249 │                                                                                          │
│   250 │   @staticmethod                                                                          │
│                                                                                                  │
│ /path/to/python3.11/site-packages/textual/widgets/_select.py:387 in _validate_value              |
│                                                                                                  │
│   384 │   │   │   # so we provide a helpful message to catch this mistake in case people didn'   │
│   385 │   │   │   # realise we use a special value to flag "no selection".                       │
│   386 │   │   │   help_text = " Did you mean to use Select.clear()?" if value is None else ""    │
│ ❱ 387 │   │   │   raise InvalidSelectValueError(                                                 │
│   388 │   │   │   │   f"Illegal select value {value!r}." + help_text                             │
│   389 │   │   │   )                                                                              │
│   390                                                                                            │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
InvalidSelectValueError: Illegal select value 'SomeChoiceType.ONE'.

This appears to occur specifically when one of the enum choices is used as a default. If the Enum is used to restrict a required argument, or an option where the default is e.g. None, Trogon works as expected.

I understand that one would need to inject a .value there somewhere to get at the actual string that the Enum is referencing, which I imagine is fully within Trogon's control to be doing.

I haven't looked at the parameter conversion code base, so I'm fully willing to hear that knowing about Enums and doing something special with them is against Trogon's design!

Here is a small reproduction if that is helpful.

Tests broken?

All tests seem to have broke after changes to UserOptionData and OptionSchema. New required attributes are never set in tests, resulting in errors when trying to run tests (using pytest):

TypeError: __init__() missing X required positional argument: ...

Do you have any CI in place to avoid such errors in the future?

[enhancement] History support

This module is insanely useful in the case of complex command lines.

One aspect that I think would make it even better is some sort of history support. Not shell history like #12, but actual built-in history within TUI where you can recall commands built in previous runs of the TUI, and either re-edit them or re-run them verbatim.

If editing a command, it would be good to also be able to have the option to either overwrite the previous history entry, or create a new one.

This would offer the ability of recalling oft-used recipes or templates without having to rebuild them from scratch each item. One could even foresee named templates in addition to just historical entries.

Application crash

I'm trying to run my simple python program with trogon, but it's crashing:

Running Ateams.py build
Traceback (most recent call last):
  File "F:\Documents\Projects\C++\Ateams\Ateams.py", line 363, in <module>
    ateams()
  File "C:\Developing\Python\lib\site-packages\click\core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
  File "C:\Developing\Python\lib\site-packages\click\core.py", line 1078, in main
    rv = self.invoke(ctx)
  File "C:\Developing\Python\lib\site-packages\click\core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "C:\Developing\Python\lib\site-packages\click\core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "C:\Developing\Python\lib\site-packages\click\core.py", line 783, in invoke
    return __callback(*args, **kwargs)
  File "C:\Developing\Python\lib\site-packages\click\decorators.py", line 33, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "C:\Developing\Python\lib\site-packages\trogon\trogon.py", line 296, in wrapped_tui
    Trogon(app, app_name=name, command_name=command, click_context=ctx).run()
  File "C:\Developing\Python\lib\site-packages\trogon\trogon.py", line 264, in run
    os.execvp(program_name, arguments)
  File "C:\Developing\Python\lib\os.py", line 575, in execvp
    _execvpe(file, args)
  File "C:\Developing\Python\lib\os.py", line 616, in _execvpe
    raise saved_exc
  File "C:\Developing\Python\lib\os.py", line 608, in _execvpe
    exec_func(fullname, *argrest)
OSError: [Errno 8] Exec format error

Togon Version: 0.5.0
Python version: 3.10.12
OS: Windows 10 x64

Thanks

Vim keybindings

It would be nice to use ctrl+h/j/k/l to navigate the form, or at least the command-tree.

Custom `ParamType`s cause `tui` commands to crash

Given a simple CLI with a custom click.ParamType:

import click
from trogon import tui


@tui()
@click.group()
def main():
    pass


class _TestSuiteCases(click.ParamType):
    name = "thingy"

    def convert(self, value, param, ctx) -> int:
        return 37


@main.command()
@click.argument("input", type=_TestSuiteCases())
def foo(input):
    pass


main()

Running bar tui starts up a TUI successfully, but moving the cursor on top of the foo subcommand crashes with e.g.:

╭────────────────────────────────────────────────────────────────────────────────────────────────────────────── Traceback (most recent call last) ───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ /Users/julian/.dotfiles/.local/share/virtualenvs/bowtie/lib/python3.10/site-packages/textual/widget.py:3100 in _on_compose                                                                                                                                     │
│                                                                                                                                                                                                                                                                │
│   3097 │                                                                                        ╭──────────────────────────────── locals ────────────────────────────────╮                                                                                     │
│   3098 │   async def _on_compose(self) -> None:                                                 │ self = ParameterControls(id='id_192ff4fe', pseudo_classes={'enabled'}) │                                                                                     │
│   3099 │   │   try:                                                                             ╰────────────────────────────────────────────────────────────────────────╯                                                                                     │
│ ❱ 3100 │   │   │   widgets = compose(self)                                                                                                                                                                                                                     │
│   3101 │   │   except TypeError as error:                                                                                                                                                                                                                      │
│   3102 │   │   │   raise TypeError(                                                                                                                                                                                                                            │
│   3103 │   │   │   │   f"{self!r} compose() method returned an invalid result; {error}"                                                                                                                                                                        │
│                                                                                                                                                                                                                                                                │
│ /Users/julian/.dotfiles/.local/share/virtualenvs/bowtie/lib/python3.10/site-packages/textual/_compose.py:26 in compose                                                                                                                                         │
│                                                                                                                                                                                                                                                                │
│   23 │   app._compose_stacks.append(compose_stack)                                            ╭───────────────────────────────────── locals ──────────────────────────────────────╮                                                                            │
│   24 │   app._composed.append(composed)                                                       │           app = Trogon(title='Trogon', classes={'-dark-mode'})                    │                                                                            │
│   25 │   try:                                                                                 │         child = Label(classes={'command-form-label'}, pseudo_classes={'enabled'}) │                                                                            │
│ ❱ 26 │   │   for child in node.compose():                                                     │ compose_stack = []                                                                │                                                                            │
│   27 │   │   │   if composed:                                                                 │      composed = [ControlGroupsContainer(pseudo_classes={'enabled'})]              │                                                                            │
│   28 │   │   │   │   nodes.extend(composed)                                                   │          node = ParameterControls(id='id_192ff4fe', pseudo_classes={'enabled'})   │                                                                            │
│   29 │   │   │   │   composed.clear()                                                         │         nodes = []                                                                │                                                                            │
│                                                                                               ╰───────────────────────────────────────────────────────────────────────────────────╯                                                                            │
│                                                                                                                                                                                                                                                                │
│ /Users/julian/.dotfiles/.local/share/virtualenvs/bowtie/lib/python3.10/site-packages/trogon/widgets/parameter_controls.py:122 in compose                                                                                                                       │
│                                                                                                                                                                                                                                                                │
│   119 │   │   │   │   # We always need to display the original group of controls,                                                                                                                                                                              │
│   120 │   │   │   │   # regardless of whether there are defaults                                                                                                                                                                                               │
│   121 │   │   │   │   if multiple or not default.values:                                                                                                                                                                                                       │
│ ❱ 122 │   │   │   │   │   widget_group = list(self.make_widget_group())                                                                                                                                                                                        │
│   123 │   │   │   │   │   with ControlGroup() as control_group:                                                                                                                                                                                                │
│   124 │   │   │   │   │   │   if len(widget_group) == 1:                                                                                                                                                                                                       │
│   125 │   │   │   │   │   │   │   control_group.add_class("single-item")                                                                                                                                                                                       │
│                                                                                                                                                                                                                                                                │
│ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────── locals ──────────────────────────────────────────────────────────────────────────────────────────────────────────╮                                 │
│ │       argument_type = <__main__._TestSuiteCases object at 0x105dbb520>                                                                                                                                                     │                                 │
│ │             default = MultiValueParamData(values=[])                                                                                                                                                                       │                                 │
│ │ first_focus_control = None                                                                                                                                                                                                 │                                 │
│ │           help_text = ''                                                                                                                                                                                                   │                                 │
│ │           is_option = False                                                                                                                                                                                                │                                 │
│ │               label = <text 'input thingy  *required' [Span(5, 12, 'dim'), Span(14, 15, 'bold red')]>                                                                                                                      │                                 │
│ │            multiple = False                                                                                                                                                                                                │                                 │
│ │                name = 'input'                                                                                                                                                                                              │                                 │
│ │              schema = ArgumentSchema(name='input', type=<__main__._TestSuiteCases object at 0x105dbb520>, required=True, key='id_192ff4fe', default=MultiValueParamData(values=[]), choices=None, multiple=False, nargs=1) │                                 │
│ │                self = ParameterControls(id='id_192ff4fe', pseudo_classes={'enabled'})                                                                                                                                      │                                 │
│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯                                 │
│                                                                                                                                                                                                                                                                │
│ /Users/julian/.dotfiles/.local/share/virtualenvs/bowtie/lib/python3.10/site-packages/trogon/widgets/parameter_controls.py:172 in make_widget_group                                                                                                             │
│                                                                                                                                                                                                                                                                │
│   169 │   │   # At this point we don't care about filling in the default values.                                                                                                                                                                               │
│   170 │   │   for _type in parameter_types:                                                                                                                                                                                                                    │
│   171 │   │   │   control_method = self.get_control_method(_type)                                                                                                                                                                                              │
│ ❱ 172 │   │   │   control_widgets = control_method(                                                                                                                                                                                                            │
│   173 │   │   │   │   default, label, multiple, schema, schema.key                                                                                                                                                                                             │
│   174 │   │   │   )                                                                                                                                                                                                                                            │
│   175 │   │   │   yield from control_widgets                                                                                                                                                                                                                   │
│                                                                                                                                                                                                                                                                │
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────── locals ────────────────────────────────────────────────────────────────────────────────────────────────────────╮                                     │
│ │           _type = <__main__._TestSuiteCases object at 0x105dbb520>                                                                                                                                                     │                                     │
│ │  control_method = None                                                                                                                                                                                                 │                                     │
│ │         default = MultiValueParamData(values=[])                                                                                                                                                                       │                                     │
│ │       is_option = False                                                                                                                                                                                                │                                     │
│ │           label = <text 'input thingy  *required' [Span(5, 12, 'dim'), Span(14, 15, 'bold red')]>                                                                                                                      │                                     │
│ │        multiple = False                                                                                                                                                                                                │                                     │
│ │            name = 'input'                                                                                                                                                                                              │                                     │
│ │  parameter_type = <__main__._TestSuiteCases object at 0x105dbb520>                                                                                                                                                     │                                     │
│ │ parameter_types = [<__main__._TestSuiteCases object at 0x105dbb520>]                                                                                                                                                   │                                     │
│ │        required = True                                                                                                                                                                                                 │                                     │
│ │          schema = ArgumentSchema(name='input', type=<__main__._TestSuiteCases object at 0x105dbb520>, required=True, key='id_192ff4fe', default=MultiValueParamData(values=[]), choices=None, multiple=False, nargs=1) │                                     │
│ │            self = ParameterControls(id='id_192ff4fe', pseudo_classes={'enabled'})                                                                                                                                      │                                     │
│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯                                     │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
TypeError: 'NoneType' object is not callable

The above exception was the direct cause of the following exception:

╭────────────────────────────────────────────────────────────────────────────────────────────────────────────── Traceback (most recent call last) ───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ /Users/julian/.dotfiles/.local/share/virtualenvs/bowtie/lib/python3.10/site-packages/textual/widget.py:3102 in _on_compose                                                                                                                                     │
│                                                                                                                                                                                                                                                                │
│   3099 │   │   try:                                                                             ╭──────────────────────────────── locals ────────────────────────────────╮                                                                                     │
│   3100 │   │   │   widgets = compose(self)                                                      │ self = ParameterControls(id='id_192ff4fe', pseudo_classes={'enabled'}) │                                                                                     │
│   3101 │   │   except TypeError as error:                                                       ╰────────────────────────────────────────────────────────────────────────╯                                                                                     │
│ ❱ 3102 │   │   │   raise TypeError(                                                                                                                                                                                                                            │
│   3103 │   │   │   │   f"{self!r} compose() method returned an invalid result; {error}"                                                                                                                                                                        │
│   3104 │   │   │   ) from error                                                                                                                                                                                                                                │
│   3105 │   │   except Exception:                                                                                                                                                                                                                               │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
TypeError: ParameterControls(id='id_192ff4fe', pseudo_classes={'enabled'}) compose() method returned an invalid result; 'NoneType' object is not callable

(This looks quite nice though! Well done. Certainly looking forward to integrating it into an app or two.)

[bug] support trogon with `--tui` flag?

So far I've really enjoyed using trogon, it's integrated really smoothly into the couple of click CLIs I'm preparing for deployment on a telescope control module. Something I don't vibe with, though, is forcing the use of groups for the scripts. Something I've just been writing is an acquire scripts with a bunch of options that I like having the TUI for. But having to rework my CLI to look like acquire int <args> or something like that to allow the acquire tui group is something I dislike.

What I think I would prefer is a --tui flag that has eager execution. I think this could be added to the api in a backwards-compatible way with a keyword argument in the decorator

@tui(eager_option=True)

and that way I can call my script with

acquire --tui

and launch into the TUI, whereas

acquire <args>...

would run the command as normal (no forced group)

Do not show hidden options

An option that is configured with hidden=True should not appear in the TUI.

Minimal example:

@tui()
@click.command()
@click.option("-f", "--foo", default=False, is_flag=True, hidden=False)
@click.option("-b", "--bar", default=False, is_flag=True, hidden=True)
def cli(foo, bar):
   pass

In the above, foo should appear, but bar shouldn't.

(By the way, keep up the good work, the fixes in 0.4.0 are awesome!)

Add version 0.4 release on GitHub, to allow Debian package to be created

I'd like to package trogon for Debian.

To make a Debian package I need a tarball that includes the test suite.

PyPI. has a tarball for trogon version 0.4 without the test suite, GitHub has version 0.3 with the tests.

It would be great if somebody could create a GitHub release for version 0.4 or add the tests to the tarball on PyPI.

trogon with `standalone_mode` of `click` is not working

import click
from trogon import tui
from typing import Any


@tui()
@click.command()
@click.option('--description', prompt="Description of the job: ", default=f"Description of running func on remotes", help="Write something that describes what this job is about.")
@click.option('--update_repo', prompt="Update repo: ", default=False, help="Update the repo on the remote machine.")
@click.pass_context
def get_choices(ctx: Any, description: str, update_repo: bool):
    return ctx


if __name__ == '__main__':
    res = get_choices(standalone_mode=False)
    print(res)

This is producing this error:

$ python C:\Users\aalsaf01\code\crocodile\myresources\crocodile\cluster\template_tui2.py tui
Running template_tui2.py get-choices
Traceback (most recent call last):
  File "C:\Users\aalsaf01\code\crocodile\myresources\crocodile\cluster\template_tui2.py", line 20, in <module>
    res = get_choices(standalone_mode=False)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\click\core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\click\core.py", line 1078, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\click\core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\click\core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\click\core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\click\decorators.py", line 33, in new_func
    return f(get_current_context(), *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\trogon\trogon.py", line 296, in wrapped_tui
    Trogon(app, app_name=name, command_name=command, click_context=ctx).run()
  File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\trogon\trogon.py", line 264, in run
    os.execvp(program_name, arguments)
  File "<frozen os>", line 574, in execvp
  File "<frozen os>", line 616, in _execvpe
  File "<frozen os>", line 607, in _execvpe
FileNotFoundError: [Errno 2] No such file or directory

```

Search crashes for `click.option` without a `help`: `AttributeError: 'NoneType' object has no attribute 'casefold'`

Description

When you try to use the search feature in a command that has a click.option without a help text, you'll get an exception as soon as you type a letter in the search bar:

AttributeError: 'NoneType' object has no attribute 'casefold'

Full traceback:

╭───────────────────────────────────────────────────────────────────── Traceback (most recent call last) ──────────────────────────────────────────────────────────────────────╮
│ /Users/samuel/workspace/tmp/trogon/.venv/lib/python3.11/site-packages/trogon/widgets/form.py:222 in apply_filter                                                             │
│                                                                                                                                                                              │
│   219 │   │   all_controls = self.query(ParameterControls)                                                                                                                   │
│   220 │   │   for control in all_controls:                                                                                                                                   │
│   221 │   │   │   filter_query = filter_query.casefold()                                                                                                                     │
│ ❱ 222 │   │   │   control.apply_filter(filter_query)                                                                                                                         │
│   223                                                                                                                                                                        │
│                                                                                                                                                                              │
│ ╭──────────────────────────────────── locals ────────────────────────────────────╮                                                                                           │
│ │ all_controls = <DOMQuery query='ParameterControls'>                            │                                                                                           │
│ │      control = ParameterControls(id='id_1aad92d0', pseudo_classes={'enabled'}) │                                                                                           │
│ │        event = Changed()                                                       │                                                                                           │
│ │ filter_query = 'f'                                                             │                                                                                           │
│ │         self = CommandForm(pseudo_classes={'focus-within', 'enabled'})         │                                                                                           │
│ ╰────────────────────────────────────────────────────────────────────────────────╯                                                                                           │
│                                                                                                                                                                              │
│ /Users/samuel/workspace/tmp/trogon/.venv/lib/python3.11/site-packages/trogon/widgets/parameter_controls.py:88 in apply_filter                                                │
│                                                                                                                                                                              │
│    85 │   │   │   │   │   filter_query in name.casefold() for name in self.schema.name                                                                                       │
│    86 │   │   │   │   )                                                                                                                                                      │
│    87 │   │   │   │   help_contains_query = (                                                                                                                                │
│ ❱  88 │   │   │   │   │   filter_query in getattr(self.schema, "help", "").casefold()                                                                                        │
│    89 │   │   │   │   )                                                                                                                                                      │
│    90 │   │   │   │   should_be_visible = name_contains_query or help_contains_query                                                                                         │
│    91                                                                                                                                                                        │
│                                                                                                                                                                              │
│ ╭─────────────────────────────────────── locals ────────────────────────────────────────╮                                                                                    │
│ │        filter_query = 'f'                                                             │                                                                                    │
│ │           help_text = None                                                            │                                                                                    │
│ │                name = ['--force', '-f']                                               │                                                                                    │
│ │ name_contains_query = True                                                            │                                                                                    │
│ │                self = ParameterControls(id='id_1aad92d0', pseudo_classes={'enabled'}) │                                                                                    │
│ ╰───────────────────────────────────────────────────────────────────────────────────────╯                                                                                    │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

To reproduce

Here's a minimal reproducible example:

requirements.txt to install:

click==8.1.3
trogon==0.4.0

cli.py

import click
from trogon import tui

@tui()
@click.command()
@click.option("--force", "-f", is_flag=True)
def something(force):
    click.echo(f"{force=:}")

something()

Steps:

  1. Run python cli.py tui
  2. Hit <ctrl + s> or click in the search bar
  3. Type in any letter

Adding a help text solves the issue:

- @click.option("--force", "-f", is_flag=True)
+ @click.option("--force", "-f", is_flag=True, help="yay")

Demo

asciicast

Search function for Trogon

A search box (at the top?) which refines the list of options.

The search should match against the help text and the switch.

For inspiration, look at Chrome settings.

Nicer handling of callable defaults

Currently, if you have an option that sets a callable default, it will simply show the str format of it, e.g. if you have:

def my_default():
    return "hi"

@click.command()
@click.option('-o', default=my_default)
def command(option): ...

it will show as <function my_default at 0x10f0fb040>

Obviously, it would be nice if there was a better representation thanks (or just no default was shown)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.