tty-box 0.3.0 → 0.7.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: de83dc517a44cecc9c6936beafcc7a9e92bbef44b9f835ce8c5f537a34fcfb99
4
- data.tar.gz: 4e5a152bf222a4a1a267fa97699b8f20cbe33be48c9d7847b83172dc26708c2a
3
+ metadata.gz: b9509817a402d4b790c91be69e5d9c11947ba1374a32ba0df8a06a18c333d371
4
+ data.tar.gz: ea1322ac1cedabf28d34c52fb666386b97013feffc6319cd9c24356039831597
5
5
  SHA512:
6
- metadata.gz: 8979a9ad2c06c9ff1762005feabfbdf28456e75a496c3a39a4276b7fe73a31fc9bb69c8941d4470efb8c2a7277bdbdce62de7c788f95622a24c299cd015a2e72
7
- data.tar.gz: 99f56b7376c5d96b933e1deb3f7282d37e7b7ce10130240516dc3408387feb38edb567de17ee72a96e9c1e5de49b3cde2c3f2d35fd0ff6cfc22767e1a010fb9b
6
+ metadata.gz: 78d7164d16610ae4fc585977880845b3bd83c2be04e5c417ec08ce1aa00d0a56d6ff90efa5a3229742e97ea4836faadd140d59aca936ed545dd3606d85bf27d4
7
+ data.tar.gz: 8c7af2b0bf020d0208065a6afdd2ad8a28edfe3ed731582fbf20f4f8b301b574ebd54b5f0c7d9f05db853a7f9e35a507e5092f8a18ab62feefcc0a84851472bb
@@ -1,5 +1,55 @@
1
1
  # Change log
2
2
 
3
+ ## [v0.7.0] - 2020-12-20
4
+
5
+ ### Added
6
+ * Add :enable_color configuration to allow control over colouring
7
+
8
+ ### Changed
9
+ * Change to ensure non-negative space filler size
10
+ * Change to enforce private visibility for the private module methods
11
+
12
+ ### Fixed
13
+ * Fix box width calculation to ignore colored text by @LainLayer
14
+ * Fix drawing frame around multiline colored content
15
+
16
+ ## [v0.6.0] - 2020-08-11
17
+
18
+ ### Changed
19
+ * Change to preserve newline characters when wrapping content
20
+ * Change gemspec to include metadata and remove test files
21
+ * Change to update pastel & strings dependencies
22
+
23
+ ### Fixed
24
+ * Fix Ruby 2.7 warnings
25
+
26
+ ## [v0.5.0] - 2019-10-08
27
+
28
+ ### Added
29
+ * Add ability to create frames without specifying width or height
30
+ * Add #info, #warn, #success, #error ready frames for status messages inspired by conversation with Konstantin Gredeskoul(@kigster)
31
+
32
+ ### Changed
33
+ * Change #frame to accept content as an argument in addition to a block
34
+ * Change to match titles with border styling
35
+
36
+ ## [v0.4.1] - 2019-08-28
37
+
38
+ ### Added
39
+ * Add example to demonstrate different line endings
40
+
41
+ ### Fixed
42
+ * Fix to handle different line endings
43
+
44
+ ## [v0.4.0] - 2019-06-05
45
+
46
+ ### Changed
47
+ * Change gemspec to require Ruby >= 2.0.0
48
+ * Change to update tty-cursor dependency
49
+
50
+ ### Fixed
51
+ * Fix issue with displaying box with colored content
52
+
3
53
  ## [v0.3.0] - 2018-10-08
4
54
 
5
55
  ### Added
@@ -7,7 +57,7 @@
7
57
  * Add :ascii border type for drawing ASCII boxes
8
58
 
9
59
  ### Fixed
10
- * Fix box color fill to corretly recognise missing borders and match the height and width
60
+ * Fix box color fill to correctly recognise missing borders and match the height and width
11
61
  * Fix absolute content positioning when borders are missing
12
62
 
13
63
  ## [v0.2.1] - 2018-09-10
@@ -25,6 +75,11 @@
25
75
 
26
76
  * Initial implementation and release
27
77
 
78
+ [v0.7.0]: https://github.com/piotrmurach/tty-box/compare/v0.6.0...v0.7.0
79
+ [v0.6.0]: https://github.com/piotrmurach/tty-box/compare/v0.5.0...v0.6.0
80
+ [v0.5.0]: https://github.com/piotrmurach/tty-box/compare/v0.4.1...v0.5.0
81
+ [v0.4.1]: https://github.com/piotrmurach/tty-box/compare/v0.4.0...v0.4.1
82
+ [v0.4.0]: https://github.com/piotrmurach/tty-box/compare/v0.3.0...v0.4.0
28
83
  [v0.3.0]: https://github.com/piotrmurach/tty-box/compare/v0.2.1...v0.3.0
29
84
  [v0.2.1]: https://github.com/piotrmurach/tty-box/compare/v0.2.0...v0.2.1
30
85
  [v0.2.0]: https://github.com/piotrmurach/tty-box/compare/v0.1.0...v0.2.0
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018 Piotr Murach
3
+ Copyright (c) 2018 Piotr Murach (https://piotrmurach.com)
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,11 +1,11 @@
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://ttytoolkit.org"><img width="130" src="https://github.com/piotrmurach/tty/blob/master/images/tty.png" alt="TTY Toolkit logo" /></a>
3
3
  </div>
4
4
 
5
5
  # TTY::Box [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter]
6
6
 
7
7
  [![Gem Version](https://badge.fury.io/rb/tty-box.svg)][gem]
8
- [![Build Status](https://secure.travis-ci.org/piotrmurach/tty-box.svg?branch=master)][travis]
8
+ [![Actions CI](https://github.com/piotrmurach/tty-box/workflows/CI/badge.svg?branch=master)][gh_actions_ci]
9
9
  [![Build status](https://ci.appveyor.com/api/projects/status/h9b88fk5xpya3fh1?svg=true)][appveyor]
10
10
  [![Maintainability](https://api.codeclimate.com/v1/badges/dfac05073e1549e9dbb6/maintainability)][codeclimate]
11
11
  [![Coverage Status](https://coveralls.io/repos/github/piotrmurach/tty-box/badge.svg)][coverage]
@@ -13,24 +13,24 @@
13
13
 
14
14
  [gitter]: https://gitter.im/piotrmurach/tty
15
15
  [gem]: http://badge.fury.io/rb/tty-box
16
- [travis]: http://travis-ci.org/piotrmurach/tty-box
16
+ [gh_actions_ci]: https://github.com/piotrmurach/tty-box/actions?query=workflow%3ACI
17
17
  [appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-box
18
18
  [codeclimate]: https://codeclimate.com/github/piotrmurach/tty-box/maintainability
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:
@@ -197,7 +236,7 @@ print box
197
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:
@@ -268,10 +307,10 @@ print box
268
307
  # ┼────────┼
269
308
  ```
270
309
 
271
- If you want to remoe a given border element as a value use `false`. For example to remove bottom border do:
310
+ If you want to remove a given border element as a value use `false`. For example to remove bottom border do:
272
311
 
273
312
  ```ruby
274
- TTY::Box.new(
313
+ TTY::Box.frame(
275
314
  width: 30, height: 10,
276
315
  border: {
277
316
  type: :thick,
@@ -296,6 +335,20 @@ style: {
296
335
 
297
336
  The above style configuration will produce the result similar to the top demo, a MS-DOS look & feel window.
298
337
 
338
+ You can disable or force output styling regardless of the terminal using the `enable_color` keyword. By default, the color support is automatically detected.
339
+
340
+ ```ruby
341
+ TTY::Box.frame({
342
+ enable_color: true, # force to always color output
343
+ style: {
344
+ border: {
345
+ fg: :bright_yellow,
346
+ bg: :blue
347
+ }
348
+ }
349
+ })
350
+ ```
351
+
299
352
  ### 2.7 formatting
300
353
 
301
354
  You can use `:align` keyword to format content either to be `:left`, `:center` or `:right` aligned:
@@ -357,6 +410,90 @@ print box
357
410
  #
358
411
  ```
359
412
 
413
+ ### 2.8 messages
414
+
415
+ ![Box messages](https://github.com/piotrmurach/tty-box/blob/master/assets/tty-box-messages.png)
416
+
417
+ #### 2.8.1 info
418
+
419
+ To draw an information type box around your content use `info`:
420
+
421
+ ```ruby
422
+ box = TTY::Box.info("Deploying application")
423
+ ```
424
+
425
+ And then print:
426
+
427
+ ```ruby
428
+ print box
429
+ # =>
430
+ # ╔ ℹ INFO ═══════════════╗
431
+ # ║ ║
432
+ # ║ Deploying application ║
433
+ # ║ ║
434
+ # ╚═══════════════════════╝
435
+ ```
436
+
437
+ #### 2.8.2 warn
438
+
439
+ To draw a warning type box around your content use `warn`:
440
+
441
+ ```ruby
442
+ box = TTY::Box.warn("Deploying application")
443
+ ```
444
+
445
+ And then print:
446
+
447
+ ```ruby
448
+ print box
449
+ # =>
450
+ # ╔ ⚠ WARNING ════════════╗
451
+ # ║ ║
452
+ # ║ Deploying application ║
453
+ # ║ ║
454
+ # ╚═══════════════════════╝
455
+ ```
456
+
457
+ #### 2.8.3 success
458
+
459
+ To draw a success type box around your content use `success`:
460
+
461
+ ```ruby
462
+ box = TTY::Box.success("Deploying application")
463
+ ```
464
+
465
+ And then print:
466
+
467
+ ```ruby
468
+ print box
469
+ # =>
470
+ # ╔ ✔ OK ═════════════════╗
471
+ # ║ ║
472
+ # ║ Deploying application ║
473
+ # ║ ║
474
+ # ╚═══════════════════════╝
475
+ ```
476
+
477
+ #### 2.8.4 error
478
+
479
+ To draw an error type box around your content use `error`:
480
+
481
+ ```ruby
482
+ box = TTY::Box.error("Deploying application")
483
+ ```
484
+
485
+ And then print:
486
+
487
+ ```ruby
488
+ print box
489
+ # =>
490
+ # ╔ ⨯ ERROR ══════════════╗
491
+ # ║ ║
492
+ # ║ Deploying application ║
493
+ # ║ ║
494
+ # ╚═══════════════════════╝
495
+ ```
496
+
360
497
  ## Development
361
498
 
362
499
  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.
@@ -1 +1 @@
1
- require_relative 'tty/box'
1
+ require_relative "tty/box"
@@ -1,16 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'strings'
4
- require 'pastel'
5
- require 'tty-cursor'
3
+ require "strings"
4
+ require "pastel"
5
+ require "tty-cursor"
6
6
 
7
- require_relative 'box/border'
8
- require_relative 'box/version'
7
+ require_relative "box/border"
8
+ require_relative "box/version"
9
9
 
10
10
  module TTY
11
11
  module Box
12
12
  module_function
13
13
 
14
+ NEWLINE = "\n"
15
+ SPACE = " "
16
+
17
+ LINE_BREAK = %r{\r\n|\r|\n}.freeze
18
+
14
19
  BOX_CHARS = {
15
20
  ascii: %w[+ + + + + + + + - | +],
16
21
  light: %w[┘ ┐ ┌ └ ┤ ┴ ┬ ├ ─ │ ┼],
@@ -65,36 +70,162 @@ module TTY
65
70
  TTY::Cursor
66
71
  end
67
72
 
68
- def color
69
- @color ||= Pastel.new
73
+ def color(enabled: nil)
74
+ @color ||= Pastel.new(enabled: enabled)
75
+ end
76
+
77
+ # A frame for info type message
78
+ #
79
+ # @param [String] message
80
+ # the message to display
81
+ #
82
+ # @api public
83
+ def info(message, **opts)
84
+ new_opts = {
85
+ title: { top_left: " ℹ INFO " },
86
+ border: { type: :thick },
87
+ padding: 1,
88
+ style: {
89
+ fg: :black,
90
+ bg: :bright_blue,
91
+ border: {
92
+ fg: :black,
93
+ bg: :bright_blue
94
+ }
95
+ }
96
+ }.merge(opts)
97
+ frame(**new_opts) { message }
98
+ end
99
+
100
+ # A frame for warning type message
101
+ #
102
+ # @param [String] message
103
+ # the message to display
104
+ #
105
+ # @api public
106
+ def warn(message, **opts)
107
+ new_opts = {
108
+ title: { top_left: " ⚠ WARNING " },
109
+ border: { type: :thick },
110
+ padding: 1,
111
+ style: {
112
+ fg: :black,
113
+ bg: :bright_yellow,
114
+ border: {
115
+ fg: :black,
116
+ bg: :bright_yellow
117
+ }
118
+ }
119
+ }.merge(opts)
120
+ frame(**new_opts) { message }
121
+ end
122
+
123
+ # A frame for for success type message
124
+ #
125
+ # @param [String] message
126
+ # the message to display
127
+ #
128
+ # @api public
129
+ def success(message, **opts)
130
+ new_opts = {
131
+ title: { top_left: " ✔ OK " },
132
+ border: { type: :thick },
133
+ padding: 1,
134
+ style: {
135
+ fg: :black,
136
+ bg: :bright_green,
137
+ border: {
138
+ fg: :black,
139
+ bg: :bright_green
140
+ }
141
+ }
142
+ }.merge(opts)
143
+ frame(**new_opts) { message }
144
+ end
145
+
146
+ # A frame for error type message
147
+ #
148
+ # @param [String] message
149
+ # the message to display
150
+ #
151
+ # @api public
152
+ def error(message, **opts)
153
+ new_opts = {
154
+ title: { top_left: " ⨯ ERROR " },
155
+ border: { type: :thick },
156
+ padding: 1,
157
+ style: {
158
+ fg: :bright_white,
159
+ bg: :red,
160
+ border: {
161
+ fg: :bright_white,
162
+ bg: :red
163
+ }
164
+ }
165
+ }.merge(opts)
166
+ frame(**new_opts) { message }
70
167
  end
71
168
 
72
169
  # Create a frame
73
170
  #
171
+ # @param [Integer] top
172
+ # the offset from the terminal top
173
+ # @param [Integer] left
174
+ # the offset from the terminal left
175
+ # @param [Integer] width
176
+ # the width of the box
177
+ # @param [Integer] height
178
+ # the height of the box
179
+ # @param [Symbol] align
180
+ # the content alignment
181
+ # @param [Integer,Array[Integer]] padding
182
+ # the padding around content
183
+ # @param [Hash] title
184
+ # the title for top or bottom border
185
+ # @param [Hash, Symbol] border
186
+ # the border type
187
+ # @param [Hash] style
188
+ # the styling for the front and background
189
+ #
74
190
  # @api public
75
- def frame(top: nil, left: nil, width: 35, height: 3, align: :left, padding: 0,
76
- title: {}, border: :light, style: {})
191
+ def frame(*content, top: nil, left: nil, width: nil, height: nil,
192
+ align: :left, padding: 0, title: {}, border: :light, style: {},
193
+ enable_color: nil)
194
+ @color = nil
195
+ color(enabled: enable_color)
77
196
  output = []
78
- content = []
197
+ sep = NEWLINE
79
198
  position = top && left
80
199
 
81
200
  border = Border.parse(border)
82
- top_size = border.top? ? 1: 0
83
- bottom_size = border.bottom? ? 1: 0
201
+ top_size = border.top? ? 1 : 0
202
+ bottom_size = border.bottom? ? 1 : 0
84
203
  left_size = border.left? ? 1 : 0
85
204
  right_size = border.right ? 1 : 0
86
205
 
87
- if block_given?
88
- content = format(yield, width, padding, align)
89
- end
206
+ str = block_given? ? yield : content_to_str(content)
207
+ sep = str[LINE_BREAK] || NEWLINE # infer line break
208
+ content_lines = str.split(sep)
90
209
 
210
+ # infer dimensions
211
+ dimensions = infer_dimensions(content_lines, padding)
212
+ width ||= left_size + dimensions[0] + right_size
213
+ width = [width,
214
+ top_space_taken(title, border),
215
+ bottom_space_taken(title, border)].max
216
+ height ||= top_size + dimensions[1] + bottom_size
217
+
218
+ # apply formatting to content
219
+ formatted_lines = format(content_lines, width, padding, align, sep)
220
+
221
+ # infer styling
91
222
  fg, bg = *extract_style(style)
92
223
  border_fg, border_bg = *extract_style(style[:border] || {})
93
224
 
94
225
  if border.top?
95
226
  output << cursor.move_to(left, top) if position
96
227
  output << top_border(title, width, border, style)
97
- output << "\n" unless position
228
+ output << sep unless position
98
229
  end
99
230
 
100
231
  (height - top_size - bottom_size).times do |i|
@@ -103,47 +234,116 @@ module TTY
103
234
  output << border_bg.(border_fg.(pipe_char(border.type)))
104
235
  end
105
236
 
106
- content_size = width - left_size - right_size
107
- unless content[i].nil?
108
- output << bg.(fg.(content[i]))
109
- content_size -= content[i].size
237
+ filler_size = width - left_size - right_size
238
+ if formatted_line = formatted_lines[i]
239
+ output << bg.(fg.(formatted_line))
240
+ line_content_size = Strings::ANSI.sanitize(formatted_line)
241
+ .scan(/[[:print:]]/).join.size
242
+ filler_size = [filler_size - line_content_size, 0].max
110
243
  end
244
+
111
245
  if style[:fg] || style[:bg] || !position # something to color
112
- output << bg.(fg.(' ' * content_size))
246
+ output << bg.(fg.(SPACE * filler_size))
113
247
  end
114
248
 
115
249
  if border.right?
116
250
  if position
117
- output << cursor.move_to(left + width - right_size, top + i + top_size)
251
+ output << cursor.move_to(left + width - right_size,
252
+ top + i + top_size)
118
253
  end
119
254
  output << border_bg.(border_fg.(pipe_char(border.type)))
120
255
  end
121
- output << "\n" unless position
256
+ output << sep unless position
122
257
  end
123
258
 
124
259
  if border.bottom?
125
260
  output << cursor.move_to(left, top + height - bottom_size) if position
126
261
  output << bottom_border(title, width, border, style)
127
- output << "\n" unless position
262
+ output << sep unless position
128
263
  end
129
264
 
130
265
  output.join
131
266
  end
132
267
 
133
- # Format content
268
+ # Convert content array to string
269
+ #
270
+ # @param [Array<String>] content
271
+ #
272
+ # @return [String]
273
+ #
274
+ # @api private
275
+ def content_to_str(content)
276
+ case content.size
277
+ when 0 then ""
278
+ when 1 then content[0]
279
+ else content.join(NEWLINE)
280
+ end
281
+ end
282
+ private_class_method :content_to_str
283
+
284
+ # Infer box dimensions based on content lines and padding
285
+ #
286
+ # @param [Array[String]] lines
287
+ # @param [Array[Integer]|Integer] padding
288
+ #
289
+ # @return [Array[Integer]]
290
+ #
291
+ # @api private
292
+ def infer_dimensions(lines, padding)
293
+ pad = Strings::Padder.parse(padding)
294
+ width = pad.left + content_width(lines) + pad.right
295
+ height = pad.top + lines.size + pad.bottom
296
+ [width, height]
297
+ end
298
+ private_class_method :infer_dimensions
299
+
300
+ # The maximum content width for all the lines
301
+ #
302
+ # @param [Array<String>] lines
303
+ #
304
+ # @return [Integer]
305
+ #
306
+ # @api private
307
+ def content_width(lines)
308
+ return 1 if lines.empty?
309
+
310
+ lines.map(&Strings::ANSI.method(:sanitize)).max_by(&:length).length
311
+ end
312
+ private_class_method :content_width
313
+
314
+ # Format content by wrapping, aligning and padding out
315
+ #
316
+ # @param [Array<String>] lines
317
+ # the lines to format
318
+ # @param [Integer] width
319
+ # the maximum width
320
+ # @param [Integer, Array<Integer>] padding
321
+ # the amount of padding
322
+ # @param [Symbol] align
323
+ # the type of alignment
324
+ # @param [String] separator
325
+ # the newline separator
134
326
  #
135
327
  # @return [Array[String]]
136
328
  #
137
329
  # @api private
138
- def format(content, width, padding, align)
330
+ def format(lines, width, padding, align, separator)
331
+ return [] if lines.empty?
332
+
139
333
  pad = Strings::Padder.parse(padding)
140
334
  total_width = width - 2 - (pad.left + pad.right)
141
335
 
142
- wrapped = Strings.wrap(content, total_width)
143
- aligned = Strings.align(wrapped, total_width, direction: align)
144
- padded = Strings.pad(aligned, padding)
145
- padded.split("\n")
336
+ formatted = lines.each_with_object([]) do |line, acc|
337
+ wrapped = Strings::Wrap.wrap(line, total_width, separator: separator)
338
+ acc << Strings::Align.align(wrapped, total_width,
339
+ direction: align,
340
+ separator: separator)
341
+ end.join(separator)
342
+
343
+ Strings::Pad.pad(formatted, padding, separator: separator)
344
+ .split(separator)
146
345
  end
346
+ private_class_method :format
147
347
 
148
348
  # Convert style keywords into styling
149
349
  #
@@ -151,10 +351,63 @@ module TTY
151
351
  #
152
352
  # @api private
153
353
  def extract_style(style)
154
- fg = style[:fg] ? color.send(style[:fg]).detach : -> (c) { c }
155
- bg = style[:bg] ? color.send(:"on_#{style[:bg]}").detach : -> (c) { c }
354
+ fg = style[:fg] ? color.send(style[:fg]).detach : ->(c) { c }
355
+ bg = style[:bg] ? color.send(:"on_#{style[:bg]}").detach : ->(c) { c }
156
356
  [fg, bg]
157
357
  end
358
+ private_class_method :extract_style
359
+
360
+ # Top space taken by titles and corners
361
+ #
362
+ # @return [Integer]
363
+ #
364
+ # @api private
365
+ def top_space_taken(title, border)
366
+ top_titles_size(title) +
367
+ top_left_corner(border).size +
368
+ top_right_corner(border).size
369
+ end
370
+ private_class_method :top_space_taken
371
+
372
+ # Top left corner
373
+ #
374
+ # @param [Border] border
375
+ #
376
+ # @return [String]
377
+ #
378
+ # @api private
379
+ def top_left_corner(border)
380
+ return "" unless border.top_left? && border.left?
381
+
382
+ send(:"#{border.top_left}_char", border.type)
383
+ end
384
+ private_class_method :top_left_corner
385
+
386
+ # Top right corner
387
+ #
388
+ # @param [Border] border
389
+ #
390
+ # @return [String]
391
+ #
392
+ # @api private
393
+ def top_right_corner(border)
394
+ return "" unless border.top_right? && border.right?
395
+
396
+ send(:"#{border.top_right}_char", border.type)
397
+ end
398
+ private_class_method :top_right_corner
399
+
400
+ # Top titles size
401
+ #
402
+ # @return [Integer]
403
+ #
404
+ # @api private
405
+ def top_titles_size(title)
406
+ color.strip(title[:top_left].to_s).size +
407
+ color.strip(title[:top_center].to_s).size +
408
+ color.strip(title[:top_right].to_s).size
409
+ end
410
+ private_class_method :top_titles_size
158
411
 
159
412
  # Top border
160
413
  #
@@ -162,28 +415,75 @@ module TTY
162
415
  #
163
416
  # @api private
164
417
  def top_border(title, width, border, style)
165
- top_titles_size = title[:top_left].to_s.size +
166
- title[:top_center].to_s.size +
167
- title[:top_right].to_s.size
168
418
  fg, bg = *extract_style(style[:border] || {})
169
419
 
170
- top_left = border.top_left? && border.left? ? send(:"#{border.top_left}_char", border.type) : ""
171
- top_right = border.top_right? && border.right? ? send(:"#{border.top_right}_char", border.type) : ""
172
-
173
- top_space_left = width - top_titles_size - top_left.size - top_right.size
420
+ top_space_left = width - top_space_taken(title, border)
174
421
  top_space_before = top_space_left / 2
175
422
  top_space_after = top_space_left / 2 + top_space_left % 2
176
423
 
177
424
  [
178
- bg.(fg.(top_left)),
179
- bg.(title[:top_left].to_s),
425
+ bg.(fg.(top_left_corner(border))),
426
+ bg.(fg.(title[:top_left].to_s)),
180
427
  bg.(fg.(line_char(border.type) * top_space_before)),
181
- bg.(title[:top_center].to_s),
428
+ bg.(fg.(title[:top_center].to_s)),
182
429
  bg.(fg.(line_char(border.type) * top_space_after)),
183
- bg.(title[:top_right].to_s),
184
- bg.(fg.(top_right))
185
- ].join('')
430
+ bg.(fg.(title[:top_right].to_s)),
431
+ bg.(fg.(top_right_corner(border)))
432
+ ].join
186
433
  end
434
+ private_class_method :top_border
435
+
436
+ # Bottom space taken by titles and corners
437
+ #
438
+ # @return [Integer]
439
+ #
440
+ # @api private
441
+ def bottom_space_taken(title, border)
442
+ bottom_titles_size(title) +
443
+ bottom_left_corner(border).size +
444
+ bottom_right_corner(border).size
445
+ end
446
+ private_class_method :bottom_space_taken
447
+
448
+ # Bottom left corner
449
+ #
450
+ # @param [Border] border
451
+ #
452
+ # @return [String]
453
+ #
454
+ # @api private
455
+ def bottom_left_corner(border)
456
+ return "" unless border.bottom_left? && border.left?
457
+
458
+ send(:"#{border.bottom_left}_char", border.type)
459
+ end
460
+ private_class_method :bottom_left_corner
461
+
462
+ # Bottom right corner
463
+ #
464
+ # @param [Border] border
465
+ #
466
+ # @return [String]
467
+ #
468
+ # @api private
469
+ def bottom_right_corner(border)
470
+ return "" unless border.bottom_right? && border.right?
471
+
472
+ send(:"#{border.bottom_right}_char", border.type)
473
+ end
474
+ private_class_method :bottom_right_corner
475
+
476
+ # Bottom titles size
477
+ #
478
+ # @return [Integer]
479
+ #
480
+ # @api private
481
+ def bottom_titles_size(title)
482
+ color.strip(title[:bottom_left].to_s).size +
483
+ color.strip(title[:bottom_center].to_s).size +
484
+ color.strip(title[:bottom_right].to_s).size
485
+ end
486
+ private_class_method :bottom_titles_size
187
487
 
188
488
  # Bottom border
189
489
  #
@@ -191,28 +491,22 @@ module TTY
191
491
  #
192
492
  # @api private
193
493
  def bottom_border(title, width, border, style)
194
- bottom_titles_size = title[:bottom_left].to_s.size +
195
- title[:bottom_center].to_s.size +
196
- title[:bottom_right].to_s.size
197
494
  fg, bg = *extract_style(style[:border] || {})
198
495
 
199
- bottom_left = border.bottom_left? && border.left? ? send(:"#{border.bottom_left}_char", border.type) : ""
200
- bottom_right = border.bottom_right? && border.right? ? send(:"#{border.bottom_right}_char", border.type) : ""
201
-
202
- bottom_space_left = width - bottom_titles_size -
203
- bottom_left.size - bottom_right.size
496
+ bottom_space_left = width - bottom_space_taken(title, border)
204
497
  bottom_space_before = bottom_space_left / 2
205
498
  bottom_space_after = bottom_space_left / 2 + bottom_space_left % 2
206
499
 
207
500
  [
208
- bg.(fg.(bottom_left)),
209
- bg.(title[:bottom_left].to_s),
501
+ bg.(fg.(bottom_left_corner(border))),
502
+ bg.(fg.(title[:bottom_left].to_s)),
210
503
  bg.(fg.(line_char(border.type) * bottom_space_before)),
211
- bg.(title[:bottom_center].to_s),
504
+ bg.(fg.(title[:bottom_center].to_s)),
212
505
  bg.(fg.(line_char(border.type) * bottom_space_after)),
213
- bg.(title[:bottom_right].to_s),
214
- bg.(fg.(bottom_right))
215
- ].join('')
506
+ bg.(fg.(title[:bottom_right].to_s)),
507
+ bg.(fg.(bottom_right_corner(border)))
508
+ ].join
216
509
  end
510
+ private_class_method :bottom_border
217
511
  end # TTY
218
512
  end # Box