Skip to content

Instantly share code, notes, and snippets.

@claymcleod
Last active April 6, 2024 23:53
Show Gist options
  • Save claymcleod/b670285f334acd56ad1c to your computer and use it in GitHub Desktop.
Save claymcleod/b670285f334acd56ad1c to your computer and use it in GitHub Desktop.
Python curses example
import sys,os
import curses
def draw_menu(stdscr):
k = 0
cursor_x = 0
cursor_y = 0
# Clear and refresh the screen for a blank canvas
stdscr.clear()
stdscr.refresh()
# Start colors in curses
curses.start_color()
curses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLACK)
curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK)
curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE)
# Loop where k is the last character pressed
while (k != ord('q')):
# Initialization
stdscr.clear()
height, width = stdscr.getmaxyx()
if k == curses.KEY_DOWN:
cursor_y = cursor_y + 1
elif k == curses.KEY_UP:
cursor_y = cursor_y - 1
elif k == curses.KEY_RIGHT:
cursor_x = cursor_x + 1
elif k == curses.KEY_LEFT:
cursor_x = cursor_x - 1
cursor_x = max(0, cursor_x)
cursor_x = min(width-1, cursor_x)
cursor_y = max(0, cursor_y)
cursor_y = min(height-1, cursor_y)
# Declaration of strings
title = "Curses example"[:width-1]
subtitle = "Written by Clay McLeod"[:width-1]
keystr = "Last key pressed: {}".format(k)[:width-1]
statusbarstr = "Press 'q' to exit | STATUS BAR | Pos: {}, {}".format(cursor_x, cursor_y)
if k == 0:
keystr = "No key press detected..."[:width-1]
# Centering calculations
start_x_title = int((width // 2) - (len(title) // 2) - len(title) % 2)
start_x_subtitle = int((width // 2) - (len(subtitle) // 2) - len(subtitle) % 2)
start_x_keystr = int((width // 2) - (len(keystr) // 2) - len(keystr) % 2)
start_y = int((height // 2) - 2)
# Rendering some text
whstr = "Width: {}, Height: {}".format(width, height)
stdscr.addstr(0, 0, whstr, curses.color_pair(1))
# Render status bar
stdscr.attron(curses.color_pair(3))
stdscr.addstr(height-1, 0, statusbarstr)
stdscr.addstr(height-1, len(statusbarstr), " " * (width - len(statusbarstr) - 1))
stdscr.attroff(curses.color_pair(3))
# Turning on attributes for title
stdscr.attron(curses.color_pair(2))
stdscr.attron(curses.A_BOLD)
# Rendering title
stdscr.addstr(start_y, start_x_title, title)
# Turning off attributes for title
stdscr.attroff(curses.color_pair(2))
stdscr.attroff(curses.A_BOLD)
# Print rest of text
stdscr.addstr(start_y + 1, start_x_subtitle, subtitle)
stdscr.addstr(start_y + 3, (width // 2) - 2, '-' * 4)
stdscr.addstr(start_y + 5, start_x_keystr, keystr)
stdscr.move(cursor_y, cursor_x)
# Refresh the screen
stdscr.refresh()
# Wait for next input
k = stdscr.getch()
def main():
curses.wrapper(draw_menu)
if __name__ == "__main__":
main()
@acrandal
Copy link

This is an absolutely gorgeous, relatively minimal example for starting with ncurses in Python! Thank you for assembling it.

@agribot2
Copy link

agribot2 commented Nov 4, 2019

Worked first time, out of the box ! Thanks for providing this example.
I'm using an RPI, using the pre-installed Geany debugger. The output was correctly displayed in the Geany terminal window.

@eder78
Copy link

eder78 commented Nov 6, 2019

Thank you, very useful. :)

Copy link

ghost commented Dec 10, 2019

This is an excellent example! Very simple and easy for demonstrating Curses. Thanks!

@klimach
Copy link

klimach commented Dec 31, 2019

thx a lot 👍
Very helpful example

@SenorRodriguez
Copy link

Neat and clean. Exactly what i was looking for.

@batera1963
Copy link

A really helpful example. Many thanks !

@DuaneNielsen
Copy link

Loving this example. My thanks!

@iyxan23
Copy link

iyxan23 commented Jun 11, 2020

Nice example, Thank You!

@Gim6626
Copy link

Gim6626 commented Jun 12, 2020

Tried to do same using npyscreen - failed after couple of hours.
Googled "python curses tutorial" and second link is this.
Brilliant!
Thank you very much!

@ThatXliner
Copy link

Instead of doing ord(‘some char’), you can do k = chr(stdscr.getch()) ( sorry I’m on mobile)

@rel1c
Copy link

rel1c commented Oct 8, 2020

This was exactly what I was looking for! Thank you for the great example.

@JuDelCo
Copy link

JuDelCo commented Oct 21, 2020

Perfect, thanks !

@mouchh
Copy link

mouchh commented Nov 5, 2020

Thanks!

Got blocked with a stupid mistake, I named my file curses.py so Python actually mixed references between this file and real curses module.
AttributeError: module 'curses' has no attribute 'wrapper'
Just rename your file with something different! In case it helps anyone.

@RyuuzakiJulio
Copy link

Can courses receive Japanese text input?

@gitcabezon
Copy link

Very excited to find this! However it suffers the same problem my testing does on the Mac (probably others) -- resizing the terminal to <7 lines or <43 cols causes it to crash.

@brunoGenX
Copy link

Very nice example. thank you very much!

@diVineProportion
Copy link

diVineProportion commented Jan 25, 2021

Can courses receive Japanese text input?

maybe the rich module can help you out

@pjfarleyiii
Copy link

pjfarleyiii commented Feb 23, 2021

I also thank Clay for his very nice code example. I took it on myself to improve it a little by adding code to more cleanly clear out the keycode line(s) and to get all three curses "key read" functions to show the results for getch(), get_wch() and getkey() all for the same single keystroke entered.

Copy pasted below if anyone is interested.

Edit: After seeing unexpected failures of the getkey() code on Linux systems using the ncursesw library (though it never failed on Windows systems using the PDCurses library underneath) , I asked for some help on the bug-ncurses mailing list, and discovered that the unget_wch() function does not know what to do with a non-character (i.e., arrow keys, Home, End, F1, etc.). Instead, if get_wch() returns an integer representing one of those "non-printing" keys, you must use ungetch() to put that key back in order for a subsequent getkey() or get_wch() to be able to retrieve it again.

I have updated the code below to reflect that technique.

Peter

import curses

def draw_menu(stdscr):
    k = 0
    kw = 0
    kk = ""
    height, width = stdscr.getmaxyx()
    min_x_keystr = width - 1
    min_x_wkeystr = width - 1
    min_x_kkeystr = width - 1
    cursor_x = 0
    cursor_y = 0

    # Clear and refresh the screen for a blank canvas
    stdscr.clear()
    stdscr.refresh()

    # Start colors in curses
    curses.start_color()
    curses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLACK)
    curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK)
    curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE)

    # Loop where k is the last character pressed
    while (k != ord('q')):

        # Initialization
        #stdscr.clear()
        height, width = stdscr.getmaxyx()

        if k == curses.KEY_DOWN:
            cursor_y = cursor_y + 1
        elif k == curses.KEY_UP:
            cursor_y = cursor_y - 1
        elif k == curses.KEY_RIGHT:
            cursor_x = cursor_x + 1
        elif k == curses.KEY_LEFT:
            cursor_x = cursor_x - 1

        cursor_x = max(0, cursor_x)
        cursor_x = min(width-1, cursor_x)

        cursor_y = max(0, cursor_y)
        cursor_y = min(height-1, cursor_y)

        # Declaration of strings
        title = "Curses example"[:width-1]
        subtitle = "Written by Clay McLeod"[:width-1]
        statusbarstr = "Press 'q' to exit | STATUS BAR | Pos: {}, {}".format(cursor_x, cursor_y)
        if k == 0:
            keystr = "No key press detected..."[:width-1]
        else:
            keystr = "Last key pressed: {}".format(k)[:width-1]
        if kw == 0:
            wkeystr = "No wide key press detected..."[:width-1]
        else:
            wkeystr = "Last wide key pressed: {}".format(kw)[:width-1]
        if kk == 0:
            kkeystr = "No 'key' press detected..."[:width-1]
        else:
            kkeystr = "Last 'key' pressed: {}".format(kk)[:width-1]

        # Centering calculations
        start_x_title = int((width // 2) - (len(title) // 2) - len(title) % 2)
        start_x_subtitle = int((width // 2) - (len(subtitle) // 2) - len(subtitle) % 2)
        start_x_keystr = int((width // 2) - (len(keystr) // 2) - len(keystr) % 2)
        start_x_wkeystr = int((width // 2) - (len(wkeystr) // 2) - len(wkeystr) % 2)
        start_x_kkeystr = int((width // 2) - (len(kkeystr) // 2) - len(kkeystr) % 2)
        start_y = int((height // 2) - 2)
        min_x_keystr = min(min_x_keystr, start_x_keystr)
        min_x_wkeystr = min(min_x_wkeystr, start_x_wkeystr)
        min_x_kkeystr = min(min_x_kkeystr, start_x_kkeystr)

        # Rendering some text
        whstr = "Width: {}, Height: {}".format(width, height)
        stdscr.addstr(0, 0, whstr, curses.color_pair(1))

        # Render status bar
        stdscr.attron(curses.color_pair(3))
        stdscr.addstr(height-1, 0, statusbarstr)
        stdscr.addstr(height-1, len(statusbarstr), " " * (width - len(statusbarstr) - 1))
        stdscr.attroff(curses.color_pair(3))

        # Turning on attributes for title
        stdscr.attron(curses.color_pair(2))
        stdscr.attron(curses.A_BOLD)

        # Rendering title
        stdscr.addstr(start_y, start_x_title, title)

        # Turning off attributes for title
        stdscr.attroff(curses.color_pair(2))
        stdscr.attroff(curses.A_BOLD)

        # Print rest of text
        stdscr.addstr(start_y + 1, start_x_subtitle, subtitle)
        stdscr.addstr(start_y + 3, (width // 2) - 2, '-' * 4)
        stdscr.addstr(start_y + 5, start_x_keystr, keystr)
        stdscr.addstr(start_y + 7, start_x_wkeystr, wkeystr)
        stdscr.addstr(start_y + 9, start_x_kkeystr, kkeystr)
        stdscr.move(cursor_y, cursor_x)

        # Refresh the screen
        stdscr.refresh()

        # Wait for next input
        k = stdscr.getch()

        # Get wide-char version
        curses.ungetch(k)
        kw = stdscr.get_wch()

        # Get key version
        if isinstance(kw, str):
            curses.unget_wch(kw)
        else:
            curses.ungetch(kw)
        kk = stdscr.getkey()

        # Clear the keystroke text
        stdscr.move(start_y + 5, min_x_keystr)
        stdscr.clrtoeol()
        stdscr.move(start_y + 7, min_x_wkeystr)
        stdscr.clrtoeol()
        stdscr.move(start_y + 9, min_x_kkeystr)
        stdscr.clrtoeol()
        stdscr.refresh()

def main():
    curses.wrapper(draw_menu)

if __name__ == "__main__":
    main()

@MegDuck
Copy link

MegDuck commented Apr 20, 2021

To get less flickering, use stdscr.erase() instead of stdscr.clear(), see this S.O answer.

Thank you!

@HyoMiYing
Copy link

Thank you for the code. When ran in the terminal, it does good to represent some of the major functionalities of the curses library 👍

@cole-wilson
Copy link

Thanks!

@arnu515
Copy link

arnu515 commented Jul 11, 2021

Thank you!

@BansheePrime
Copy link

Thank you for great example.

@Shasless
Copy link

thanks

@lucarhee
Copy link

thank you.

@fezela
Copy link

fezela commented Oct 15, 2022

why do you have the import for sys, and os if you don't use them in your code?

@Gus-The-Forklift-Driver

I added stdscr.keypad(True) to make the cursor move.

@mouseroot
Copy link

great little snippet, I just tested it with the latest termux build from github on android and it works great.

@boardkeystown
Copy link

very nice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment