make_menu 0.1.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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