image

Published: 2019-09-22

Using Gooey as a Universal Frontend for any Language or CLI Application

Gooey is a tool for transforming command line interfaces into beautiful desktop applications. It can be used as the frontend client for any language or program. Whether you've built your application in Java, Node, or Haskell, or you just want to put a pretty interface on an existing tool like FFMPEG, Gooey can be used to create a fast, practically free UI with just a little bit of Python (about 20 lines!). 

Don't want to write no stinkin' Python? Good news! Gooey is actually controlled entirely by a plain ol' JSON file. We're only using Python here because it has convenient bindings for generating the JSON. Contribute bindings for your language!

To show how this all fits together, and that it really works for anything, we're going to walk through building a graphical interface to one of my favorite tools of all time: FFMPEG. These steps apply to anything, though! You could swap out FFMPEG for a .jar you've written, or an arbitrary windows .exe, an OSX .app bundle, or anything on linux that's executable! 

Why wrap up another program at all? While FFMPEG is amazing, it is anything but friendly. Borderline hostile, in fact. The learning curve is basically just a brick wall. To make it friendly for our theoretical end user, we're going to expose a subset of it as a simple, familiar GUI in which they can point-and-click their way to success. Additionally, since FFMPEG has a staggeringly deep suite of video processing tools, to keep things simple we're going to expose just a very small subset of its functionality. We're going to build a UI in which users can specify 1. an input video file, 2. a timestamp where they'd like to extract a screenshot, and finally 3. the output filename where they want the screenshot saved.

In short, it will transform a "scary" terminal command line this:

ffmpeg -ss 300 -i /home/user/path/to/movie.mkv -frames:v 1 outfile.png

into an easy to use desktop application that you could hand over to users.

image

Getting Started

To play along at home, grab the appropriate FFMPEG binary for your OS here and install it.

If all has gone well, you should be able to invoke it from the command line and get back some useful version info

ffmpeg -version

With that working, let's get onto building the UI side of things.

Step 1. Installing Python

If you're coming from other languages / backgrounds, step one will be installing Python. You can find an appropriate guide for your OS here. If you're on Windows, the shortest path to done boils down to visiting python.org, clicking the big "Download" button, and then double clicking the downloaded file.

Step 2. Create a directory and source file for the project

We'll need a single python file to describe the GUI we want to build. To stay organized, we'll put any files we create in a directory called myproject.

mkdir myproject 
cd myproject

Inside here we'll create the source file we need.

touch main.py

Step 3. Installing Gooey

Before we start writing the little bits of code required, we have to install Gooey. In Python land, this is done via a tool called pip. This should have been installed automatically along side Python in step 1.

pip install Gooey

Note: you would normally install Python dependencies inside of a virtual environment, but we're ignoring that detail for ease of example.

Step 4. Let's make a UI!

Open the main.py file we created in step 2 and paste in the following.

from gooey import Gooey, GooeyParser


@Gooey(target="ffmpeg", program_name='Frame Extraction v1.0', suppress_gooey_flag=True) 
def main():
    parser = GooeyParser(description="Extracting frames from a movie using FFMPEG")
    ffmpeg = parser.add_argument_group('Frame Extraction Util')
    ffmpeg.add_argument('-i',
                        metavar='Input Movie',
                        help='The movie for which you want to extract frames',
                        widget='FileChooser')
    ffmpeg.add_argument('output',
                        metavar='Output Image',
                        help='Where to save the extracted frame',
                        widget='FileSaver',
                        )
    ffmpeg.add_argument('-ss',
                        metavar='Timestamp',
                        help='Timestamp of snapshot (in seconds)')
    ffmpeg.add_argument('-frames:v',
                        metavar='Timestamp',
                        default=1,
                        gooey_options={'visible': False})

    parser.parse_args()

if __name__ == '__main__':
    main()

note: if you're familiar with Python, this code will look suspiciously similar to the Argparse library, and that's because it is Argparse code! Gooey is a drop-in replacement for all projects backed by Argparse!

That's all it takes to get a UI bolted on in front of an existing CLI application! Before we walk through the details of the code, let's give it a run and enjoy the spoils of our work!

python main.py

image

Pretty awesome!

How it works:

A fair bit of the code is just boilerplate: function definitions, imports, bookkeeping. You can learn about them in detail via the docs. We'll focus just on the interesting bits. Going from top to bottom, the first notable line in our code is the one that begins with @Gooey

@Gooey(target="ffmpeg", program_name='Frame Extraction v1.0', suppress_gooey_flag=True)

target is the most important argument on this line. This is how we tell Gooey that we're trying to run an ffmpeg command. If you were using Gooey with, say, a .jar, you'd similarly provide that info here (e.g. target='java -jar '). At run time, the input provided by the user in the UI gets stitched together and handed over to whatever is specified in target`. This is what enables Gooey to work with any CLI app! 

The next notable lines are the ones beginning with ffmpeg.add_argument. These are how we specify what shows up as a form field in the UI. Each field we want to present to the user will have an associated call to add_argument. They all have a few arguments in common:

  • first positional argument - this is where we specify the CLI variable for which we're receiving input. In our case, we've set it to -i, which is how you declare the input file to FFMPEG
  • metavar - This is the human readable name we want to appear over the input field.
  • help - This is the descriptive text that appears next to the input field
  • widget - Gooey has a bunch of different input widgets. Since we're dealing with file IO, we set the relevant fields to FileChooser/FileSaver which spawns the native file dialog used by your host OS.

There are a few slightly advanced pieces of Gooey in here as well.

    ffmpeg.add_argument('-frames:v',
                        metavar='Timestamp',
                        default=1,
                        required=True,  # Look at me!
                        gooey_options={'visible': False})

This one uses the gooey_options API to hide this specific component from the interface. The frames:v argument is a minor detail of FFMPEG that end users need not worry about, so we set it up with the values ffmpeg needs, but don't actually expose it.

And that's all there is to it! In just a few lines of Python, we've build a super polished UI that's easy to use and turns the complex task of frame extraction via FFMPEG into something that can be used by anyone. 

We can take this a little further, though..!

Bonus: Validation

Our UI is pretty good, but it has a few problems. For instance, users could click start without filling out all the fields, or they could fill out the fields with junk data! Let's be helpful citizens and give them some guidance.

Make required fields required:

We can add required=True to any fields we want to force the user to manage.

@Gooey(target="ffmpeg", program_name='Frame Extraction v1.0', suppress_gooey_flag=True)
def main():
    parser = GooeyParser(description="Extracting frames from a movie using FFMPEG")
    ffmpeg = parser.add_argument_group('Frame Extraction Util')
    ffmpeg.add_argument('-i',
                        metavar='Input Movie',
                        help='The movie for which you want to extract frames',
                        required=True,  # Look at me!
                        widget='FileChooser')
    ffmpeg.add_argument('output',
                        metavar='Output Image',
                        help='Where to save the extracted frame',
                        widget='FileSaver',
                        )
    ffmpeg.add_argument('-ss',
                        metavar='Timestamp',
                        required=True,  # Look at me!
                        help='Timestamp of snapshot (in seconds)')
    ffmpeg.add_argument('-frames:v',
                        metavar='Timestamp',
                        default=1,
                        required=True,  # Look at me!
                        gooey_options={'visible': False})

    parser.parse_args()

Now if we try to advance without filling out any of the forms we get a helpful notification!

image

Input Validation:

Now let's make sure that the user enters appropriate data types. Update the -ss argument to look like the following.

    ffmpeg.add_argument('-ss',
                        metavar='Timestamp',
                        required=True,  # Look at me!
                        help='Timestamp of snapshot (in seconds)',
                        gooey_options={    # NEW!
                            'validator': {
                                'test': 'user_input.isdigit()',
                                'message': 'Please enter a number'
                            }
                        })

You can read all about validation in the docs, but in short, this tells Gooey to verify that the user's input is actually a number before passing things of to ffmpeg. With this in place, now we've gone really far towards making something that's truly user friendly!

image

Wrapping Up

Where should we go from here? There are lots of options! You could package your new Gooey powered app as a stand alone executable, customize the look and feel of the UI, internationalize your text, and a whole lot more!