termline 1.1.1 → 1.1.3

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: a07c43d93ed4574a6264d64b32f29fccc25dd0e98f6cf86ec851e8c367c7a026
4
- data.tar.gz: 8347c016ceea2947a30ca216a38d8f9fc23b1f946e2421a7e5c320675871558a
3
+ metadata.gz: 1b593daa817cc923186c32b2f3170ba26038d3a8dde434ab9b47dd6ebc87511b
4
+ data.tar.gz: '048cced682cad7f691f3e2f37bf0e2ea53fa4ed3a8417a351abd8e7c692747b1'
5
5
  SHA512:
6
- metadata.gz: 18586ea12b6729ae4b0688d59437c322946982124cd75319acf8c89a980c113d671ce97a22b37ace1b134a7348be22811ed63dd3272d47702554dac3bc4d984d
7
- data.tar.gz: d7820e373323cf76f2660379874cf8e416441083e46a40cdea2abebbc2ac49d33c4c72e8de3f1c5119f9beb154888aeee041df1fc449e9441d0bff13a021f9e0
6
+ metadata.gz: f50437c2d6fdf48228d60cc086c6ef687ad0e8700aa36b2375fcd040e7cef3b7e6ec16d87f4347c89128cd358ecda03d52ba35b7e528d7a849de0316d926a615
7
+ data.tar.gz: d64e15285ad6cdd6f3266d1afc2b1b8a97a9518a306b560dc44276802671c746e353b47ecac984e3df8fb0876da9a73c2facf1bb991143be3ab1a053b74a1150
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Termline
4
+ module Details
5
+ class << self
6
+ def builder(data=nil, color: :blue, separator: ": ", icon: nil, icons: nil, **additional_data)
7
+ data = additional_data if data.nil?
8
+ data = data.merge(additional_data) if !additional_data.empty? && data.respond_to?(:merge)
9
+
10
+ unless data.respond_to?(:each_pair)
11
+ raise ArgumentError, "Details data must be a hash-like object"
12
+ end
13
+
14
+ pairs = data.each_pair.to_a
15
+ return "" if pairs.empty?
16
+
17
+ icon_map = normalize_icon_map(icons)
18
+ show_icon_column = !icon.nil? || !icon_map.empty?
19
+ key_width = pairs.map { |key, _value| key.to_s.length }.max
20
+
21
+ pairs.map do |key, value|
22
+ row_icon = icon_map.fetch(key.to_s, icon)
23
+ icon_prefix = format_icon(row_icon, color, show_icon_column)
24
+ label = Style.colorize(key.to_s.ljust(key_width), color)
25
+ "#{icon_prefix}#{label}#{separator}#{format_value(value)}"
26
+ end.join("\n")
27
+ end
28
+
29
+ private
30
+
31
+ def format_value(value)
32
+ case value
33
+ when nil
34
+ "nil"
35
+ when String
36
+ value
37
+ else
38
+ value.to_s
39
+ end
40
+ end
41
+
42
+ def normalize_icon_map(icons)
43
+ return {} if icons.nil?
44
+
45
+ unless icons.respond_to?(:each_pair)
46
+ raise ArgumentError, "Details icons must be a hash-like object"
47
+ end
48
+
49
+ icons.each_pair.each_with_object({}) do |(key, value), normalized|
50
+ normalized[key.to_s] = value
51
+ end
52
+ end
53
+
54
+ def format_icon(icon, color, show_icon_column)
55
+ return "" unless show_icon_column
56
+ return " " if icon.nil?
57
+
58
+ "#{Style.colorize(Style.icon(icon), color)} "
59
+ end
60
+ end
61
+ end
62
+ end
data/lib/termline/list.rb CHANGED
@@ -33,8 +33,26 @@ Building a better future, one line of code at a time.
33
33
  module Termline
34
34
  module List
35
35
  class << self
36
- def builder(items, icon:, color:)
37
- items.each { |item| puts(" #{Style.colorize(Style.icon(icon), color)} #{ item }" )}
36
+ def builder(items, icon: :debug, color: :default, ordered: false, start: 1)
37
+ first_number = integer_start(start)
38
+
39
+ items.each_with_index.map do |item, index|
40
+ marker = if ordered
41
+ Style.colorize("#{first_number + index}.", color)
42
+ else
43
+ Style.colorize(Style.icon(icon), color)
44
+ end
45
+
46
+ " #{marker} #{item}"
47
+ end.join("\n")
48
+ end
49
+
50
+ private
51
+
52
+ def integer_start(value)
53
+ Integer(value)
54
+ rescue ArgumentError, TypeError
55
+ raise ArgumentError, "List start must be an integer"
38
56
  end
39
57
  end
40
58
  end
data/lib/termline/msg.rb CHANGED
@@ -40,21 +40,31 @@ module Termline
40
40
  icon: nil,
41
41
  color: :default,
42
42
  bgcolor: :default,
43
- timestamp: Time.now.strftime('%H:%M:%S:%L'), data:nil)
43
+ timestamp: nil,
44
+ time:nil,
45
+ data:nil)
44
46
 
47
+ unless time.nil? || time == true || time == false
48
+ raise ArgumentError, "Message time must be true, false, or nil"
49
+ end
50
+
51
+ show_time = time.nil? ? Termline.timestamps? : time
45
52
  parts = []
46
- parts << "[#{timestamp}]" if timestamp
53
+ parts << "[#{timestamp || current_timestamp}]" if show_time
47
54
  parts << Style.colorize(Style.icon(icon), color) if icon
48
55
  parts << Style.colorize(tag, color) if tag
49
- parts << message
56
+ parts << if bgcolor == :default
57
+ message
58
+ else
59
+ Style.colorize(message, color, bgcolor)
60
+ end
50
61
  parts << data_text(data, color) if data
51
62
 
52
63
  parts.compact.map(&:to_s).reject(&:empty?).join(" ").strip
53
64
  end
54
65
 
55
- def alert message
56
- puts("\e[5m#{message}\e[0m")
57
- puts("\e[5m#{Style.colorize(message,:red)}\e[0m")
66
+ def alert(message)
67
+ Style.colorize(" #{message} ", :white, :red)
58
68
  end
59
69
 
60
70
  private
@@ -70,7 +80,11 @@ module Termline
70
80
 
71
81
  def data_text(data, color)
72
82
  return nil if data.nil? || data.empty?
73
- data.map { |key, value| "#{key}#{Style.colorize('=>', color)}#{format_value(value)}" }.join(" ")
83
+ data.map { |key, value| "#{Style.colorize(key, color)}=#{format_value(value)}" }.join(" ")
84
+ end
85
+
86
+ def current_timestamp
87
+ Time.now.strftime(Termline.timestamp_format)
74
88
  end
75
89
  end
76
90
  end
@@ -33,19 +33,34 @@ Building a better future, one line of code at a time.
33
33
  module Termline
34
34
  module Space
35
35
  class << self
36
- def builder(character, count)
36
+ def builder(character, count=8)
37
37
  character * count
38
38
  end
39
- def br count=1
40
- puts("\n" * count);
41
- end
42
-
43
- def line count=8
44
- puts('-·- ' * count)
39
+
40
+ def line(character, width)
41
+ character = character.to_s
42
+ raise ArgumentError, "Separator character cannot be empty" if character.empty?
43
+
44
+ width = positive_width(width)
45
+
46
+ (character * ((width.to_f / character.length).ceil))[0, width]
47
+ end
48
+
49
+ def color(color)
50
+ return "" unless Termline.terminal.ansi?
51
+
52
+ Style.pretty("", :black, color)
45
53
  end
46
54
 
47
- def color color
48
- Config.pretty("", :black, color)
55
+ private
56
+
57
+ def positive_width(value)
58
+ width = Integer(value)
59
+ raise ArgumentError unless width.positive?
60
+
61
+ width
62
+ rescue ArgumentError, TypeError
63
+ raise ArgumentError, "Separator width must be a positive integer"
49
64
  end
50
65
  end
51
66
  end
@@ -48,19 +48,19 @@ module Termline
48
48
  # standard color palette for texts
49
49
  COLORS = {
50
50
  skyblue: '96',
51
- default: '38',
51
+ default: '39',
52
52
  yellow: '1;33',
53
53
  white: '1;37',
54
54
  green: '32',
55
55
  black: '30',
56
56
  gray: '90',
57
- blue: '36',
57
+ blue: '34',
58
58
  red: '31'
59
59
  }.freeze
60
60
 
61
61
  # standard color palette for backgrounds
62
62
  BG_COLORS = {
63
- default: '0',
63
+ default: '49',
64
64
  yellow: '103',
65
65
  white: '107',
66
66
  green: '42',
@@ -68,39 +68,33 @@ module Termline
68
68
  blue: '44',
69
69
  red: '41'
70
70
  }.freeze
71
-
72
-
73
- # calculate the console width
74
- # tputcols is not available on windows
75
- WIDTH = begin
76
- term = ENV['TERM']
77
- term ? `tput cols`.to_i : 1
78
- rescue
79
- 1
80
- end
81
-
82
71
  class << self
83
- def icon icon
84
- ICONS[icon]
72
+ def icon(icon)
73
+ ICONS.fetch(icon) do
74
+ raise ArgumentError, "Unknown icon: #{icon.inspect}. Available icons: #{ICONS.keys.join(', ')}"
75
+ end
85
76
  end
86
77
 
87
78
  # Concat color to text
88
79
  def colorize(text, colour = :default, bg_colour = :default)
89
- colour_code = COLORS[colour]
90
- bg_colour_code = BG_COLORS[bg_colour]
91
- return "\e[#{bg_colour_code};#{colour_code}m#{text}\e[0m".squeeze(';')
80
+ colour_code = COLORS.fetch(colour) do
81
+ raise ArgumentError, "Unknown color: #{colour.inspect}. Available colors: #{COLORS.keys.join(', ')}"
82
+ end
83
+ bg_colour_code = BG_COLORS.fetch(bg_colour) do
84
+ raise ArgumentError, "Unknown background color: #{bg_colour.inspect}. Available background colors: #{BG_COLORS.keys.join(', ')}"
85
+ end
86
+ return text.to_s unless Termline.terminal.ansi?
87
+
88
+ return "\e[#{colour_code};#{bg_colour_code}m#{text}\e[0m"
92
89
  end
93
90
 
94
91
  # Format text
95
92
  def pretty(message, colour = :default, bg_colour = :default)
96
- width = 1
97
-
98
- unless bg_colour == :default
99
- width = WIDTH - message.length - 4
100
- width = 1 if width.negative?
101
- end
102
-
103
- return colorize("#{ message } #{"\ " * width}", colour, bg_colour)
93
+ text = message.to_s
94
+ return colorize(text, colour, bg_colour) if bg_colour == :default
95
+
96
+ padding = [Termline.terminal.width - text.length, 0].max
97
+ colorize("#{text}#{" " * padding}", colour, bg_colour)
104
98
  end
105
99
  end
106
100
  end
@@ -33,58 +33,180 @@ Building a better future, one line of code at a time.
33
33
  module Termline
34
34
  module Table
35
35
  class << self
36
- def builder data, header:true
36
+ ALIGNMENTS = [:auto, :left, :center, :right].freeze
37
37
 
38
- return if Gem.win_platform?
39
- return unless data.size > 0
38
+ def builder(data, header:true, align: :auto)
39
+ # Convert every supported input into a predictable matrix first.
40
+ # Rendering can then stay independent from Active Record and hashes.
41
+ source = normalize_source(data)
42
+ return "" if source.empty?
40
43
 
41
- table_from_array = true if data.first.class == Array
42
- header = false if table_from_array
44
+ keys, rows, hash_rows = normalize_rows(source)
45
+ return "" if rows.empty? || rows.first.empty?
43
46
 
44
- if data.class.name == "ActiveRecord::Relation"
45
- data = data.to_a.map(&:serializable_hash)
47
+ headers = hash_rows && header ? keys.map { |key| key.to_s.upcase } : nil
48
+ text_rows = rows.map { |row| row.map { |value| normalize_value(value) } }
49
+
50
+ # Widths are calculated from visible text before padding or borders
51
+ # are added, so the completed line never exceeds the terminal width.
52
+ widths = column_widths(text_rows, headers, Termline.terminal.width)
53
+ return "" unless widths
54
+
55
+ alignments = widths.each_index.map do |index|
56
+ column_alignment(align, index, keys[index], rows.map { |row| row[index] })
57
+ end
58
+
59
+ separator = render_separator(widths)
60
+ lines = []
61
+
62
+ if headers
63
+ lines << render_row(headers, widths, alignments)
64
+ lines << separator
65
+ else
66
+ lines << separator
67
+ end
68
+
69
+ text_rows.each do |row|
70
+ lines << render_row(row, widths, alignments)
71
+ end
72
+
73
+ lines << separator unless headers
74
+ lines.join("\n")
75
+ end
76
+
77
+ private
78
+
79
+ def normalize_source(data)
80
+ # A single hash is one row. Active Record support is detected only
81
+ # when Active Record is already loaded, keeping it an optional dependency.
82
+ return [] if data.nil?
83
+ return [data] if data.is_a?(Hash)
84
+
85
+ if active_record_relation?(data)
86
+ return data.to_a.map(&:serializable_hash)
87
+ end
88
+
89
+ unless data.respond_to?(:to_a)
90
+ raise ArgumentError, "Table data must be a hash or collection of rows"
91
+ end
92
+
93
+ data.to_a
94
+ end
95
+
96
+ def active_record_relation?(data)
97
+ defined?(::ActiveRecord::Relation) && data.is_a?(::ActiveRecord::Relation)
98
+ end
99
+
100
+ def normalize_rows(source)
101
+ if source.all? { |row| row.is_a?(Hash) }
102
+ # Use the union of keys in insertion order. This preserves columns
103
+ # that appear only in later rows and leaves missing values blank.
104
+ keys = source.flat_map(&:keys).uniq
105
+ rows = source.map { |row| keys.map { |key| row[key] } }
106
+ return [keys, rows, true]
107
+ end
108
+
109
+ unless source.all? { |row| row.is_a?(Array) }
110
+ raise ArgumentError, "Table rows must all be hashes or all be arrays"
46
111
  end
47
112
 
48
- # get the available characters in terminal width
49
- #terminal_width = `tput cols`.to_i
50
- terminal_width = Style::WIDTH
113
+ # Array rows have no column names. Pad short rows so every rendered
114
+ # row has the same number of cells and borders remain aligned.
115
+ column_count = source.map(&:length).max.to_i
116
+ rows = source.map { |row| row + Array.new(column_count - row.length) }
117
+ [Array.new(column_count) { |index| index }, rows, false]
118
+ end
119
+
120
+ def normalize_value(value)
121
+ # Tables are intentionally single-line components; embedded whitespace
122
+ # is collapsed instead of allowing one value to break the layout.
123
+ value.nil? ? "" : value.to_s.gsub(/\s+/, " ").strip
124
+ end
51
125
 
52
- # get the number of columns to print base on the data hash
53
- cols = data.first.length + 1
126
+ def column_widths(rows, headers, terminal_width)
127
+ column_count = rows.first.length
54
128
 
55
- # get the available space for every column
56
- col_width = (terminal_width / cols) - 1
129
+ # Each column contributes two padding spaces and one separator.
130
+ # The final `+ 1` accounts for the closing border.
131
+ available = terminal_width - ((column_count * 3) + 1)
132
+ return nil if available < column_count
57
133
 
58
- # validate that we have minimum space to render the table
59
- return if col_width <= 0
134
+ widths = Array.new(column_count, 1)
135
+ column_count.times do |index|
136
+ values = rows.map { |row| row[index].length }
137
+ values << headers[index].length if headers
138
+ widths[index] = values.max
139
+ end
60
140
 
61
- # separator for every column and row
62
- separator = ('| ' << ('- ' * (col_width / 2)))
141
+ # Repeatedly shrink the widest column. This favors compact columns
142
+ # while sharing scarce space fairly between long text columns.
143
+ while widths.sum > available
144
+ index = widths.each_index.select { |position| widths[position] > 1 }.max_by { |position| widths[position] }
145
+ break unless index
63
146
 
64
- # add extra blank spaces to adjust the col_width only if col_width not a even number
65
- separator += (' ') if (col_width - separator.size).odd?
147
+ widths[index] -= 1
148
+ end
149
+
150
+ widths
151
+ end
66
152
 
67
- # only for table header
68
- if header
153
+ def column_alignment(align, index, key, values)
154
+ # Alignment may be global, positional, or keyed by the original hash
155
+ # key. Missing per-column settings fall back to automatic alignment.
156
+ requested = case align
157
+ when Hash
158
+ align.fetch(key) do
159
+ align.fetch(key.to_s) { align.fetch(index, :auto) }
160
+ end
161
+ when Array
162
+ align[index] || :auto
163
+ else
164
+ align
165
+ end
69
166
 
70
- # print table titles
71
- puts '| ' << data.first.keys.map { |key| key.to_s.upcase.ljust(col_width) }.join('| ')
72
- end
167
+ requested = requested.to_sym if requested.respond_to?(:to_sym)
168
+ unless ALIGNMENTS.include?(requested)
169
+ raise ArgumentError, "Unknown table alignment: #{requested.inspect}. Available alignments: #{ALIGNMENTS.join(', ')}"
170
+ end
73
171
 
74
- # print header separators, only for visible columns
75
- puts separator * (cols - 1)
172
+ return requested unless requested == :auto
76
173
 
77
- # print data as table :)
78
- data.each_with_index do |row, index|
174
+ # A column is numeric only when all of its non-nil source values are
175
+ # numeric. Numeric-looking strings remain left-aligned intentionally.
176
+ printable_values = values.compact
177
+ printable_values.any? && printable_values.all? { |value| value.is_a?(Numeric) } ? :right : :left
178
+ end
79
179
 
80
- # join hash values as a line and justify every value to print value in its own column
81
- puts '| ' << row.values.map { |value| value.to_s.ljust(col_width) }.join('| ') unless table_from_array
180
+ def render_separator(widths)
181
+ "|#{widths.map { |width| "-" * (width + 2) }.join("|")}|"
182
+ end
82
183
 
83
- # print simple value from the array data
84
- puts '| ' << row.map { |value| value.to_s.ljust(col_width) }.join('| ') if table_from_array
184
+ def render_row(row, widths, alignments)
185
+ cells = row.each_index.map do |index|
186
+ text = truncate(row[index], widths[index])
187
+ align_text(text, widths[index], alignments[index])
85
188
  end
86
189
 
87
- puts separator * (cols - 1) unless header
190
+ "| #{cells.join(' | ')} |"
191
+ end
192
+
193
+ def truncate(text, width)
194
+ return text if text.length <= width
195
+ return "…" if width == 1
196
+
197
+ # Reserve the final cell for an ellipsis so truncation is visible.
198
+ "#{text[0, width - 1]}…"
199
+ end
200
+
201
+ def align_text(text, width, alignment)
202
+ case alignment
203
+ when :right
204
+ text.rjust(width)
205
+ when :center
206
+ text.center(width)
207
+ else
208
+ text.ljust(width)
209
+ end
88
210
  end
89
211
  end
90
212
  end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "io/console"
4
+
5
+ module Termline
6
+ class Terminal
7
+ DEFAULT_WIDTH = 80
8
+ ANSI_MODES = [true, false, :auto].freeze
9
+
10
+ attr_reader :ansi
11
+
12
+ def initialize(output: nil, ansi: :auto, width: nil)
13
+ self.output = output
14
+ self.ansi = ansi
15
+ self.width = width
16
+ end
17
+
18
+ def output
19
+ @output || $stdout
20
+ end
21
+
22
+ def output=(io)
23
+ unless io.nil? || io.respond_to?(:puts)
24
+ raise ArgumentError, "Output must respond to #puts"
25
+ end
26
+
27
+ @output = io
28
+ end
29
+
30
+ def ansi=(mode)
31
+ unless ANSI_MODES.include?(mode)
32
+ raise ArgumentError, "ANSI mode must be true, false, or :auto"
33
+ end
34
+
35
+ @ansi = mode
36
+ end
37
+
38
+ def ansi?
39
+ return ansi if ansi == true || ansi == false
40
+
41
+ output.respond_to?(:tty?) && output.tty? && !ENV.key?("NO_COLOR") && ENV["TERM"] != "dumb"
42
+ end
43
+
44
+ def width
45
+ return @width if @width
46
+
47
+ width_from(output) || environment_width || width_from(IO.console) || DEFAULT_WIDTH
48
+ end
49
+
50
+ def width=(value)
51
+ if value.nil?
52
+ @width = nil
53
+ return
54
+ end
55
+
56
+ width = Integer(value)
57
+ raise ArgumentError, "Terminal width must be greater than zero" unless width.positive?
58
+
59
+ @width = width
60
+ rescue ArgumentError, TypeError
61
+ raise ArgumentError, "Terminal width must be a positive integer"
62
+ end
63
+
64
+ private
65
+
66
+ def environment_width
67
+ value = Integer(ENV.fetch("COLUMNS", 0))
68
+ value if value.positive?
69
+ rescue ArgumentError, TypeError
70
+ nil
71
+ end
72
+
73
+ def width_from(io)
74
+ return unless io.respond_to?(:winsize)
75
+
76
+ value = io.winsize.last.to_i
77
+ value if value.positive?
78
+ rescue IOError, SystemCallError
79
+ nil
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Termline
4
+ module Text
5
+ class << self
6
+ def title(text, color: :blue, character: "─", width: Termline.terminal.width)
7
+ character = character.to_s
8
+ raise ArgumentError, "Title character cannot be empty" if character.empty?
9
+
10
+ width = positive_width(width)
11
+ prefix = "#{character * 2} #{text}"
12
+ plain = fit_title(prefix, character, width)
13
+
14
+ Style.colorize(plain, color)
15
+ end
16
+
17
+ private
18
+
19
+ def fit_title(prefix, character, width)
20
+ return truncate(prefix, width) if prefix.length >= width
21
+
22
+ remaining = width - prefix.length - 1
23
+ "#{prefix} #{fill(character, remaining)}"
24
+ end
25
+
26
+ def fill(character, width)
27
+ return "" unless width.positive?
28
+
29
+ (character * ((width.to_f / character.length).ceil))[0, width]
30
+ end
31
+
32
+ def truncate(text, width)
33
+ return text if text.length <= width
34
+ return "…" if width == 1
35
+
36
+ "#{text[0, width - 1]}…"
37
+ end
38
+
39
+ def positive_width(value)
40
+ width = Integer(value)
41
+ raise ArgumentError unless width.positive?
42
+
43
+ width
44
+ rescue ArgumentError, TypeError
45
+ raise ArgumentError, "Text width must be a positive integer"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,6 +1,8 @@
1
1
  =begin
2
2
 
3
- Copyright (c) 2022, Lesli Technologies, S. A.
3
+ Lesli
4
+
5
+ Copyright (c) 2026, Lesli Technologies, S. A.
4
6
 
5
7
  This program is free software: you can redistribute it and/or modify
6
8
  it under the terms of the GNU General Public License as published by
@@ -13,23 +15,22 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
15
  GNU General Public License for more details.
14
16
 
15
17
  You should have received a copy of the GNU General Public License
16
- along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ along with this program. If not, see http://www.gnu.org/licenses/.
17
19
 
18
- Lesli Ruby Messages - Message utilities for the Ruby console
20
+ Lesli · Ruby on Rails SaaS Development Framework.
19
21
 
20
- Powered by https://www.lesli.tech
22
+ Made with ♥ by LesliTech
21
23
  Building a better future, one line of code at a time.
22
24
 
23
- @contact <hello@lesli.tech>
24
- @website <https://www.lesli.tech>
25
+ @contact hello@lesli.tech
26
+ @website https://www.lesli.tech
25
27
  @license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
26
- @author The Lesli Development Team
27
28
 
28
- // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
29
+ // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
29
30
  // ·
30
31
  =end
31
32
 
32
33
  module Termline
33
- VERSION = "1.1.1"
34
- BUILD = "1775000800"
34
+ VERSION = "1.1.3"
35
+ BUILD = ""
35
36
  end