[PYTHON] Create a GUI on the terminal using curses

What is curses

According to the Official Documentation,

The curses library provides terminal-independent screen drawing and keyboard processing for text-based terminals (terminals) such as VT100s, Linux consoles, and emulation terminals provided by various programs. The terminal supports various control codes for performing common operations such as moving the cursor, scrolling the screen, and erasing the area. Different types of terminals may use very different control codes and often have a peculiar habit.

It may be useful when you want to create a simple GUI or when you want to work with server data that can only be accessed with ssh. You can also display the contents of CSV and DB.

Thing you want to do

For example, suppose you have the following csv file.

ID Prefecture Capital Population Area Density
1 Aichi Nagoya 70,43,235 5,153.81 1,366
2 Akita Akita 11,89,215 11,612.11 102
3 Aomori Aomori 14,75,635 9,606.26 154
... ... ... ... ... ...
45 Yamagata Yamagata 12,44,040 9,323.34 133
46 Yamaguchi Yamaguchi 15,28,107 6,110.76 250
47 Yamanashi Kofu 8,88,170 4,465.37 199

I would like to create a GUI that can display the contents on the terminal and select the prefecture to process.

Implementation

main.py


# -*- coding: utf-8 -*-
import curses
import csv
from math import ceil

ROWS_PER_PAGE = 20

ENTER = ord( "\n" )
ESC = 27
DOWN = curses.KEY_DOWN
UP = curses.KEY_UP

class UI():

    def __init__(self, header, rows):
        super().__init__()
        self.header = header 
        self.rows = rows
        #Initialization
        self.screen = curses.initscr()
        curses.noecho()
        curses.cbreak()
        curses.start_color()
        self.screen.keypad(1)
        #Color settings
        curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN)
        self.highlight_text = curses.color_pair(1) #Use the pair id in the line above
        self.normal_text = curses.A_NORMAL
        self.screen.border(0)
        curses.curs_set(0)
        self.rows_per_page = ROWS_PER_PAGE
        self.total_rows = len(self.rows)
        #Record the width of each column
        self.widths = []
        #Border drawing
        self.tavnit = '|'
        self.separator = '+'

        for index, title in enumerate(self.header):
            #Make the column title and the longest value of each row the width of the column
            max_col_length = max([len(row[index]) for row in self.rows])
            max_col_length = max(max_col_length, len(title))
            self.widths.append(max_col_length)

        #Border settings
        for w in self.widths:
            #It looks like this:
            # | %-2s | %-10s | %-10s | %-11s | %-9s | %-7s |
            self.tavnit += " %-"+"%ss |" % (w,)
            #It looks like this:
            # +----+------------+------------+-------------+-----------+---------+
            self.separator += '-'*w + '--+'

        self.total_pages = int(ceil(self.total_rows / self.rows_per_page))
        self.position = 1
        self.page = 1
        #Message to be displayed
        self.msg = 'Page: {}/{}'.format(self.page, self.total_pages)

    def end(self):
        curses.endwin()

    def draw(self):
        self.screen.erase()
        #Show message at the top
        self.screen.addstr(1, 2, self.msg, self.normal_text)
        #Border on the table
        self.screen.addstr(2, 2, self.separator, self.normal_text)
        #Show header
        self.screen.addstr(3, 2, self.tavnit % tuple(self.header), self.normal_text)
        #Border between header and content
        self.screen.addstr(4, 2, self.separator, self.normal_text)
        #Draw every line
        row_start = 1 + (self.rows_per_page * (self.page - 1))
        row_end = self.rows_per_page + 1 + (self.rows_per_page * (self.page - 1))
        for i in range(row_start, row_end):
            if i >= self.total_rows + 1:
                break
            row_number = i + (self.rows_per_page * (self.page - 1))
            #Highlight line
            if (row_number == self.position + (self.rows_per_page * (self.page - 1))):
                color = self.highlight_text
            else:
                color = self.normal_text
            #Since there are 4 lines such as messages and borders above+4
            draw_number = i - (self.rows_per_page * (self.page - 1)) + 4 #Since there are 4 lines such as messages and borders above
            self.screen.addstr(draw_number , 2, self.tavnit % tuple(self.rows[i - 1]), color)
        #Bottom border of the table,Since there are 4 lines such as messages and borders above+4
        bottom = min(row_end, self.total_rows + 1) - (self.rows_per_page * (self.page - 1)) + 4
        self.screen.addstr(bottom, 2, self.separator, self.normal_text)
        self.screen.refresh()

    def down(self):
        if self.page == self.total_pages:
            if self.position < self.total_rows:
                self.position += 1
        else:
            if self.position < self.rows_per_page + (self.rows_per_page * (self.page - 1)):
                self.position += 1
            else:
                self.page += 1
                self.position = 1 + (self.rows_per_page * (self.page - 1))
                self.msg = 'Page: {}/{}'.format(self.page, self.total_pages)
        self.draw()

    def up(self):
        if self.page == 1:
            if self.position > 1:
                self.position -= 1
        else:
            if self.position > (1 + (self.rows_per_page * (self.page - 1))):
                self.position -= 1
            else:
                self.page -= 1
                self.position = self.rows_per_page + (self.rows_per_page * (self.page - 1))
                self.msg = 'Page: {}/{}'.format(self.page, self.total_pages)
        self.draw()

    def esc(self):
        self.end()

    def enter(self):
        #What you want to do here
        prefecture_id = self.rows[self.position - 1][0]
        prefecture = self.rows[self.position - 1][1]
        self.msg = 'Page: {}/{} ({} {} was selected.)' \
                .format(self.page, self.total_pages, prefecture_id, prefecture)
        self.draw()

    def loop(self):
        #Detects the entered key
        key = self.screen.getch()
        while 1:
            if key == ENTER:
                self.enter()
            elif key == ESC:
                self.esc()
                break
            elif key == DOWN:
                self.down()
            elif key == UP:
                self.up()
            key = self.screen.getch()

if __name__ == '__main__':
    with open('prefectures.csv') as f:
        reader = csv.reader(f)
        data = list(reader)
    header = data[0]
    rows = data[1:]
    
    ui = UI(header, rows)
    ui.draw()
    ui.loop()

Run

$ python main.py

result

The maximum number of lines on the page is 20. You can scroll with and . ʻEnter (return) will display the information of the corresponding prefecture in the message. Exit the GUI with ʻESC.

result

Recommended Posts

Create a GUI on the terminal using curses
Create a python GUI using tkinter
Create a graph using the Sympy module
[Python] A progress bar on the terminal
How to write a GUI using the maya command
[CRUD] [Django] Create a CRUD site using the Python framework Django ~ 1 ~
Create a QR code for the URL on Linux
[CRUD] [Django] Create a CRUD site using the Python framework Django ~ 2 ~
[CRUD] [Django] Create a CRUD site using the Python framework Django ~ 3 ~
[CRUD] [Django] Create a CRUD site using the Python framework Django ~ 4 ~
[CRUD] [Django] Create a CRUD site using the Python framework Django ~ 5 ~
Create a shape on the trajectory of an object
Create a dictionary by searching the table using sqlalchemy
Create a classroom on Jupyterhub
Create a new csv with pandas based on the local csv
Create a real-time auto-reply bot using the Twitter Streaming API
I made a VGG16 model using TensorFlow (on the way)
Create a Python environment on Mac (2017/4)
Create a nested dictionary using defaultdict
Create a SlackBot service on Pepper
Create a Linux environment on Windows 10
Create a python environment on centos
Using a serial console on Ubuntu 20.04
Notes on using matplotlib on the server
Create a C wrapper using Boost.Python
Control the motor with a motor driver using python on Raspberry Pi 3!
Create a pandas Dataflame by searching the DB table using sqlalchemy
[Ev3dev] Create a program that captures the LCD (screen) using python
Create a shortcut to run a Python file in VScode on your terminal
Create a python environment on your Mac
Create a GUI app with Python's Tkinter
Notes on using OpenCL on Linux on the RX6800
Create an application using the Spotify API
Estimate the probability that a coin will appear on the table using MCMC
Create a record with attachments in KINTONE using the Python requests module
Create a Linux virtual machine on Windows
Create a dataframe from excel using pandas
Add a layer using the Keras backend
[2015/11/19] How to register a service locally using the python SDK on naoqi os
[pyqtgraph] Understand SignalProxy and create a crosshair that follows the cursor on the graph
How to quickly create a morphological analysis environment using Elasticsearch on macOS Sierra
Plot the environmental concentration of organofluorine compounds on a map using open data
Create a REST API using the model learned in Lobe and TensorFlow Serving.
Using the 1-Wire Digital Temperature Sensor DS18B20 from Python on a Raspberry Pi
[AWS Lambda] Create a deployment package using the Docker image of Amazon Linux
A addictive story when using tensorflow on Android
Create a GIF file using Pillow in Python
Create a GUI executable file created with tkinter
Try using a QR code on a Raspberry Pi
A note on customizing the dict list class
Let's create a REST API using SpringBoot + MongoDB
Python: Try using the UI on Pythonista 3 on iPad
You can easily create a GUI with Python
Create a phylogenetic tree from Biopyton using ClustalW2
Create a web map using Python and GDAL
Syntax highlighting on the command line using Pygments
[Venv] Create a python virtual environment on Ubuntu
Try to create a new command on linux
Create interactive 3D graphs on JupyterLab using matplotlib
Sound the buzzer using python on Raspberry Pi 3!
Create a visitor notification system using Raspberry Pi