Writing useful terminal TUI on Linux with dialog and jq

Photo by Riku Lu on Unsplash

Why a Text User Interface?

Many use the terminal on a daily basis. A Text User Interface (TUI) is a tool that will minimize user errors and allow you to become more productive with the terminal interface.

Let me give you an example: I connect on a daily basis from my home computer into my Physical PC,ย using Linux. All remote networking is protected using a private VPN.ย After a while it was irritating to be repeating the same commands over and over when connecting.

Having a bash function like this was a nice improvement:

export REMOTE_RDP_USER="myremoteuser"
function remote_machine() {
/usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:$REMOTE_RDP_USER /v:$1 /p:$2
}

But then I was constantly doing this (on a single line):

remote_pass=(/bin/cat/.mypassfile) remote_machine $remote_machine $remote_pass

That was annoying. Not to mention that I had my password in clear text on my machine (I have an encrypted drive but still…)

So I decided to spend a little time and came up with a nice script to handle my basic needs.

What information do I need to connect to my remote desktop?

Not much information is needed. It just needs to be structured so a simple JSON file will do:

{"machines": [
{
"name": "machine1.domain.com",
"description": "Personal-PC"
},
{
"name": "machine2.domain.com",
"description": "Virtual-Machine"
}
],
"remote_user": "MYUSER@DOMAIN",
"title" : "MY COMPANY RDP connection"
}

JSON is not the best format for configuration files (as it doesn’t support comments, for example) but it has plenty of tools available on Linux to parse its contents from the command line. A very useful tool that stands out is jq. Let me show you how I can extract the list of machines:

/usr/bin/jq --compact-output --raw-output '.machines[]| .name' \
($HOME/.config/scripts/kodegeek_rdp.json) \
"machine1.domain.com" "machine2.domain.com"

Documentation for jq is available here. You can try your expressions at jq play just by copying and pasting your JSON files there and then use the expression in your scripts.

So now that I have all the ingredients I need to connect to my remote computer, let’s build a nice TUI for it.

Dialog to the rescue

Dialog is one of those underrated Linux tools that you wish you knew about a long time ago. You can build a very nice and simple UI that will work perfectly on your terminal.

For example, to create a simple checkbox list with my favorite languages, selecting Python by default:

dialog --clear --checklist "Favorite programming languages:" 10 30 7 1 \ 
Python on 2 Java off 3 Bash off 4 Perl off 5 Ruby off

We told dialog a few things:

  • Clear the screen (all the options start with –)
  • Create a checklist with title (first positional argument)
  • A list of dimensions (height width list height, 3 items)
  • Then each element of the list is a pair of a Label and a value.

Surprisingly is very concise and intuitive to get a nice selection list with a single line of code.

Full documentation for dialog is available here.

Putting everything together: Writing a TUI with Dialog and JQ

I wrote a TUI that uses jq to extract my configuration details from my JSON file and organized the flow with dialog. I ask for the password every single time and save it in a temporary file which gets removed after the script is done using it.

The script is pretty basic but is more secure and also lets me focus on more serious tasks ๐Ÿ™‚

So what does the script looks like? Let me show you the code:

#!/bin/bash
# Author Jose Vicente Nunez
# Do not use this script on a public computer. It is not secure...
# https://invisible-island.net/dialog/
# Below some constants to make it easier to handle Dialog 
# return codes
: ${DIALOG_OK=0}
: ${DIALOG_CANCEL=1}
: ${DIALOG_HELP=2}
: ${DIALOG_EXTRA=3}
: ${DIALOG_ITEM_HELP=4}
: ${DIALOG_ESC=255}
# Temporary file to store sensitive data. Use a 'trap' to remove 
# at the end of the script or if it gets interrupted
declare tmp_file=$(/usr/bin/mktemp 2>/dev/null) || declare tmp_file=/tmp/test$$
trap "/bin/rm -f $tmp_file" QUIT EXIT INT
/bin/chmod go-wrx ${tmp_file} > /dev/null 2>&1
:<<DOC
Extract details like title, remote user and machines using jq from the JSON file
Use a subshell for the machine list
DOC
declare TITLE=$(/usr/bin/jq --compact-output --raw-output '.title' $HOME/.config/scripts/kodegeek_rdp.json)|| exit 100
declare REMOTE_USER=$(/usr/bin/jq --compact-output --raw-output '.remote_user' $HOME/.config/scripts/kodegeek_rdp.json)|| exit 100
declare MACHINES=$(
    declare tmp_file2=$(/usr/bin/mktemp 2>/dev/null) || declare tmp_file2=/tmp/test$$
    # trap "/bin/rm -f $tmp_file2" 0 1 2 5 15 EXIT INT
    declare -a MACHINE_INFO=$(/usr/bin/jq --compact-output --raw-output '.machines[]| join(",")' $HOME/.config/scripts/kodegeek_rdp.json > $tmp_file2)
    declare -i i=0
    while read line; do
        declare machine=$(echo $line| /usr/bin/cut -d',' -f1)
        declare desc=$(echo $line| /usr/bin/cut -d',' -f2)
        declare toggle=off
        if [ $i -eq 0 ]; then
            toggle=on
            ((i=i+1))
        fi
        echo $machine $desc $toggle
    done < $tmp_file2
    /bin/cp /dev/null $tmp_file2
) || exit 100
# Create a dialog with a radio list and let the user select the
# remote machine
/usr/bin/dialog \
    --clear \
    --title "$TITLE" \
    --radiolist "Which machine do you want to use?" 20 61 2 \
    $MACHINES 2> ${tmp_file}
return_value=$?
# Handle the return codes from the machine selection in the 
# previous step
export remote_machine=""
case $return_value in
  $DIALOG_OK)
    export remote_machine=$(/bin/cat ${tmp_file})
    ;;
  $DIALOG_CANCEL)
    echo "Cancel pressed.";;
  $DIALOG_HELP)
    echo "Help pressed.";;
  $DIALOG_EXTRA)
    echo "Extra button pressed.";;
  $DIALOG_ITEM_HELP)
    echo "Item-help button pressed.";;
  $DIALOG_ESC)
    if test -s $tmp_file ; then
      /bin/rm -f $tmp_file
    else
      echo "ESC pressed."
    fi
    ;;
esac

# No machine selected? No service ...
if [ -z "${remote_machine}" ]; then
  /usr/bin/dialog \
  	--clear  \
	--title "Error, no machine selected?" --clear "$@" \
       	--msgbox "No machine was selected!. Will exit now..." 15 30
  exit 100
fi

# Send 4 packets to the remote machine. I assume your network 
# administration allows ICMP packets
# If there is an error show  message box
/bin/ping -c 4 ${remote_machine} >/dev/null 2>&1
if [ $? -ne 0 ]; then
  /usr/bin/dialog \
  	--clear  \
	--title "VPN issues or machine is off?" --clear "$@" \
       	--msgbox "Could not ping ${remote_machine}. Time to troubleshoot..." 15 50
  exit 100
fi

# Remote machine is visible, ask for credentials and handle user 
# choices (like password with a password box)
/bin/rm -f ${tmp_file}
/usr/bin/dialog \
  --title "$TITLE" \
  --clear  \
  --insecure \
  --passwordbox "Please enter your Windows password for ${remote_machine}\n" 16 51 2> $tmp_file
return_value=$?
case $return_value in
  $DIALOG_OK)
    # We have all the information, try to connect using RDP protocol
    /usr/bin/mkdir -p -v $HOME/logs
    /usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:$REMOTE_USER /v:${remote_machine} /p:$(/bin/cat ${tmp_file})| \
    /usr/bin/tee $HOME/logs/$(/usr/bin/basename $0)-$remote_machine.log
    ;;
  $DIALOG_CANCEL)
    echo "Cancel pressed.";;
  $DIALOG_HELP)
    echo "Help pressed.";;
  $DIALOG_EXTRA)
    echo "Extra button pressed.";;
  $DIALOG_ITEM_HELP)
    echo "Item-help button pressed.";;
  $DIALOG_ESC)
    if test -s $tmp_file ; then
      /bin/rm -f $tmp_file
    else
      echo "ESC pressed."
    fi
    ;;
esac

You can see from the code that dialog expects positional arguments and also allows you to capture user responses in variables. This effectively makes it an extension to Bash to write text user interfaces.

My small example above only covers the usage of some of the Widgets, there is plenty more documentation on the official dialog site.

Are dialog and JQ the best options?

You can skin this rabbit in many ways (Textual, Gnome Zenity, Python TKinker, etc.) I just wanted to show you one nice way to accomplish this in a short time, with only 100 lines of code.

It is not perfect. Specficially, integration with Bash makes the code very verbose, but it is still easy to debug and maintain. This is also much better than copying and pasting the same long command over and over.

One last thing: If you liked jq for JSON processing from Bash, then you will appreciate this nice collection of jq recipes.

Fedora Project community For Developers For System Administrators Using Software

15 Comments

  1. Greater Productivity

    NICE!

  2. Thomas

    I have similar functions in my .bashrc to handle connections to different aws accounts, connect to different machines using a different tool called

    choose

    (https://github.com/tmonjalo/choose) that is itself made of just 377 lines of bash.

  3. james

    It looks great, and I will definitely investigate Dialog. I am using gtkdialog for other purposes currently, and the version I am using, whilst great, is a bit dated.
    But where you say:
    ‘But then I was constantly doing this (on a single line):’
    Could you not use an alias? Or two, one for each machine? And use openssl or similar to encrypt your password?
    Just a thought. If I were going to adopt a bash script like the one above, I might write it in a process agnostic manner, so that it could handle any json file, with perhaps a schema file processed to address the json needing to be displayed in dialog. Save reinventing the wheel, so to speak…

    • Jose Nunez

      Hi, glad that you liked the small article. I don’t want to make it agnostic, only to work with RDP . An alias for every machine sounds great if you have 2 machines, when you have 10, well not so great. Also the TUI allows me to make changes on the password if I make a typo.

  4. John

    For temp credentials in a txt file, I “>” to it while the script needs it. I end my routine using “wipe” on said txt file to ensure total clean up. Even if it takes a bit for the script to exit which in the case of a simple text file it only takes seconds.

    • Jose Nunez

      That works too, great idea. I mention on the article than my disk has encryption so it felt over-killing to add this there.

  5. xenlo

    Typo detected!

    %s/I connect on a daily basics/I connect on a daily basis/g

    ๐Ÿ˜‹

  6. The idea of a TUI isn’t bad in itself, but in the year 2023 you should probably consider not allowing services using a password on the Internet. Even if you are capable of generating good passwords, average humans aren’t.

    Try doing something like this instead:
    https://swjm.blog/the-complete-guide-to-rdp-with-yubikeys-fido2-cba-1bfc50f39b43

  7. Fedora Magazine isn’t the venue to report code bugs, but I think this one’s worth flagging: the statement

    declare foo=$(false) || echo failed

    does not report “failed” as one might expect. That’s because `declare’ only fails if it’s given an invalid option or if the variable assignment fails – not if the statement fails. The correct way to write this is:

    declare foo=”

    foo=$(false) || echo failed

    It’s also generally a good idea to wrap code in functions and then call the functions at the end of the script. Happy hacking ๐Ÿ™‚

    • Jose Nunez

      Hello Andrew,

      You are correct, declaration of variables and testing should be separate to avoid the issue you mention above. Or something like:

      if ! foo=$(false); then
      echo false
      fi

      Or even:

      set -x # Stop everything after the first error.

  8. tertol

    I need to try this dialog. This will add an extra help my bash. Thanks for the info

  9. wololo

    jq is one of the finest pieces of software I know.
    Not a fan of dialog, just a shell script and it shows.

Comments are Closed

The opinions expressed on this website are those of each author, not of the author's employer or of Red Hat. Fedora Magazine aspires to publish all content under a Creative Commons license but may not be able to do so in all cases. You are responsible for ensuring that you have the necessary permission to reuse any work on this site. The Fedora logo is a trademark of Red Hat, Inc. Terms and Conditions