make_menu 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
...
|