Example app

We’re going to use pytabby to control program flow in an app that takes a directory full of files and allows you to categorize them into subdirectories.

There are two menus: [d]irectory management, where we see if folders exist and create them if needed; and [f]ile management, where we assign files to subfolders.

Our subfolders will be named interesting and boring.

Here’s the python file, app.py. (It can also be found in the project github repo under example_app/). Instead of dealing with an external YAML config file, I’ve just hardcoded the config as a dict into the python app:

"""A simple app that shows some capabilities of pytabby."""

import glob
import os
import shutil

from pytabby import Menu

# to make this a self-contained app, hardcode the config dict

CONFIG = {
    "case_sensitive": False,
    "screen_width": 80,
    "tabs": [{"tab_header_input": "subdirs",
              "items": [{"item_choice_displayed": "c",
                         "item_description": "Create missing subdirectories",
                         "item_inputs": ["c"],
                         "item_returns": "create_subdirs"},
                        {"item_choice_displayed": "h",
                         "item_description": "Help",
                         "item_inputs": ["h"],
                         "item_returns": "help"},
                        {"item_choice_displayed": "q",
                         "item_description": "Quit",
                         "item_inputs": ["q"],
                         "item_returns": "quit"}]},
             {"tab_header_input": "files",
              "items": [{"item_choice_displayed": "i",
                         "item_description": "Move to interesting/",
                         "item_inputs": ["i"],
                         "item_returns": "interesting"},
                        {"item_choice_displayed": "b",
                         "item_description": "Move to boring/",
                         "item_inputs": ["b"],
                         "item_returns": "boring"},
                        {"item_choice_displayed": "s",
                         "item_description": "Skip",
                         "item_inputs": ["s"],
                         "item_returns": "skip"}]}]}


def print_help():
    """Print help string to stdout"""
    help_text = (
        "This app goes through the contents of a directory and allows you to categorize the files, "
        "either moving them to subdirectories called interesting/ and boring/ or skipping them. This "
        "functionality is handled by the second tab\n    The first tab allows you to check if the "
        "subdirectories already exist, allows you to create them if they are missing, shows this help "
        "text and allows you to quit the app\n"
    )
    print(help_text)


def get_directory():
    """Get the name of a directory to use, or uses the current one"""
    valid = False
    while not valid:
        directory = input("Enter directory (blank for current): ")
        if directory.strip() == "":
            directory = os.getcwd()
        if os.path.isdir(directory):
            valid = True
        else:
            print("That directory does not exist.")
    return directory


def get_files():
    """Determine sorted list of files in the current working directory"""
    files = []
    for item in glob.glob("./*"):
        # add current .py file in case it's in the directory
        if os.path.isfile(item) and os.path.split(item)[1] != os.path.split(__file__)[1]:
            files.append(item)
    return sorted(files)


def create_subdirectories():
    """Create subdirectories if they do not exist"""
    for subdir in ["interesting", "boring"]:
        if os.path.isdir(subdir):
            print("./{0}/ EXISTS".format(subdir))
        else:
            os.mkdir(subdir)
            print("./{0}/ CREATED".format(subdir))
    print("")


def move_to_subdir(filename, subdirname):
    """Move filename to subdirname"""
    if os.path.isfile(os.path.join(subdirname, filename)):
        raise ValueError("File already exists in that subdirectory!")
    shutil.move(filename, subdirname)
    print("{0} moved to ./{1}/".format(filename, subdirname))
    print("")

def main_loop():  # noqa: C901
    """Contain all the logic for the app"""
    menu = Menu(CONFIG)
    files = get_files()
    current_position = 0
    quit_early = False
    files_exhausted = False
    while not (quit_early or files_exhausted):
        filename = files[current_position]
        files_message = "Current_file: {0} of {1}: {2}".format(current_position + 1, len(files),
                                                               os.path.split(filename)[1])
        # message will be shown only when we are on the files tab
        result = menu.run(message={'files': files_message})
        if result == ("subdirs", "create_subdirs"):
            create_subdirectories()
        elif result == ("subdirs", "help"):
            print_help()
        elif result == ("subdirs", "quit"):
            quit_early = True
        elif result[0] == "files" and result[1] in ["interesting", "boring"]:
            if not os.path.isdir(result[1]):
                raise ValueError("Directory must be created first")
            move_to_subdir(files[current_position], result[1])
            print('File moved to {}'.format(result[1]))
            current_position += 1
            files_exhausted = current_position >= len(files)
        elif result == ("files", "skip"):
            current_position += 1
            files_exhausted = current_position >= len(files)
        else:
            raise AssertionError("Unrecognized input, this should have been caught by Menu validator")
    if files_exhausted:
        print("All files done.")
    else:
        print("Program quit early.")


if __name__ == "__main__":

    CWD = os.getcwd()
    os.chdir(get_directory())
    main_loop()
    os.chdir(CWD)

You can try this out on any folder of files; in the GitHub repo, there’s a folder called example_app with this script, app.py, and six photos downloaded from Unsplash, and resized to take up less space. Note that this program doesn’t show the images, but feel free to build that capability into it!

Here’s an example terminal session using the above script:

example_app$ ls
app.py                              cade-roberts-769333-unsplash.jpg
prince-akachi-728006-unsplash.jpg    tyler-nix-597157-unsplash.jpg
brandon-nelson-667507-unsplash.jpg  colton-duke-732468-unsplash.jpg
raj-eiamworakul-514562-unsplash.jpg

There are six jpgs we will classify as interesting or boring, plus the app.py script that is smart enough to ignore itself when moving files. The boring and interesting folders are not yet present.

example_app$ python app.py
Enter directory (blank for current):

[subdirs|files]
 ======= ------
[c] Create missing subdirectories
[h] Help
[q] Quit
?: c
./interesting/ CREATED
./boring/ CREATED

If we try to create the directories again, we’ll just be told they already exist

[subdirs|files]
 ======= ------
[c] Create missing subdirectories
[h] Help
[q] Quit
?: c
./interesting/ EXISTS
./boring/ EXISTS


[subdirs|files]
 ======= ------
[c] Create missing subdirectories
[h] Help
[q] Quit
?: h
This app goes through the contents of a directory and allows you to
categorize the files, either moving them to subdirectories called
interesting/ and boring/ or skipping them. This functionality is
handled by the second tab
    The first tab allows you to check if the subdirectories already
exist, allows you to create them if they are missing, shows this help
text and allows you to quit the app


[subdirs|files]
 ======= ------
[c] Create missing subdirectories
[h] Help
[q] Quit
?: files
Change tab to files

[subdirs|files]
 ------- ======
[i] Move to interesting/
[b] Move to boring/
[s] Skip
Current_file: 1 of 6: brandon-nelson-667507-unsplash.jpg
?: i
./brandon-nelson-667507-unsplash.jpg moved to ./interesting/

File moved to interesting

[subdirs|files]
 ------- ======
[i] Move to interesting/
[b] Move to boring/
[s] Skip
Current_file: 2 of 6: cade-roberts-769333-unsplash.jpg
?: b
./cade-roberts-769333-unsplash.jpg moved to ./boring/

File moved to boring

[subdirs|files]
 ------- ======
[i] Move to interesting/
[b] Move to boring/
[s] Skip
Current_file: 3 of 6: colton-duke-732468-unsplash.jpg
?: s

[subdirs|files]
 ------- ======
[i] Move to interesting/
[b] Move to boring/
[s] Skip
Current_file: 4 of 6: prince-akachi-728006-unsplash.jpg
?: i
./prince-akachi-728006-unsplash.jpg moved to ./interesting/

File moved to interesting

[subdirs|files]
 ------- ======
[i] Move to interesting/
[b] Move to boring/
[s] Skip
Current_file: 5 of 6: raj-eiamworakul-514562-unsplash.jpg
?: i
./raj-eiamworakul-514562-unsplash.jpg moved to ./interesting/

File moved to interesting

[subdirs|files]
 ------- ======
[i] Move to interesting/
[b] Move to boring/
[s] Skip
Current_file: 6 of 6: tyler-nix-597157-unsplash.jpg
?: i
./tyler-nix-597157-unsplash.jpg moved to ./interesting/

File moved to interesting
All files done.

Now the program exits, and we can verify all the files are where we expect

example_app$ ls
app.py  boring  colton-duke-732468-unsplash.jpg  interesting
example_app$ ls boring/
cade-roberts-769333-unsplash.jpg
example_app$ ls interesting/
brandon-nelson-667507-unsplash.jpg  prince-akachi-728006-unsplash.jpg
raj-eiamworakul-514562-unsplash.jpg  tyler-nix-597157-unsplash.jpg