tty-box 0.2.1 → 0.6.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: 5b0c9524c939271bcc5168d3cc467b790657109218f53284a6328db8173225b8
4
- data.tar.gz: cfe75b8c4b2f3567e03bd511ecd5a914fcda3180ab4339fb82412255cfd24206
3
+ metadata.gz: 66cd38e818e47ac372868586d8a76cdeb41a98aa9efc59430ec89acb84629410
4
+ data.tar.gz: d96e96b93e46a9ed8b7432632618af9c54e4da5f9575df820c6f77987104baf2
5
5
  SHA512:
6
- metadata.gz: 0c5988a4893aa1c2411429c40d88445ad51a3a3306ecde927125868aa2741686835eabbebe538ea8338639981cf0985318d48e27f586057eb73b492b7797996d
7
- data.tar.gz: ce051a4e7751ecd3a8caaa50ff626fb4d7929bdb348025c41ed2324c74cd5b055225112d85bb2ffdb90420d05c36b8308131e4756f652eefb4755781e7bd94bc
6
+ metadata.gz: 6bc7bddb63b580b8430d3dcdf596801db37e2f5813c12b8512a89dc9aff4140ebf6478e91d828020ffb182c84ca81b990d3d346c27718491c420a5a0099deca7
7
+ data.tar.gz: 43756dd006eec4de539d4a802c69afa04ca1b56fdd29505725d7001418a9e37f94005fc4606daa6be2564a8a12d2b876cbfb4d2ff1f0b43254c7580a99961b48
@@ -1,5 +1,52 @@
1
1
  # Change log
2
2
 
3
+ ## [v0.6.0] - 2020-08-11
4
+
5
+ ### Changed
6
+ * Change to preserve newline characters when wrapping content
7
+ * Change gemspec to include metadata and remove test files
8
+ * Change to update pastel & strings dependencies
9
+
10
+ ### Fixed
11
+ * Fix Ruby 2.7 warnings
12
+
13
+ ## [v0.5.0] - 2019-10-08
14
+
15
+ ### Added
16
+ * Add ability to create frames without specifying width or height
17
+ * Add #info, #warn, #success, #error ready frames for status messages inspired by conversation with Konstantin Gredeskoul(@kigster)
18
+
19
+ ### Changed
20
+ * Change #frame to accept content as an argument in addition to a block
21
+ * Change to match titles with border styling
22
+
23
+ ## [v0.4.1] - 2019-08-28
24
+
25
+ ### Added
26
+ * Add example to demonstrate different line endings
27
+
28
+ ### Fixed
29
+ * Fix to handle different line endings
30
+
31
+ ## [v0.4.0] - 2019-06-05
32
+
33
+ ### Changed
34
+ * Change gemspec to require Ruby >= 2.0.0
35
+ * Change to update tty-cursor dependency
36
+
37
+ ### Fixed
38
+ * Fix issue with displaying box with colored content
39
+
40
+ ## [v0.3.0] - 2018-10-08
41
+
42
+ ### Added
43
+ * Add border parameters :top_left, :top_right, :bottom_left & :bottom_right to allow specifying values for box corners
44
+ * Add :ascii border type for drawing ASCII boxes
45
+
46
+ ### Fixed
47
+ * Fix box color fill to correctly recognise missing borders and match the height and width
48
+ * Fix absolute content positioning when borders are missing
49
+
3
50
  ## [v0.2.1] - 2018-09-10
4
51
 
5
52
  ### Fixed
@@ -15,6 +62,11 @@
15
62
 
16
63
  * Initial implementation and release
17
64
 
65
+ [v0.6.0]: https://github.com/piotrmurach/tty-box/compare/v0.5.0...v0.6.0
66
+ [v0.5.0]: https://github.com/piotrmurach/tty-box/compare/v0.4.1...v0.5.0
67
+ [v0.4.1]: https://github.com/piotrmurach/tty-box/compare/v0.4.0...v0.4.1
68
+ [v0.4.0]: https://github.com/piotrmurach/tty-box/compare/v0.3.0...v0.4.0
69
+ [v0.3.0]: https://github.com/piotrmurach/tty-box/compare/v0.2.1...v0.3.0
18
70
  [v0.2.1]: https://github.com/piotrmurach/tty-box/compare/v0.2.0...v0.2.1
19
71
  [v0.2.0]: https://github.com/piotrmurach/tty-box/compare/v0.1.0...v0.2.0
20
72
  [v0.1.0]: https://github.com/piotrmurach/tty-box/compare/v0.1.0
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <div align="center">
2
- <a href="https://piotrmurach.github.io/tty" target="_blank"><img width="130" src="https://cdn.rawgit.com/piotrmurach/tty/master/images/tty.png" alt="tty logo" /></a>
2
+ <a href="https://piotrmurach.github.io/tty" target="_blank"><img width="130" src="https://github.com/piotrmurach/tty/blob/master/images/tty.png" alt="tty logo" /></a>
3
3
  </div>
4
4
 
5
5
  # TTY::Box [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter]
@@ -19,18 +19,18 @@
19
19
  [coverage]: https://coveralls.io/github/piotrmurach/tty-box
20
20
  [inchpages]: http://inch-ci.org/github/piotrmurach/tty-box
21
21
 
22
- > Draw various frames and boxes in your terminal interface.
22
+ > Draw various frames and boxes in the terminal window.
23
23
 
24
24
  **TTY::Box** provides box drawing component for [TTY](https://github.com/piotrmurach/tty) toolkit.
25
25
 
26
- ![Box drawing](https://cdn.rawgit.com/piotrmurach/tty-box/master/assets/tty-box-drawing.png)
26
+ ![Box drawing](https://github.com/piotrmurach/tty-box/blob/master/assets/tty-box-drawing.png)
27
27
 
28
28
  ## Installation
29
29
 
30
30
  Add this line to your application's Gemfile:
31
31
 
32
32
  ```ruby
33
- gem 'tty-box'
33
+ gem "tty-box"
34
34
  ```
35
35
 
36
36
  And then execute:
@@ -52,49 +52,90 @@ Or install it yourself as:
52
52
  * [2.5 border](#25-border)
53
53
  * [2.6 styling](#26-styling)
54
54
  * [2.7 formatting](#27-formatting)
55
+ * [2.8 messages](#28-messages)
56
+ * [2.8.1 info](#281-info)
57
+ * [2.8.2 warn](#282-warn)
58
+ * [2.8.3 success](#283-success)
59
+ * [2.8.4 error](#284-error)
55
60
 
56
61
  ## 1. Usage
57
62
 
58
63
  Using the `frame` method, you can draw a box in a terminal emulator:
59
64
 
60
65
  ```ruby
61
- box = TTY::Box.frame(
62
- width: 30,
63
- height: 10,
64
- align: :center,
65
- padding: 3
66
- ) do
67
- "Drawing a box in terminal emulator"
68
- end
69
-
66
+ box = TTY::Box.frame "Drawing a box in", "terminal emulator", padding: 3, align: :center
70
67
  ```
71
68
 
69
+ And then print:
70
+
72
71
  ```ruby
73
72
  print box
74
73
  # =>
75
- # ┌────────────────────────────┐
76
- # │
77
- # │
78
- # │
79
- # │ Drawing a box in
80
- # │ terminal emulator
81
- # │
82
- # │
83
- # │
84
- # └────────────────────────────┘
74
+ # ┌───────────────────────┐
75
+ # │
76
+ # │
77
+ # │
78
+ # │ Drawing a box in
79
+ # │ terminal emulator
80
+ # │
81
+ # │
82
+ # │
83
+ # └───────────────────────┘
85
84
  ```
86
85
 
87
86
  ## 2. Interface
88
87
 
89
88
  ### 2.1 frame
90
89
 
91
- You can draw a box in the top left corner of your terminal window by using the `frame` method and providing at the very minimum the height and the width:
90
+ You can draw a box in your terminal window by using the `frame` method and passing a content to display. By default the box will be drawn around the content.
91
+
92
+ ```ruby
93
+ print TTY::Box.frame "Hello world!"
94
+ # =>
95
+ # ┌────────────┐
96
+ # │Hello world!│
97
+ # └────────────┘
98
+ ```
99
+
100
+ You can also provide multi line content as separate arguments.
101
+
102
+ ```ruby
103
+ print TTY::Box.frame "Hello", "world!"
104
+ # =>
105
+ # ┌──────┐
106
+ # │Hello │
107
+ # │world!│
108
+ # └──────┘
109
+ ```
110
+
111
+ Alternatively, provide a multi line content using newline chars in a single argument:
112
+
113
+ ```ruby
114
+ print TTY::Box.frame "Hello\nworld!"
115
+ # =>
116
+ # ┌──────┐
117
+ # │Hello │
118
+ # │world!│
119
+ # └──────┘
120
+ ```
121
+
122
+ Finally, you can use a block to specify content:
123
+
124
+ ```ruby
125
+ print TTY::Box.frame { "Hello world!" }
126
+ # =>
127
+ # ┌────────────┐
128
+ # │Hello world!│
129
+ # └────────────┘
130
+ ```
131
+
132
+ You can also enforce a given box size without any content and use [tty-cursor](https://github.com/piotrmurach/tty-cursor) to position content whatever you like.
92
133
 
93
134
  ```ruby
94
135
  box = TTY::Box.frame(width: 30, height: 10)
95
136
  ```
96
137
 
97
- which when printed will prodcue the following output in your terminal:
138
+ When printed will produce the following output in your terminal:
98
139
 
99
140
  ```ruby
100
141
  print box
@@ -111,8 +152,6 @@ print box
111
152
  # └────────────────────────────┘
112
153
  ```
113
154
 
114
- Then you can use [tty-cursor](https://github.com/piotrmurach/tty-cursor) to directly manipulate content to be displayed inside the box.
115
-
116
155
  Alternatively, you can also pass a block to provide a content for the box:
117
156
 
118
157
  ```ruby
@@ -121,7 +160,7 @@ box = TTY::Box.frame(width: 30, height: 10) do
121
160
  end
122
161
  ```
123
162
 
124
- which when printed will produce the following output in your terminal:
163
+ When printed will produce the following output in your terminal:
125
164
 
126
165
  ```ruby
127
166
  print box
@@ -172,7 +211,7 @@ You can specify titles using the `:title` keyword and a hash value that contains
172
211
 
173
212
 
174
213
  ```ruby
175
- box = TTY::Box.frame(width: 30, height: 10, title: {top_left: 'TITLE', bottom_right: 'v1.0'})
214
+ box = TTY::Box.frame(width: 30, height: 10, title: {top_left: "TITLE", bottom_right: "v1.0"})
176
215
  ```
177
216
 
178
217
  which when printed in console will render the following:
@@ -194,10 +233,10 @@ print box
194
233
 
195
234
  ### 2.5 border
196
235
 
197
- There are two types of border `:light` and `:thick`. By default the `:light` border is used. This can be changed using the `:border` keyword:
236
+ There are three types of border `:ascii`, `:light`, `:thick`. By default the `:light` border is used. This can be changed using the `:border` keyword:
198
237
 
199
238
  ```ruby
200
- box = TTY::Box.new(width 30, height: 10, border: :thick)
239
+ box = TTY::Box.frame(width: 30, height: 10, border: :thick)
201
240
  ```
202
241
 
203
242
  and printing the box out to console will produce:
@@ -217,10 +256,66 @@ print box
217
256
  # ╚════════════════════════════╝
218
257
  ```
219
258
 
220
- You can also selectively switch off border sides by passing options hash. For example to remove bottom border do:
259
+ You can also selectively specify and turn off border parts by passing a hash with a `:border` key. The border parts are:
260
+
261
+ ```
262
+ :top
263
+ :top_left ┌────────┐ :top_right
264
+ │ │
265
+ :left │ │ :right
266
+ │ │
267
+ :bottom_left └────────┘ :bottom_right
268
+ :bottom
269
+ ```
270
+
271
+ The following are available border parts values:
272
+
273
+ | Border values | ASCII | Unicode Light | Unicode Thick |
274
+ | -------------------- |:-----:|:-------------:|:-------------:|
275
+ | :line | `-` | `─` | `═` |
276
+ | :pipe | `\|` | `\│` | `\║` |
277
+ | :cross | `+` | `┼` | `╬` |
278
+ | :divider_up | `+` | `┴` | `╩` |
279
+ | :divider_down | `+` | `┬` | `╦` |
280
+ | :divider_left | `+` | `┤` | `╣` |
281
+ | :divider_right | `+` | `├` | `╠` |
282
+ | :corner_top_left | `+` | `┌` | `╔` |
283
+ | :corner_top_right | `+` | `┐` | `╗` |
284
+ | :corner_bottom_left | `+` | `└` | `╚` |
285
+ | :corner_bottom_right | `+` | `┘` | `╝` |
286
+
287
+ For example, to change all box corners to be a `:cross` do:
288
+
289
+ ```ruby
290
+ box = TTY::Box.frame(
291
+ width: 10, height: 4,
292
+ border: {
293
+ top_left: :cross,
294
+ top_right: :cross,
295
+ bottom_left: :cross,
296
+ bottom_right: :cross
297
+ }
298
+ )
299
+ ```
221
300
 
222
301
  ```ruby
223
- TTY::Box.new(width: 30, height: 10, border: {type: :thick, bottom: false})
302
+ print box
303
+ # =>
304
+ # ┼────────┼
305
+ # │ │
306
+ # │ │
307
+ # ┼────────┼
308
+ ```
309
+
310
+ If you want to remove a given border element as a value use `false`. For example to remove bottom border do:
311
+
312
+ ```ruby
313
+ TTY::Box.new(
314
+ width: 30, height: 10,
315
+ border: {
316
+ type: :thick,
317
+ bottom: false
318
+ })
224
319
  ```
225
320
 
226
321
  ### 2.6 styling
@@ -301,6 +396,90 @@ print box
301
396
  #
302
397
  ```
303
398
 
399
+ ### 2.8 messages
400
+
401
+ ![Box messages](https://github.com/piotrmurach/tty-box/blob/master/assets/tty-box-messages.png)
402
+
403
+ #### 2.8.1 info
404
+
405
+ To draw an information type box around your content use `info`:
406
+
407
+ ```ruby
408
+ box = TTY::Box.info("Deploying application")
409
+ ```
410
+
411
+ And then print:
412
+
413
+ ```ruby
414
+ print box
415
+ # =>
416
+ # ╔ ℹ INFO ═══════════════╗
417
+ # ║ ║
418
+ # ║ Deploying application ║
419
+ # ║ ║
420
+ # ╚═══════════════════════╝
421
+ ```
422
+
423
+ #### 2.8.2 warn
424
+
425
+ To draw a warning type box around your content use `warn`:
426
+
427
+ ```ruby
428
+ box = TTY::Box.warn("Deploying application")
429
+ ```
430
+
431
+ And then print:
432
+
433
+ ```ruby
434
+ print box
435
+ # =>
436
+ # ╔ ⚠ WARNING ════════════╗
437
+ # ║ ║
438
+ # ║ Deploying application ║
439
+ # ║ ║
440
+ # ╚═══════════════════════╝
441
+ ```
442
+
443
+ #### 2.8.3 success
444
+
445
+ To draw a success type box around your content use `success`:
446
+
447
+ ```ruby
448
+ box = TTY::Box.success("Deploying application")
449
+ ```
450
+
451
+ And then print:
452
+
453
+ ```ruby
454
+ print box
455
+ # =>
456
+ # ╔ ✔ OK ═════════════════╗
457
+ # ║ ║
458
+ # ║ Deploying application ║
459
+ # ║ ║
460
+ # ╚═══════════════════════╝
461
+ ```
462
+
463
+ #### 2.8.4 error
464
+
465
+ To draw an error type box around your content use `error`:
466
+
467
+ ```ruby
468
+ box = TTY::Box.error("Deploying application")
469
+ ```
470
+
471
+ And then print:
472
+
473
+ ```ruby
474
+ print box
475
+ # =>
476
+ # ╔ ⨯ ERROR ══════════════╗
477
+ # ║ ║
478
+ # ║ Deploying application ║
479
+ # ║ ║
480
+ # ╚═══════════════════════╝
481
+ ```
482
+
304
483
  ## Development
305
484
 
306
485
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -11,53 +11,58 @@ module TTY
11
11
  module Box
12
12
  module_function
13
13
 
14
+ NEWLINE = "\n"
15
+
16
+ LINE_BREAK = %r{\r\n|\r|\n}.freeze
17
+
14
18
  BOX_CHARS = {
19
+ ascii: %w[+ + + + + + + + - | +],
15
20
  light: %w[┘ ┐ ┌ └ ┤ ┴ ┬ ├ ─ │ ┼],
16
21
  thick: %w[╝ ╗ ╔ ╚ ╣ ╩ ╦ ╠ ═ ║ ╬]
17
22
  }.freeze
18
23
 
19
- def line_char(border = :light)
20
- BOX_CHARS[border][8]
24
+ def corner_bottom_right_char(border = :light)
25
+ BOX_CHARS[border][0]
21
26
  end
22
27
 
23
- def pipe_char(border = :light)
24
- BOX_CHARS[border][9]
28
+ def corner_top_right_char(border = :light)
29
+ BOX_CHARS[border][1]
25
30
  end
26
31
 
27
- def cross_char(border = :light)
28
- BOX_CHARS[border][10]
32
+ def corner_top_left_char(border = :light)
33
+ BOX_CHARS[border][2]
29
34
  end
30
35
 
31
- def left_divider_char(border = :light)
32
- BOX_CHARS[border][7]
36
+ def corner_bottom_left_char(border = :light)
37
+ BOX_CHARS[border][3]
33
38
  end
34
39
 
35
- def right_divider_char(border = :light)
40
+ def divider_left_char(border = :light)
36
41
  BOX_CHARS[border][4]
37
42
  end
38
43
 
39
- def top_left_char(border = :light)
40
- BOX_CHARS[border][2]
44
+ def divider_up_char(border = :light)
45
+ BOX_CHARS[border][5]
41
46
  end
42
47
 
43
- def top_divider_char(border = :light)
48
+ def divider_down_char(border = :light)
44
49
  BOX_CHARS[border][6]
45
50
  end
46
51
 
47
- def top_right_char(border = :light)
48
- BOX_CHARS[border][1]
52
+ def divider_right_char(border = :light)
53
+ BOX_CHARS[border][7]
49
54
  end
50
55
 
51
- def bottom_left_char(border = :light)
52
- BOX_CHARS[border][3]
56
+ def line_char(border = :light)
57
+ BOX_CHARS[border][8]
53
58
  end
54
59
 
55
- def bottom_divider_char(border = :light)
56
- BOX_CHARS[border][5]
60
+ def pipe_char(border = :light)
61
+ BOX_CHARS[border][9]
57
62
  end
58
63
 
59
- def bottom_right_char(border = :light)
60
- BOX_CHARS[border][0]
64
+ def cross_char(border = :light)
65
+ BOX_CHARS[border][10]
61
66
  end
62
67
 
63
68
  def cursor
@@ -68,70 +73,228 @@ module TTY
68
73
  @color ||= Pastel.new
69
74
  end
70
75
 
76
+ # A frame for info type message
77
+ #
78
+ # @param [String] message
79
+ # the message to display
80
+ #
81
+ # @api public
82
+ def info(message, **opts)
83
+ new_opts = {
84
+ title: { top_left: " ℹ INFO " },
85
+ border: { type: :thick },
86
+ padding: 1,
87
+ style: {
88
+ fg: :black,
89
+ bg: :bright_blue,
90
+ border: {
91
+ fg: :black,
92
+ bg: :bright_blue
93
+ }
94
+ }
95
+ }.merge(opts)
96
+ frame(**new_opts) { message }
97
+ end
98
+
99
+ # A frame for warning type message
100
+ #
101
+ # @param [String] message
102
+ # the message to display
103
+ #
104
+ # @api public
105
+ def warn(message, **opts)
106
+ new_opts = {
107
+ title: { top_left: " ⚠ WARNING " },
108
+ border: { type: :thick },
109
+ padding: 1,
110
+ style: {
111
+ fg: :black,
112
+ bg: :bright_yellow,
113
+ border: {
114
+ fg: :black,
115
+ bg: :bright_yellow
116
+ }
117
+ }
118
+ }.merge(opts)
119
+ frame(**new_opts) { message }
120
+ end
121
+
122
+ # A frame for for success type message
123
+ #
124
+ # @param [String] message
125
+ # the message to display
126
+ #
127
+ # @api public
128
+ def success(message, **opts)
129
+ new_opts = {
130
+ title: { top_left: " ✔ OK " },
131
+ border: { type: :thick },
132
+ padding: 1,
133
+ style: {
134
+ fg: :black,
135
+ bg: :bright_green,
136
+ border: {
137
+ fg: :black,
138
+ bg: :bright_green,
139
+ }
140
+ }
141
+ }.merge(opts)
142
+ frame(**new_opts) { message }
143
+ end
144
+
145
+ # A frame for error type message
146
+ #
147
+ # @param [String] message
148
+ # the message to display
149
+ #
150
+ # @api public
151
+ def error(message, **opts)
152
+ new_opts = {
153
+ title: { top_left: " ⨯ ERROR " },
154
+ border: { type: :thick },
155
+ padding: 1,
156
+ style: {
157
+ fg: :bright_white,
158
+ bg: :red,
159
+ border: {
160
+ fg: :bright_white,
161
+ bg: :red
162
+ }
163
+ }
164
+ }.merge(opts)
165
+ frame(**new_opts) { message }
166
+ end
167
+
71
168
  # Create a frame
72
169
  #
170
+ # @param [Integer] top
171
+ # the offset from the terminal top
172
+ # @param [Integer] left
173
+ # the offset from the terminal left
174
+ # @param [Integer] width
175
+ # the width of the box
176
+ # @param [Integer] height
177
+ # the height of the box
178
+ # @param [Symbol] align
179
+ # the content alignment
180
+ # @param [Integer,Array[Integer]] padding
181
+ # the padding around content
182
+ # @param [Hash] title
183
+ # the title for top or bottom border
184
+ # @param [Hash, Symbol] border
185
+ # the border type
186
+ # @param [Hash] style
187
+ # the styling for the front and background
188
+ #
73
189
  # @api public
74
- def frame(top: nil, left: nil, width: 35, height: 3, align: :left, padding: 0,
75
- title: {}, border: :light, style: {})
190
+ def frame(*content, top: nil, left: nil, width: nil, height: nil, align: :left,
191
+ padding: 0, title: {}, border: :light, style: {})
76
192
  output = []
77
- content = []
193
+ sep = NEWLINE
78
194
  position = top && left
79
195
 
80
196
  border = Border.parse(border)
197
+ top_size = border.top? ? 1: 0
198
+ bottom_size = border.bottom? ? 1: 0
199
+ left_size = border.left? ? 1 : 0
200
+ right_size = border.right ? 1 : 0
81
201
 
82
202
  if block_given?
83
- content = format(yield, width, padding, align)
203
+ str = yield
204
+ else
205
+ str = case content.size
206
+ when 0 then ""
207
+ when 1 then content[0]
208
+ else content.join(NEWLINE)
209
+ end
84
210
  end
85
211
 
212
+ sep = str[LINE_BREAK] || NEWLINE # infer line break
213
+ lines = str.split(sep)
214
+ # infer dimensions
215
+ dimensions = infer_dimensions(lines, padding)
216
+ width ||= left_size + dimensions[0] + right_size
217
+ width = [width, top_space_taken(title, border), bottom_space_taken(title, border)].max
218
+
219
+ height ||= top_size + dimensions[1] + bottom_size
220
+ content = format(str, width, padding, align) # adjust content
221
+ # infer styling
86
222
  fg, bg = *extract_style(style)
87
223
  border_fg, border_bg = *extract_style(style[:border] || {})
88
224
 
89
225
  if border.top?
90
226
  output << cursor.move_to(left, top) if position
91
- output << top_border(title, width, border.type, style)
92
- output << "\n" unless position
227
+ output << top_border(title, width, border, style)
228
+ output << sep unless position
93
229
  end
94
- (height - 2).times do |i|
230
+
231
+ (height - top_size - bottom_size).times do |i|
232
+ output << cursor.move_to(left, top + i + top_size) if position
95
233
  if border.left?
96
- output << cursor.move_to(left, top + i + 1) if position
97
234
  output << border_bg.(border_fg.(pipe_char(border.type)))
98
235
  end
99
- if content[i].nil? && (style[:fg] || style[:bg] || !position)
100
- output << bg.(fg.(' ' * (width - 2)))
101
- else
236
+
237
+ content_size = width - left_size - right_size
238
+ unless content[i].nil?
102
239
  output << bg.(fg.(content[i]))
103
- if style[:fg] || style[:bg]
104
- output << bg.(fg.(' ' * (width - 2 - content[i].size)))
105
- end
240
+ size = Strings::ANSI.sanitize(content[i]).scan(/[[:print:]]/).join.size
241
+ content_size -= size
242
+ end
243
+ if style[:fg] || style[:bg] || !position # something to color
244
+ output << bg.(fg.(" " * content_size))
106
245
  end
246
+
107
247
  if border.right?
108
- output << cursor.move_to(left + width - 1, top + i + 1) if position
248
+ if position
249
+ output << cursor.move_to(left + width - right_size, top + i + top_size)
250
+ end
109
251
  output << border_bg.(border_fg.(pipe_char(border.type)))
110
252
  end
111
- output << "\n" unless position
253
+ output << sep unless position
112
254
  end
255
+
113
256
  if border.bottom?
114
- output << cursor.move_to(left, top + height - 1) if position
115
- output << bottom_border(title, width, border.type, style)
116
- output << "\n" unless position
257
+ output << cursor.move_to(left, top + height - bottom_size) if position
258
+ output << bottom_border(title, width, border, style)
259
+ output << sep unless position
117
260
  end
118
261
 
119
262
  output.join
120
263
  end
121
264
 
265
+ # Infer box dimensions based on content lines and padding
266
+ #
267
+ # @param [Array[String]] lines
268
+ # @param [Array[Integer]|Integer] padding
269
+ #
270
+ # @return [Array[Integer]]
271
+ #
272
+ # @api private
273
+ def infer_dimensions(lines, padding)
274
+ pad = Strings::Padder.parse(padding)
275
+ content_height = lines.size
276
+ content_width = lines.empty? ? 1 : lines.max_by(&:length).length
277
+ width = pad.left + content_width + pad.right
278
+ height = pad.top + content_height + pad.bottom
279
+ [width, height]
280
+ end
281
+
122
282
  # Format content
123
283
  #
124
284
  # @return [Array[String]]
125
285
  #
126
286
  # @api private
127
287
  def format(content, width, padding, align)
288
+ return "" if content.to_s.empty?
289
+
128
290
  pad = Strings::Padder.parse(padding)
129
291
  total_width = width - 2 - (pad.left + pad.right)
292
+ sep = content[LINE_BREAK] || NEWLINE # infer line break
130
293
 
131
294
  wrapped = Strings.wrap(content, total_width)
132
295
  aligned = Strings.align(wrapped, total_width, direction: align)
133
296
  padded = Strings.pad(aligned, padding)
134
- padded.split("\n")
297
+ padded.split(sep)
135
298
  end
136
299
 
137
300
  # Convert style keywords into styling
@@ -145,32 +308,99 @@ module TTY
145
308
  [fg, bg]
146
309
  end
147
310
 
311
+ # Top space taken by titles and corners
312
+ #
313
+ # @return [Integer]
314
+ #
315
+ # @api private
316
+ def top_space_taken(title, border)
317
+ top_titles_size(title) + top_left_corner(border).size + top_right_corner(border).size
318
+ end
319
+
320
+ # Top left corner
321
+ #
322
+ # @return [String]
323
+ #
324
+ # @api private
325
+ def top_left_corner(border)
326
+ border.top_left? && border.left? ? send(:"#{border.top_left}_char", border.type) : ""
327
+ end
328
+
329
+ # Top right corner
330
+ #
331
+ # @return [String]
332
+ #
333
+ # @api private
334
+ def top_right_corner(border)
335
+ border.top_right? && border.right? ? send(:"#{border.top_right}_char", border.type) : ""
336
+ end
337
+
338
+ # Top titles size
339
+ #
340
+ # @return [Integer]
341
+ #
342
+ # @api private
343
+ def top_titles_size(title)
344
+ title[:top_left].to_s.size + title[:top_center].to_s.size + title[:top_right].to_s.size
345
+ end
346
+
148
347
  # Top border
149
348
  #
150
349
  # @return [String]
151
350
  #
152
351
  # @api private
153
352
  def top_border(title, width, border, style)
154
- top_titles_size = title[:top_left].to_s.size +
155
- title[:top_center].to_s.size +
156
- title[:top_right].to_s.size
157
353
  fg, bg = *extract_style(style[:border] || {})
158
354
 
159
- top_space_left = width - top_titles_size -
160
- top_left_char.size - top_right_char.size
355
+ top_space_left = width - top_space_taken(title, border)
161
356
  top_space_before = top_space_left / 2
162
- top_space_after = top_space_left / 2 + top_space_left % 2
357
+ top_space_after = top_space_left / 2 + top_space_left % 2
163
358
 
164
359
  [
165
- bg.(fg.(top_left_char(border))),
166
- bg.(title[:top_left].to_s),
167
- bg.(fg.(line_char(border) * top_space_before)),
168
- bg.(title[:top_center].to_s),
169
- bg.(fg.(line_char(border) * top_space_after)),
170
- bg.(title[:top_right].to_s),
171
- bg.(fg.(top_right_char(border)))
360
+ bg.(fg.(top_left_corner(border))),
361
+ bg.(fg.(title[:top_left].to_s)),
362
+ bg.(fg.(line_char(border.type) * top_space_before)),
363
+ bg.(fg.(title[:top_center].to_s)),
364
+ bg.(fg.(line_char(border.type) * top_space_after)),
365
+ bg.(fg.(title[:top_right].to_s)),
366
+ bg.(fg.(top_right_corner(border)))
172
367
  ].join('')
173
368
  end
369
+ # Bottom space taken by titles and corners
370
+ #
371
+ # @return [Integer]
372
+ #
373
+ # @api private
374
+ def bottom_space_taken(title, border)
375
+ bottom_titles_size(title) + bottom_left_corner(border).size + bottom_right_corner(border).size
376
+ end
377
+
378
+ # Bottom left corner
379
+ #
380
+ # @return [String]
381
+ #
382
+ # @api private
383
+ def bottom_left_corner(border)
384
+ border.bottom_left? && border.left? ? send(:"#{border.bottom_left}_char", border.type) : ""
385
+ end
386
+
387
+ # Bottom right corner
388
+ #
389
+ # @return [String]
390
+ #
391
+ # @api private
392
+ def bottom_right_corner(border)
393
+ border.bottom_right? && border.right? ? send(:"#{border.bottom_right}_char", border.type) : ""
394
+ end
395
+
396
+ # Bottom titles size
397
+ #
398
+ # @return [Integer]
399
+ #
400
+ # @api private
401
+ def bottom_titles_size(title)
402
+ title[:bottom_left].to_s.size + title[:bottom_center].to_s.size + title[:bottom_right].to_s.size
403
+ end
174
404
 
175
405
  # Bottom border
176
406
  #
@@ -178,24 +408,20 @@ module TTY
178
408
  #
179
409
  # @api private
180
410
  def bottom_border(title, width, border, style)
181
- bottom_titles_size = title[:bottom_left].to_s.size +
182
- title[:bottom_center].to_s.size +
183
- title[:bottom_right].to_s.size
184
411
  fg, bg = *extract_style(style[:border] || {})
185
412
 
186
- bottom_space_left = width - bottom_titles_size -
187
- bottom_left_char.size - bottom_right_char.size
413
+ bottom_space_left = width - bottom_space_taken(title, border)
188
414
  bottom_space_before = bottom_space_left / 2
189
415
  bottom_space_after = bottom_space_left / 2 + bottom_space_left % 2
190
416
 
191
417
  [
192
- bg.(fg.(bottom_left_char(border))),
193
- bg.(title[:bottom_left].to_s),
194
- bg.(fg.(line_char(border) * bottom_space_before)),
195
- bg.(title[:bottom_center].to_s),
196
- bg.(fg.(line_char(border) * bottom_space_after)),
197
- bg.(title[:bottom_right].to_s),
198
- bg.(fg.(bottom_right_char(border)))
418
+ bg.(fg.(bottom_left_corner(border))),
419
+ bg.(fg.(title[:bottom_left].to_s)),
420
+ bg.(fg.(line_char(border.type) * bottom_space_before)),
421
+ bg.(fg.(title[:bottom_center].to_s)),
422
+ bg.(fg.(line_char(border.type) * bottom_space_after)),
423
+ bg.(fg.(title[:bottom_right].to_s)),
424
+ bg.(fg.(bottom_right_corner(border)))
199
425
  ].join('')
200
426
  end
201
427
  end # TTY