make_menu 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +17 -0
- data/lib/make_menu/color_string.rb +141 -0
- data/lib/make_menu/menu.rb +166 -0
- data/lib/make_menu/menu_item.rb +42 -0
- data/lib/make_menu/menu_item_group.rb +47 -0
- data/lib/make_menu/status_panel.rb +107 -0
- data/lib/make_menu/text_column.rb +43 -0
- data/lib/make_menu/text_table.rb +75 -0
- data/lib/make_menu/version.rb +3 -0
- data/lib/make_menu.rb +32 -0
- data/make_menu.gemspec +59 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fde049dc673ab7e43c9254d5b394b9ed592a9d9799e46046c1d935a1d55e4774
|
4
|
+
data.tar.gz: 0b289aa24e021fa1ba4f2add414978de9783827fac12e028d38ebb39adcbf80f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 443c5ad6a9eb87e424e7e693ea3e618ecafdbf1d979c03cdbe4c0c31782b6ca3905648fe01a950901b39b7706d3f7ff9870c3cd854f9d5a99218bcd1688553b8
|
7
|
+
data.tar.gz: eaf82c95dc0df11c6422c76ed49d634501bcc7ed9a0894d08ca77cb091887766c1767b6bb525a112e067925f3cb8f73647343bb6ff27858800e3a530a5c38d95
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MakeMenu
|
4
|
+
# Monkeypatch for `String`, adds methods to change console text colo(u)r
|
5
|
+
module ColorString
|
6
|
+
COLORS = {
|
7
|
+
white: 0,
|
8
|
+
normal: 0,
|
9
|
+
bold: 1,
|
10
|
+
dark: 2,
|
11
|
+
underline: 4,
|
12
|
+
blink: 5,
|
13
|
+
invert: 7,
|
14
|
+
|
15
|
+
black: 30,
|
16
|
+
red: 31,
|
17
|
+
green: 32,
|
18
|
+
yellow: 33,
|
19
|
+
blue: 34,
|
20
|
+
magenta: 35,
|
21
|
+
cyan: 36,
|
22
|
+
grey: 37,
|
23
|
+
|
24
|
+
black_bg: 40,
|
25
|
+
red_bg: 41,
|
26
|
+
green_bg: 42,
|
27
|
+
yellow_bg: 43,
|
28
|
+
blue_bg: 44,
|
29
|
+
magenta_bg: 45,
|
30
|
+
cyan_bg: 46,
|
31
|
+
grey_bg: 47,
|
32
|
+
|
33
|
+
dark_grey: 90,
|
34
|
+
light_red: 91,
|
35
|
+
light_green: 92,
|
36
|
+
light_yellow: 93,
|
37
|
+
light_blue: 94,
|
38
|
+
light_magenta: 95,
|
39
|
+
light_cyan: 96,
|
40
|
+
light_grey: 97,
|
41
|
+
|
42
|
+
dark_grey_bg: 100,
|
43
|
+
light_red_bg: 101,
|
44
|
+
light_green_bg: 102,
|
45
|
+
light_yellow_bg: 103,
|
46
|
+
light_blue_bg: 104,
|
47
|
+
light_magenta_bg: 105,
|
48
|
+
light_cyan_bg: 106,
|
49
|
+
light_grey_bg: 107
|
50
|
+
}.freeze
|
51
|
+
|
52
|
+
COLORS.each do |name, code|
|
53
|
+
define_method name do
|
54
|
+
color(code)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Apply specified color code to the String
|
59
|
+
# @param [Array, Symbol, Integer] color_code Can be a key in the COLORS array,
|
60
|
+
# an integer ANSI code for text color, or an array of either to be applied in order
|
61
|
+
# @return [String] String enclosed by formatting characters
|
62
|
+
def color(color_code)
|
63
|
+
case color_code
|
64
|
+
when Array
|
65
|
+
color_code.inject(self) { |string, code| string.color(code) }
|
66
|
+
when Symbol
|
67
|
+
color(COLORS[color_code])
|
68
|
+
else
|
69
|
+
"\e[#{color_code}m#{self}\e[0m"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Changes all occurrences of a specified character to one color,
|
74
|
+
# and all other characters to another
|
75
|
+
# @param [String] char Character to highlight
|
76
|
+
# @param [Symbol] fore_color Key of color to use for highlighted character
|
77
|
+
# @param [Symbol] back_color Key of color to use for other characters
|
78
|
+
# @return [String] Highlighted text
|
79
|
+
# @example "==$$==".highlight('$', :light_yellow, :red)
|
80
|
+
# rubocop:disable Metrics/MethodLength
|
81
|
+
def highlight(char, fore_color, back_color)
|
82
|
+
inside_highlight = false
|
83
|
+
output = ''
|
84
|
+
buffer = ''
|
85
|
+
each_char do |c|
|
86
|
+
if c == char
|
87
|
+
unless inside_highlight
|
88
|
+
output += buffer.color(COLORS[back_color.to_sym])
|
89
|
+
buffer = ''
|
90
|
+
inside_highlight = true
|
91
|
+
end
|
92
|
+
elsif inside_highlight
|
93
|
+
output += buffer.color(COLORS[fore_color.to_sym]).bold
|
94
|
+
buffer = ''
|
95
|
+
inside_highlight = false
|
96
|
+
end
|
97
|
+
buffer += c
|
98
|
+
end
|
99
|
+
|
100
|
+
output += if inside_highlight
|
101
|
+
buffer.color(COLORS[fore_color.to_sym]).bold
|
102
|
+
else
|
103
|
+
buffer.color(COLORS[back_color.to_sym])
|
104
|
+
end
|
105
|
+
|
106
|
+
output
|
107
|
+
end
|
108
|
+
# rubocop:enable Metrics/MethodLength
|
109
|
+
|
110
|
+
# Remove color codes from the string
|
111
|
+
# @return [String] The modified string
|
112
|
+
def decolor
|
113
|
+
gsub(/\e\[\d+m/, '')
|
114
|
+
end
|
115
|
+
|
116
|
+
# Align the string, ignoring color code characters which would otherwise mess up String#center, etc.
|
117
|
+
# @param [Symbol] alignment :left, :center, or :right
|
118
|
+
# @param [Integer] width The number of characters to spread the string over (default to terminal width)
|
119
|
+
# @param [String] char The character to use for padding
|
120
|
+
# @param [Boolean] pad_right Set true to include trailing spaces when aligning to :center
|
121
|
+
# @return [String] The padded string
|
122
|
+
# rubocop:disable Metrics/MethodLength
|
123
|
+
def align(alignment = :left, width: nil, char: ' ', pad_right: false)
|
124
|
+
width = ::TTY::Screen.cols unless width
|
125
|
+
|
126
|
+
case alignment
|
127
|
+
when :left
|
128
|
+
right_pad = width - decolor.length
|
129
|
+
"#{self}#{char * right_pad}"
|
130
|
+
when :center
|
131
|
+
left_pad = [(width - decolor.length) / 2, 0].max
|
132
|
+
right_pad = width - left_pad - decolor.length
|
133
|
+
"#{char * left_pad}#{self}#{pad_right ? char * right_pad : ''}"
|
134
|
+
when :right
|
135
|
+
left_pad = width - decolor.length
|
136
|
+
"#{char * left_pad}#{self}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
# rubocop:enable Metrics/MethodLength
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'menu_item_group'
|
4
|
+
require_relative 'color_string'
|
5
|
+
require_relative 'text_table'
|
6
|
+
|
7
|
+
module MakeMenu
|
8
|
+
# This class builds and displays a number-selection menu from a Makefile
|
9
|
+
# then prompts for a number and executes the target.
|
10
|
+
class Menu
|
11
|
+
# @param [String] makefile Makefile name
|
12
|
+
def initialize(makefile)
|
13
|
+
@groups = []
|
14
|
+
@items = []
|
15
|
+
@status_present = false
|
16
|
+
build makefile
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :groups, :items
|
20
|
+
attr_accessor :status_present
|
21
|
+
|
22
|
+
# Display menu and prompt for command
|
23
|
+
# rubocop:disable Metrics/MethodLength
|
24
|
+
def run
|
25
|
+
running = true
|
26
|
+
|
27
|
+
while running
|
28
|
+
system 'clear' if clear_screen?
|
29
|
+
|
30
|
+
display_header
|
31
|
+
|
32
|
+
puts colorize(TextTable.new(groups).to_s)
|
33
|
+
puts
|
34
|
+
puts 'Press ENTER to quit'.align(:center).bold
|
35
|
+
puts
|
36
|
+
print 'Select option: '.align(:center)
|
37
|
+
|
38
|
+
running = false unless execute_option(gets.strip)
|
39
|
+
end
|
40
|
+
|
41
|
+
puts
|
42
|
+
|
43
|
+
system 'clear' if clear_screen?
|
44
|
+
end
|
45
|
+
|
46
|
+
# rubocop:enable Metrics/MethodLength
|
47
|
+
|
48
|
+
# Display the company logo and the status bar (if set)
|
49
|
+
def display_header
|
50
|
+
puts formatted_logo if logo
|
51
|
+
puts `make status` if status_present
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# Build a menu from the specified Makefile
|
57
|
+
# @param [String] makefile Filename
|
58
|
+
# rubocop:disable Metrics/MethodLength
|
59
|
+
def build(makefile)
|
60
|
+
File.open(makefile, 'r') do |file|
|
61
|
+
option_number = 1
|
62
|
+
current_group = nil
|
63
|
+
|
64
|
+
file.each_line do |line|
|
65
|
+
if line.start_with? '###'
|
66
|
+
# Group header
|
67
|
+
group_title = line.gsub(/###\s+/, '').strip
|
68
|
+
current_group = MenuItemGroup.new(group_title.color(group_title_color))
|
69
|
+
groups << current_group
|
70
|
+
|
71
|
+
elsif line.match(/^[a-zA-Z_-]+:.*?## .*$$/)
|
72
|
+
# Menu item
|
73
|
+
target = line.split(':').first.strip
|
74
|
+
description = line.split('##').last.strip
|
75
|
+
|
76
|
+
# Target 'menu' should not appear
|
77
|
+
next if target == 'menu'
|
78
|
+
|
79
|
+
# Target 'status' should not appear, but is run automatically when the menu is rendered
|
80
|
+
if target == 'status'
|
81
|
+
self.status_present = true
|
82
|
+
next
|
83
|
+
end
|
84
|
+
|
85
|
+
unless current_group
|
86
|
+
current_group = MenuItemGroup.new
|
87
|
+
groups << current_group
|
88
|
+
end
|
89
|
+
|
90
|
+
items << current_group.add_item(
|
91
|
+
MenuItem.new(option_number, target, description)
|
92
|
+
)
|
93
|
+
|
94
|
+
option_number += 1
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# rubocop:enable Metrics/MethodLength
|
101
|
+
|
102
|
+
# Execute the selected menu item
|
103
|
+
# @param [String] selected Value entered by user
|
104
|
+
# @return [Boolean] False to signify that menu should exit
|
105
|
+
def execute_option(selected)
|
106
|
+
return false if selected.empty?
|
107
|
+
|
108
|
+
selected = selected.to_i
|
109
|
+
|
110
|
+
items.each do |item|
|
111
|
+
next unless item.option_number == selected
|
112
|
+
|
113
|
+
system 'clear' if clear_screen?
|
114
|
+
item.execute
|
115
|
+
return true
|
116
|
+
end
|
117
|
+
|
118
|
+
true
|
119
|
+
end
|
120
|
+
|
121
|
+
# Apply word colorings
|
122
|
+
# @param [String] string
|
123
|
+
# @return [String]
|
124
|
+
def colorize(string)
|
125
|
+
return string unless Object.const_defined?("#{self.class.name}::HIGHLIGHTS")
|
126
|
+
|
127
|
+
Object.const_get("#{self.class.name}::HIGHLIGHTS").each do |word, color|
|
128
|
+
case color
|
129
|
+
when Array
|
130
|
+
color.each { |c| string.gsub!(word, word.send(c)) }
|
131
|
+
else
|
132
|
+
string.gsub!(word, word.send(color))
|
133
|
+
end
|
134
|
+
end
|
135
|
+
string
|
136
|
+
end
|
137
|
+
|
138
|
+
# Center each line of the logo across the screen
|
139
|
+
def formatted_logo
|
140
|
+
logo.split("\n")
|
141
|
+
.map { |line| line.align(:center) }
|
142
|
+
.join("\n")
|
143
|
+
end
|
144
|
+
|
145
|
+
# Get the menu logo from the LOGO constant
|
146
|
+
def logo
|
147
|
+
return "\n#{' make '.black_bg.light_yellow}#{' menu '.light_yellow_bg.black}\n".bold unless Object.const_defined?("#{self.class.name}::LOGO")
|
148
|
+
|
149
|
+
Object.const_get("#{self.class.name}::LOGO")
|
150
|
+
end
|
151
|
+
|
152
|
+
protected
|
153
|
+
|
154
|
+
# Override the following methods to customise the menu display
|
155
|
+
|
156
|
+
# @return [Symbol] Color for group title
|
157
|
+
def group_title_color
|
158
|
+
:light_green
|
159
|
+
end
|
160
|
+
|
161
|
+
# Clean screen before and after each command
|
162
|
+
def clear_screen?
|
163
|
+
true
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MakeMenu
|
4
|
+
# This class represents an option in the menu which runs a target from the Makefile
|
5
|
+
class MenuItem
|
6
|
+
INDENT = 6
|
7
|
+
|
8
|
+
# @param [Integer] option_number Number user enters for this command
|
9
|
+
# @param [String] target Name of target defined in Makefile
|
10
|
+
# @param [String] description Text to display for this command, taken from Makefile comment
|
11
|
+
def initialize(option_number = nil, target = nil, description = nil)
|
12
|
+
@option_number = option_number
|
13
|
+
@target = target
|
14
|
+
@description = description || target
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :option_number, :target, :description
|
18
|
+
|
19
|
+
# Run the make target
|
20
|
+
def execute
|
21
|
+
cmd = ['make', target]
|
22
|
+
puts "> #{cmd.join(' ').cyan}\n"
|
23
|
+
unless system(*cmd)
|
24
|
+
# Indicates the command failed, so we pause to allow user to see error message
|
25
|
+
puts "\nPress ENTER key to continue....\n"
|
26
|
+
gets
|
27
|
+
end
|
28
|
+
rescue StandardError => _e
|
29
|
+
# ignore CTRL+C from within Make target
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [Integer] Number of characters required to display the item
|
33
|
+
def width
|
34
|
+
description.size + INDENT + 1
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [String] Text to display for this item
|
38
|
+
def to_s
|
39
|
+
"#{option_number.to_s.rjust(INDENT, ' ').bold}. #{description}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'menu_item'
|
4
|
+
|
5
|
+
module MakeMenu
|
6
|
+
# This class represents a group of menu items, with a title line
|
7
|
+
class MenuItemGroup
|
8
|
+
INDENT = 2
|
9
|
+
|
10
|
+
# @param [String] title The title text to display at the top of the group
|
11
|
+
def initialize(title = 'Commands')
|
12
|
+
@title = title
|
13
|
+
@items = []
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :title, :items
|
17
|
+
|
18
|
+
# Add a new item to the group
|
19
|
+
# @param [MenuItem] item The item to add
|
20
|
+
# @return [MenuItem] The added item
|
21
|
+
def add_item(item)
|
22
|
+
items << item
|
23
|
+
item
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Integer] Number of characters needed to display the widest item
|
27
|
+
def width
|
28
|
+
[items.map(&:width).max, title.length + INDENT].max
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Integer] Number of rows needed to display the group
|
32
|
+
def height
|
33
|
+
items.size + 2
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [String] Text representation of group
|
37
|
+
def to_s
|
38
|
+
result = "#{' ' * INDENT}#{title}\n"
|
39
|
+
|
40
|
+
items.each do |item|
|
41
|
+
result += "#{item}\n"
|
42
|
+
end
|
43
|
+
|
44
|
+
"#{result} "
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'color_string'
|
4
|
+
require 'readline'
|
5
|
+
|
6
|
+
module MakeMenu
|
7
|
+
# A panel above the menu displaying the status of Docker containers.
|
8
|
+
# The mapping of TextLabel => ContainerName must be defined in a constant called CONTAINERS
|
9
|
+
class StatusPanel
|
10
|
+
String.include(ColorString)
|
11
|
+
|
12
|
+
# Print panel
|
13
|
+
def display
|
14
|
+
return if containers.empty?
|
15
|
+
|
16
|
+
puts "\n#{panel}"
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
# Return a hash mapping label to container name
|
22
|
+
# This is assumed to be provided as a constant called CONTAINERS
|
23
|
+
# @return [Hash{String=>String}]
|
24
|
+
def containers
|
25
|
+
Object.const_get "#{self.class.name}::CONTAINERS"
|
26
|
+
rescue NameError
|
27
|
+
{}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Override this to change the colors for running / not running
|
31
|
+
def colors_if_running
|
32
|
+
{
|
33
|
+
true => %i[green_bg bold white],
|
34
|
+
false => %i[red_bg bold dark]
|
35
|
+
}.freeze
|
36
|
+
end
|
37
|
+
|
38
|
+
# Override this to limit each row to a maximum number of labels
|
39
|
+
def max_labels_per_line
|
40
|
+
containers.size
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# @return [String] Text representation of the panel
|
46
|
+
# rubocop:disable Metrics/MethodLength
|
47
|
+
def panel
|
48
|
+
return @panel if @panel
|
49
|
+
|
50
|
+
@panel = ''
|
51
|
+
labels_on_this_line = 0
|
52
|
+
line_buffer = ''
|
53
|
+
|
54
|
+
containers.each do |label, container|
|
55
|
+
if (labels_on_this_line + 1) > labels_per_line
|
56
|
+
@panel += "#{left_indent(labels_on_this_line)}#{line_buffer}\n\n"
|
57
|
+
labels_on_this_line = 0
|
58
|
+
line_buffer = ''
|
59
|
+
end
|
60
|
+
|
61
|
+
text = label.align(:center, width: max_label_width, pad_right: true)
|
62
|
+
.color(colors_if_running[running?(container)])
|
63
|
+
|
64
|
+
line_buffer += " #{text} "
|
65
|
+
|
66
|
+
labels_on_this_line += 1
|
67
|
+
end
|
68
|
+
|
69
|
+
@panel += "#{left_indent(labels_on_this_line)}#{line_buffer}\n\n"
|
70
|
+
end
|
71
|
+
# rubocop:enable Metrics/MethodLength
|
72
|
+
|
73
|
+
# @return [String] List of Docker containers and information
|
74
|
+
def docker_ps
|
75
|
+
@docker_ps ||= `docker compose ps`
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Boolean] whether specified container is running
|
79
|
+
def running?(container)
|
80
|
+
docker_ps.include? container
|
81
|
+
end
|
82
|
+
|
83
|
+
# Return the left indent for this line of labels
|
84
|
+
# @param [Integer] number_of_labels Number of labels on this line
|
85
|
+
# @return [String]
|
86
|
+
def left_indent(number_of_labels)
|
87
|
+
spaces = (::TTY::Screen.cols - (number_of_labels * (max_label_width + 2))) / 2
|
88
|
+
spaces = [spaces, 0].max
|
89
|
+
' ' * spaces
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Integer] Maximum label width, with padding
|
93
|
+
def max_label_width
|
94
|
+
@max_label_width ||= containers.map do |label, _container|
|
95
|
+
label.length
|
96
|
+
end.max + 2
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [Integer] Number of labels that can fit on one line
|
100
|
+
def labels_per_line
|
101
|
+
@labels_per_line ||= [
|
102
|
+
(::TTY::Screen.cols / max_label_width) - 1,
|
103
|
+
max_labels_per_line
|
104
|
+
].min
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MakeMenu
|
4
|
+
# A column of text with a fixed with
|
5
|
+
class TextColumn
|
6
|
+
# @param [Integer] width Width of the column, in characters
|
7
|
+
def initialize(width)
|
8
|
+
@width = width
|
9
|
+
@rows = []
|
10
|
+
@row_index = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :width
|
14
|
+
|
15
|
+
attr_accessor :rows, :row_index
|
16
|
+
|
17
|
+
# Add a block of text to the column. Each row will be padded to the column width
|
18
|
+
# @param [String] text The text to add, may be multi-line
|
19
|
+
def add(text)
|
20
|
+
self.rows += text.split("\n").map do |row|
|
21
|
+
row.gsub("\r", '')
|
22
|
+
end
|
23
|
+
self.row_index += text.lines.size
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return the specified row of text
|
27
|
+
# @param [Integer] index The index of the row
|
28
|
+
# @return [String] The row at the specified index
|
29
|
+
def row(index)
|
30
|
+
(rows[index] || '').align(width: width)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Integer] The number of rows in the column
|
34
|
+
def height
|
35
|
+
row_index
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Boolean] True if the column is empty
|
39
|
+
def empty?
|
40
|
+
row_index.zero?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'text_column'
|
4
|
+
|
5
|
+
module MakeMenu
|
6
|
+
# This class displays the menu groups in columns across the screen.
|
7
|
+
# Each group is kept together in a column, and once a column has exceeded the
|
8
|
+
# calculated height a new column is added.
|
9
|
+
class TextTable
|
10
|
+
MAX_COLUMNS = 4
|
11
|
+
|
12
|
+
# @param [Array<MenuItemGroup>] groups
|
13
|
+
def initialize(groups)
|
14
|
+
@groups = groups
|
15
|
+
@columns = []
|
16
|
+
calculate_table_dimensions
|
17
|
+
build_table
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [String] The entire table, centered on the screen
|
21
|
+
def to_s
|
22
|
+
buffer = ''
|
23
|
+
|
24
|
+
max_height.times do |i|
|
25
|
+
row = ''
|
26
|
+
columns.each do |column|
|
27
|
+
row += column.row(i) unless column.empty?
|
28
|
+
end
|
29
|
+
buffer += "#{row.align(:center)}\n"
|
30
|
+
end
|
31
|
+
|
32
|
+
buffer
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_reader :groups, :columns, :column_width, :column_height
|
38
|
+
|
39
|
+
attr_accessor :current_column
|
40
|
+
|
41
|
+
# Calculate width and minimum height of columns
|
42
|
+
def calculate_table_dimensions
|
43
|
+
@column_width = groups.map(&:width).max + 5
|
44
|
+
total_rows = groups.map(&:height).sum
|
45
|
+
column_count = (::TTY::Screen.cols / column_width).clamp(1, MAX_COLUMNS)
|
46
|
+
@column_height = total_rows / column_count
|
47
|
+
end
|
48
|
+
|
49
|
+
# Build columns from groups
|
50
|
+
def build_table
|
51
|
+
column_break
|
52
|
+
groups.each do |group|
|
53
|
+
add_text_block group.to_s
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Add a block of text to the current column. If the column is now larger than
|
58
|
+
# the minimum height, a new column is added
|
59
|
+
def add_text_block(text)
|
60
|
+
current_column.add(text)
|
61
|
+
column_break if current_column.height >= column_height
|
62
|
+
end
|
63
|
+
|
64
|
+
# Add a new column to the table
|
65
|
+
def column_break
|
66
|
+
self.current_column = TextColumn.new(column_width)
|
67
|
+
columns << current_column
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Integer] Maximum column height (rows)
|
71
|
+
def max_height
|
72
|
+
columns.map(&:height).max
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/make_menu.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative 'make_menu/color_string'
|
2
|
+
require_relative 'make_menu/menu'
|
3
|
+
require_relative 'make_menu/status_panel'
|
4
|
+
|
5
|
+
require 'tty-screen'
|
6
|
+
|
7
|
+
module MakeMenu
|
8
|
+
String.include MakeMenu::ColorString
|
9
|
+
|
10
|
+
def self.run
|
11
|
+
# Allows CTRL+C to return to the menu instead of exiting the script
|
12
|
+
trap('SIGINT') { throw StandardError }
|
13
|
+
|
14
|
+
makefile = ENV.fetch('MAKEFILE', './Makefile')
|
15
|
+
|
16
|
+
if (menu_name = ENV.fetch('MENU', nil))
|
17
|
+
require "./#{menu_name.downcase}_menu.rb"
|
18
|
+
Object.const_get("#{menu_name.capitalize}Menu").new(makefile).run
|
19
|
+
else
|
20
|
+
MakeMenu::Menu.new(makefile).run
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.status
|
25
|
+
if (menu_name = ENV.fetch('MENU', nil))
|
26
|
+
require "./#{menu_name.downcase}_status_panel.rb"
|
27
|
+
Object.const_get("#{menu_name.capitalize}StatusPanel").new.display
|
28
|
+
else
|
29
|
+
MakeMenu::StatusPanel.new.display
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/make_menu.gemspec
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.expand_path('lib/make_menu/version', __dir__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "make_menu"
|
5
|
+
s.version = MakeMenu::VERSION
|
6
|
+
s.summary = "Generates an interactive menu from a Makefile"
|
7
|
+
s.authors = ["Barri Mason"]
|
8
|
+
s.email = "loki@amarantha.net"
|
9
|
+
s.files = Dir[
|
10
|
+
'lib/**/*.rb',
|
11
|
+
'make_menu.gemspec',
|
12
|
+
'Gemfile',
|
13
|
+
'Gemfile.lock'
|
14
|
+
]
|
15
|
+
s.homepage =
|
16
|
+
"https://rubygems.org/gems/make_menu"
|
17
|
+
s.license = "MIT"
|
18
|
+
s.add_dependency 'tty-screen', '~> 0.8.2'
|
19
|
+
s.description = %(
|
20
|
+
Creates a number-selection menu from a Makefile. The menu will attempt to fill the width of the terminal window.
|
21
|
+
|
22
|
+
- Any targets in the Makefile with a double-hash comment will be displayed, e.g.:
|
23
|
+
serve: ## Start Rails server in background
|
24
|
+
This will display a line such as '1. Start Rails server in background' which runs the command `make serve`.
|
25
|
+
|
26
|
+
- A line that starts with a triple-hash will create a new menu group, e.g.:
|
27
|
+
### Docker Commands
|
28
|
+
This will begin a new group with the header 'Docker Commands'
|
29
|
+
|
30
|
+
- The environment variable MENU can be used to specify a custom menu class, e.g.:
|
31
|
+
export MENU=Accounts
|
32
|
+
This assumes that a class `AccountsMenu` is defined in the file `accounts_menu.rb`
|
33
|
+
|
34
|
+
You can define two constants in your custom class:
|
35
|
+
LOGO (String) text or ASCII art to display above the menu
|
36
|
+
HIGHLIGHTS (Hash{String=>[Symbol,Array<Symbol>]}) Add coloring to specific words or phrases
|
37
|
+
|
38
|
+
- The environment variable MAKEFILE can specify a Makefile. The default is './Makefile'.
|
39
|
+
|
40
|
+
The menu will not display any targets called 'menu' or 'status'. The latter, if present, is called each
|
41
|
+
time the menu displays.
|
42
|
+
|
43
|
+
-----------------------------
|
44
|
+
Docker Container Status Panel
|
45
|
+
-----------------------------
|
46
|
+
|
47
|
+
Displays a color-coded panel indicating whether or not a Docker container is running.
|
48
|
+
|
49
|
+
You must define a custom class inheriting from `MakeMenu::StatusPanel` and indicate this using
|
50
|
+
the environment variable MENU, e.g.:
|
51
|
+
export MENU=Accounts
|
52
|
+
This assumes that a class `AccountsStatusPanel` is defined in the file `accounts_status_panel.rb`
|
53
|
+
|
54
|
+
You can define a constant CONTAINERS {String=>String} in this custom class to map the displayed
|
55
|
+
label to the container name, e.g.:
|
56
|
+
CONTAINERS = { 'Backend' => 'myapp-backend-1' }
|
57
|
+
|
58
|
+
)
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: make_menu
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Barri Mason
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-12-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: tty-screen
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.8.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.8.2
|
27
|
+
description: |2+
|
28
|
+
|
29
|
+
Creates a number-selection menu from a Makefile. The menu will attempt to fill the width of the terminal window.
|
30
|
+
|
31
|
+
- Any targets in the Makefile with a double-hash comment will be displayed, e.g.:
|
32
|
+
serve: ## Start Rails server in background
|
33
|
+
This will display a line such as '1. Start Rails server in background' which runs the command `make serve`.
|
34
|
+
|
35
|
+
- A line that starts with a triple-hash will create a new menu group, e.g.:
|
36
|
+
### Docker Commands
|
37
|
+
This will begin a new group with the header 'Docker Commands'
|
38
|
+
|
39
|
+
- The environment variable MENU can be used to specify a custom menu class, e.g.:
|
40
|
+
export MENU=Accounts
|
41
|
+
This assumes that a class `AccountsMenu` is defined in the file `accounts_menu.rb`
|
42
|
+
|
43
|
+
You can define two constants in your custom class:
|
44
|
+
LOGO (String) text or ASCII art to display above the menu
|
45
|
+
HIGHLIGHTS (Hash{String=>[Symbol,Array<Symbol>]}) Add coloring to specific words or phrases
|
46
|
+
|
47
|
+
- The environment variable MAKEFILE can specify a Makefile. The default is './Makefile'.
|
48
|
+
|
49
|
+
The menu will not display any targets called 'menu' or 'status'. The latter, if present, is called each
|
50
|
+
time the menu displays.
|
51
|
+
|
52
|
+
-----------------------------
|
53
|
+
Docker Container Status Panel
|
54
|
+
-----------------------------
|
55
|
+
|
56
|
+
Displays a color-coded panel indicating whether or not a Docker container is running.
|
57
|
+
|
58
|
+
You must define a custom class inheriting from `MakeMenu::StatusPanel` and indicate this using
|
59
|
+
the environment variable MENU, e.g.:
|
60
|
+
export MENU=Accounts
|
61
|
+
This assumes that a class `AccountsStatusPanel` is defined in the file `accounts_status_panel.rb`
|
62
|
+
|
63
|
+
You can define a constant CONTAINERS {String=>String} in this custom class to map the displayed
|
64
|
+
label to the container name, e.g.:
|
65
|
+
CONTAINERS = { 'Backend' => 'myapp-backend-1' }
|
66
|
+
|
67
|
+
email: loki@amarantha.net
|
68
|
+
executables: []
|
69
|
+
extensions: []
|
70
|
+
extra_rdoc_files: []
|
71
|
+
files:
|
72
|
+
- Gemfile
|
73
|
+
- Gemfile.lock
|
74
|
+
- lib/make_menu.rb
|
75
|
+
- lib/make_menu/color_string.rb
|
76
|
+
- lib/make_menu/menu.rb
|
77
|
+
- lib/make_menu/menu_item.rb
|
78
|
+
- lib/make_menu/menu_item_group.rb
|
79
|
+
- lib/make_menu/status_panel.rb
|
80
|
+
- lib/make_menu/text_column.rb
|
81
|
+
- lib/make_menu/text_table.rb
|
82
|
+
- lib/make_menu/version.rb
|
83
|
+
- make_menu.gemspec
|
84
|
+
homepage: https://rubygems.org/gems/make_menu
|
85
|
+
licenses:
|
86
|
+
- MIT
|
87
|
+
metadata: {}
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubygems_version: 3.3.26
|
104
|
+
signing_key:
|
105
|
+
specification_version: 4
|
106
|
+
summary: Generates an interactive menu from a Makefile
|
107
|
+
test_files: []
|
108
|
+
...
|