Skip to content

Instantly share code, notes, and snippets.

@Reyuu
Created June 5, 2021 15:20
Show Gist options
  • Save Reyuu/5462155adf581829c65470f0ba7adfc4 to your computer and use it in GitHub Desktop.
Save Reyuu/5462155adf581829c65470f0ba7adfc4 to your computer and use it in GitHub Desktop.
Simple curses GUI in 100 lines.
import curses
class Entry:
def __init__(self, text, fn, x_offset=0, y_offset=0, left_fn=None, right_fn=None) -> None:
self.text = text
self.fn = fn
self.x_pos_offset = x_offset
self.y_pos_offset = y_offset
self.left_fn = left_fn
self.right_fn = right_fn
class Decoration:
def __init__(self, text="-", n=3, x=0, y=0) -> None:
self.text = text*n
self.x = x
self.y = y
pass
class Menu:
screen = curses.initscr()
curses.noecho()
screen.keypad(1)
def __init__(self, title=None) -> None:
self.entries = []
self.decorations = []
self.position = 0
self.running = False
self.title = title
self.pointer = ">"
def add_entry(self, text, fn=None, x_offset=0, y_offset=0) -> int:
self.entries += [Entry(text, fn, x_offset, y_offset)]
return len(self.entries) - 1
def add_decoration(self, text="-", n=3, x=0, y=0) -> None:
self.decorations += [Decoration(text, n, x, y)]
# Go to main loop here
def show(self) -> None:
self.__render_menu()
g = None
self.running = True
while self.running:
g = self.screen.getch()
if (g == curses.KEY_UP):
if (self.position - 1 < 0):
self.position = len(self.entries) - 1
else:
self.position -= 1
self.__render_menu()
if (g == curses.KEY_DOWN):
if (self.position + 1 > len(self.entries) - 1):
self.position = 0
else:
self.position += 1
self.__render_menu()
if (g == curses.KEY_LEFT):
self.__trigger_function(self.entries[self.position].left_fn)
if (g == curses.KEY_RIGHT):
self.__trigger_function(self.entries[self.position].right_fn)
# Enter is 10 here as well lol
if (g == 10):
self.__trigger_function(self.entries[self.position].fn)
pass
def terminate(self) -> None:
curses.echo(1)
curses.endwin()
## Internal methods
def __trigger_function(self, fn) -> None:
try:
fn(self.position)
except TypeError:
try:
fn()
except TypeError:
pass
self.__render_menu()
def __screen_cleanup(self) -> None:
self.screen.clear()
self.screen.refresh()
def __disable_curses(self) -> None:
curses.endwin()
def __render_decorations(self) -> None:
for deco in self.decorations:
self.screen.addstr(deco.y+(-1 if self.title is None else 0)+1, deco.x, deco.text)
def __render_menu(self) -> None:
self.screen.clear()
self.__render_decorations()
if len(self.entries) > 0:
c = 0
c_offset = 0
if not(self.title is None):
c_offset += 1
self.screen.addstr(0, 0, self.title)
for entry in self.entries:
if self.position == c:
self.screen.addstr(c+c_offset+entry.y_pos_offset, 0+entry.x_pos_offset, self.pointer)
self.screen.addstr(c+c_offset+entry.y_pos_offset, 2+entry.x_pos_offset, entry.text)
c += 1
self.screen.refresh()
### Testing goes here ###
if __name__ == "__main__":
# Initialization with optional title
m = Menu("Choose something")
# Switching buttons
def a(pos):
if m.entries[pos].text == "yay":
m.entries[pos].text = "no"
else:
m.entries[pos].text = "yay"
m.add_entry("yay", a)
m.add_entry("more")
m.add_entry("even more")
# Removing the title
def d(pos):
if m.title:
m.title = None
m.entries[pos].text = "add stupid title"
else:
m.title = "Choose something"
m.entries[pos].text = "remove stupid title"
tmp_y = m.add_entry("remove stupid title", d)
# Decorations
# Adding 1 and 2 since tmp_y is the y position of the last entry
m.add_decoration("* ", 10, 0, tmp_y+1)
m.add_decoration("* ", 10, 0, tmp_y+2)
# Offsetting the entries, x=2, y=2, since we finished on the +2 offset in the decorations.
# Can be anything though.
m.add_entry("this is slanted", None, 2, 2)
m.add_entry("more here", None, 2, 2)
# Show the menu, this goes to the main event loop, blocking the execution.
m.show()
# Once done, terminate curses and revert the terminal to default state.
m.terminate()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment