Skip to content

Instantly share code, notes, and snippets.

@emdeeeks
Created April 27, 2020 22:33
Show Gist options
  • Save emdeeeks/6e9fa523829dd0176616bfb67253a785 to your computer and use it in GitHub Desktop.
Save emdeeeks/6e9fa523829dd0176616bfb67253a785 to your computer and use it in GitHub Desktop.
Customizable Terminal Menu implemented in Ruby
require File.expand_path('KeyListener.rb', File.dirname(__FILE__))
require File.expand_path('ConsoleMods.rb', File.dirname(__FILE__))
# TODO Colorizations
# TODO Eventsystem? Any standarts?
# Creates a console menu consisting of different items.
# ::Navigate using arrowkeys
# ::Select item by pressing enter
# ::Exit by pressing escape
class ConsoleMenu
include KeyListener
# Initializes the menu from given item collection and procs collection.
# The procs are executed when the corresponding item is selected.
# Params:
# +menuItems+:: +Collection+ of items which are parsed using the +to_s+ method.
# +menuProcs+:: +Collection+ consisting of +Procs+ which should have the
# same position in the collection as the corresponding item in the +menuItems+
# collection.
# +modcode+:: The modcode described in ConsoleMods.rb with wich the MenuItems
# are highlighted.
def initialize(menuItems, menuProcs, highlightcode = [43,30])
menuProcs.each { |lam|
raise ArgumentError, 'Second argument has to consist of Procs!' if !(lam.is_a? Proc)
}
@MenuItems = menuItems.collect { |point| point.to_s }
@MenuProcs = menuProcs
#Collection in which different event procs are stored.
@Calls = {
# Executed directly afte entering the menu by using the enter method.
:whenEntered => [],
# Executed before exiting the menu.
:whenExited => [],
# Executed every time before reprinting the menu.
:beforePrinting => [],
# Executed each time before Printing one of the Items
# Params with which the Procs are called:
# +i+:: The index of the printed item.
:beforePrintingItemN => [],
# Executed every time after reprinting the menu.
:afterPrinting => [],
# Executed every time before processing the input received from the KeyListener.
# Params with which the Procs are called :
# +char+:: The Character received from the KeyListener.
:beforeInputProcessign => [],
# Executed every time after processing the input received from the KeyListener.
# Params with which the Procs are called:
# +char+:: The Character received from the KeyListener.
# +exited+:: +Bool+ describing if the menu will be exited.
:afterInputProcessign => [],
# Executed when the KeyListener received Keys other than arrowkeys,
# the return key or the escape key.
# Params with which the Procs are called:
# +char+:: The Character received from the KeyListener.
:defaultInput => []
}
@Highlightcode = highlightcode
@selected = 0
# Is set to true as soon as the return or escape key was hit and the input
# was processed.
@exited = false
end
# Method to enter the menu
def enter
@Calls[:whenEntered].each { |lam| lam.call if lam.is_a? Proc }
@exited = false
while !@exited do inputLoop end
@Calls[:whenExited].each { |lam| lam.call if lam.is_a? Proc }
end
# Loop function clearing the screen, printing the menu and processing the input.
def inputLoop
system("clear")
@Calls[:beforePrinting].each { |lam| lam.call if lam.is_a? Proc }
print_menu
@Calls[:afterPrinting].each { |lam| lam.call if lam.is_a? Proc }
char = read_char
@Calls[:beforeInputProcessign].each { |lam| lam.call(char) if lam.is_a? Proc }
process_Input(char)
@Calls[:afterInputProcessign].each { |lam| lam.call(@exited,char) if lam.is_a? Proc }
end
# Processes the input received by the KeyListener.
def process_Input(char)
return if @exited
case char
when "\e[A"
@selected = (@selected - 1) % @MenuItems.count
when "\e[B"
@selected = (@selected + 1) % @MenuItems.count
when "\e"
@exited = true
when "\r"
@exited = true
@MenuProcs[@selected].call
else
@Calls[:defaultInput].each { |lam| lam.call(char) if lam.is_a? Proc }
end
end
# Prints the menu to the console highlighting the current selection.
def print_menu
tmp = 0
@MenuItems.each { |item|
@Calls[:beforePrintingItemN].each { |lam| lam.call(tmp) if lam.is_a? Proc }
tmp == @selected ? (puts item.mod(@Highlightcode)) : (puts item)
tmp += 1
}
end
# Instructing the menu to exit after current Loop.
def exitAfterLoop
@exited = true
end
# Returns if the menu was already instructed to exit.
def wasExited
return @exited
end
# Calls a specific Proc.
# Params:
# +int+:: Position of executed proc in the proc collection. Corresponding to
# item on the same position.
def executeMenuLambda(int)
@MenuProcs[int].call
end
# Adds proc to Calls hash. See above for further Detail.
# Params:
# +eventKey+:: One of the +Symbols+ defined in the Calls hash.
# +eventProc+:: +Proc+ that is executed at the time defined by the eventKey.
def addEvent (eventKey, eventProc)
@Calls[eventKey] << eventProc if eventProc.is_a? Proc
end
# Removes proc from Calls hash. See above for further Detail.
# Params:
# +eventKey+:: One of the +Symbols+ defined in the Calls hash.
# +eventProc+:: +Proc+ which is removed.
def removeEvent (eventKey, eventProc)
@Calls[eventKey].delete(eventProc)
end
end
require File.expand_path('ConsoleMods.rb', File.dirname(__FILE__))
#Defines a Header for a given ConsoleMenu.
class ConsoleMenuHeader
# Initializes the Header.
# Params:
# +header+:: +String+ displayed as header.
# +consoleMenu+:: The +ConsoleMenu+ on which the Header is applied.
# +modcode+:: (Optional) Defines the colorizations as described in ConsoleMods.rb
def initialize(header, consoleMenu, modcode = nil)
@ConsoleMenu = consoleMenu
@Proc = lambda { puts header.mod(modcode)}
enable
end
# Displays the Header
def enable
@ConsoleMenu.addEvent(:beforePrinting, @Proc )
end
# Disables the header's displaying
def disable
@ConsoleMenu.removeEvent(:beforePrinting, @Proc)
end
# Changes the Header.
# Params:
# +newheader+:: +String+ displayed as header.
# +modcode+:: (Optional) Defines the colorizations as described in ConsoleMods.rb
def change(newheader, modcode = nil)
disable
@Proc = lambda { puts newheader.mod(modcode)}
enable
end
end
# Adds colorizations to the String class
class String
# Colorizes the Sting using following integer codes:
# (Note that not all codes are supported in every Environment)
#
# 0: Reset / Normal
# 1: Bold or increased intensity
# 2: Faint (decreased intensity) - Not widely supported.
# 3: Italic: on - Not widely supported. Sometimes treated as inverse.
# 4: Underline: Single
# 5: Blink: Slow - less than 150 per minute
# 6: Blink: Rapid - MS-DOS ANSI.SYS; 150+ per minute; not widely supported
# 7: Image: Negative inverse or reverse; swap foreground and background (reverse video)
# 8: Conceal - Not widely supported.
# 9: Crossed-out Characters legible, but marked for deletion. - Not widely supported.
# 10: Primary(default) font
# 11–19: {\displaystyle n} n-th alternate font Select the {\displaystyle n}
# n-th alternate font (14 being the fourth alternate font, up to 19 being the 9th alternate font).
# 20: Fraktur hardly ever supported
# 21: Bold: off or Underline: Double Bold off not widely supported; double underline hardly ever supported.
# 22: Normal color or intensity Neither bold nor faint
# 23: Not italic, not Fraktur
# 24: Underline: None Not singly or doubly underlined
# 25: Blink: off
# 26: Reserved
# 27: Image: Positive
# 28: Reveal conceal off
# 29: Not crossed out
# 30–37: Set text color (foreground) 30 + {\displaystyle n} n, where {\displaystyle n} n is from the color table below
# 38: Reserved for extended set foreground color typical supported next arguments are 5;n where {\displaystyle n} n is color index (0..255) or 2;r;g;b where {\displaystyle r,g,b} r,g,b are red, green and blue color channels (out of 255)
# 39: Default text color (foreground) implementation defined (according to standard)
# 40–47: Set background color 40 + {\displaystyle n} n, where {\displaystyle n} n is from the color table below
# 48: Reserved for extended set background color typical supported next arguments are 5;n where {\displaystyle n} n is color index (0..255) or 2;r;g;b where {\displaystyle r,g,b} r,g,b are red, green and blue color channels (out of 255)
# 49: Default background color implementation defined (according to standard)
# 50: Reserved
# 51: Framed
# 52: Encircled
# 53: Overlined
# 54: Not framed or encircled
# 55: Not overlined
# 56–59: Reserved
#
# Params:
# +modcode+:: composition of one ore more of the above integer codes.
# To use multiple codes either compose an Array or concatenate the different
# codes into a single String seperated by ';'.
def mod(modcode)
return self if modcode == nil
tmp = ''
if modcode.is_a? Array
modcode.each { |mod|
tmp += ';' if tmp != ''
tmp += "#{mod}"
}
elsif modcode.is_a? String or modcode.is_a? Integer
tmp = modcode
end
"\e[#{tmp}m#{self}\e[0m"
end
end
require 'io/console'
module KeyListener
# Reads keypresses from the user including 2 and 3 escape character sequences.
def read_char
STDIN.echo = false
STDIN.raw!
input = STDIN.getc.chr
if input == "\e" then
input << STDIN.read_nonblock(3) rescue nil
input << STDIN.read_nonblock(2) rescue nil
end
ensure
STDIN.echo = true
STDIN.cooked!
return input
end
# oringal case statement from:
# http://www.alecjacobson.com/weblog/?p=75
def show_single_key
c = read_char
case c
when " "
puts "SPACE"
when "\t"
puts "TAB"
when "\r"
puts "RETURN"
when "\n"
puts "LINE FEED"
when "\e"
puts "ESCAPE"
when "\e[A"
puts "UP ARROW"
when "\e[B"
puts "DOWN ARROW"
when "\e[C"
puts "RIGHT ARROW"
when "\e[D"
puts "LEFT ARROW"
when "\177"
puts "BACKSPACE"
when "\004"
puts "DELETE"
when "\e[3~"
puts "ALTERNATE DELETE"
when "\u0003"
puts "CONTROL-C"
exit 0
when /^.$/
puts "SINGLE CHAR HIT: #{c.inspect}"
else
puts "SOMETHING ELSE: #{c.inspect}"
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment