Creating a Terminal Menu Controlled by Arrow Keys in Python (Utilizing Escape Sequences)

2022-07-24 17:46 (2 years ago) ytyng

Information on creating a terminal menu that can be operated with arrow keys without using curses.

Since there were many unclear points, I wrote this while referencing other websites. Thanks.

It was a good study on escape sequences.

CSI

\033[ is a frequently appearing escape sequence, known as the CSI (Control Sequence Introducer), used for a wide range of purposes such as cursor movement and color changes.

\033[ is the octal notation. In hexadecimal, it is \x1b[, and both represent the same character. (Incidentally, in decimal, it is 27, so chr(27) + [ is also the same)

It can also be expressed as ESC + [.

Details about CSI can be found on the English version of Wikipedia. There doesn't seem to be a Japanese version.

https://en.wikipedia.org/wiki/ANSI_escape_code

Drawing

It is necessary to update multiple drawn lines.

After drawing multiple lines, print \033[nA (n is a number representing the number of lines) to move the cursor up and print again.

Reference Sites

How to Output and Overwrite Multiple Lines in Python

Key Operations

It is necessary to accept key inputs. input() is not suitable as it requires pressing the Enter key.

You need to use the readchar library or read using tty.

Note that readchar uses tty internally.

However, it did not work in the terminal within PyCharm. It worked without issues in iTerm.

Reference Sites

Python Console Single Character Input - Emotion Explorer

Detecting Key Inputs in Python (tty) - Qiita

Color

Changing the text color makes it easier to see. Use escape sequences to change the color.

Reference Sites

[python] Try Adding Color to Print Statements – Nomura Mathematics Institute

Code

#!/usr/bin/env python3

import readchar

menu = [
'Arcturus',
'Betelgeuse',
'Capella',
'Deneb',
'Eltanin',
'Fomalhaut',
'Gacrux',
]


class Color:
BLACK = '\033[30m' # (Text) Black
RED = '\033[31m' # (Text) Red
GREEN = '\033[32m' # (Text) Green
YELLOW = '\033[33m' # (Text) Yellow
BLUE = '\033[34m' # (Text) Blue
MAGENTA = '\033[35m' # (Text) Magenta
CYAN = '\033[36m' # (Text) Cyan
WHITE = '\033[37m' # (Text) White
COLOR_DEFAULT = '\033[39m' # Reset text color to default
BOLD = '\033[1m' # Bold text
UNDERLINE = '\033[4m' # Underline text
INVISIBLE = '\033[08m' # Invisible text
REVERCE = '\033[07m' # Reverse text and background color
BG_BLACK = '\033[40m' # (Background) Black
BG_RED = '\033[41m' # (Background) Red
BG_GREEN = '\033[42m' # (Background) Green
BG_YELLOW = '\033[43m' # (Background) Yellow
BG_BLUE = '\033[44m' # (Background) Blue
BG_MAGENTA = '\033[45m' # (Background) Magenta
BG_CYAN = '\033[46m' # (Background) Cyan
BG_WHITE = '\033[47m' # (Background) White
BG_DEFAULT = '\033[49m' # Reset background color to default
RESET = '\033[0m' # Reset all


def tint_color(text):
return f'{Color.BG_CYAN}{Color.BLACK}{text}{Color.RESET}'


def help_color(text):
return f'{Color.GREEN}{text}{Color.RESET}'


def strong_color(text):
return f'{Color.YELLOW}{text}{Color.RESET}'


def main():
current = 0
c = ''
while True:
for i, item in enumerate(menu):
if current == i:
print(tint_color(f'> {item}'))
else:
print(f' {item}')
print(help_color(f'c={repr(c)}, current={current}'))
c = readchar.readkey()
if c == 'q':
# Exit with q
break
elif c == '\r':
# Confirm with Enter
print(f'selected:{strong_color(menu[current])}')
break
if c == 'j' or c == '\x1b[B':
# Move cursor with j or ↓
if current < len(menu) - 1:
current += 1
if c == 'k' or c == '\x1b[A':
# Move cursor with k or ↑
if current > 0:
current -= 1
print(f'\033[{len(menu) + 1}A', end='')


if __name__ == '__main__':
main()

Current rating: 1

Comments

Archive

2024
2023
2022
2021
2020
2019
2018
2017
2016
2015
2014
2013
2012
2011