Skip to content

Instantly share code, notes, and snippets.

@nolageek
Last active June 4, 2023 22:10
Show Gist options
  • Save nolageek/870d22914fe3ccb6d0bd3a400dde5e0d to your computer and use it in GitHub Desktop.
Save nolageek/870d22914fe3ccb6d0bd3a400dde5e0d to your computer and use it in GitHub Desktop.
mystic scrolling list class
from mystic_bbs import *
x,y = termsize()
# ScrollingList is a class that provides functionality for displaying a list of items in a scrollable format.
# It supports various features like scrolling up and down, wrapping around the list, and selecting items.
# It also supports customization of the display format, including prefixes and suffixes for items,
# and separators for item attributes.
class ScrollingList:
# ListItem is a nested class that represents an item in the list.
# It supports arbitrary attributes through keyword arguments,
# and provides a method for displaying the item.
class ListItem:
def __init__(self, **kwargs):
self.attributes = kwargs
# Get an attribute with a default value if the attribute doesn't exist.
def get_attribute(self, attr_name, default=None):
return self.attributes.get(attr_name, default)
# Display the item with a given format.
def display(self, index, prefix, display_attributes, show_attribute_titles, attribute_separator,
item_suffix, selected_item_suffix, selected_item_prefix, index_separator,justify):
item_str = "{}{}{}".format(prefix, str(index+1).rjust(justify,'0'), index_separator) if 'index' in display_attributes else prefix
if not display_attributes:
display_attributes = ['index'] + list(self.attributes.keys())
attributes = []
for attr_name in display_attributes:
if attr_name in self.attributes:
if show_attribute_titles:
attributes.append("{}: {}".format(attr_name, self.attributes[attr_name]))
else:
attributes.append("{}".format(self.attributes[attr_name]))
item_str += attribute_separator.join(attributes)
item_str += selected_item_suffix if selected_item_prefix in prefix.strip() else item_suffix
writeln(item_str)
# Initialize the ScrollingList with a list of items and various default display options.
def __init__(self,items):
self.current_selection_global = 1
self.current_selection = 1
self.items = items
self.items_per_page = y/2 if len(self.items) > y/2 else len(self.items) / 2
self.set_display_attributes('index', *list(self.items[0].attributes.keys()))
self.item_prefix = ' |07'
self.selected_item_prefix = '|23|00>'
self.attribute_separator = ', '
self.item_suffix = '|$X79.|16|07'
self.selected_item_suffix = '|$X79 |16|07'
self.display_menu = True
self.show_attribute_titles = False
self.index_separator = ') '
self.scrolling_wrap = False
self.item_x_prefix = None
self.item_y_prefix = None
# Set the attributes to be displayed for each item.
def set_display_attributes(self, *attributes):
self.display_attributes = list(attributes)
# Scroll up in the list.
def scroll_up(self):
if self.current_selection_global > 1:
self.current_selection_global -= 1
if self.current_selection > 1:
self.current_selection -= 1
elif self.scrolling_wrap:
self.current_selection_global = len(self.items)
self.current_selection = min(self.items_per_page, len(self.items))
# Scroll down in the list.
def scroll_down(self):
if self.current_selection_global < len(self.items):
self.current_selection_global += 1
if self.current_selection < self.items_per_page:
self.current_selection += 1
elif self.scrolling_wrap:
self.current_selection_global = 1
self.current_selection = 1
# Display the current page of items in the list.
def display(self):
start_index = self.current_selection_global - self.current_selection
end_index = min(len(self.items), start_index + self.items_per_page)
displayed_items = self.items[start_index:end_index]
if self.display_menu:
for i, item in zip(range(start_index, end_index), displayed_items):
prefix = ''
if self.item_x_prefix is not None: # If item_x_prefix has been set
prefix += '|[X' + str(self.item_x_prefix).rjust(2,'0') + '|XX'
if self.item_y_prefix is not None: # If item_yx_prefix has been set
prefix += '|[Y' + str(self.item_y_prefix + (i - start_index)).rjust(2,'0') + '|XX'
prefix += self.selected_item_prefix if (i + 1) == self.current_selection_global else self.item_prefix
if isinstance(item, self.ListItem):
item.display(i, prefix, self.display_attributes, self.show_attribute_titles,
self.attribute_separator, self.item_suffix, self.selected_item_suffix,
self.selected_item_prefix, self.index_separator,int(len(str(len(self.items)))))
else:
item_str = "{}{}{} {}".format(prefix, (i+1).rjust(int(len(str(len(self.items)))),'0'), self.index_separator, item)
writeln(item_str)
for i in range(self.items_per_page - len(displayed_items)):
prefix = ''
if self.item_x_prefix is not None: # If item_x_prefix has been set
prefix += '|[X' + str(self.item_x_prefix).rjust(2,'0') + '|XX'
if self.item_y_prefix is not None: # If item_y_prefix has been set
prefix += '|[Y' + str(self.item_y_prefix + len(displayed_items) + i).rjust(2,'0') + '|XX'
prefix += self.item_prefix
writeln(prefix + self.item_suffix)
writeln('') # Add a new line
# get attribues of currently selected item
def get_selected_item(self):
if 0 < self.current_selection_global <= len(self.items):
return self.items[self.current_selection_global - 1]
return self.ListItem() # Return an empty ListItem if no item is selected
# Get number of currently selected item (out of total items)
def get_selected_index(self):
return self.current_selection_global
# Move forward or backward one page, go to first page, go to last page
def scroll_page(self, direction='next'):
# Scroll forward one page
if direction == 'next':
if self.current_selection_global + self.items_per_page <= len(self.items):
self.current_selection_global += self.items_per_page - self.current_selection + 1
self.current_selection = 1
elif self.scrolling_wrap and self.current_selection_global < len(self.items):
self.current_selection_global = len(self.items)
self.current_selection = min(self.items_per_page, len(self.items) % self.items_per_page or self.items_per_page)
elif self.scrolling_wrap:
self.current_selection_global = 1
self.current_selection = 1
# Scroll backward one page
elif direction == 'previous':
if self.current_selection_global - self.items_per_page > 1:
self.current_selection_global -= self.items_per_page
self.current_selection = 1
elif self.scrolling_wrap and self.current_selection_global > 1:
self.current_selection_global = 1
self.current_selection = 1
elif self.scrolling_wrap:
self.current_selection_global = len(self.items) - (len(self.items) % self.items_per_page) + 1
self.current_selection = 1
else:
self.current_selection_global = 1
self.current_selection = 1
# Goto the first page
elif direction == 'first':
self.current_selection_global = 1
self.current_selection = 1
# Goto the last page
elif direction == 'last':
if len(self.items) % self.items_per_page == 0:
self.current_selection_global = len(self.items) - self.items_per_page + 1
else:
self.current_selection_global = len(self.items) - (len(self.items) % self.items_per_page) + 1
self.current_selection = min(self.items_per_page, len(self.items) - self.current_selection_global + 1)
self.current_selection_global += self.current_selection - 1
from mystic_bbs import *
# This is the ScrollingList object class demo script.
# This will explain how to use this class as well as give you a working
# demo.
# First, save the ScrollingList.js file somewhere where python (2.7) will find it.
# Load it:
from ScrollingList import *
# I'm adding some variables for navigational keys. For some reason Page Down is
# the same as 'Q' so in this example I will be using KEY_ESCAPE (and KEY_SPACE) to
# exit the script. Hopefully there's a way around that because I'd like to use 'Q'
KEY_UP = chr(72)
KEY_DOWN = chr(80)
KEY_ESCAPE = chr(27)
KEY_ENTER = chr(13)
KEY_TAB = chr(9)
KEY_BACKSPACE= chr(8)
KEY_LEFT = chr(75)
KEY_RIGHT = chr(77)
KEY_PGUP = chr(73)
KEY_PGDN = chr(81)
KEY_END = chr(79)
KEY_HOME = chr(71)
KEY_SPACE = chr(32)
# First things first. If we're going to make a scrolling list, we need to have items
# in that list to scroll through.
# Create a list of items, in this case it's called menu_items:
menu_items = [
# Now we use ScrollingList.ListItem() to add items to this list.
# Note, that they are added in key="value" pairs, or whatever they're called.
# Note also, that each iten does NOT need the same sets of pairs - although it woul
# generally be more useful if they did, but I wanted to throw that out there so
# so you know it wont break anything.
#
# This is where you would loop through some JSON, or pull rows from a db to fill
# your list.
ScrollingList.ListItem(name="Apple", description="Red fruit", item_type="Fruit", color="Red"),
ScrollingList.ListItem(name="Banana", description="Yellow fruit", item_type="Fruit", color="Yellow", tasty="no"),
ScrollingList.ListItem(name="Orange"),
ScrollingList.ListItem(name="Strawberry", description="Sweet fruit", item_type="Fruit", color="Red"),
ScrollingList.ListItem(name="Mango", description="Tropical fruit", item_type="Fruit", color="Yellow"),
ScrollingList.ListItem(name="Pineapple", description="Exotic fruit", item_type="Fruit", color="Yellow"),
ScrollingList.ListItem(name="Watermelon", description="Juicy fruit", item_type="Fruit", large="Yes"),
ScrollingList.ListItem(name="Grapes", description="Purple fruit", item_type="Fruit", color="Red"),
ScrollingList.ListItem(name="Kiwi", description="Fuzzy fruit", item_type="Fruit", color="Yellow", tasty="no"),
ScrollingList.ListItem(name="Avacado", description="Buttery fruit", item_type="Fruit", color="Dark Green", ripe="no"),
]
# Now, ititialize your list. Make note of what you named in, for this demo
# our menu object is called 'menu' SO CLEVER!
menu = ScrollingList(menu_items)
# BAM! You've got a list now.
################################################################################
# A BUNCH OF OPTIONS! I suggest you run it and uncomment things one at a time #
################################################################################
# menu.index_separator = ": " # This goes between the index number and the rest of the line
# menu.items_per_page = 8 # items per-page
# menu.item_prefix = "|08 " # Unselected item prefix
# menu.item_suffix = "|$X71." # Unselected item suffix
# menu.selected_item_prefix = "|15>|04" # Selected item prefix
# menu.selected_item_suffix = "|$X70.|15>|11" # Selected item suffix
# menu.set_display_attributes('index','name','description') # Specify attributes here, they dont have to exist in every item
# menu.show_attribute_titles=False # True = show keys, False = hode keys
# menu.attribute_separator = "," # Attribute seperator
# menu.display_menu=True # false = Hide scrolling menu (but selected_item.get_attribute() still works)
# menu.scrolling_wrap = True # Wrap around once end or beginning of items is reached
# menu.item_x_prefix = 6 # Give X coordinate for first item.
# menu.item_y_prefix = 7 # Give Y coordinate for first item
# Here's the main menu loop. There's other ways to do this if you want.
# We're setting a variable named QUIT to 0.
QUIT = 0
# As long as QUIT = 0, keep up the loop of displaying the list, and waiting for a key press.
while QUIT == 0 and not shutdown():
# We're gonna clean the screen so that the scrolling list keeps overwriting itself.
write('|CL') # Pro tip! If you use the item_y_prefix you don't need to clear the screen
# and if you don't clear the screen, you wont have to re-draw with every keypress.
# Display the menu!
menu.display()
# If you wanted to pull data from the items at this point, you can:
# <menuname>.get_selected_item
#
# Replace <menuname> with whatever you named the menu before.
#
# Let's get the data from the currently selected item and store it in 'selected_item'
selected_item = menu.get_selected_item()
# The index (item number) is not stored IN the item, but we can get that with
# get_selected_index
index = menu.get_selected_index()
# now we're gonna create some variables and assign them data from the current item
# key=value attribute pairs.
#
# the format is selected_item.get_attribute('key', 'None') where 'key' is the key of
# the value you want and 'None' is the text you would like if that Value is empty or
# does not exist.
name = selected_item.get_attribute('name', 'None')
description = selected_item.get_attribute('description','None')
total_items = len(menu.items)
#writeln("|[X02|[Y21Total items: " + str(total_items))
#writeln("|[X02|[Y22Selected item: " + str(index))
#writeln("|[X02|[Y23Name: " + name)
# Note that EVERYTHING between menu.display() and this line has been optional. If you're not
# going to use data from thecurrent items, then you don't need to do any of that.
# Now the interaction! use onekey() to get input from the user and we'll decide what to do with
# their selection.
# onkey() requires us to explicitly list what keys are valid. If that last item is True
# their selection will be echoed on the terminal. Not needed.
ch = onekey(KEY_UP + KEY_DOWN + KEY_PGUP + KEY_PGDN + KEY_HOME + KEY_END + KEY_SPACE + KEY_ENTER + KEY_ESCAPE, True);
# Scroll up one item if the up arrow is pressed.
if ch == KEY_UP:
menu.scroll_up()
# Scroll down one item if the down arrow is pressed.
if ch == KEY_DOWN:
menu.scroll_down()
# Move back one page if page up is pressed.
if ch == KEY_PGUP:
menu.scroll_page('previous')
# Move forward one page if page down is pressed.
if ch == KEY_PGDN or ch == '}':
menu.scroll_page('next')
# next and previous page options are aware if the option 'scrolling_wrap'
# is enabled. This is probably the place where bugs will be present.
# Let me know if you see anything wonky.
# Goto the first page if home is pressed.
if ch == KEY_HOME:
menu.scroll_page('first')
# Goto the last page if end is pressed.
if ch == KEY_END:
menu.scroll_page('last')
# Assign QUIT to 1, and exit menu if spacebar, escape is pressed.
# Here's where I'd like to use Q, but for some reason Q = ESCAPE
if ch == KEY_SPACE or ch == KEY_ESCAPE:
QUIT= 1
# HERE IS A GOOD PLACE TO USE THAT DATA WE GRABBED FROM THE
# CURRENTLY SELECTED ITEM!
#
# In this example, when enter is pressed the Description is display on the 20th line
# for a half a second and is then erased. OMG COOL. GO CRAZY.
if ch == KEY_ENTER: # RETURN
write('|[X02|[Y20Description: ' + description + '|DE|[X02|[Y20|[K')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment