Skip to content

josevnz/CLIWithClickAndTrogon

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

How to Use TUI Applications with Click and Trogon - Linux Tutorial

Linux and terminal applications are almost synonymous. If you have used applications like grep, cat, sed, AWK those are command line interfaces (CLI) and when they work together they allow you to unleash the power of your computer by mixing and matching a few commands. Some times CLI get too complex and that's when they can get complemented with more exploratory versions of the programs called text user interfaces (TUI). TUI like HTOP, glances, Midnight commander and others allow you to mix the power of the CLI without sacrificing the ease of use. So what to do when your Python CLI gets lots of options and becomes intimidating to the user? Wouldn't be nice if you could have a way to 'self' discover the app and once you are familiar with it, perform your task quickly using the options supported by the script?

Python has a very healthy ecosystem of GUI and TUI frameworks that you can use to write nice looking and intuitive applications.

I'll show you two of them that can help you solve the following two problems::

  1. Avoid overwhelm complex and intimidating API when writing applications. Will use Click to solve that problem.
  2. Allow discoverability. This is very important when you have an application that supports many options or that you haven't used in a while. That is where Trogon comes handy.

We will reuse the source code of one of my Open Source applications, rpm_query as a base. Rpm_query is a collection of simple applications that can query your system RPM database from the command line.

What do you need for this tutorial

  1. Linux's distribution, preferably one that uses RPM (Like Fedora or RedHat enterprise Linux)
  2. Python 3.8+
  3. Git
  4. Familiarity with virtual environments
  5. An Internet connection so you can download dependencies

A quick refresher, how a common CLI looks like

This script uses a module inside the reporter package to query the RPM database

#!/usr/bin/env python
"""
# rpmq_simple.py - A simple CLI to query the sizes of RPM on your system
Author: Jose Vicente Nunez
"""
import argparse
import textwrap

from reporter import __is_valid_limit__
from reporter.rpm_query import QueryHelper

if __name__ == "__main__":

    parser = argparse.ArgumentParser(description=textwrap.dedent(__doc__))
    parser.add_argument(
        "--limit",
        type=__is_valid_limit__,  # Custom limit validator
        action="store",
        default=QueryHelper.MAX_NUMBER_OF_RESULTS,
        help="By default results are unlimited but you can cap the results"
    )
    parser.add_argument(
        "--name",
        type=str,
        action="store",
        help="You can filter by a package name."
    )
    parser.add_argument(
        "--sort",
        action="store_false",
        help="Sorted results are enabled bu default, but you fan turn it off"
    )
    args = parser.parse_args()

    with QueryHelper(
        name=args.name,
        limit=args.limit,
        sorted_val=args.sort
    ) as rpm_query:
        for package in rpm_query:
            print(f"{package['name']}-{package['version']}: {package['size']:,.0f}")

Let's install it, in editable mode:

git clone XXXX CLIWithCLickAndTrogon
python3 -m venv ~/virtualenv/CLIWithCLickAndTrogon
. ~/virtualenv/CLIWithCLickAndTrogon/bin/activate
pip install --editable .

And see it in action:

(CLIWithClickAndTrogon) [josevnz@dmaf5 CLIWithClickAndTrogon]$ rpmq_simple.py --help
usage: rpmq_simple.py [-h] [--limit LIMIT] [--name NAME] [--sort]

# rpmq_simple.py - A simple CLI to query the sizes of RPM on your system Author: Jose Vicente Nunez

options:
  -h, --help     show this help message and exit
  --limit LIMIT  By default results are unlimited but you can cap the results
  --name NAME    You can filter by a package name.
  --sort         Sorted results are enabled bu default, but you fan turn it off
(CLIWithClickAndTrogon) [josevnz@dmaf5 CLIWithClickAndTrogon]$ rpmq_simple.py --name kernel --limit 5
kernel-6.2.11: 0
kernel-6.2.14: 0
kernel-6.2.15: 0

So it seems than most of the code on the rpmq_simple.py script is boilerplate for the command line interface, using the standard 'ArgParse' library.

ArgParse is powerful, but it is also intimidating at first, specially when you have to support multiple use cases.

A new way to process the CLI with Click

The Click framework promises to make it easier to parse out command line arguments. To prove that, we will convert out script from ArgParse to Click:

#!/usr/bin/env python
"""
# rpmq_click.py - A simple CLI to query the sizes of RPM on your system
Author: Jose Vicente Nunez
"""
import click

from reporter.rpm_query import QueryHelper


@click.command()
@click.option('--limit', default=QueryHelper.MAX_NUMBER_OF_RESULTS,
              help="By default results are unlimited but you can cap the results")
@click.option('--name', help="You can filter by a package name.")
@click.option('--sort', default=True, help="Sorted results are enabled bu default, but you fan turn it off")
def command(
        name: str,
        limit: int,
        sort: bool
) -> None:
    with QueryHelper(
            name=name,
            limit=limit,
            sorted_val=sort
    ) as rpm_query:
        for package in rpm_query:
            click.echo(f"{package['name']}-{package['version']}: {package['size']:,.0f}")


if __name__ == "__main__":
    command()

So you will notice to big changes here:

  1. Most of the boilerplate code from ArgParse is done, replaced by annotations
  2. Click works by adding decorators to a new function called 'command', that takes arguments and executes the RPM query

If you run the new script you will see that it works exactly as before:

(CLIWithClickAndTrogon) [josevnz@dmaf5 CLIWithClickAndTrogon]$ rpmq_click.py --help
Usage: rpmq_click.py [OPTIONS]

Options:
  --limit INTEGER  By default results are unlimited but you can cap the
                   results
  --name TEXT      You can filter by a package name.
  --sort BOOLEAN   Sorted results are enabled bu default, but you fan turn it
                   off
  --help           Show this message and exit.
(CLIWithClickAndTrogon) [josevnz@dmaf5 CLIWithClickAndTrogon]$ rpmq_click.py --name kernel --limit 5
kernel-6.2.11: 0
kernel-6.2.14: 0
kernel-6.2.15: 0

Using setuptools and Click

The Click documentation mention that we should use setuptools to create a wrapper for our tool, automatically.

The documentation has the deprecated syntax for setup.py, but we will use the new setup.cfg format instead:

[metadata]
name = CLIWithClickAndTrogon
version = 0.0.1
author = Jose Vicente Nunez Zuleta
author-email = kodegeek.com@protonmail.com
license = Apache 2.0
summary = Simple TUI that queries the RPM database
home-page = https://github.com/josevnz/cliwithclickandtrogon
description = Simple TUI that queries the RPM database. A tutorial.
long_description = file: README.md
long_description_content_type = text/markdown

[options]
packages = reporter
setup_requires =
    setuptools
    wheel
    build
    pip
    twine
install_requires =
    importlib; python_version == "3.9"
    click
scripts =
    scripts/rpmq_simple.py
    scripts/rpmq_click.py
[options.entry_points]
console_scripts =
    rpmq = reporter.scripts:command

Then we create a package called 'scripts' inside the package called 'reporter' with the CLI logic using click.

setuptools will generate a script called 'rpmq' for us that behaves exactly as the previous script, but again no boilerplate code to pass arguments to click is needed:

CLIWithClickAndTrogon) [josevnz@dmaf5 CLIWithClickAndTrogon]$ pip install --editable .
(CLIWithClickAndTrogon) [josevnz@dmaf5 CLIWithClickAndTrogon]$ rpmq --help
Usage: rpmq [OPTIONS]

Options:
  --limit INTEGER  By default results are unlimited but you can cap the
                   results
  --name TEXT      You can filter by a package name.
  --sort BOOLEAN   Sorted results are enabled bu default, but you fan turn it
                   off
  --help           Show this message and exit.
(CLIWithClickAndTrogon) [josevnz@dmaf5 CLIWithClickAndTrogon]$ rpmq --name kernel --limit 5
kernel-6.2.11: 0
kernel-6.2.14: 0
kernel-6.2.15: 0

Making your CLI discoverable with Trogon

Let's solve the problem of making a CLI discoverable with Trogon. Besides adding the new trogon library as part of the requirements (requirements.txt and setup.cfg we need to add a new decorator to our CLI:

#!/usr/bin/env python
"""
A simple CLI to query the sizes of RPM on your system
Author: Jose Vicente Nunez
"""
import click
from trogon import tui

from reporter.rpm_query import QueryHelper

@tui()
@click.command()
@click.option('--limit', default=QueryHelper.MAX_NUMBER_OF_RESULTS,
              help="By default results are unlimited but you can cap the results")
@click.option('--name', help="You can filter by a package name.")
@click.option('--sort', default=True, help="Sorted results are enabled bu default, but you fan turn it off")
def command(
        name: str,
        limit: int,
        sort: bool
) -> None:
    with QueryHelper(
            name=name,
            limit=limit,
            sorted_val=sort
    ) as rpm_query:
        for package in rpm_query:
            click.echo(f"{package['name']}-{package['version']}: {package['size']:,.0f}")


if __name__ == "__main__":
    command()

Just one annotation, @tui and a new import.

Time to see it in action:

(CLIWithClickAndTrogon) [josevnz@dmaf5 CLIWithClickAndTrogon]$ rpmq_trogon.py --help
Usage: rpmq_trogon.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  command
  tui      Open Textual TUI.

Same results, however you will notice 2 changes:

  1. If you want to use the CLI options you need to prepend 'command' before the switches
  2. There is a new 'tui' command

Wait a second ..., what happened with the other flags? No worries, if you ask for more help for 'command' you will see them there:

(CLIWithClickAndTrogon) [josevnz@dmaf5 CLIWithClickAndTrogon]$ rpmq_trogon.py command --help
Usage: rpmq_trogon.py command [OPTIONS]

Options:
  --limit INTEGER  By default results are unlimited but you can cap the
                   results
  --name TEXT      You can filter by a package name.
  --sort BOOLEAN   Sorted results are enabled bu default, but you fan turn it
                   off
  --help           Show this message and exit.

Ah, much better, let's run the CLI the similar way we did before:

(CLIWithClickAndTrogon) [josevnz@dmaf5 CLIWithClickAndTrogon]$ rpmq_trogon.py command --limit 5 --name kernel
kernel-6.2.11: 0
kernel-6.2.14: 0
kernel-6.2.15: 0

And what about support for setuptools? Just add the import and the annotation to the 'command function':

import click
from trogon import tui

from reporter.rpm_query import QueryHelper
@tui()
@click.command()
@click.option('--limit', default=QueryHelper.MAX_NUMBER_OF_RESULTS,
              help="By default results are unlimited but you can cap the results")
@click.option('--name', help="You can filter by a package name.")
@click.option('--sort', default=True, help="Sorted results are enabled bu default, but you fan turn it off")
def command(
        name: str,
        limit: int,
        sort: bool
) -> None:
    # .... real code goes here
    pass

Allow me to demonstrate now with the tui mode the auto discoverable mode:

asciicast

Nice! We got a TUI where some options are automatically populated for us, it gives us a clear idea how to use the programs without knowing too much about them.

What is next

  1. Download the source code for this tutorial and start experimenting.
  2. Both Click and Trogon have great documentation and online support. Take advantage of them.
  3. Click has much more complex examples, feel free to check their gallery.

About

Tutorial that shows how to migrate from ArgParse to Click and Trogon for beautiful TUI

Resources

License

Apache-2.0, Apache-2.0 licenses found

Licenses found

Apache-2.0
LICENSE
Apache-2.0
LICENSE.txt

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages