Skip to content

Instantly share code, notes, and snippets.

@larskanis
Last active December 25, 2025 18:39
Show Gist options
  • Select an option

  • Save larskanis/6a0fd4b75866c0e2b20ea76f31c013d5 to your computer and use it in GitHub Desktop.

Select an option

Save larskanis/6a0fd4b75866c0e2b20ea76f31c013d5 to your computer and use it in GitHub Desktop.
A console-based startmenu for Rubyinstaller
require "io/console"
class ButtonMatrix
attr_accessor :selected
def initialize(ncols: 3)
@ncols = ncols
@boxes = []
@con = IO.console
@selected = 0
end
Box = Data.define :text, :action
def add_button(text, &action)
@boxes << Box.new(text, action)
end
def cursor(direction)
case direction
when :left
@selected -= 1 if @selected > 0
when :up
@selected -= @ncols if @selected - @ncols >= 0
when :right
@selected += 1 if @selected + 1 < @boxes.size
when :down
@selected += @ncols if @selected + @ncols < @boxes.size
end
end
def select
@boxes[@selected].action.call
end
BUTTON_BORDERS = {
thin: "┌─┐" +
"│ │" +
"└─┘" ,
fat: "┏━┓" +
"┃ ┃" +
"┗━┛" ,
double: "╔═╗" +
"║ ║" +
"╚═╝" ,
}
private def box_border(ncol, nrow)
box_idx = ncol + nrow * @ncols
selected = box_idx == @selected
border = BUTTON_BORDERS[selected ? :double : :thin]
box = @boxes[box_idx]&.text
return box, border
end
# Paint the boxes
def repaint(width: @con.winsize[1], height: @con.winsize[0] - 1)
obw = (width.to_f / @ncols).floor
spw = obw - 4
nrows = (@boxes.size.to_f / @ncols).ceil
obh = (height.to_f / nrows).floor
sph = obh - 2
line = +""
nrows.times do |nrow|
@ncols.times do |ncol|
box, border = box_border(ncol, nrow)
if box
line += " #{border[0]}" + "#{border[1]}" * spw + "#{border[2]} "
end
end
line += "\n"
sph.times do |spy|
@ncols.times do |ncol|
box, border = box_border(ncol, nrow)
if box
text_lines = box.lines.map(&:chomp)
text_pos = spy - (sph - text_lines.size) / 2
text_line = text_lines[text_pos] if text_pos >= 0
text_line ||= ""
spl = (spw - text_line.size) / 2
spr = spw - spl - text_line.size
line += " #{border[3]}" + " " * spl + text_line + " " * spr + "#{border[5]} "
end
end
line += "\n"
end
@ncols.times do |ncol|
box, border = box_border(ncol, nrow)
if box
line += " #{border[6]}" + "#{border[7]}" * spw + "#{border[8]} "
end
end
line += "\n"
end
print line
end
end
class ConsoleApp
attr_accessor :widget
def initialize
@ev_r, @ev_w = IO.pipe
register_term_size_change
register_stdin
end
private def register_term_size_change
if RUBY_PLATFORM =~ /mingw|mswin/
IO.console.check_winsize_changed do
@ev_w.write "\x01"
end
else
Signal.trap('SIGWINCH') do
@ev_w.write "\x01"
end
end
end
private def register_stdin
Thread.new do
str = +""
while char=$stdin.getch(intr: true)
str << char
next if !str.valid_encoding? || str == "\e" || str == "\e["
@ev_w.write [2, str.size, str].pack("CCa*")
str = +""
end
end
end
private def handle_key_input(str)
case str
when "\e[D" # cursor left
widget.cursor(:left)
when "\e[A" # cursor up
widget.cursor(:up)
when "\e[C" # cursor right
widget.cursor(:right)
when "\e[B" # cursor down
widget.cursor(:down)
when "\r" # enter
widget.select
end
widget.repaint
end
private def main_loop
str = +""
while char=@ev_r.getc
case char
when "\x01"
widget.repaint
when "\x02"
strlen = @ev_r.getc.unpack1("C")
str = @ev_r.read(strlen)
handle_key_input(str)
widget.repaint
else
raise "unexpected event: #{char.inspect}"
end
end
end
def run!
widget.repaint
main_loop
end
end
app = ConsoleApp.new
bm = ButtonMatrix.new ncols: 3
bm.add_button "text1\nabc" do
p :button_1
exit
end
bm.add_button "text2" do
p :button_2
end
bm.add_button "text3\nabc\noollla\n text3\nabc\noollla" do
p :button_3
end
bm.add_button "text4\ndef" do
p :button_4
end
bm.add_button "text5\nabc" do
p :button_5
end
app.widget = bm
app.run!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment