make_menu 0.1.1 → 1.0.0
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 +4 -4
- data/Gemfile.lock +1 -1
- data/lib/make_menu/builder.rb +51 -0
- data/lib/make_menu/console/color_string.rb +135 -0
- data/lib/make_menu/console/prompter.rb +47 -0
- data/lib/make_menu/menu.rb +75 -115
- data/lib/make_menu/menu_item.rb +2 -6
- data/lib/make_menu/text/column.rb +41 -0
- data/lib/make_menu/text/table.rb +77 -0
- data/lib/make_menu/version.rb +4 -2
- data/lib/make_menu.rb +8 -26
- data/make_menu.gemspec +10 -8
- metadata +7 -5
- data/lib/make_menu/color_string.rb +0 -141
- data/lib/make_menu/text_column.rb +0 -43
- data/lib/make_menu/text_table.rb +0 -75
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c28ded8e47d7c6efaf2f84003d55f829e0c6d3c0cfdf869b2715d7391c78dad
|
4
|
+
data.tar.gz: 6fa3d05c94ed8f9f4de3b867477abf4df9970fe01b8e7edd76f6df65bd3dc2e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1aa18122b85d3ee30b910b0db5f92ea126b69cb962885020ab63bc915e37ac5702ecc078010c78157e9407dee587d6dadcc1461a00dd3d50e65f63d7c95ab5a4
|
7
|
+
data.tar.gz: 5610e7ed9c56ee84b6d54a0a618ea96a16b73c2ba63bd8b63bd2c1bb3112f887def6a90d8c434272d705cad7b898b3913c17e9e9cd7a1d78898dec16f6efb9f0
|
data/Gemfile.lock
CHANGED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MakeMenu
|
4
|
+
module Builder
|
5
|
+
def self.build(makefile, menu)
|
6
|
+
File.open(makefile, 'r') do |file|
|
7
|
+
option_number = 1
|
8
|
+
current_group = nil
|
9
|
+
|
10
|
+
file.each_line do |line|
|
11
|
+
if line.start_with? '###'
|
12
|
+
# Group header
|
13
|
+
group_title = line.gsub(/###\s+/, '').strip
|
14
|
+
current_group = menu.add_group MenuItemGroup.new(group_title.color(menu.group_title_color))
|
15
|
+
|
16
|
+
elsif line.match(/^[a-zA-Z_-]+:.*?## .*$$/)
|
17
|
+
# Menu item
|
18
|
+
target = line.split(':').first.strip
|
19
|
+
description = line.split('##').last.strip
|
20
|
+
|
21
|
+
# Target 'menu' should not appear
|
22
|
+
next if target == 'menu'
|
23
|
+
|
24
|
+
current_group ||= menu.add_group MenuItemGroup.new('Commands'.color(menu.group_title_color))
|
25
|
+
|
26
|
+
menu.add_item current_group.add_item(MenuItem.new(option_number, target, description))
|
27
|
+
|
28
|
+
option_number += 1
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if option_number == 1
|
33
|
+
puts
|
34
|
+
puts 'No annotated targets found!'.red.bold
|
35
|
+
puts
|
36
|
+
puts 'Expecting something like this....'
|
37
|
+
puts " #{'my_target:'.cyan} #{'## Do some things'.yellow}"
|
38
|
+
puts
|
39
|
+
exit 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
rescue Errno::ENOENT => _e
|
43
|
+
puts
|
44
|
+
puts 'No Makefile!'.red.bold
|
45
|
+
puts
|
46
|
+
puts "File '#{makefile}' could not be found.".yellow
|
47
|
+
puts
|
48
|
+
exit 1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MakeMenu
|
4
|
+
module Console
|
5
|
+
# Monkeypatch for `String`, adds methods to change console text colo(u)r
|
6
|
+
module ColorString
|
7
|
+
COLORS = {
|
8
|
+
white: 0,
|
9
|
+
normal: 0,
|
10
|
+
bold: 1,
|
11
|
+
dark: 2,
|
12
|
+
underline: 4,
|
13
|
+
blink: 5,
|
14
|
+
invert: 7,
|
15
|
+
|
16
|
+
black: 30,
|
17
|
+
red: 31,
|
18
|
+
green: 32,
|
19
|
+
yellow: 33,
|
20
|
+
blue: 34,
|
21
|
+
magenta: 35,
|
22
|
+
cyan: 36,
|
23
|
+
grey: 37,
|
24
|
+
|
25
|
+
black_bg: 40,
|
26
|
+
red_bg: 41,
|
27
|
+
green_bg: 42,
|
28
|
+
yellow_bg: 43,
|
29
|
+
blue_bg: 44,
|
30
|
+
magenta_bg: 45,
|
31
|
+
cyan_bg: 46,
|
32
|
+
grey_bg: 47,
|
33
|
+
|
34
|
+
dark_grey: 90,
|
35
|
+
light_red: 91,
|
36
|
+
light_green: 92,
|
37
|
+
light_yellow: 93,
|
38
|
+
light_blue: 94,
|
39
|
+
light_magenta: 95,
|
40
|
+
light_cyan: 96,
|
41
|
+
light_grey: 97,
|
42
|
+
|
43
|
+
dark_grey_bg: 100,
|
44
|
+
light_red_bg: 101,
|
45
|
+
light_green_bg: 102,
|
46
|
+
light_yellow_bg: 103,
|
47
|
+
light_blue_bg: 104,
|
48
|
+
light_magenta_bg: 105,
|
49
|
+
light_cyan_bg: 106,
|
50
|
+
light_grey_bg: 107
|
51
|
+
}.freeze
|
52
|
+
|
53
|
+
COLORS.each do |name, code|
|
54
|
+
define_method name do
|
55
|
+
color(code)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Apply specified color code to the String
|
60
|
+
# @param [Array, Symbol, Integer] color_code Can be a key in the COLORS array,
|
61
|
+
# an integer ANSI code for text color, or an array of either to be applied in order
|
62
|
+
# @return [String] String enclosed by formatting characters
|
63
|
+
def color(color_code)
|
64
|
+
case color_code
|
65
|
+
when Array
|
66
|
+
color_code.inject(self) { |string, code| string.color(code) }
|
67
|
+
when Symbol
|
68
|
+
color(COLORS[color_code])
|
69
|
+
else
|
70
|
+
"\e[#{color_code}m#{self}\e[0m"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Changes all occurrences of `char` to `fore_color`
|
75
|
+
# and all other characters to `back_color`
|
76
|
+
def highlight(char, fore_color, back_color)
|
77
|
+
inside_highlight = false
|
78
|
+
output = ''
|
79
|
+
buffer = ''
|
80
|
+
each_char do |c|
|
81
|
+
if c == char
|
82
|
+
unless inside_highlight
|
83
|
+
output += buffer.color(COLORS[back_color.to_sym])
|
84
|
+
buffer = ''
|
85
|
+
inside_highlight = true
|
86
|
+
end
|
87
|
+
elsif inside_highlight
|
88
|
+
output += buffer.color(COLORS[fore_color.to_sym]).bold
|
89
|
+
buffer = ''
|
90
|
+
inside_highlight = false
|
91
|
+
end
|
92
|
+
buffer += c
|
93
|
+
end
|
94
|
+
|
95
|
+
output += if inside_highlight
|
96
|
+
buffer.color(COLORS[fore_color.to_sym]).bold
|
97
|
+
else
|
98
|
+
buffer.color(COLORS[back_color.to_sym])
|
99
|
+
end
|
100
|
+
|
101
|
+
output
|
102
|
+
end
|
103
|
+
|
104
|
+
# Remove color codes from the string
|
105
|
+
def decolor
|
106
|
+
gsub(/\e\[\d+m/, '')
|
107
|
+
end
|
108
|
+
|
109
|
+
# Align the string, ignoring color code characters which would otherwise mess up String#center, etc.
|
110
|
+
def align(alignment = :left, width: nil, char: ' ', pad_right: false)
|
111
|
+
width ||= ::TTY::Screen.cols
|
112
|
+
|
113
|
+
case alignment
|
114
|
+
when :left
|
115
|
+
right_pad = width - decolor.length
|
116
|
+
"#{self}#{char * right_pad}"
|
117
|
+
when :center
|
118
|
+
left_pad = [(width - decolor.length) / 2, 0].max
|
119
|
+
right_pad = width - left_pad - decolor.length
|
120
|
+
"#{char * left_pad}#{self}#{pad_right ? char * right_pad : ''}"
|
121
|
+
when :right
|
122
|
+
left_pad = width - decolor.length
|
123
|
+
"#{char * left_pad}#{self}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Align each line of a string
|
128
|
+
def align_block(alignment = :left)
|
129
|
+
split("\n")
|
130
|
+
.map { |line| line.align(alignment) }
|
131
|
+
.join("\n")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'io/console'
|
4
|
+
|
5
|
+
module MakeMenu
|
6
|
+
module Console
|
7
|
+
module Prompter
|
8
|
+
PressedEscape = Class.new(StandardError)
|
9
|
+
|
10
|
+
def self.prompt(text = '', obscure: false)
|
11
|
+
print text
|
12
|
+
|
13
|
+
input = ''
|
14
|
+
char = ''
|
15
|
+
|
16
|
+
until !char.empty? && char.ord == 13
|
17
|
+
char = $stdin.getch
|
18
|
+
|
19
|
+
case char.ord
|
20
|
+
when 127
|
21
|
+
# BACKSPACE
|
22
|
+
input = input[0..-2]
|
23
|
+
print "\r#{text}#{' ' * input.size} "
|
24
|
+
print "\r#{text}#{obscure ? '*' * input.size : input}"
|
25
|
+
|
26
|
+
when 27
|
27
|
+
# ESC
|
28
|
+
raise PressedEscape
|
29
|
+
|
30
|
+
when 13
|
31
|
+
# ENTER
|
32
|
+
|
33
|
+
else
|
34
|
+
input += char
|
35
|
+
if obscure
|
36
|
+
print '*'
|
37
|
+
else
|
38
|
+
print char
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
input
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/make_menu/menu.rb
CHANGED
@@ -1,173 +1,133 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'builder'
|
3
4
|
require_relative 'menu_item_group'
|
4
|
-
require_relative '
|
5
|
-
require_relative '
|
5
|
+
require_relative 'text/table'
|
6
|
+
require_relative 'console/prompter'
|
6
7
|
|
7
8
|
module MakeMenu
|
8
9
|
# This class builds and displays a number-selection menu from a Makefile
|
9
10
|
# then prompts for a number and executes the target.
|
10
11
|
class Menu
|
11
|
-
# @param [String] makefile Makefile name
|
12
12
|
def initialize(makefile)
|
13
|
+
@makefile = makefile
|
14
|
+
|
13
15
|
@groups = []
|
14
16
|
@items = []
|
15
|
-
|
17
|
+
|
18
|
+
@options = {
|
19
|
+
group_title_color: :underline,
|
20
|
+
clear_screen: true,
|
21
|
+
pause_on_success: false
|
22
|
+
}
|
23
|
+
@highlights = {}
|
24
|
+
@header = nil
|
16
25
|
end
|
17
26
|
|
18
|
-
attr_reader :groups, :items
|
27
|
+
attr_reader :makefile, :groups, :items, :options, :highlights
|
19
28
|
|
20
|
-
# Display menu and prompt for command
|
21
|
-
# rubocop:disable Metrics/MethodLength
|
22
29
|
def run
|
23
|
-
|
30
|
+
yield self if block_given?
|
31
|
+
|
32
|
+
Builder.build(makefile, self)
|
24
33
|
|
25
|
-
|
34
|
+
loop do
|
26
35
|
system 'clear' if clear_screen?
|
27
36
|
|
28
37
|
display_header
|
29
38
|
|
30
|
-
puts colorize(
|
39
|
+
puts colorize(MakeMenu::Text::Table.new(groups).to_s)
|
31
40
|
puts
|
32
|
-
puts 'Press
|
41
|
+
puts 'Press ESC to quit'.align(:center).bold
|
33
42
|
puts
|
34
|
-
print 'Select option: '.align(:center)
|
35
|
-
|
36
|
-
running = false unless execute_option(gets.strip)
|
37
|
-
end
|
38
|
-
|
39
|
-
puts
|
40
|
-
|
41
|
-
system 'clear' if clear_screen?
|
42
|
-
end
|
43
43
|
|
44
|
-
|
44
|
+
prompt = 'Select option: '.align(:center)
|
45
45
|
|
46
|
-
|
47
|
-
|
48
|
-
puts formatted_logo if logo
|
49
|
-
end
|
50
|
-
|
51
|
-
private
|
52
|
-
|
53
|
-
# Build a menu from the specified Makefile
|
54
|
-
# @param [String] makefile Filename
|
55
|
-
# rubocop:disable Metrics/MethodLength
|
56
|
-
def build(makefile)
|
57
|
-
File.open(makefile, 'r') do |file|
|
58
|
-
option_number = 1
|
59
|
-
current_group = nil
|
60
|
-
|
61
|
-
file.each_line do |line|
|
62
|
-
if line.start_with? '###'
|
63
|
-
# Group header
|
64
|
-
group_title = line.gsub(/###\s+/, '').strip
|
65
|
-
current_group = MenuItemGroup.new(group_title.color(group_title_color))
|
66
|
-
groups << current_group
|
67
|
-
|
68
|
-
elsif line.match(/^[a-zA-Z_-]+:.*?## .*$$/)
|
69
|
-
# Menu item
|
70
|
-
target = line.split(':').first.strip
|
71
|
-
description = line.split('##').last.strip
|
72
|
-
|
73
|
-
# Target 'menu' should not appear
|
74
|
-
next if target == 'menu'
|
75
|
-
|
76
|
-
unless current_group
|
77
|
-
current_group = MenuItemGroup.new('Commands'.color(group_title_color))
|
78
|
-
groups << current_group
|
79
|
-
end
|
80
|
-
|
81
|
-
items << current_group.add_item(
|
82
|
-
MenuItem.new(option_number, target, description)
|
83
|
-
)
|
84
|
-
|
85
|
-
option_number += 1
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
if option_number == 1
|
46
|
+
begin
|
47
|
+
execute_option(MakeMenu::Console::Prompter.prompt(prompt).strip)
|
90
48
|
puts
|
91
|
-
|
92
|
-
|
93
|
-
puts 'Expecting something like this....'
|
94
|
-
puts " #{'my_target:'.cyan} #{'## Do some things'.yellow}"
|
95
|
-
puts
|
96
|
-
exit 1
|
49
|
+
rescue MakeMenu::Console::Prompter::PressedEscape
|
50
|
+
break
|
97
51
|
end
|
98
52
|
end
|
99
53
|
|
100
|
-
rescue Errno::ENOENT => _e
|
101
|
-
puts
|
102
|
-
puts 'No Makefile!'.red.bold
|
103
54
|
puts
|
104
|
-
puts "File '#{makefile}' could not be found.".yellow
|
105
|
-
puts
|
106
|
-
exit 1
|
107
|
-
end
|
108
55
|
|
109
|
-
|
56
|
+
system 'clear' if clear_screen?
|
57
|
+
end
|
110
58
|
|
111
|
-
# Execute the selected menu item
|
112
|
-
# @param [String] selected Value entered by user
|
113
|
-
# @return [Boolean] False to signify that menu should exit
|
114
59
|
def execute_option(selected)
|
115
|
-
return false if selected.empty?
|
116
|
-
|
117
60
|
selected = selected.to_i
|
118
61
|
|
119
62
|
items.each do |item|
|
120
63
|
next unless item.option_number == selected
|
121
64
|
|
122
65
|
system 'clear' if clear_screen?
|
66
|
+
|
123
67
|
item.execute
|
124
|
-
return true
|
125
|
-
end
|
126
68
|
|
127
|
-
|
69
|
+
if pause_on_success?
|
70
|
+
puts "\nPress ENTER to continue....".dark
|
71
|
+
gets
|
72
|
+
end
|
73
|
+
end
|
128
74
|
end
|
129
75
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
Object.const_get("#{self.class.name}::HIGHLIGHTS").each do |word, color|
|
137
|
-
case color
|
138
|
-
when Array
|
139
|
-
color.each { |c| string.gsub!(word, word.send(c)) }
|
140
|
-
else
|
141
|
-
string.gsub!(word, word.send(color))
|
142
|
-
end
|
76
|
+
def display_header
|
77
|
+
if @header
|
78
|
+
@header.call
|
79
|
+
else
|
80
|
+
logo = " #{Dir.pwd.split('/').last.upcase} ".invert.bold.to_s
|
81
|
+
puts "\n#{logo.align(:center)}\n \n"
|
143
82
|
end
|
144
|
-
string
|
145
83
|
end
|
146
84
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
.map { |line| line.align(:center) }
|
151
|
-
.join("\n")
|
85
|
+
def options
|
86
|
+
@options.merge!(yield) if block_given?
|
87
|
+
@options
|
152
88
|
end
|
153
89
|
|
154
|
-
|
155
|
-
|
156
|
-
|
90
|
+
def highlights
|
91
|
+
@highlights.merge!(yield) if block_given?
|
92
|
+
@highlights
|
93
|
+
end
|
157
94
|
|
158
|
-
|
95
|
+
def header(&block)
|
96
|
+
@header = block
|
159
97
|
end
|
160
98
|
|
161
|
-
|
99
|
+
def add_group(group)
|
100
|
+
groups << group
|
101
|
+
group
|
102
|
+
end
|
103
|
+
|
104
|
+
def add_item(item)
|
105
|
+
items << item
|
106
|
+
item
|
107
|
+
end
|
162
108
|
|
163
|
-
# @return [Symbol,Array[Symbol]] Color for group title
|
164
109
|
def group_title_color
|
165
|
-
|
110
|
+
options[:group_title_color]
|
166
111
|
end
|
167
112
|
|
168
|
-
# Clear screen before and after each command
|
169
113
|
def clear_screen?
|
170
|
-
|
114
|
+
options[:clear_screen]
|
115
|
+
end
|
116
|
+
|
117
|
+
def pause_on_success?
|
118
|
+
options[:pause_on_success]
|
119
|
+
end
|
120
|
+
|
121
|
+
def colorize(text)
|
122
|
+
highlights.each do |word, color|
|
123
|
+
case color
|
124
|
+
when Array
|
125
|
+
color.each { |c| text.gsub!(word, word.send(c)) }
|
126
|
+
else
|
127
|
+
text.gsub!(word, word.send(color))
|
128
|
+
end
|
129
|
+
end
|
130
|
+
text
|
171
131
|
end
|
172
132
|
end
|
173
133
|
end
|
data/lib/make_menu/menu_item.rb
CHANGED
@@ -5,9 +5,6 @@ module MakeMenu
|
|
5
5
|
class MenuItem
|
6
6
|
INDENT = 6
|
7
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
8
|
def initialize(option_number = nil, target = nil, description = nil)
|
12
9
|
@option_number = option_number
|
13
10
|
@target = target
|
@@ -16,17 +13,16 @@ module MakeMenu
|
|
16
13
|
|
17
14
|
attr_reader :option_number, :target, :description
|
18
15
|
|
19
|
-
# Run the make target
|
20
16
|
def execute
|
21
17
|
cmd = ['make', target]
|
22
18
|
puts "> #{cmd.join(' ').cyan}\n"
|
23
19
|
unless system(*cmd)
|
24
20
|
# Indicates the command failed, so we pause to allow user to see error message
|
25
|
-
puts "\nPress ENTER
|
21
|
+
puts "\nPress ENTER to continue....".dark
|
26
22
|
gets
|
27
23
|
end
|
28
24
|
rescue StandardError => _e
|
29
|
-
#
|
25
|
+
# Sink keyboard interrupt from within Make target
|
30
26
|
end
|
31
27
|
|
32
28
|
# @return [Integer] Number of characters required to display the item
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MakeMenu
|
4
|
+
module Text
|
5
|
+
# A column of text with a fixed with
|
6
|
+
class Column
|
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
|
+
def add(text)
|
19
|
+
self.rows += text.split("\n").map do |row|
|
20
|
+
row.gsub("\r", '')
|
21
|
+
end
|
22
|
+
self.row_index += text.lines.size
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String] The row at the specified index
|
26
|
+
def row(index)
|
27
|
+
(rows[index] || '').align(width: width)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Integer] The number of rows in the column
|
31
|
+
def height
|
32
|
+
row_index
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Boolean] True if the column is empty
|
36
|
+
def empty?
|
37
|
+
row_index.zero?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'column'
|
4
|
+
|
5
|
+
module MakeMenu
|
6
|
+
module Text
|
7
|
+
# This class displays the menu groups in columns across the screen.
|
8
|
+
# Each group is kept together in a column, and once a column has exceeded the
|
9
|
+
# calculated height, a new column is added.
|
10
|
+
class Table
|
11
|
+
MAX_COLUMNS = 4
|
12
|
+
|
13
|
+
# @param [Array<MenuItemGroup>] groups
|
14
|
+
def initialize(groups)
|
15
|
+
@groups = groups
|
16
|
+
@columns = []
|
17
|
+
calculate_table_dimensions
|
18
|
+
build_table
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [String] The entire table, centered on the screen
|
22
|
+
def to_s
|
23
|
+
buffer = ''
|
24
|
+
|
25
|
+
max_height.times do |i|
|
26
|
+
row = ''
|
27
|
+
columns.each do |column|
|
28
|
+
row += column.row(i) unless column.empty?
|
29
|
+
end
|
30
|
+
buffer += "#{row.align(:center)}\n"
|
31
|
+
end
|
32
|
+
|
33
|
+
buffer
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :groups, :columns, :column_width, :column_height
|
39
|
+
|
40
|
+
attr_accessor :current_column
|
41
|
+
|
42
|
+
# Calculate width and minimum height of columns
|
43
|
+
def calculate_table_dimensions
|
44
|
+
@column_width = groups.map(&:width).max + 5
|
45
|
+
total_rows = groups.map(&:height).sum
|
46
|
+
column_count = (::TTY::Screen.cols / column_width).clamp(1, MAX_COLUMNS)
|
47
|
+
@column_height = total_rows / column_count
|
48
|
+
end
|
49
|
+
|
50
|
+
# Build columns from groups
|
51
|
+
def build_table
|
52
|
+
column_break
|
53
|
+
groups.each do |group|
|
54
|
+
add_text_block group.to_s
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Add a block of text to the current column. If the column is now larger than
|
59
|
+
# the minimum height, a new column is added
|
60
|
+
def add_text_block(text)
|
61
|
+
current_column.add(text)
|
62
|
+
column_break if current_column.height >= column_height
|
63
|
+
end
|
64
|
+
|
65
|
+
# Add a new column to the table
|
66
|
+
def column_break
|
67
|
+
self.current_column = Column.new(column_width)
|
68
|
+
columns << current_column
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Integer] Maximum column height (rows)
|
72
|
+
def max_height
|
73
|
+
columns.map(&:height).max
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/make_menu/version.rb
CHANGED
data/lib/make_menu.rb
CHANGED
@@ -1,34 +1,16 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'make_menu/console/color_string'
|
2
4
|
require_relative 'make_menu/menu'
|
3
5
|
|
4
6
|
require 'tty-screen'
|
5
7
|
|
6
8
|
module MakeMenu
|
7
|
-
String.include MakeMenu::ColorString
|
8
|
-
|
9
|
-
def self.run
|
10
|
-
# Allows CTRL+C to return to the menu instead of exiting the script
|
11
|
-
trap('SIGINT') { throw StandardError }
|
12
|
-
|
13
|
-
makefile = ENV.fetch('MAKEFILE', './Makefile')
|
9
|
+
String.include MakeMenu::Console::ColorString
|
14
10
|
|
15
|
-
|
16
|
-
require "./#{menu_name.downcase}_menu.rb"
|
17
|
-
Object.const_get("#{menu_name.capitalize}Menu").new(makefile).run
|
18
|
-
else
|
19
|
-
MakeMenu::Menu.new(makefile).run
|
20
|
-
end
|
11
|
+
trap('SIGINT') { throw StandardError }
|
21
12
|
|
22
|
-
|
23
|
-
|
24
|
-
puts 'No customisation class found!'.red.bold
|
25
|
-
puts
|
26
|
-
puts 'Expected file:'
|
27
|
-
puts " ./#{menu_name.downcase}_menu.rb".cyan
|
28
|
-
puts
|
29
|
-
puts 'To define class:'
|
30
|
-
puts " #{menu_name.capitalize}Menu < MakeMenu::Menu".yellow
|
31
|
-
puts
|
32
|
-
exit 1
|
13
|
+
def self.run(makefile = './Makefile', &block)
|
14
|
+
MakeMenu::Menu.new(makefile).run(&block)
|
33
15
|
end
|
34
|
-
end
|
16
|
+
end
|
data/make_menu.gemspec
CHANGED
@@ -1,11 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require File.expand_path('lib/make_menu/version', __dir__)
|
2
4
|
|
3
5
|
Gem::Specification.new do |s|
|
4
|
-
s.name =
|
6
|
+
s.name = 'make_menu'
|
5
7
|
s.version = MakeMenu::VERSION
|
6
|
-
s.summary =
|
7
|
-
s.authors = [
|
8
|
-
s.email =
|
8
|
+
s.summary = 'Generates an interactive menu from a Makefile'
|
9
|
+
s.authors = ['Barri Mason']
|
10
|
+
s.email = 'loki@amarantha.net'
|
9
11
|
s.files = Dir[
|
10
12
|
'lib/**/*.rb',
|
11
13
|
'make_menu.gemspec',
|
@@ -13,8 +15,8 @@ Gem::Specification.new do |s|
|
|
13
15
|
'Gemfile.lock'
|
14
16
|
]
|
15
17
|
s.homepage =
|
16
|
-
|
17
|
-
s.license =
|
18
|
+
'https://github.com/MisterGrimalkin/make_menu'
|
19
|
+
s.license = 'MIT'
|
18
20
|
s.add_dependency 'tty-screen', '~> 0.8.2'
|
19
|
-
s.description =
|
20
|
-
end
|
21
|
+
s.description = ''
|
22
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: make_menu
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Barri Mason
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: tty-screen
|
@@ -33,12 +33,14 @@ files:
|
|
33
33
|
- Gemfile
|
34
34
|
- Gemfile.lock
|
35
35
|
- lib/make_menu.rb
|
36
|
-
- lib/make_menu/
|
36
|
+
- lib/make_menu/builder.rb
|
37
|
+
- lib/make_menu/console/color_string.rb
|
38
|
+
- lib/make_menu/console/prompter.rb
|
37
39
|
- lib/make_menu/menu.rb
|
38
40
|
- lib/make_menu/menu_item.rb
|
39
41
|
- lib/make_menu/menu_item_group.rb
|
40
|
-
- lib/make_menu/
|
41
|
-
- lib/make_menu/
|
42
|
+
- lib/make_menu/text/column.rb
|
43
|
+
- lib/make_menu/text/table.rb
|
42
44
|
- lib/make_menu/version.rb
|
43
45
|
- make_menu.gemspec
|
44
46
|
homepage: https://github.com/MisterGrimalkin/make_menu
|
@@ -1,141 +0,0 @@
|
|
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
|
@@ -1,43 +0,0 @@
|
|
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
|
data/lib/make_menu/text_table.rb
DELETED
@@ -1,75 +0,0 @@
|
|
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
|