make_menu 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +8 -6
- 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:
|
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,15 +33,17 @@ 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
|
-
homepage: https://
|
46
|
+
homepage: https://github.com/MisterGrimalkin/make_menu
|
45
47
|
licenses:
|
46
48
|
- MIT
|
47
49
|
metadata: {}
|
@@ -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
|