clinuxbashtui

How do I make a TUI dialog box?


Is there a way to replicate this: TUI dialog(screenshot not mine of course) on something Shell Script or something like C? This looks better than using dialog package.


Solution

  • Basically every programming language allows you to make TUI or CLI.

    Reproducing what you want to achieve

    Probably, the most dumb way to implement that is to use ANSI escape codes and UNICODE characters. You can draw a colored menu and move the cursor to override the output or to hide the useless stuff.
    Example using bare bash: link to GitHub Gist - password_input_tui.sh

    #!/bin/bash
    
    max_pass_length=40
    pass_length=30
    i=0
    password=
    prompt=$'     \u2502 Passphrase: '
    
    clear
    echo -e "\e[32;40m\e[H"
    
    echo -e "     \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510     "
    echo -e "     \u2502 Please enter the passphrase to                       \u2502     "
    echo -e "     \u2502 protect your new key                                 \u2502     "
    echo -e "     \u2502                                                      \u2502     "
    echo -e -n "     \u2502 Passphrase: "; while [ $i -lt $pass_length ]; do echo -e -n "_"; i=$(expr $i + 1); done; while [ $i -lt $max_pass_length ]; do echo -e -n " "; i=$(expr $i + 1); done; echo -e " \u2502     "
    echo -e "     \u2502                                                      \u2502     "
    echo -e "     \u2502       <OK>                              <Cancel>     \u2502     "
    echo -e "     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518     "
    
    
    echo -e "\e[5;H"
    
    # Read password
    
    i=0
    while IFS= read -p "$prompt" -r -s -n 1 char
    do
        if [[ $i -eq $(expr $pass_length - 1) ]]
        then
            password+=$'\0'
            break
        fi
        if [[ $char == $'\0' ]]
        then
            break
        fi
        if [[ $char == $'\177' || $char == $'\b' ]]
        then
            if [ $i -gt 0 ]
            then
                i=$(expr $i - 1)
                prompt=$'\b_\b'
                password="${password%?}"
            else
                prompt=''
            fi
        else
            i=$(expr $i + 1)
            prompt='*'
            password+="$char"
        fi
    done
    
    # Select <OK> or <Cancel>
    
    choice=1
    
    echo -e -n "\e[8;14H\e[30;42m<OK>\e[0m\e[10H"
    while read -r -s -n 1 ui; do
        case "$ui" in
        $'\x1b')    # Handle ESC sequence.
            # Flush read. We account for sequences for Fx keys as
            # well. 6 should suffice far more then enough.
            read -r -s -n 1 -t 0.1 char
            if [[ "$char" == "[" ]]; then
                read -r -s -n 1 -t 0.1 char
                case "$char" in
                #"A") printf "Up\n";;
                #"B") printf "Down\n";;
                "C") # right
                    echo -e -n "\e[8;14H\e[32;40m<OK>\e[0m" 
                    echo -e -n "\e[8;48H\e[30;42m<Cancel>\e[0m"
                    echo -e -n "\e[10H"
                    choice=0;;
                "D") # left
                    echo -e -n "\e[8;14H\e[30;42m<OK>\e[0m" 
                    echo -e -n "\e[8;48H\e[32;40m<Cancel>\e[0m"
                    echo -e -n "\e[10H"
                    choice=1;;
                esac
            fi
            # Flush "stdin" with 0.1  sec timeout.
            read -rsn5 -t 0.1
            ;;
        $'\0')
            break ;;     
        # Other one byte (char) cases. Here only quit.
        q)
            choice=1
            break;;
        esac
    done
    
    # Show result
    
    if [[ choice -eq 1 ]]
    then
        echo "Password: $password"
    else
        echo "Operation canceled"
    fi
    
    exit 0
    

    Output: bash.gif

    References:

    But that example has many problems and bits that could be improved (for example this code doesn't validate the password characters, and special characters like CTRL+stuff are considered as 2+ characters) and it's probably much harder than using tools made exactly for this task (e.g. dialog).

    Another example (C++) using curses: Link to GitHub Gist - password_input_tui.cpp

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <curses.h>
    
    #define MAX_PASSWD_LENGTH 32
    #define M1Y     7
    #define M1X     8
    #define M2Y     7
    #define M2X     33
    
    void menu(int choice);
    
    int yBottom, xBottom;
    int yTop, xTop;
    WINDOW* win;
    
    int main(int argc, char** argv)
    {
        initscr();
        
        noecho();
        curs_set(0);
        
        
        int xMax, yMax, i, choice, exit;
        char password[MAX_PASSWD_LENGTH];
        char ch;
        
        getmaxyx(stdscr, yMax, xMax);
        yBottom = yMax / 1.5, xBottom = xMax / 1.5;
        yTop = yMax / 6, xTop = xMax / 6;
        win = newwin(yBottom, xBottom, yTop, xTop);
        
        
        box(win, 0, 0);
        
        mvwprintw(win, 2, 2, "Please enter the passphrase to");
        mvwprintw(win, 3, 2, "protect your new key");
        mvwprintw(win, 5, 2, "Passphrase: ");
        
        i=0;
        while (i < MAX_PASSWD_LENGTH) {
            ch = wgetch(win);
            
            if (ch == ' ' || ch == 27 || ch == 127) {
                continue;
            }
            else if (ch == '\b' || ch == 8) {
                if (i > 0) {
                    mvwprintw(win, 5, 14 + i, "\b \b");
                    --i;
                }
                else {
                    continue;
                }
            }
            else if (ch == '\n' || ch == 10 || ch == '\r' || ch == '\t') {
                break;
            }
            else if (ch == 0 || ch == 224) {
                ch = wgetch(win);
                continue;
            }
            else {
                mvwprintw(win, 5, 14 + i, "*");
                password[i++] = ch;
            }
        }
        password[i] = '\0';
        
        wattron(win, A_STANDOUT);
        mvwprintw(win, M1Y, M1X, "<OK>");
        wattroff(win, A_STANDOUT);
        mvwprintw(win, M2Y, M2X, "<Cancel>");
        
        choice = 1;
        exit = 0;
        while(ch = wgetch(win))
        {
            switch(ch)
            {
                case '\033':
                {
                    wgetch(win);
                    switch(wgetch(win))
                    {
                        case 'D':
                        {
                            choice = 1;
                            break;
                        }
                        case 'C':
                        {
                            choice = 2;
                            break;
                        }
                    }
                    break;
                }
                case '\n':
                {
                    exit = 1;
                    break;
                }
                default:
                    break;
            }
            if(exit)
                break;
            menu(choice);
        }
        
        if(choice == 1)
            mvwprintw(win, 10, 10, "Password: %s", password);
        else
            mvwprintw(win, 10, 10, "Operation canceled");
        wgetch(win);
        
        endwin();
        
        return 0;
    }
    
    void menu(int choice)
    {
        switch(choice)
        {
            case 1:
                wattron(win, A_STANDOUT);
                mvwprintw(win, M1Y, M1X, "<OK>");
                wattroff(win, A_STANDOUT);
                mvwprintw(win, M2Y, M2X, "<Cancel>");
                break;
            case 2:
                mvwprintw(win, M1Y, M1X, "<OK>");
                wattron(win, A_STANDOUT);
                mvwprintw(win, M2Y, M2X, "<Cancel>");
                wattroff(win, A_STANDOUT);
                break;
            default:
                wattron(win, A_STANDOUT);
                mvwprintw(win, M1Y, M1X, "<OK>");
                wattroff(win, A_STANDOUT);
                mvwprintw(win, M2Y, M2X, "<Cancel>");
                break;
        }
    }
    

    Output: cpp.gif

    To make this work (as per dialog) obviously you have to make sure you have installed curses (Example for Debian: sudo apt-get install libncurses5-dev libncursesw5-dev, and use -lncurses flag when you compile it with g++).

    Some C Code for Windows

    I suggest you take a look at TurboVision, which is a TUI framework released for MS-DOS in 1990. There's a interesting modern port on GitHub, which is cross platform. Here's a bit of what you can do: preview.

    Otherwise you could include windows.h header and use SetConsoleTextAttribute() to color the output:

    #include <stdio.h>
    #include <windows.h>
    
    int main(int argc, char** argv)
    {
        HANDLE hConsole;
        int k;
    
        hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    
        // you can loop k higher to see more color choices
        for (k = 1; k < 255; k++)
        {
            // pick the colorattribute k you want
            SetConsoleTextAttribute(hConsole, k);
            printf("%3d  %s\n", k, "I want to be nice today!");
        }
    
        return 0;
    }
    

    Output: example

    Or simply handle the input stream with _getch() to mask the password with asterisks:

    #include <stdio.h>
    #define PASSWORD_LENGTH 32
    
    int main(int argc, char** argv) {
        int i = 0;
        char password[PASSWORD_LENGTH + 1];
        int ch;
        
        printf("Enter Password (max length: %d): ", PASSWORD_LENGTH);
    
        while (i < PASSWORD_LENGTH) {
            ch = _getch();
            if (ch == ' ' || ch == 27) {
                continue;
            }
            else if (ch == '\b') {
                if (i > 0) {
                    printf("\b \b");
                    --i;
                }
                else {
                    continue;
                }
            }
            else if (ch == '\r' || ch == '\t') {
                break;
            }
            else if (ch == 0 || ch == 224) {
                ch = _getch();
                continue;
            }
            else {
                password[i++] = ch;
                printf("*");
            }
        }
        password[i] = '\0';
    
        printf("\nPassword: %s", password);
    
        return 0;
    }
    

    Output: example