aegirhall / console-menu

A simple Python menu system for building terminal user interfaces.
MIT License
366 stars 58 forks source link

Usage Example Request: getting returned values and user selections #43

Open txoof opened 4 years ago

txoof commented 4 years ago

I can see that this is based on the python-curses module. I think that console-menu launches something of a daemon thread that runs in the background continually grabbing user input.

I've never worked with something like this. Can you provide a simple example that shows how to interact with the running menu? How do I get the return values from executed functions or grab the most recent selection?

I cobbled together this example based on what I could piece together from the examples and docs, but I'm not sure where to go from here:

from consolemenu import *
from consolemenu.items import *

import logging
logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)

def add(number):
    return number+number

m = ConsoleMenu('Title')
item = MenuItem('Item one')
item_two = MenuItem('Item Two')
func = FunctionItem('add numbers', add, [5])

m.append_item(item)
m.append_item(item_two)
m.append_item(func)
logging.debug('starting up')
m.start()

while m.is_alive():
    # this is where the work gets done, but I'm not sure how to go about doing that.
    pass

m.join()
FelipeEmos commented 3 years ago

@txoof , I'm also new to these concepts, but I think I understand what you mean. First of all, you don't need your while loop to do some basic interactions with the menu. You can get the "return" value from menu options just by calling a function that handles more than just the operation. I've modified your code to show you what I mean. In this example, the function "add_and_show" could do much more than just a display, you can handle more complex operations if you want. If you need to store a state, the best practice would be to create a separate class just for dealing with your states :)

from consolemenu import *
from consolemenu.items import *

import threading

import logging
logging.getLogger(__name__)
logging.basicConfig(filename='test.log', level=logging.DEBUG,
                    format='%(asctime)s:%(levelname)s:%(message)s')

def add(number):
    return number+number

def add_and_show(number):
    result = add(number)
    menu.subtitle = f"Your result: {result}"
    menu.screen.clear()
    menu.draw()

menu = ConsoleMenu('Title')
item = MenuItem('Item one')
item_two = MenuItem('Item Two')
func = FunctionItem('add numbers', add_and_show, [5])

menu.append_item(item)
menu.append_item(item_two)
menu.append_item(func)
logging.debug('starting up')
menu.start()

menu.join()

Sometimes this is not enough. When dealing with UI, it's not a good idea to keep operations and display in the same thread... Especially if the operation takes a long time to complete. Bear in mind that every time you want to pass a function (heavy in computation) for a UI item to call, it's best to do it in another thread. It will look something like this:

t1 = threading.Thread(target=calculate_PI)
item = FunctionItem('calculate PI', calculate_PI.start, [])
menu.append(item)

menu.start()
menu.join()
t1.join()

Fixing your code

I'll show you the way to do your "while loop" properly. In your original code, the interface is never displayed to the user because you need to join the menu thread. My solution is to start a different thread before joining the menu's one. The following code is just an example... if the user is typing an input while we refresh the page, things won't work well.

from consolemenu import *
from consolemenu.items import *

import logging, time, threading
logging.getLogger(__name__)
logging.basicConfig(filename='test.log', level=logging.DEBUG,
                    format='%(asctime)s:%(levelname)s:%(message)s')

def add(number):
    return number+number

def add_and_show(number):
    result = add(number)
    menu.subtitle = f"Your result: {result}"
    menu.screen.clear()
    menu.draw()

menu = ConsoleMenu('Title')
item = MenuItem('Item one')
item_two = MenuItem('Item Two')
func = FunctionItem('add numbers', add_and_show, [5])

menu.append_item(item)
menu.append_item(item_two)
menu.append_item(func)
logging.debug('starting up')

menu.start()

def listen_menu():
    counter = 1

    while(not menu.is_alive()):
        logging.debug(f'waiting {counter}...\n')
        time.sleep(1)
        counter += 1

    while(menu.is_alive()):
        logging.debug(f'running {counter}...\n')
        time.sleep(1)
        counter += 1

        if(counter > 10):
            menu.subtitle = f"Changed the subtitle! Counter={counter}"

            # This messes up with user input! Just an example
            menu.screen.clear()
            menu.draw()

    logging.debug(f'Finished at {counter}...\n')

t1 = threading.Thread(target=listen_menu)
t1.start()

menu.join()
t1.join()

I hoped it helped ;)

For those who want to remember (or learn) threads in python, here is the best teacher on the web explaining it very well: Python Threading Tutorial by Corey Schafer

txoof commented 3 years ago

@FelipeEmos,

Thank you for this explanation! This could change my world! I frequently need to churn out tiny scripts for our staff that do mundane things like split PDFs or create a folder structure on a network share. Our staff is not technically ready for anything command line based so I have to slap some sort of interface on top.

The code that does the job is typically 5-10 lines, then there's another 50 for error handling and dumb-catching and then another 400-800 to build a tkinter or py-gui interface depending on the complexity. This kills me.

Maintaining the GUI is the huge time suck between handling weird QT errors, setting up build environments and debugging. I often wish I could just dump that time into teaching staff to use the command line, but that won't fly with management.

Making simple text menus might just save my sanity. Thank you! I'll see if I can roll this learning into my next project.

FelipeEmos commented 3 years ago

Good to know, @txoof ! I'm currently developing a project for a client with basically the same limitations you've described. The good news is that everything is working perfectly with this "console-menu" package! I took a while to get the hang of it but now I'm totally comfortable. What helped A LOT was:

If you want me to be more specific about something, please ask, it would be a pleasure to help 🙏🏻

But really, I'm not kidding, it's really easy once you get the hang of it. I was inspired by your response and I've created a small project incorporating the things I've learned in the past few days. It's a fully functional menu along with some other functionalities I believe you were looking for when you started this thread. Check it out!

https://github.com/FelipeEmos/candy-factory-menu