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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1e6fc0d7ef57702a6b3921de109c4107cbfd91ae82ad2c3b6c4bb4d965336ce
4
- data.tar.gz: ac58b2cbc0b2b6e01451598194494a924e707ee8c0e29b6462961f8f1a51b3eb
3
+ metadata.gz: 6c28ded8e47d7c6efaf2f84003d55f829e0c6d3c0cfdf869b2715d7391c78dad
4
+ data.tar.gz: 6fa3d05c94ed8f9f4de3b867477abf4df9970fe01b8e7edd76f6df65bd3dc2e1
5
5
  SHA512:
6
- metadata.gz: 9a41d2f31a4f67d4dae2b62252fa4a4ac81b60561d72f780f5b2718fa1ba98ba6c3b716323e4e3e25ed7e2ad93b755f3f4403fa86265b0c9c849bf93f52f1fe1
7
- data.tar.gz: 1a0cabf2eefb7b9d9c8f03dc11c6dc730a4db17ade1e4e20c2e0937c91139b6e37846bec0058f994b6b1053047d22ecbf7ef466859daf674330cc074e5912f7f
6
+ metadata.gz: 1aa18122b85d3ee30b910b0db5f92ea126b69cb962885020ab63bc915e37ac5702ecc078010c78157e9407dee587d6dadcc1461a00dd3d50e65f63d7c95ab5a4
7
+ data.tar.gz: 5610e7ed9c56ee84b6d54a0a618ea96a16b73c2ba63bd8b63bd2c1bb3112f887def6a90d8c434272d705cad7b898b3913c17e9e9cd7a1d78898dec16f6efb9f0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- make_menu (0.1.1)
4
+ make_menu (1.0.0)
5
5
  tty-screen (~> 0.8.2)
6
6
 
7
7
  GEM
@@ -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
@@ -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 'color_string'
5
- require_relative 'text_table'
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
- build makefile
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
- running = true
30
+ yield self if block_given?
31
+
32
+ Builder.build(makefile, self)
24
33
 
25
- while running
34
+ loop do
26
35
  system 'clear' if clear_screen?
27
36
 
28
37
  display_header
29
38
 
30
- puts colorize(TextTable.new(groups).to_s)
39
+ puts colorize(MakeMenu::Text::Table.new(groups).to_s)
31
40
  puts
32
- puts 'Press ENTER to quit'.align(:center).bold
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
- # rubocop:enable Metrics/MethodLength
44
+ prompt = 'Select option: '.align(:center)
45
45
 
46
- # Display the company logo and the status bar (if set)
47
- def display_header
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
- puts 'No annotated targets found!'.red.bold
92
- puts
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
- # rubocop:enable Metrics/MethodLength
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
- true
69
+ if pause_on_success?
70
+ puts "\nPress ENTER to continue....".dark
71
+ gets
72
+ end
73
+ end
128
74
  end
129
75
 
130
- # Apply word colorings
131
- # @param [String] string
132
- # @return [String]
133
- def colorize(string)
134
- return string unless Object.const_defined?("#{self.class.name}::HIGHLIGHTS")
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
- # Center each line of the logo across the screen
148
- def formatted_logo
149
- logo.split("\n")
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
- # Get the menu logo from the LOGO constant
155
- def logo
156
- return "\n#{" #{Dir.pwd.split('/').last} ".light_yellow_bg.black.bold}\n \n" unless Object.const_defined?("#{self.class.name}::LOGO")
90
+ def highlights
91
+ @highlights.merge!(yield) if block_given?
92
+ @highlights
93
+ end
157
94
 
158
- Object.const_get("#{self.class.name}::LOGO")
95
+ def header(&block)
96
+ @header = block
159
97
  end
160
98
 
161
- protected
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
- %i[yellow bold]
110
+ options[:group_title_color]
166
111
  end
167
112
 
168
- # Clear screen before and after each command
169
113
  def clear_screen?
170
- true
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
@@ -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 key to continue....\n"
21
+ puts "\nPress ENTER to continue....".dark
26
22
  gets
27
23
  end
28
24
  rescue StandardError => _e
29
- # ignore CTRL+C from within Make target
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MakeMenu
2
- VERSION = '0.1.1'
3
- end
4
+ VERSION = '1.0.0'
5
+ end
data/lib/make_menu.rb CHANGED
@@ -1,34 +1,16 @@
1
- require_relative 'make_menu/color_string'
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
- if (menu_name = ENV.fetch('MENU', nil))
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
- rescue LoadError, NameError => _e
23
- puts
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 = "make_menu"
6
+ s.name = 'make_menu'
5
7
  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"
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
- "https://github.com/MisterGrimalkin/make_menu"
17
- s.license = "MIT"
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.1.1
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: 2023-12-27 00:00:00.000000000 Z
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/color_string.rb
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/text_column.rb
41
- - lib/make_menu/text_table.rb
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
@@ -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