tty-box 0.2.1 → 0.6.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: 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