├── .editorconfig
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── BUG_REPORT.md
│ ├── FEATURE_REQUEST.md
│ └── config.yml
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── ci.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── appveyor.yml
├── assets
├── tty-box-drawing.png
└── tty-box-messages.png
├── bin
├── console
└── setup
├── examples
├── basic.rb
├── commander.rb
├── connected.rb
├── messages.rb
└── newline.rb
├── lib
├── tty-box.rb
└── tty
│ ├── box.rb
│ └── box
│ ├── border.rb
│ └── version.rb
├── spec
├── perf
│ └── frame_spec.rb
├── spec_helper.rb
└── unit
│ ├── align_spec.rb
│ ├── border
│ └── parse_spec.rb
│ ├── border_spec.rb
│ ├── custom_frame_spec.rb
│ ├── frame_spec.rb
│ ├── new_spec.rb
│ ├── padding_spec.rb
│ ├── position_spec.rb
│ ├── style_spec.rb
│ └── title_spec.rb
├── tasks
├── console.rake
├── coverage.rake
└── spec.rake
└── tty-box.gemspec
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.rb]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | indent_style = space
8 | indent_size = 2
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: piotrmurach
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG_REPORT.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Report something not working correctly or as expected
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 | ---
8 |
9 | ### Describe the problem
10 |
11 | A brief description of the issue.
12 |
13 | ### Steps to reproduce the problem
14 |
15 | ```
16 | Your code here to reproduce the issue
17 | ```
18 |
19 | ### Actual behaviour
20 |
21 | What happened? This could be a description, log output, error raised etc.
22 |
23 | ### Expected behaviour
24 |
25 | What did you expect to happen?
26 |
27 | ### Describe your environment
28 |
29 | * OS version:
30 | * Ruby version:
31 | * TTY::Box version:
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest new functionality
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 | ---
8 |
9 | ### Describe the problem
10 |
11 | A brief description of the problem you're trying to solve.
12 |
13 | ### How would the new feature work?
14 |
15 | A short explanation of the new feature.
16 |
17 | ```
18 | Example code that shows possible usage
19 | ```
20 |
21 | ### Drawbacks
22 |
23 | Can you see any potential drawbacks?
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: TTY Community Discussions
4 | url: https://github.com/piotrmurach/tty/discussions
5 | about: Suggest ideas, ask and answer questions
6 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Describe the change
2 | What does this Pull Request do?
3 |
4 | ### Why are we doing this?
5 | Any related context as to why is this is a desirable change.
6 |
7 | ### Benefits
8 | How will the library improve?
9 |
10 | ### Drawbacks
11 | Possible drawbacks applying this change.
12 |
13 | ### Requirements
14 |
15 | - [ ] Tests written & passing locally?
16 | - [ ] Code style checked?
17 | - [ ] Rebased with `master` branch?
18 | - [ ] Documentation updated?
19 | - [ ] Changelog updated?
20 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: CI
3 | on:
4 | push:
5 | branches:
6 | - master
7 | paths-ignore:
8 | - "bin/**"
9 | - "examples/**"
10 | - "*.md"
11 | pull_request:
12 | branches:
13 | - master
14 | paths-ignore:
15 | - "bin/**"
16 | - "examples/**"
17 | - "*.md"
18 | jobs:
19 | tests:
20 | name: Ruby ${{ matrix.ruby }}
21 | runs-on: ${{ matrix.os }}
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | os:
26 | - ubuntu-latest
27 | ruby:
28 | - 2.3
29 | - 2.4
30 | - 2.5
31 | - 2.6
32 | - 3.0
33 | - ruby-head
34 | - jruby-9.2.13.0
35 | - jruby-head
36 | - truffleruby-head
37 | include:
38 | - ruby: 2.1
39 | os: ubuntu-latest
40 | coverage: false
41 | bundler: 1
42 | - ruby: 2.2
43 | os: ubuntu-latest
44 | coverage: false
45 | bundler: 1
46 | - ruby: 2.7
47 | os: ubuntu-latest
48 | coverage: true
49 | bundler: latest
50 | env:
51 | COVERAGE: ${{ matrix.coverage }}
52 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
53 | BUNDLE_CACHE_PATH: vendor/bundle
54 | continue-on-error: ${{ endsWith(matrix.ruby, 'head') }}
55 | steps:
56 | - uses: actions/checkout@v2
57 | - name: Set up Ruby
58 | uses: ruby/setup-ruby@v1
59 | with:
60 | ruby-version: ${{ matrix.ruby }}
61 | bundler: ${{ matrix.bundler }}
62 | - name: Cache dependencies
63 | id: cache-gems
64 | uses: actions/cache@v2
65 | with:
66 | path: vendor/bundle
67 | key: ${{ matrix.ruby }}-gems-${{ hashFiles('*.gemspec') }}-${{ hashFiles('Gemfile') }}
68 | - name: Install dependencies
69 | if: steps.cache-gems.outputs.cache-hit != 'true'
70 | run: bundle install --jobs 4 --retry 3
71 | - name: Update dependencies
72 | if: steps.cache-gems.outputs.cache-hit == 'true'
73 | run: bundle update
74 | - name: Run tests
75 | run: bundle exec rake ci
76 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /Gemfile.lock
3 | /.yardoc
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 |
11 | # rspec failure tracking
12 | .rspec_status
13 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 | --warnings
4 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | NewCops: enable
3 |
4 | Layout/LineLength:
5 | Max: 80
6 |
7 | Lint/AssignmentInCondition:
8 | Enabled: false
9 |
10 | Metrics/AbcSize:
11 | Max: 40
12 |
13 | Metrics/BlockLength:
14 | CountComments: true
15 | Max: 25
16 | IgnoredMethods: []
17 | Exclude:
18 | - "spec/**/*"
19 |
20 | Metrics/ClassLength:
21 | Max: 1500
22 |
23 | Metrics/CyclomaticComplexity:
24 | Enabled: false
25 |
26 | Metrics/MethodLength:
27 | Max: 20
28 |
29 | Metrics/ModuleLength:
30 | Max: 300
31 |
32 | Metrics/ParameterLists:
33 | Max: 20
34 |
35 | Naming/BinaryOperatorParameterName:
36 | Enabled: false
37 |
38 | Style/AsciiComments:
39 | Enabled: false
40 |
41 | Style/LambdaCall:
42 | EnforcedStyle: braces
43 |
44 | Style/StringLiterals:
45 | EnforcedStyle: double_quotes
46 |
47 | Style/TrivialAccessors:
48 | Enabled: false
49 |
50 | # { ... } for multi-line blocks is okay
51 | Style/BlockDelimiters:
52 | Enabled: false
53 |
54 | Style/CommentedKeyword:
55 | Enabled: false
56 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change log
2 |
3 | ## [v0.8.0] - unreleased
4 |
5 | ### Added
6 | * Add render method to retrieve box content wrapped in a frame
7 | * Add ability to access box height and width
8 | * Add #content_width and #content_height to access content size excluding border and padding
9 |
10 | ### Changed
11 | * Change Box to be a class to allow instance-level configuration
12 |
13 | ### Fixed
14 | * Fix content formatting to account for border size
15 |
16 | ## [v0.7.0] - 2020-12-20
17 |
18 | ### Added
19 | * Add :enable_color configuration to allow control over colouring
20 |
21 | ### Changed
22 | * Change to ensure non-negative space filler size
23 | * Change to enforce private visibility for the private module methods
24 |
25 | ### Fixed
26 | * Fix box width calculation to ignore colored text by @LainLayer
27 | * Fix drawing frame around multiline colored content
28 |
29 | ## [v0.6.0] - 2020-08-11
30 |
31 | ### Changed
32 | * Change to preserve newline characters when wrapping content
33 | * Change gemspec to include metadata and remove test files
34 | * Change to update pastel & strings dependencies
35 |
36 | ### Fixed
37 | * Fix Ruby 2.7 warnings
38 |
39 | ## [v0.5.0] - 2019-10-08
40 |
41 | ### Added
42 | * Add ability to create frames without specifying width or height
43 | * Add #info, #warn, #success, #error ready frames for status messages inspired by conversation with Konstantin Gredeskoul(@kigster)
44 |
45 | ### Changed
46 | * Change #frame to accept content as an argument in addition to a block
47 | * Change to match titles with border styling
48 |
49 | ## [v0.4.1] - 2019-08-28
50 |
51 | ### Added
52 | * Add example to demonstrate different line endings
53 |
54 | ### Fixed
55 | * Fix to handle different line endings
56 |
57 | ## [v0.4.0] - 2019-06-05
58 |
59 | ### Changed
60 | * Change gemspec to require Ruby >= 2.0.0
61 | * Change to update tty-cursor dependency
62 |
63 | ### Fixed
64 | * Fix issue with displaying box with colored content
65 |
66 | ## [v0.3.0] - 2018-10-08
67 |
68 | ### Added
69 | * Add border parameters :top_left, :top_right, :bottom_left & :bottom_right to allow specifying values for box corners
70 | * Add :ascii border type for drawing ASCII boxes
71 |
72 | ### Fixed
73 | * Fix box color fill to correctly recognise missing borders and match the height and width
74 | * Fix absolute content positioning when borders are missing
75 |
76 | ## [v0.2.1] - 2018-09-10
77 |
78 | ### Fixed
79 | * Fix content alignment by @DannyBen
80 |
81 | ## [v0.2.0] - 2018-07-31
82 |
83 | ### Changed
84 | * Change to stop positioning box without `:top` & `:left` coordinates
85 | * Change to load manually required files in gemspec without using git
86 |
87 | ## [v0.1.0] - 2018-07-23
88 |
89 | * Initial implementation and release
90 |
91 | [v0.8.0]: https://github.com/piotrmurach/tty-box/compare/v0.7.0...v0.8.0
92 | [v0.7.0]: https://github.com/piotrmurach/tty-box/compare/v0.6.0...v0.7.0
93 | [v0.6.0]: https://github.com/piotrmurach/tty-box/compare/v0.5.0...v0.6.0
94 | [v0.5.0]: https://github.com/piotrmurach/tty-box/compare/v0.4.1...v0.5.0
95 | [v0.4.1]: https://github.com/piotrmurach/tty-box/compare/v0.4.0...v0.4.1
96 | [v0.4.0]: https://github.com/piotrmurach/tty-box/compare/v0.3.0...v0.4.0
97 | [v0.3.0]: https://github.com/piotrmurach/tty-box/compare/v0.2.1...v0.3.0
98 | [v0.2.1]: https://github.com/piotrmurach/tty-box/compare/v0.2.0...v0.2.1
99 | [v0.2.0]: https://github.com/piotrmurach/tty-box/compare/v0.1.0...v0.2.0
100 | [v0.1.0]: https://github.com/piotrmurach/tty-box/compare/00a8a85...v0.1.0
101 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8 |
9 | ## Our Standards
10 |
11 | Examples of behavior that contributes to a positive environment for our community include:
12 |
13 | * Demonstrating empathy and kindness toward other people
14 | * Being respectful of differing opinions, viewpoints, and experiences
15 | * Giving and gracefully accepting constructive feedback
16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17 | * Focusing on what is best not just for us as individuals, but for the overall community
18 |
19 | Examples of unacceptable behavior include:
20 |
21 | * The use of sexualized language or imagery, and sexual attention or
22 | advances of any kind
23 | * Trolling, insulting or derogatory comments, and personal or political attacks
24 | * Public or private harassment
25 | * Publishing others' private information, such as a physical or email
26 | address, without their explicit permission
27 | * Other conduct which could reasonably be considered inappropriate in a
28 | professional setting
29 |
30 | ## Enforcement Responsibilities
31 |
32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33 |
34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35 |
36 | ## Scope
37 |
38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39 |
40 | ## Enforcement
41 |
42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at piotr@piotrmurach.com. All complaints will be reviewed and investigated promptly and fairly.
43 |
44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45 |
46 | ## Enforcement Guidelines
47 |
48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49 |
50 | ### 1. Correction
51 |
52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53 |
54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55 |
56 | ### 2. Warning
57 |
58 | **Community Impact**: A violation through a single incident or series of actions.
59 |
60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61 |
62 | ### 3. Temporary Ban
63 |
64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65 |
66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67 |
68 | ### 4. Permanent Ban
69 |
70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71 |
72 | **Consequence**: A permanent ban from any sort of public interaction within the community.
73 |
74 | ## Attribution
75 |
76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78 |
79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80 |
81 | [homepage]: https://www.contributor-covenant.org
82 |
83 | For answers to common questions about this code of conduct, see the FAQ at
84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
85 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec
4 |
5 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.1.0")
6 | gem "rspec-benchmark", "~> 0.6"
7 | end
8 | gem "json", "2.4.1" if RUBY_VERSION == "2.0.0"
9 |
10 | group :test do
11 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.5.0")
12 | gem "coveralls_reborn", "~> 0.21.0"
13 | gem "simplecov", "~> 0.21.0"
14 | end
15 | end
16 |
17 | group :metrics do
18 | gem "yardstick", "~> 0.9.9"
19 | end
20 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Piotr Murach (https://piotrmurach.com)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | # TTY::Box [][gitter]
6 |
7 | [][gem]
8 | [][gh_actions_ci]
9 | [][appveyor]
10 | [][codeclimate]
11 | [][coverage]
12 | [][inchpages]
13 |
14 | [gitter]: https://gitter.im/piotrmurach/tty
15 | [gem]: http://badge.fury.io/rb/tty-box
16 | [gh_actions_ci]: https://github.com/piotrmurach/tty-box/actions?query=workflow%3ACI
17 | [appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-box
18 | [codeclimate]: https://codeclimate.com/github/piotrmurach/tty-box/maintainability
19 | [coverage]: https://coveralls.io/github/piotrmurach/tty-box
20 | [inchpages]: http://inch-ci.org/github/piotrmurach/tty-box
21 |
22 | > Draw various frames and boxes in the terminal window.
23 |
24 | **TTY::Box** provides box drawing component for [TTY](https://github.com/piotrmurach/tty) toolkit.
25 |
26 | 
27 |
28 | ## Installation
29 |
30 | Add this line to your application's Gemfile:
31 |
32 | ```ruby
33 | gem "tty-box"
34 | ```
35 |
36 | And then execute:
37 |
38 | $ bundle
39 |
40 | Or install it yourself as:
41 |
42 | $ gem install tty-box
43 |
44 | ## Contents
45 |
46 | * [1. Usage](#1-usage)
47 | * [2. Interface](#2-interface)
48 | * [2.1 frame](#21-frame)
49 | * [2.2 position](#22-position)
50 | * [2.3 dimension](#23-dimension)
51 | * [2.4 title](#24-title)
52 | * [2.5 border](#25-border)
53 | * [2.6 styling](#26-styling)
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)
60 |
61 | ## 1. Usage
62 |
63 | Using the `frame` method, you can draw a box in a terminal emulator:
64 |
65 | ```ruby
66 | box = TTY::Box.frame "Drawing a box in", "terminal emulator", padding: 3, align: :center
67 | ```
68 |
69 | And then print:
70 |
71 | ```ruby
72 | print box
73 | # =>
74 | # ┌───────────────────────┐
75 | # │ │
76 | # │ │
77 | # │ │
78 | # │ Drawing a box in │
79 | # │ terminal emulator │
80 | # │ │
81 | # │ │
82 | # │ │
83 | # └───────────────────────┘
84 | ```
85 |
86 | ## 2. Interface
87 |
88 | ### 2.1 frame
89 |
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.
133 |
134 | ```ruby
135 | box = TTY::Box.frame(width: 30, height: 10)
136 | ```
137 |
138 | When printed will produce the following output in your terminal:
139 |
140 | ```ruby
141 | print box
142 | # =>
143 | # ┌────────────────────────────┐
144 | # │ │
145 | # │ │
146 | # │ │
147 | # │ │
148 | # │ │
149 | # │ │
150 | # │ │
151 | # │ │
152 | # └────────────────────────────┘
153 | ```
154 |
155 | Alternatively, you can also pass a block to provide a content for the box:
156 |
157 | ```ruby
158 | box = TTY::Box.frame(width: 30, height: 10) do
159 | "Drawin a box in terminal emulator"
160 | end
161 | ```
162 |
163 | When printed will produce the following output in your terminal:
164 |
165 | ```ruby
166 | print box
167 | # =>
168 | # ┌────────────────────────────┐
169 | # │Drawing a box in terminal │
170 | # │emulator │
171 | # │ │
172 | # │ │
173 | # │ │
174 | # │ │
175 | # │ │
176 | # │ │
177 | # └────────────────────────────┘
178 | ```
179 |
180 | ### 2.2 position
181 |
182 | By default, a box will not be positioned. To position your box absolutely within a terminal window use `:top` and `:left` keyword arguments:
183 |
184 | ```ruby
185 | TTY::Box.frame(top: 5, left: 10)
186 | ```
187 |
188 | This will place box 10 columns to the right and 5 lines down counting from the top left corner.
189 |
190 | If you wish to center your box within the terminal window then consider using [tty-screen](https://github.com/piotrmurach/tty-screen) for gathering terminal screen size information.
191 |
192 | ### 2.3 dimension
193 |
194 | At the very minimum a box requires to be given size by using two keyword arguments `:width` and `:height`:
195 |
196 | ```ruby
197 | TTY::Box.frame(width: 30, height: 10)
198 | ```
199 |
200 | If you wish to create a box that depends on the terminal window size then consider using [tty-screen](https://github.com/piotrmurach/tty-screen) for gathering terminal screen size information.
201 |
202 | For example to print a box that spans the whole terminal window do:
203 |
204 | ```ruby
205 | TTY::Box.frame(width: TTY::Screen.width, height: TTY::Screen.height)
206 | ```
207 |
208 | ### 2.4 title
209 |
210 | You can specify titles using the `:title` keyword and a hash value that contains one of the `:top_left`, `:top_center`, `:top_right`, `:bottom_left`, `:bottom_center`, `:bottom_right` keys and actual title as value. For example, to add titles to top left and bottom right of the frame do:
211 |
212 |
213 | ```ruby
214 | box = TTY::Box.frame(width: 30, height: 10, title: {top_left: "TITLE", bottom_right: "v1.0"})
215 | ```
216 |
217 | which when printed in console will render the following:
218 |
219 | ```ruby
220 | print box
221 | # =>
222 | # ┌TITLE───────────────────────┐
223 | # │ │
224 | # │ │
225 | # │ │
226 | # │ │
227 | # │ │
228 | # │ │
229 | # │ │
230 | # │ │
231 | # └──────────────────────(v1.0)┘
232 | ```
233 |
234 | ### 2.5 border
235 |
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:
237 |
238 | ```ruby
239 | box = TTY::Box.frame(width: 30, height: 10, border: :thick)
240 | ```
241 |
242 | and printing the box out to console will produce:
243 |
244 | ```ruby
245 | print box
246 | # =>
247 | # ╔════════════════════════════╗
248 | # ║ ║
249 | # ║ ║
250 | # ║ ║
251 | # ║ ║
252 | # ║ ║
253 | # ║ ║
254 | # ║ ║
255 | # ║ ║
256 | # ╚════════════════════════════╝
257 | ```
258 |
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 | ```
300 |
301 | ```ruby
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.frame(
314 | width: 30, height: 10,
315 | border: {
316 | type: :thick,
317 | bottom: false
318 | })
319 | ```
320 |
321 | ### 2.6 styling
322 |
323 | By default drawing a box doesn't apply any styling. You can change this using the `:style` keyword with foreground `:fg` and background `:bg` keys for both the main content and the border:
324 |
325 | ```ruby
326 | style: {
327 | fg: :bright_yellow,
328 | bg: :blue,
329 | border: {
330 | fg: :bright_yellow,
331 | bg: :blue
332 | }
333 | }
334 | ```
335 |
336 | The above style configuration will produce the result similar to the top demo, a MS-DOS look & feel window.
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 |
352 | ### 2.7 formatting
353 |
354 | You can use `:align` keyword to format content either to be `:left`, `:center` or `:right` aligned:
355 |
356 | ```ruby
357 | box = TTY::Box.frame(width: 30, height: 10, align: :center) do
358 | "Drawing a box in terminal emulator"
359 | end
360 | ```
361 |
362 | The above will create the following output in your terminal:
363 |
364 | ```ruby
365 | print box
366 | # =>
367 | # ┌────────────────────────────┐
368 | # │ Drawing a box in terminal │
369 | # │ emulator │
370 | # │ │
371 | # │ │
372 | # │ │
373 | # │ │
374 | # │ │
375 | # │ │
376 | # └────────────────────────────┘
377 | ```
378 |
379 | You can also use `:padding` keyword to further format the content using the following values:
380 |
381 | ```ruby
382 | [1,3,1,3] # => pad content left & right with 3 spaces and add 1 line above & below
383 | [1,3] # => pad content left & right with 3 spaces and add 1 line above & below
384 | 1 # => shorthand for [1,1,1,1]
385 | ```
386 |
387 | For example, if you wish to pad content all around do:
388 |
389 | ```ruby
390 | box = TTY::Box.frame(width: 30, height: 10, align: :center, padding: 3) do
391 | "Drawing a box in terminal emulator"
392 | end
393 | ```
394 |
395 | Here's an example output:
396 |
397 | ```ruby
398 | print box
399 | # =>
400 | # ┌────────────────────────────┐
401 | # │ │
402 | # │ │
403 | # │ │
404 | # │ Drawing a box in │
405 | # │ terminal emulator │
406 | # │ │
407 | # │ │
408 | # │ │
409 | # └────────────────────────────┘
410 | #
411 | ```
412 |
413 | ### 2.8 messages
414 |
415 | 
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 |
497 | ## Development
498 |
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.
500 |
501 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
502 |
503 | ## Contributing
504 |
505 | Bug reports and pull requests are welcome on GitHub at https://github.com/piotrmurach/tty-box. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/piotrmurach/tty-box/blob/master/CODE_OF_CONDUCT.md).
506 |
507 | ## License
508 |
509 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
510 |
511 | ## Code of Conduct
512 |
513 | Everyone interacting in the TTY::Box project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/piotrmurach/tty-box/blob/master/CODE_OF_CONDUCT.md).
514 |
515 | ## Copyright
516 |
517 | Copyright (c) 2018 Piotr Murach. See LICENSE for further details.
518 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 |
3 | FileList['tasks/**/*.rake'].each(&method(:import))
4 |
5 | desc 'Run all specs'
6 | task ci: %w[ spec ]
7 |
8 | task default: :spec
9 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | ---
2 | skip_commits:
3 | files:
4 | - "bin/**"
5 | - "examples/**"
6 | - "*.md"
7 | install:
8 | - SET PATH=C:\Ruby%ruby_version%\bin;%PATH%
9 | - gem install bundler -v '< 2.0'
10 | - bundle config mirror.https://rubygems.org http://rubygems.org
11 | - bundle install
12 | before_test:
13 | - ruby -v
14 | - gem -v
15 | - bundle -v
16 | build: off
17 | test_script:
18 | - bundle exec rake ci
19 | environment:
20 | matrix:
21 | - ruby_version: "200"
22 | - ruby_version: "200-x64"
23 | - ruby_version: "21"
24 | - ruby_version: "21-x64"
25 | - ruby_version: "22"
26 | - ruby_version: "22-x64"
27 | - ruby_version: "23"
28 | - ruby_version: "23-x64"
29 | - ruby_version: "24"
30 | - ruby_version: "24-x64"
31 | - ruby_version: "25"
32 | - ruby_version: "25-x64"
33 | - ruby_version: "26"
34 | - ruby_version: "26-x64"
35 |
--------------------------------------------------------------------------------
/assets/tty-box-drawing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piotrmurach/tty-box/c8d5f2f443a625cee1dcc8ac2867a1a9e0df5da9/assets/tty-box-drawing.png
--------------------------------------------------------------------------------
/assets/tty-box-messages.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piotrmurach/tty-box/c8d5f2f443a625cee1dcc8ac2867a1a9e0df5da9/assets/tty-box-messages.png
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "bundler/setup"
4 | require "tty/box"
5 |
6 | # You can add fixtures and/or initialization code here to make experimenting
7 | # with your gem easier. You can also use a different console, if you like.
8 |
9 | # (If you use this, don't forget to add pry to your Gemfile!)
10 | # require "pry"
11 | # Pry.start
12 |
13 | require "irb"
14 | IRB.start(__FILE__)
15 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/examples/basic.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "../lib/tty-box"
4 |
5 | box = TTY::Box.frame "Drawing a box in", "terminal emulator",
6 | padding: 3, align: :center
7 |
8 | puts box
9 |
--------------------------------------------------------------------------------
/examples/commander.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "../lib/tty-box"
4 |
5 | print TTY::Cursor.clear_screen
6 |
7 | box1 = TTY::Box.frame(
8 | top: 2,
9 | left: 10,
10 | width: 30,
11 | height: 10,
12 | border: :thick,
13 | align: :center,
14 | padding: 3,
15 | title: {
16 | top_left: " file1 "
17 | },
18 | style: {
19 | fg: :bright_yellow,
20 | bg: :blue,
21 | border: {
22 | fg: :bright_yellow,
23 | bg: :blue
24 | }
25 | }
26 | ) do
27 | "Drawing a box in terminal emulator"
28 | end
29 |
30 | box2 = TTY::Box.frame(
31 | top: 8,
32 | left: 34,
33 | width: 30,
34 | height: 10,
35 | border: :thick,
36 | align: :center,
37 | padding: 3,
38 | title: {
39 | top_left: " file2 "
40 | },
41 | style: {
42 | fg: :bright_yellow,
43 | bg: :blue,
44 | border: {
45 | fg: :bright_yellow,
46 | bg: :blue
47 | }
48 | }
49 | ) do
50 | "Drawing a box in terminal emulator"
51 | end
52 |
53 | puts box1 + box2
54 | print "\n" * 5
55 |
--------------------------------------------------------------------------------
/examples/connected.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "../lib/tty-box"
4 |
5 | print TTY::Cursor.clear_screen
6 |
7 | box1 = TTY::Box.frame(
8 | top: 3,
9 | left: 10,
10 | width: 15,
11 | height: 5,
12 | border: {
13 | type: :thick,
14 | right: false
15 | },
16 | align: :center,
17 | padding: [1, 2],
18 | style: {
19 | bg: :red,
20 | border: {
21 | bg: :red
22 | }
23 | }
24 | ) { "Space" }
25 |
26 | box2 = TTY::Box.frame(
27 | top: 3,
28 | left: 25,
29 | width: 15,
30 | height: 5,
31 | border: {
32 | type: :thick,
33 | top_left: :divider_down,
34 | bottom_left: :divider_up
35 | },
36 | align: :center,
37 | padding: [1, 2],
38 | style: {
39 | bg: :red,
40 | border: {
41 | bg: :red
42 | }
43 | }
44 | ) { "Invaders!" }
45 |
46 | puts box1 + box2
47 | print "\n" * 5
48 |
--------------------------------------------------------------------------------
/examples/messages.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "../lib/tty-box"
4 |
5 | puts TTY::Cursor.clear_screen
6 | puts TTY::Box.info "Deploying application", top: 2, left: 2
7 | puts TTY::Box.success "Deploying application", top: 2, left: 29
8 | puts TTY::Box.warn "Deploying application", top: 8, left: 2
9 | puts TTY::Box.error "Deploying application", top: 8, left: 29
10 |
11 | puts
12 |
--------------------------------------------------------------------------------
/examples/newline.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "../lib/tty-box"
4 |
5 | box = TTY::Box.frame(
6 | width: 29,
7 | height: 7,
8 | align: :center,
9 | padding: 1
10 | ) do
11 | "Closes #360\r\n\r\nCloses !217"
12 | end
13 |
14 | puts box
15 |
--------------------------------------------------------------------------------
/lib/tty-box.rb:
--------------------------------------------------------------------------------
1 | require_relative "tty/box"
2 |
--------------------------------------------------------------------------------
/lib/tty/box.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "strings"
4 | require "pastel"
5 | require "tty-cursor"
6 |
7 | require_relative "box/border"
8 | require_relative "box/version"
9 |
10 | module TTY
11 | # Responsible for drawing a box around content
12 | #
13 | # @api public
14 | class Box
15 | NEWLINE = "\n"
16 | SPACE = " "
17 | LINE_BREAK = /\r\n|\r|\n/.freeze
18 |
19 | # A frame for info type message
20 | #
21 | # @param [Array] messages
22 | # the message(s) to display
23 | #
24 | # @return [String]
25 | #
26 | # @api public
27 | def self.info(*messages, **opts, &block)
28 | new_opts = {
29 | title: {top_left: " ℹ INFO "},
30 | border: {type: :thick},
31 | padding: 1,
32 | style: {
33 | fg: :black,
34 | bg: :bright_blue,
35 | border: {
36 | fg: :black,
37 | bg: :bright_blue
38 | }
39 | }
40 | }.merge(opts)
41 | frame(*messages, **new_opts, &block)
42 | end
43 |
44 | # A frame for warning type message
45 | #
46 | # @param [Array] messages
47 | # the message(s) to display
48 | #
49 | # @return [String]
50 | #
51 | # @api public
52 | def self.warn(*messages, **opts, &block)
53 | new_opts = {
54 | title: {top_left: " ⚠ WARNING "},
55 | border: {type: :thick},
56 | padding: 1,
57 | style: {
58 | fg: :black,
59 | bg: :bright_yellow,
60 | border: {
61 | fg: :black,
62 | bg: :bright_yellow
63 | }
64 | }
65 | }.merge(opts)
66 | frame(*messages, **new_opts, &block)
67 | end
68 |
69 | # A frame for for success type message
70 | #
71 | # @param [Array] messages
72 | # the message(s) to display
73 | #
74 | # @return [String]
75 | #
76 | # @api public
77 | def self.success(*messages, **opts, &block)
78 | new_opts = {
79 | title: {top_left: " ✔ OK "},
80 | border: {type: :thick},
81 | padding: 1,
82 | style: {
83 | fg: :black,
84 | bg: :bright_green,
85 | border: {
86 | fg: :black,
87 | bg: :bright_green
88 | }
89 | }
90 | }.merge(opts)
91 | frame(*messages, **new_opts, &block)
92 | end
93 |
94 | # A frame for error type message
95 | #
96 | # @param [String] messages
97 | # the message(s) to display
98 | #
99 | # @return [String]
100 | #
101 | # @api public
102 | def self.error(*messages, **opts, &block)
103 | new_opts = {
104 | title: {top_left: " ⨯ ERROR "},
105 | border: {type: :thick},
106 | padding: 1,
107 | style: {
108 | fg: :bright_white,
109 | bg: :red,
110 | border: {
111 | fg: :bright_white,
112 | bg: :red
113 | }
114 | }
115 | }.merge(opts)
116 | frame(*messages, **new_opts, &block)
117 | end
118 |
119 | # Render frame around content
120 | #
121 | # @example
122 | # TTY::Box.frame { "Hello World" }
123 | #
124 | # @return [String]
125 | # the rendered content inside a box
126 | #
127 | # @api public
128 | def self.frame(*content, **options, &block)
129 | new(*content, **options, &block).render
130 | end
131 |
132 | # The top position
133 | #
134 | # @return [Integer]
135 | #
136 | # @api public
137 | attr_reader :top
138 |
139 | # The left position
140 | #
141 | # @return [Integer]
142 | #
143 | # @api public
144 | attr_reader :left
145 |
146 | # The maximum width with border
147 | #
148 | # @return [Integer]
149 | #
150 | # @api public
151 | attr_reader :width
152 |
153 | # The maximum height with border
154 | #
155 | # @return [Integer]
156 | #
157 | # @api public
158 | attr_reader :height
159 |
160 | # The content colouring
161 | #
162 | # @return [Pastel]
163 | #
164 | # @api public
165 | attr_reader :color
166 |
167 | # The box title(s)
168 | #
169 | # @return [Hash{Symbol => String}]
170 | #
171 | # @api public
172 | attr_reader :title
173 |
174 | # The cursor movement
175 | #
176 | # @return [TTY::Cursor]
177 | #
178 | # @api public
179 | def cursor
180 | TTY::Cursor
181 | end
182 |
183 | # Create a Box instance
184 | #
185 | # @example
186 | # box = TTY::Box.new("Hello World")
187 | #
188 | # @param [Integer] top
189 | # the offset from the terminal top
190 | # @param [Integer] left
191 | # the offset from the terminal left
192 | # @param [Integer] width
193 | # the width of the box
194 | # @param [Integer] height
195 | # the height of the box
196 | # @param [Symbol] align
197 | # the content alignment
198 | # @param [Integer, Array] padding
199 | # the padding around content
200 | # @param [Hash] title
201 | # the title for top or bottom border
202 | # @param [Hash, Symbol] border
203 | # the border type out of ascii, light and thick
204 | # @param [Hash] style
205 | # the styling for the content and border
206 | #
207 | # @api public
208 | def initialize(*content, top: nil, left: nil, width: nil, height: nil,
209 | align: :left, padding: 0, title: {}, border: :light,
210 | style: {}, enable_color: nil)
211 | @color = Pastel.new(enabled: enable_color)
212 | @style = style
213 | @top = top
214 | @left = left
215 | @title = title
216 | @align = align
217 | @padding = Strings::Padder.parse(padding)
218 | @border = Border.parse(border)
219 | # infer styling
220 | @fg, @bg = *extract_style(@style)
221 | @border_fg, @border_bg = *extract_style(@style[:border] || {})
222 | str = block_given? ? yield : content_to_str(content)
223 | @sep = str[LINE_BREAK] || NEWLINE # infer line break
224 | @content_lines = str.split(@sep)
225 | # infer dimensions
226 | total_width = @border.left_size + @padding.left +
227 | original_content_width +
228 | @border.right_size + @padding.right
229 | width ||= total_width
230 | @width = [width, top_space_taken, bottom_space_taken].max
231 | @formatted_lines = format_content(@content_lines, content_width)
232 | @height = height ||
233 | @border.top_size + @formatted_lines.size + @border.bottom_size
234 | end
235 |
236 | # The maximum content width without border and padding
237 | #
238 | # @return [Integer]
239 | #
240 | # @api public
241 | def content_width
242 | @width - @border.left_size - @padding.left -
243 | @padding.right - @border.right_size
244 | end
245 |
246 | # The maximum content height without border and padding
247 | #
248 | # @return [Integer]
249 | #
250 | # @api public
251 | def content_height
252 | @height - @border.top_size - @padding.top -
253 | @padding.bottom - @border.bottom_size
254 | end
255 |
256 | # Check whether this box is positioned or not
257 | #
258 | # @return [Boolean]
259 | #
260 | # @api public
261 | def position?
262 | !@top.nil? || !@left.nil?
263 | end
264 |
265 | # Check whether the content is styled or not
266 | #
267 | # @return [Boolean]
268 | #
269 | # @api public
270 | def content_style?
271 | !@style[:fg].nil? || !@style[:bg].nil?
272 | end
273 |
274 | # Render content inside a box
275 | #
276 | # @example
277 | # box.render
278 | #
279 | # @return [String]
280 | # the rendered box
281 | #
282 | # @api public
283 | def render
284 | output = []
285 | output << render_top_border if @border.top?
286 |
287 | (@height - @border.top_size - @border.bottom_size).times do |y|
288 | output << render_left_border(y)
289 | output << render_content_line(@formatted_lines[y])
290 | output << render_right_border(y) if @border.right?
291 | output << @sep unless position?
292 | end
293 |
294 | output << render_bottom_border if @border.bottom?
295 | output.join
296 | end
297 |
298 | private
299 |
300 | # Render top border
301 | #
302 | # @return [String]
303 | #
304 | # @api private
305 | def render_top_border
306 | position = cursor.move_to(@left, @top) if position?
307 | "#{position}#{top_border}#{@sep unless position?}"
308 | end
309 |
310 | # Render left border
311 | #
312 | # @param [Integer] offset
313 | # the offset from the top border
314 | #
315 | # @return [String]
316 | #
317 | # @api private
318 | def render_left_border(offset)
319 | if position?
320 | position = cursor.move_to(@left, @top + offset + @border.top_size)
321 | end
322 | "#{position}#{color_border(@border.pipe_char) if @border.left?}"
323 | end
324 |
325 | # Render content line
326 | #
327 | # @param [String] formatted_line
328 | # the formatted line
329 | #
330 | # @return [String]
331 | #
332 | # @api private
333 | def render_content_line(formatted_line)
334 | line = []
335 | filler_size = @width - @border.left_size - @border.right_size
336 |
337 | if formatted_line
338 | line << color_content(formatted_line)
339 | line_content_size = Strings::ANSI.sanitize(formatted_line)
340 | .scan(/[[:print:]]/).join.size
341 | filler_size = [filler_size - line_content_size, 0].max
342 | end
343 |
344 | if content_style? || !position?
345 | line << color_content(SPACE * filler_size)
346 | end
347 |
348 | line.join
349 | end
350 |
351 | # Render right border
352 | #
353 | # @param [Integer] offset
354 | # the offset from the top border
355 | #
356 | # @return [String]
357 | #
358 | # @api private
359 | def render_right_border(offset)
360 | if position?
361 | position = cursor.move_to(@left + @width - @border.right_size,
362 | @top + offset + @border.top_size)
363 | end
364 | "#{position}#{color_border(@border.pipe_char)}"
365 | end
366 |
367 | # Render bottom border
368 | #
369 | # @return [String]
370 | #
371 | # @api private
372 | def render_bottom_border
373 | if position?
374 | position = cursor.move_to(@left, @top + @height - @border.bottom_size)
375 | end
376 | "#{position}#{bottom_border}#{@sep unless position?}"
377 | end
378 |
379 | # Convert content array to string
380 | #
381 | # @param [Array] content
382 | #
383 | # @return [String]
384 | #
385 | # @api private
386 | def content_to_str(content)
387 | case content.size
388 | when 0 then ""
389 | when 1 then content[0]
390 | else content.join(NEWLINE)
391 | end
392 | end
393 |
394 | # The maximum original content width for all the lines
395 | #
396 | # @return [Integer]
397 | #
398 | # @api private
399 | def original_content_width
400 | return 1 if @content_lines.empty?
401 |
402 | @content_lines.map(&Strings::ANSI.method(:sanitize))
403 | .max_by(&:length).length
404 | end
405 |
406 | # Convert style keywords into styling Proc objects
407 | #
408 | # @example
409 | # extract_style({fg: :bright_yellow, bg: :blue})
410 | #
411 | # @param [Hash{Symbol => Symbol}] style
412 | # the style configuration to extract from
413 | #
414 | # @return [Array]]
415 | #
416 | # @api private
417 | def extract_style(style)
418 | [
419 | style[:fg] ? color.send(style[:fg]).detach : ->(c) { c },
420 | style[:bg] ? color.send(:"on_#{style[:bg]}").detach : ->(c) { c }
421 | ]
422 | end
423 |
424 | # Apply colour style to border
425 | #
426 | # @param [String] char
427 | # the border character to colour
428 | #
429 | # @return [String]
430 | #
431 | # @api private
432 | def color_border(char)
433 | @border_bg.(@border_fg.(char))
434 | end
435 |
436 | # Apply colour style to content
437 | #
438 | # @param [String] content
439 | # the content to colour
440 | #
441 | # @return [String]
442 | #
443 | # @api private
444 | def color_content(content)
445 | @bg.(@fg.(content))
446 | end
447 |
448 | # Format content by wrapping, aligning and padding out
449 | #
450 | # @param [Array] lines
451 | # the content lines to format
452 | # @param [Integer] width
453 | # the maximum content width
454 | #
455 | # @return [Array[String]]
456 | # the formatted content
457 | #
458 | # @api private
459 | def format_content(lines, width)
460 | return [] if lines.empty?
461 |
462 | formatted = lines.each_with_object([]) do |line, acc|
463 | wrapped = Strings::Wrap.wrap(line, width, separator: @sep)
464 | acc << Strings::Align.align(wrapped, width,
465 | direction: @align,
466 | separator: @sep)
467 | end.join(@sep)
468 |
469 | Strings::Pad.pad(formatted, @padding, separator: @sep).split(@sep)
470 | end
471 |
472 | # Top space taken by titles and corners
473 | #
474 | # @return [Integer]
475 | #
476 | # @api private
477 | def top_space_taken
478 | @border.top_left_corner.size +
479 | top_titles_size +
480 | @border.top_right_corner.size
481 | end
482 |
483 | # Top titles size
484 | #
485 | # @return [Integer]
486 | #
487 | # @api private
488 | def top_titles_size
489 | color.strip(title[:top_left].to_s).size +
490 | color.strip(title[:top_center].to_s).size +
491 | color.strip(title[:top_right].to_s).size
492 | end
493 |
494 | # Top border
495 | #
496 | # @return [String]
497 | #
498 | # @api private
499 | def top_border
500 | top_space_left = width - top_space_taken
501 | top_space_before = top_space_left / 2
502 | top_space_after = top_space_left / 2 + top_space_left % 2
503 |
504 | [
505 | color_border(@border.top_left_corner),
506 | color_border(title[:top_left].to_s),
507 | color_border(@border.line_char * top_space_before),
508 | color_border(title[:top_center].to_s),
509 | color_border(@border.line_char * top_space_after),
510 | color_border(title[:top_right].to_s),
511 | color_border(@border.top_right_corner)
512 | ].join
513 | end
514 |
515 | # Bottom space taken by titles and corners
516 | #
517 | # @return [Integer]
518 | #
519 | # @api private
520 | def bottom_space_taken
521 | @border.bottom_left_corner.size +
522 | bottom_titles_size +
523 | @border.bottom_right_corner.size
524 | end
525 |
526 | # Bottom titles size
527 | #
528 | # @return [Integer]
529 | #
530 | # @api private
531 | def bottom_titles_size
532 | color.strip(title[:bottom_left].to_s).size +
533 | color.strip(title[:bottom_center].to_s).size +
534 | color.strip(title[:bottom_right].to_s).size
535 | end
536 |
537 | # Bottom border
538 | #
539 | # @return [String]
540 | #
541 | # @api private
542 | def bottom_border
543 | bottom_space_left = width - bottom_space_taken
544 | bottom_space_before = bottom_space_left / 2
545 | bottom_space_after = bottom_space_left / 2 + bottom_space_left % 2
546 |
547 | [
548 | color_border(@border.bottom_left_corner),
549 | color_border(title[:bottom_left].to_s),
550 | color_border(@border.line_char * bottom_space_before),
551 | color_border(title[:bottom_center].to_s),
552 | color_border(@border.line_char * bottom_space_after),
553 | color_border(title[:bottom_right].to_s),
554 | color_border(@border.bottom_right_corner)
555 | ].join
556 | end
557 | end # Box
558 | end # TTY
559 |
--------------------------------------------------------------------------------
/lib/tty/box/border.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module TTY
4 | class Box
5 | # A class reponsible for retrieving border options
6 | #
7 | # @api private
8 | class Border
9 | BOX_CHARS = {
10 | ascii: %w[+ + + + + + + + - | +],
11 | light: %w[┘ ┐ ┌ └ ┤ ┴ ┬ ├ ─ │ ┼],
12 | thick: %w[╝ ╗ ╔ ╚ ╣ ╩ ╦ ╠ ═ ║ ╬]
13 | }.freeze
14 |
15 | BORDER_VALUES = %i[
16 | corner_bottom_right
17 | corner_top_right
18 | corner_top_left
19 | corner_bottom_left
20 | divider_left
21 | divider_up
22 | divider_down
23 | divider_right
24 | line
25 | pipe
26 | cross
27 | ].freeze
28 |
29 | # Parse border configuration
30 | #
31 | # @api public
32 | def self.parse(border)
33 | case border
34 | when Hash
35 | new(**border)
36 | when *BOX_CHARS.keys
37 | new(type: border)
38 | else
39 | raise ArgumentError,
40 | "Wrong value `#{border}` for :border configuration option"
41 | end
42 | end
43 |
44 | attr_reader :type, :top, :top_left, :top_right, :left, :right,
45 | :bottom, :bottom_left, :bottom_right
46 |
47 | alias top? top
48 | alias left? left
49 | alias right? right
50 | alias bottom? bottom
51 | alias top_left? top_left
52 | alias top_right? top_right
53 | alias bottom_left? bottom_left
54 | alias bottom_right? bottom_right
55 |
56 | def initialize(type: :light,
57 | top: :line,
58 | top_left: :corner_top_left,
59 | top_right: :corner_top_right,
60 | left: :pipe,
61 | right: :pipe,
62 | bottom: :line,
63 | bottom_left: :corner_bottom_left,
64 | bottom_right: :corner_bottom_right)
65 |
66 | @type = type
67 | @top = check_name(:top, top)
68 | @top_left = check_name(:top_left, top_left)
69 | @top_right = check_name(:top_right, top_right)
70 | @left = check_name(:left, left)
71 | @right = check_name(:right, right)
72 | @bottom = check_name(:bottom, bottom)
73 | @bottom_left = check_name(:bottom_left, bottom_left)
74 | @bottom_right = check_name(:bottom_right, bottom_right)
75 | end
76 |
77 | # Top border size
78 | #
79 | # @return [Integer]
80 | #
81 | # @api public
82 | def top_size
83 | top? ? 1 : 0
84 | end
85 |
86 | # Left border size
87 | #
88 | # @return [Integer]
89 | #
90 | # @api public
91 | def left_size
92 | left? ? 1 : 0
93 | end
94 |
95 | # Right border size
96 | #
97 | # @return [Integer]
98 | #
99 | # @api public
100 | def right_size
101 | right? ? 1 : 0
102 | end
103 |
104 | # Bottom border size
105 | #
106 | # @return [Integer]
107 | #
108 | # @api public
109 | def bottom_size
110 | bottom? ? 1 : 0
111 | end
112 |
113 | # Top left corner
114 | #
115 | # @return [String]
116 | #
117 | # @api public
118 | def top_left_corner
119 | return "" unless top_left? && left?
120 |
121 | send(:"#{top_left}_char")
122 | end
123 |
124 | # Top right corner
125 | #
126 | # @return [String]
127 | #
128 | # @api public
129 | def top_right_corner
130 | return "" unless top_right? && right?
131 |
132 | send(:"#{top_right}_char")
133 | end
134 |
135 | # Bottom left corner
136 | #
137 | # @return [String]
138 | #
139 | # @api public
140 | def bottom_left_corner
141 | return "" unless bottom_left? && left?
142 |
143 | send(:"#{bottom_left}_char")
144 | end
145 |
146 | # Bottom right corner
147 | #
148 | # @return [String]
149 | #
150 | # @api public
151 | def bottom_right_corner
152 | return "" unless bottom_right? && right?
153 |
154 | send(:"#{bottom_right}_char")
155 | end
156 |
157 | # Bottom right corner character
158 | #
159 | # @api public
160 | def corner_bottom_right_char
161 | BOX_CHARS[type][0]
162 | end
163 |
164 | # Top right corner character
165 | #
166 | # @api public
167 | def corner_top_right_char
168 | BOX_CHARS[type][1]
169 | end
170 |
171 | # Top left corner character
172 | #
173 | # @api public
174 | def corner_top_left_char
175 | BOX_CHARS[type][2]
176 | end
177 |
178 | # Bottom left corner character
179 | #
180 | # @api public
181 | def corner_bottom_left_char
182 | BOX_CHARS[type][3]
183 | end
184 |
185 | # Left divider character
186 | #
187 | # @api public
188 | def divider_left_char
189 | BOX_CHARS[type][4]
190 | end
191 |
192 | # Up divider character
193 | #
194 | # @api public
195 | def divider_up_char
196 | BOX_CHARS[type][5]
197 | end
198 |
199 | # Down divider character
200 | #
201 | # @api public
202 | def divider_down_char
203 | BOX_CHARS[type][6]
204 | end
205 |
206 | # Right divider character
207 | #
208 | # @api public
209 | def divider_right_char
210 | BOX_CHARS[type][7]
211 | end
212 |
213 | # Horizontal line character
214 | #
215 | # @api public
216 | def line_char
217 | BOX_CHARS[type][8]
218 | end
219 |
220 | # Vertical line character
221 | #
222 | # @api public
223 | def pipe_char
224 | BOX_CHARS[type][9]
225 | end
226 |
227 | # Intersection character
228 | #
229 | # @api public
230 | def cross_char
231 | BOX_CHARS[type][10]
232 | end
233 |
234 | private
235 |
236 | # Check if border values name is allowed
237 | #
238 | # @raise [ArgumentError]
239 | #
240 | # @api private
241 | def check_name(key, value)
242 | unless BORDER_VALUES.include?(:"#{value}") ||
243 | [true, false].include?(value)
244 | raise ArgumentError, "invalid #{key.inspect} border value: " \
245 | "#{value.inspect}"
246 | end
247 |
248 | value
249 | end
250 | end # Border
251 | end # Box
252 | end # TTY
253 |
--------------------------------------------------------------------------------
/lib/tty/box/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module TTY
4 | class Box
5 | VERSION = "0.7.0"
6 | end # Box
7 | end # TTY
8 |
--------------------------------------------------------------------------------
/spec/perf/frame_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rspec-benchmark"
4 | require "yaml"
5 |
6 | RSpec.describe TTY::Box do
7 | include RSpec::Benchmark::Matchers
8 |
9 | let(:output) { StringIO.new }
10 |
11 | it "displays box 12.5x slower than YAML output" do
12 | content = "Hello World"
13 |
14 | expect {
15 | TTY::Box.frame(content)
16 | }.to perform_slower_than {
17 | YAML.dump(content)
18 | }.at_most(12.5).times
19 | end
20 |
21 | it "displays box allocating no more than 423 objects" do
22 | content = "Hello World"
23 |
24 | expect {
25 | TTY::Box.frame(content)
26 | }.to perform_allocation(423).objects
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | if ENV["COVERAGE"] == "true"
4 | require "simplecov"
5 | require "coveralls"
6 |
7 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
8 | SimpleCov::Formatter::HTMLFormatter,
9 | Coveralls::SimpleCov::Formatter
10 | ]
11 |
12 | SimpleCov.start do
13 | command_name "spec"
14 | add_filter "spec"
15 | end
16 | end
17 |
18 | require "bundler/setup"
19 | require "tty/box"
20 |
21 | RSpec.configure do |config|
22 | # Enable flags like --only-failures and --next-failure
23 | config.example_status_persistence_file_path = ".rspec_status"
24 |
25 | # Disable RSpec exposing methods globally on `Module` and `main`
26 | config.disable_monkey_patching!
27 |
28 | config.expect_with :rspec do |c|
29 | c.syntax = :expect
30 | c.max_formatted_output_length = nil
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/unit/align_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe TTY::Box, ":align option" do
4 | it "aligns content without positioning" do
5 | box = TTY::Box.frame(width: 26, height: 4, align: :center) do
6 | "Drawing a box in terminal emulator"
7 | end
8 |
9 | expect(box).to eq([
10 | "┌────────────────────────┐\n",
11 | "│ Drawing a box in │\n",
12 | "│ terminal emulator │\n",
13 | "└────────────────────────┘\n"
14 | ].join)
15 | end
16 |
17 | it "aligns content with the option" do
18 | box = TTY::Box.frame(top: 0, left: 0, width: 26, height: 4,
19 | align: :center) do
20 | "Drawing a box in terminal emulator"
21 | end
22 |
23 | expect(box).to eq([
24 | "\e[1;1H┌────────────────────────┐",
25 | "\e[2;1H│ Drawing a box in \e[2;26H│",
26 | "\e[3;1H│ terminal emulator \e[3;26H│",
27 | "\e[4;1H└────────────────────────┘"
28 | ].join)
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/unit/border/parse_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe TTY::Box::Border, ".parse" do
4 | it "parses default border" do
5 | border = TTY::Box::Border.parse({})
6 | top_border = [border.top_left, border.top, border.top_right]
7 | bottom_border = [border.bottom_left, border.bottom, border.bottom_right]
8 |
9 | expect(border.type).to eq(:light)
10 | expect(top_border).to eq(%i[corner_top_left line corner_top_right])
11 | expect(bottom_border).to eq(%i[corner_bottom_left line corner_bottom_right])
12 | expect(border.left).to eq(:pipe)
13 | expect(border.right).to eq(:pipe)
14 | end
15 |
16 | it "parses only border type" do
17 | border = TTY::Box::Border.parse(:thick)
18 | top_border = [border.top_left, border.top, border.top_right]
19 | bottom_border = [border.bottom_left, border.bottom, border.bottom_right]
20 |
21 | expect(border.type).to eq(:thick)
22 | expect(top_border).to eq(%i[corner_top_left line corner_top_right])
23 | expect(bottom_border).to eq(%i[corner_bottom_left line corner_bottom_right])
24 | expect(border.left).to eq(:pipe)
25 | expect(border.right).to eq(:pipe)
26 | end
27 |
28 | it "parses custom border" do
29 | border = TTY::Box::Border.parse({
30 | top: true,
31 | top_left: :cross,
32 | top_right: :cross,
33 | bottom: true,
34 | bottom_left: :cross,
35 | bottom_right: :cross
36 | })
37 |
38 | top_border = [border.top_left, border.top, border.top_right]
39 | bottom_border = [border.bottom_left, border.bottom, border.bottom_right]
40 |
41 | expect(border.type).to eq(:light)
42 | expect(top_border).to eq([:cross, true, :cross])
43 | expect(bottom_border).to eq([:cross, true, :cross])
44 | end
45 |
46 | it "parses divider values" do
47 | border = TTY::Box::Border.parse({
48 | top_left: :divider_right,
49 | top_right: :divider_left,
50 | bottom_left: :divider_down,
51 | bottom_right: :divider_up
52 | })
53 |
54 | top_border = [border.top_left, border.top, border.top_right]
55 | bottom_border = [border.bottom_left, border.bottom, border.bottom_right]
56 |
57 | expect(border.type).to eq(:light)
58 | expect(top_border).to eq(%i[divider_right line divider_left])
59 | expect(bottom_border).to eq(%i[divider_down line divider_up])
60 | end
61 |
62 | it "defaults border size to one" do
63 | border = TTY::Box::Border.parse({})
64 |
65 | expect(border.top_size).to eq(1)
66 | expect(border.bottom_size).to eq(1)
67 | expect(border.left_size).to eq(1)
68 | expect(border.right_size).to eq(1)
69 | end
70 |
71 | it "returns zero size for no border" do
72 | border = TTY::Box::Border.parse({
73 | top: false,
74 | left: false,
75 | right: false,
76 | bottom: false
77 | })
78 |
79 | expect(border.top_size).to eq(0)
80 | expect(border.left_size).to eq(0)
81 | expect(border.right_size).to eq(0)
82 | expect(border.bottom_size).to eq(0)
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/spec/unit/border_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe TTY::Box, ":border option" do
4 | it "creates frame with double lines and no position" do
5 | box = TTY::Box.frame(
6 | width: 35, height: 4,
7 | border: :thick
8 | )
9 |
10 | expect(box).to eq([
11 | "╔═════════════════════════════════╗\n",
12 | "║ ║\n",
13 | "║ ║\n",
14 | "╚═════════════════════════════════╝\n"
15 | ].join)
16 | end
17 |
18 | it "creates frame with double lines and absolute position" do
19 | box = TTY::Box.frame(
20 | top: 0, left: 0,
21 | width: 35, height: 4,
22 | border: :thick
23 | )
24 |
25 | expect(box).to eq([
26 | "\e[1;1H╔═════════════════════════════════╗",
27 | "\e[2;1H║\e[2;35H║",
28 | "\e[3;1H║\e[3;35H║",
29 | "\e[4;1H╚═════════════════════════════════╝"
30 | ].join)
31 | end
32 |
33 | it "creates an ASCII box" do
34 | box = TTY::Box.frame(
35 | width: 10, height: 4,
36 | border: :ascii
37 | )
38 |
39 | expect(box).to eq([
40 | "+--------+\n",
41 | "| |\n",
42 | "| |\n",
43 | "+--------+\n"
44 | ].join)
45 | end
46 |
47 | it "creates frame with without top & bottom borders" do
48 | box = TTY::Box.frame(
49 | top: 0, left: 0,
50 | width: 15, height: 4,
51 | border: {
52 | type: :thick,
53 | top: false,
54 | bottom: false
55 | }
56 | ) { "Hello Piotr!" }
57 |
58 | expect(box).to eq([
59 | "\e[1;1H║Hello Piotr! \e[1;15H║",
60 | "\e[2;1H║\e[2;15H║",
61 | "\e[3;1H║\e[3;15H║",
62 | "\e[4;1H║\e[4;15H║"
63 | ].join)
64 | end
65 |
66 | it "creates frame without left & right borders" do
67 | box = TTY::Box.frame(
68 | top: 0, left: 0,
69 | width: 15, height: 4,
70 | border: {
71 | left: false,
72 | right: false
73 | }
74 | ) { "Hello Piotr!" }
75 |
76 | expect(box).to eq([
77 | "\e[1;1H───────────────",
78 | "\e[2;1HHello Piotr! ",
79 | "\e[3;1H",
80 | "\e[4;1H───────────────"
81 | ].join)
82 | end
83 |
84 | it "creates frame without left & top borders" do
85 | box = TTY::Box.frame(
86 | top: 0, left: 0,
87 | width: 15, height: 4,
88 | border: {
89 | left: false,
90 | top: false
91 | }
92 | ) { "Hello Piotr!" }
93 |
94 | expect(box).to eq([
95 | "\e[1;1HHello Piotr! \e[1;15H│",
96 | "\e[2;1H\e[2;15H│",
97 | "\e[3;1H\e[3;15H│",
98 | "\e[4;1H──────────────┘"
99 | ].join)
100 | end
101 |
102 | it "creates frame with all corners changed to cross" do
103 | box = TTY::Box.frame(
104 | width: 10, height: 4,
105 | border: {
106 | top_left: :cross,
107 | top_right: :cross,
108 | bottom_left: :cross,
109 | bottom_right: :cross
110 | }
111 | )
112 |
113 | expect(box).to eq([
114 | "┼────────┼\n",
115 | "│ │\n",
116 | "│ │\n",
117 | "┼────────┼\n"
118 | ].join)
119 | end
120 |
121 | it "creates frame with all corners changed to dividers" do
122 | box = TTY::Box.frame(
123 | width: 10, height: 4,
124 | border: {
125 | top_left: :divider_down,
126 | top_right: :divider_left,
127 | bottom_left: :divider_right,
128 | bottom_right: :divider_up
129 | }
130 | ) { "hello" }
131 |
132 | expect(box).to eq([
133 | "┬────────┤\n",
134 | "│hello │\n",
135 | "│ │\n",
136 | "├────────┴\n"
137 | ].join)
138 | end
139 |
140 | it "fails to recognise border value" do
141 | expect {
142 | TTY::Box.frame(border: { left: :unknown })
143 | }.to raise_error(ArgumentError, "invalid :left border value: :unknown")
144 | end
145 |
146 | it "fails to recognise border option" do
147 | expect {
148 | TTY::Box.frame(width: 35, height: 4, border: [:unknown])
149 | }.to raise_error(
150 | ArgumentError,
151 | "Wrong value `[:unknown]` for :border configuration option"
152 | )
153 | end
154 | end
155 |
--------------------------------------------------------------------------------
/spec/unit/custom_frame_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe TTY::Box, "custom frames" do
4 | it "draws info type message" do
5 | box = TTY::Box.info("Hello world!", enable_color: true)
6 |
7 | expect(box).to eq([
8 | "\e[104m\e[30m╔\e[0m\e[0m\e[104m\e[30m ℹ INFO \e[0m\e[0m\e[104m\e[30m═══\e[0m\e[0m\e[104m\e[30m═══\e[0m\e[0m\e[104m\e[30m╗\e[0m\e[0m\n",
9 | "\e[104m\e[30m║\e[0m\e[0m\e[104m\e[30m \e[0m\e[0m\e[104m\e[30m║\e[0m\e[0m\n",
10 | "\e[104m\e[30m║\e[0m\e[0m\e[104m\e[30m Hello world! \e[0m\e[0m\e[104m\e[30m║\e[0m\e[0m\n",
11 | "\e[104m\e[30m║\e[0m\e[0m\e[104m\e[30m \e[0m\e[0m\e[104m\e[30m║\e[0m\e[0m\n",
12 | "\e[104m\e[30m╚\e[0m\e[0m\e[104m\e[30m═══════\e[0m\e[0m\e[104m\e[30m═══════\e[0m\e[0m\e[104m\e[30m╝\e[0m\e[0m\n"
13 | ].join)
14 | end
15 |
16 | it "draws warning type message" do
17 | box = TTY::Box.warn("Hello world!", enable_color: true)
18 |
19 | expect(box).to eq([
20 | "\e[103m\e[30m╔\e[0m\e[0m\e[103m\e[30m ⚠ WARNING \e[0m\e[0m\e[103m\e[30m═\e[0m\e[0m\e[103m\e[30m══\e[0m\e[0m\e[103m\e[30m╗\e[0m\e[0m\n",
21 | "\e[103m\e[30m║\e[0m\e[0m\e[103m\e[30m \e[0m\e[0m\e[103m\e[30m║\e[0m\e[0m\n",
22 | "\e[103m\e[30m║\e[0m\e[0m\e[103m\e[30m Hello world! \e[0m\e[0m\e[103m\e[30m║\e[0m\e[0m\n",
23 | "\e[103m\e[30m║\e[0m\e[0m\e[103m\e[30m \e[0m\e[0m\e[103m\e[30m║\e[0m\e[0m\n",
24 | "\e[103m\e[30m╚\e[0m\e[0m\e[103m\e[30m═══════\e[0m\e[0m\e[103m\e[30m═══════\e[0m\e[0m\e[103m\e[30m╝\e[0m\e[0m\n"
25 | ].join)
26 | end
27 |
28 | it "draws success type message" do
29 | box = TTY::Box.success("Hello world!", enable_color: true)
30 |
31 | expect(box).to eq([
32 | "\e[102m\e[30m╔\e[0m\e[0m\e[102m\e[30m ✔ OK \e[0m\e[0m\e[102m\e[30m════\e[0m\e[0m\e[102m\e[30m════\e[0m\e[0m\e[102m\e[30m╗\e[0m\e[0m\n",
33 | "\e[102m\e[30m║\e[0m\e[0m\e[102m\e[30m \e[0m\e[0m\e[102m\e[30m║\e[0m\e[0m\n",
34 | "\e[102m\e[30m║\e[0m\e[0m\e[102m\e[30m Hello world! \e[0m\e[0m\e[102m\e[30m║\e[0m\e[0m\n",
35 | "\e[102m\e[30m║\e[0m\e[0m\e[102m\e[30m \e[0m\e[0m\e[102m\e[30m║\e[0m\e[0m\n",
36 | "\e[102m\e[30m╚\e[0m\e[0m\e[102m\e[30m═══════\e[0m\e[0m\e[102m\e[30m═══════\e[0m\e[0m\e[102m\e[30m╝\e[0m\e[0m\n"
37 | ].join)
38 | end
39 |
40 | it "draws error type message" do
41 | box = TTY::Box.error("Hello world!", enable_color: true)
42 |
43 | expect(box).to eq([
44 | "\e[41m\e[97m╔\e[0m\e[0m\e[41m\e[97m ⨯ ERROR \e[0m\e[0m\e[41m\e[97m══\e[0m\e[0m\e[41m\e[97m═══\e[0m\e[0m\e[41m\e[97m╗\e[0m\e[0m\n",
45 | "\e[41m\e[97m║\e[0m\e[0m\e[41m\e[97m \e[0m\e[0m\e[41m\e[97m║\e[0m\e[0m\n",
46 | "\e[41m\e[97m║\e[0m\e[0m\e[41m\e[97m Hello world! \e[0m\e[0m\e[41m\e[97m║\e[0m\e[0m\n",
47 | "\e[41m\e[97m║\e[0m\e[0m\e[41m\e[97m \e[0m\e[0m\e[41m\e[97m║\e[0m\e[0m\n",
48 | "\e[41m\e[97m╚\e[0m\e[0m\e[41m\e[97m═══════\e[0m\e[0m\e[41m\e[97m═══════\e[0m\e[0m\e[41m\e[97m╝\e[0m\e[0m\n"
49 | ].join)
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/spec/unit/frame_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe TTY::Box, "#frame" do
4 | it "creates frame without content & width & height values" do
5 | output = TTY::Box.frame
6 |
7 | expect(output).to eq([
8 | "┌─┐\n",
9 | "└─┘\n"
10 | ].join)
11 | end
12 |
13 | it "creates frame around single character content without width & height" do
14 | output = TTY::Box.frame("H")
15 |
16 | expect(output).to eq([
17 | "┌─┐\n",
18 | "│H│\n",
19 | "└─┘\n"
20 | ].join)
21 | end
22 |
23 | it "creates frame around content without width & height values" do
24 | output = TTY::Box.frame "Hello world!"
25 |
26 | expect(output).to eq([
27 | "┌────────────┐\n",
28 | "│Hello world!│\n",
29 | "└────────────┘\n"
30 | ].join)
31 | end
32 |
33 | it "creates frame around content without width & height values" do
34 | output = TTY::Box.frame "Hello\nworld!"
35 |
36 | expect(output).to eq([
37 | "┌──────┐\n",
38 | "│Hello │\n",
39 | "│world!│\n",
40 | "└──────┘\n"
41 | ].join)
42 | end
43 |
44 | it "creates frame based on multiline content without width & height values" do
45 | output = TTY::Box.frame "Hello", "world!"
46 |
47 | expect(output).to eq([
48 | "┌──────┐\n",
49 | "│Hello │\n",
50 | "│world!│\n",
51 | "└──────┘\n"
52 | ].join)
53 | end
54 |
55 | it "creates frame around block content without width & height values" do
56 | output = TTY::Box.frame do
57 | "Hello world!"
58 | end
59 |
60 | expect(output).to eq([
61 | "┌────────────┐\n",
62 | "│Hello world!│\n",
63 | "└────────────┘\n"
64 | ].join)
65 | end
66 |
67 | it "creates frame with only width & height values" do
68 | output = TTY::Box.frame(width: 35, height: 4)
69 |
70 | expect(output).to eq([
71 | "┌─────────────────────────────────┐\n",
72 | "│ │\n",
73 | "│ │\n",
74 | "└─────────────────────────────────┘\n"
75 | ].join)
76 | end
77 |
78 | it "creates frame at a position with direct width & height values" do
79 | output = TTY::Box.frame(top: 0, left: 0, width: 35, height: 4)
80 |
81 | expect(output).to eq([
82 | "\e[1;1H┌─────────────────────────────────┐",
83 | "\e[2;1H│\e[2;35H│",
84 | "\e[3;1H│\e[3;35H│",
85 | "\e[4;1H└─────────────────────────────────┘"
86 | ].join)
87 | end
88 |
89 | it "displays content when block provided" do
90 | output = TTY::Box.frame(top: 0, left: 0, width: 35, height: 4) do
91 | "Hello world!"
92 | end
93 |
94 | expect(output).to eq([
95 | "\e[1;1H┌─────────────────────────────────┐",
96 | "\e[2;1H│Hello world! \e[2;35H│",
97 | "\e[3;1H│\e[3;35H│",
98 | "\e[4;1H└─────────────────────────────────┘"
99 | ].join)
100 | end
101 |
102 | it "wraps content when exceeding width" do
103 | box = TTY::Box.frame(top: 0, left: 0, width: 20, height: 4) do
104 | "Drawing a box in terminal emulator"
105 | end
106 |
107 | expect(box).to eq([
108 | "\e[1;1H┌──────────────────┐",
109 | "\e[2;1H│Drawing a box in \e[2;20H│",
110 | "\e[3;1H│terminal emulator \e[3;20H│",
111 | "\e[4;1H└──────────────────┘"
112 | ].join)
113 | end
114 |
115 | it "correctly displays colored content" do
116 | box = TTY::Box.frame(width: 35, height: 3) do
117 | Pastel.new(enabled: true).green.on_red("Hello world!")
118 | end
119 |
120 | expect(box).to eq([
121 | "┌─────────────────────────────────┐\n",
122 | "│\e[32;41mHello world!\e[0m │\n",
123 | "└─────────────────────────────────┘\n"
124 | ].join)
125 | end
126 |
127 | it "correctly envelopes colored text" do
128 | box = TTY::Box.frame do
129 | Pastel.new(enabled: true).green.on_red("Hello world!")
130 | end
131 |
132 | expect(box).to eq([
133 | "┌────────────┐\n",
134 | "│\e[32;41mHello world!\e[0m│\n",
135 | "└────────────┘\n"
136 | ].join)
137 | end
138 |
139 | it "displays multiline colored content" do
140 | p = Pastel.new(enabled: true)
141 | content = p.green("Hello") + "\n" + p.yellow("world!")
142 | box = TTY::Box.frame(content, padding: 1)
143 |
144 | expect(box).to eq([
145 | "┌────────┐\n",
146 | "│ │\n",
147 | "│ \e[32mHello\e[0m │\n",
148 | "│ \e[33mworld!\e[0m │\n",
149 | "│ │\n",
150 | "└────────┘\n"
151 | ].join)
152 | end
153 |
154 | it "correctly spaces colored titles" do
155 | p = Pastel.new(enabled: true)
156 | box = TTY::Box.frame(title: {
157 | top_left: p.green.on_red("TITLE"),
158 | bottom_right: p.green.on_red("(v1.0)")
159 | }) do
160 | "Hello world!"
161 | end
162 |
163 | expect(box).to eq([
164 | "┌\e[32;41mTITLE\e[0m───────┐\n",
165 | "│Hello world!│\n",
166 | "└──────\e[32;41m(v1.0)\e[0m┘\n"
167 | ].join)
168 | end
169 |
170 | it "handles \r\n line breaks" do
171 | box = TTY::Box.frame(width: 29, height: 7) do
172 | "Closes #360\r\n\r\nCloses !217"
173 | end
174 |
175 | expect(box).to eq([
176 | "┌───────────────────────────┐\r\n",
177 | "│Closes #360 │\r\n",
178 | "│ │\r\n",
179 | "│Closes !217 │\r\n",
180 | "│ │\r\n",
181 | "│ │\r\n",
182 | "└───────────────────────────┘\r\n"
183 | ].join)
184 | end
185 |
186 | it "preserves newline character breaks" do
187 | box = TTY::Box.frame("foo\n\n\nbar\n\nbaz")
188 |
189 | expect(box).to eq([
190 | "┌───┐\n",
191 | "│foo│\n",
192 | "│ │\n",
193 | "│ │\n",
194 | "│bar│\n",
195 | "│ │\n",
196 | "│baz│\n",
197 | "└───┘\n"
198 | ].join)
199 | end
200 | end
201 |
--------------------------------------------------------------------------------
/spec/unit/new_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe TTY::Box, ".new" do
4 | it "calculates dimensions for a box with border and single line content" do
5 | box = TTY::Box.new("Hello world")
6 |
7 | expect(box.width).to eq(13)
8 | expect(box.height).to eq(3)
9 | expect(box.content_width).to eq(11)
10 | expect(box.content_height).to eq(1)
11 |
12 | expect(box.render).to eq([
13 | "┌───────────┐\n",
14 | "│Hello world│\n",
15 | "└───────────┘\n"
16 | ].join)
17 | end
18 |
19 | it "calculates dimensions for a box with border and multiline content" do
20 | box = TTY::Box.new("Hello\nnew\nworld")
21 |
22 | expect(box.width).to eq(7)
23 | expect(box.height).to eq(5)
24 | expect(box.content_width).to eq(5)
25 | expect(box.content_height).to eq(3)
26 |
27 | expect(box.render).to eq([
28 | "┌─────┐\n",
29 | "│Hello│\n",
30 | "│new │\n",
31 | "│world│\n",
32 | "└─────┘\n"
33 | ].join)
34 | end
35 |
36 | it "calculates dimensions for a box with padding and multiline content" do
37 | box = TTY::Box.new("Hello\nnew\nworld", padding: [1, 2])
38 |
39 | expect(box.width).to eq(11)
40 | expect(box.height).to eq(7)
41 | expect(box.content_width).to eq(5)
42 | expect(box.content_height).to eq(3)
43 |
44 | expect(box.render).to eq([
45 | "┌─────────┐\n",
46 | "│ │\n",
47 | "│ Hello │\n",
48 | "│ new │\n",
49 | "│ world │\n",
50 | "│ │\n",
51 | "└─────────┘\n"
52 | ].join)
53 | end
54 |
55 | it "calculates dimensions for a box with padding and wrapped content" do
56 | box = TTY::Box.new("Hello new world", width: 12, padding: [1, 2])
57 |
58 | expect(box.width).to eq(12)
59 | expect(box.height).to eq(7)
60 | expect(box.content_width).to eq(6)
61 | expect(box.content_height).to eq(3)
62 |
63 | expect(box.render).to eq([
64 | "┌──────────┐\n",
65 | "│ │\n",
66 | "│ Hello │\n",
67 | "│ new │\n",
68 | "│ world │\n",
69 | "│ │\n",
70 | "└──────────┘\n"
71 | ].join)
72 | end
73 |
74 | it "calculates box dimensions without border" do
75 | no_border = {top: false, left: false, right: false, bottom: false}
76 | box = TTY::Box.new("Hello world", border: no_border)
77 |
78 | expect(box.width).to eq(11)
79 | expect(box.height).to eq(1)
80 | expect(box.content_width).to eq(11)
81 | expect(box.content_height).to eq(1)
82 |
83 | expect(box.render).to eq([
84 | "Hello world\n"
85 | ].join)
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/spec/unit/padding_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe TTY::Box, ":padding option" do
4 | it "padds internal content without width and height" do
5 | box = TTY::Box.frame(padding: 1) do
6 | "Drawing a box in terminal emulator"
7 | end
8 |
9 | expect(box).to eq([
10 | "┌────────────────────────────────────┐\n",
11 | "│ │\n",
12 | "│ Drawing a box in terminal emulator │\n",
13 | "│ │\n",
14 | "└────────────────────────────────────┘\n"
15 | ].join)
16 | end
17 |
18 | it "padds internal content without position arguments" do
19 | box = TTY::Box.frame(width: 30, height: 6, padding: 1) do
20 | "Drawing a box in terminal emulator"
21 | end
22 |
23 | expect(box).to eq([
24 | "┌────────────────────────────┐\n",
25 | "│ │\n",
26 | "│ Drawing a box in terminal │\n",
27 | "│ emulator │\n",
28 | "│ │\n",
29 | "└────────────────────────────┘\n"
30 | ].join)
31 | end
32 |
33 | it "padds internal content with with padding as integer" do
34 | box = TTY::Box.frame(top: 0, left: 0, width: 30, height: 6, padding: 1) do
35 | "Drawing a box in terminal emulator"
36 | end
37 |
38 | expect(box).to eq([
39 | "\e[1;1H┌────────────────────────────┐",
40 | "\e[2;1H│ \e[2;30H│",
41 | "\e[3;1H│ Drawing a box in terminal \e[3;30H│",
42 | "\e[4;1H│ emulator \e[4;30H│",
43 | "\e[5;1H│ \e[5;30H│",
44 | "\e[6;1H└────────────────────────────┘"
45 | ].join)
46 | end
47 |
48 | it "padds internal content with padding as array" do
49 | box = TTY::Box.frame(top: 0, left: 0, width: 30,
50 | height: 6, padding: [1, 3, 1, 3]) do
51 | "Drawing a box in terminal emulator"
52 | end
53 |
54 | expect(box).to eq([
55 | "\e[1;1H┌────────────────────────────┐",
56 | "\e[2;1H│ \e[2;30H│",
57 | "\e[3;1H│ Drawing a box in \e[3;30H│",
58 | "\e[4;1H│ terminal emulator \e[4;30H│",
59 | "\e[5;1H│ \e[5;30H│",
60 | "\e[6;1H└────────────────────────────┘"
61 | ].join)
62 | end
63 |
64 | it "handles \r\n line breaks when padding" do
65 | box = TTY::Box.frame(width: 29, height: 7, padding: 1) do
66 | "Closes #360\r\n\r\nCloses !217"
67 | end
68 |
69 | expect(box).to eq([
70 | "┌───────────────────────────┐\r\n",
71 | "│ │\r\n",
72 | "│ Closes #360 │\r\n",
73 | "│ │\r\n",
74 | "│ Closes !217 │\r\n",
75 | "│ │\r\n",
76 | "└───────────────────────────┘\r\n"
77 | ].join)
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/spec/unit/position_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe TTY::Box, ":top, :left options" do
4 | it "skips positioning when no top & left values provided" do
5 | box = TTY::Box.new(width: 35, height: 4)
6 |
7 | expect(box.top).to eq(nil)
8 | expect(box.left).to eq(nil)
9 | expect(box.position?).to eq(false)
10 |
11 | expect(box.render).to eq([
12 | "┌─────────────────────────────────┐\n",
13 | "│ │\n",
14 | "│ │\n",
15 | "└─────────────────────────────────┘\n"
16 | ].join)
17 | end
18 |
19 | it "allows to absolutely position within the terminal window" do
20 | box = TTY::Box.new(top: 10, left: 40, width: 35, height: 4)
21 |
22 | expect(box.top).to eq(10)
23 | expect(box.left).to eq(40)
24 | expect(box.position?).to eq(true)
25 |
26 | expect(box.render).to eq([
27 | "\e[11;41H┌─────────────────────────────────┐",
28 | "\e[12;41H│\e[12;75H│",
29 | "\e[13;41H│\e[13;75H│",
30 | "\e[14;41H└─────────────────────────────────┘"
31 | ].join)
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/unit/style_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe TTY::Box, ":style option" do
4 | it "applies styling to content and border" do
5 | box = TTY::Box.new(
6 | enable_color: true,
7 | top: 0,
8 | left: 0,
9 | width: 30,
10 | height: 4,
11 | border: :thick,
12 | title: {
13 | top_left: " file1 "
14 | },
15 | style: {
16 | fg: :bright_yellow,
17 | bg: :blue,
18 | border: {
19 | fg: :bright_yellow,
20 | bg: :blue
21 | }
22 | }
23 | ) do
24 | "Midnight Commander\nis the best"
25 | end
26 |
27 | expect(box.content_style?).to eq(true)
28 | expect(box.render).to eq([
29 | "\e[1;1H\e[44m\e[93m╔\e[0m\e[0m\e[44m\e[93m",
30 | " file1 \e[0m\e[0m\e[44m\e[93m══════════\e[0m\e[0m",
31 | "\e[44m\e[93m═══════════\e[0m\e[0m\e[44m\e[93m╗\e[0m\e[0m",
32 |
33 | "\e[2;1H\e[44m\e[93m║\e[0m\e[0m\e[44m\e[93m",
34 | "Midnight Commander ",
35 | "\e[0m\e[0m\e[2;30H\e[44m\e[93m║\e[0m\e[0m",
36 |
37 | "\e[3;1H\e[44m\e[93m║\e[0m\e[0m\e[44m\e[93m",
38 | "is the best \e[0m\e[0m\e[3;30H\e[44m\e[93m║\e[0m\e[0m",
39 |
40 | "\e[4;1H\e[44m\e[93m╚\e[0m\e[0m\e[44m\e[93m══════════════\e[0m\e[0m",
41 | "\e[44m\e[93m══════════════\e[0m\e[0m\e[44m\e[93m╝\e[0m\e[0m"
42 | ].join)
43 | end
44 |
45 | it "creates box without corners and only color fill" do
46 | box = TTY::Box.new(
47 | enable_color: true,
48 | width: 10, height: 4,
49 | border: {
50 | top_left: false,
51 | top_right: false,
52 | bottom_left: false,
53 | bottom_right: false
54 | },
55 | style: {
56 | fg: :bright_yellow,
57 | bg: :blue
58 | }
59 | )
60 |
61 | expect(box.content_style?).to eq(true)
62 | expect(box.render).to eq([
63 | "──────────\n",
64 | "│\e[44m\e[93m \e[0m\e[0m│\n",
65 | "│\e[44m\e[93m \e[0m\e[0m│\n",
66 | "──────────\n"
67 | ].join)
68 | end
69 |
70 | it "creates box without left & right borders and only color fill" do
71 | box = TTY::Box.frame(
72 | enable_color: true,
73 | width: 10, height: 4,
74 | border: {
75 | left: false,
76 | right: false
77 | },
78 | style: {
79 | fg: :bright_yellow,
80 | bg: :blue
81 | }
82 | )
83 |
84 | expect(box).to eq([
85 | "──────────\n",
86 | "\e[44m\e[93m \e[0m\e[0m\n",
87 | "\e[44m\e[93m \e[0m\e[0m\n",
88 | "──────────\n"
89 | ].join)
90 | end
91 |
92 | it "creates box without top & bottom borders and only color fill" do
93 | box = TTY::Box.frame(
94 | enable_color: true,
95 | width: 10, height: 4,
96 | border: {
97 | top: false,
98 | bottom: false
99 | },
100 | style: {
101 | fg: :bright_yellow,
102 | bg: :blue
103 | }
104 | )
105 |
106 | expect(box).to eq([
107 | "│\e[44m\e[93m \e[0m\e[0m│\n",
108 | "│\e[44m\e[93m \e[0m\e[0m│\n",
109 | "│\e[44m\e[93m \e[0m\e[0m│\n",
110 | "│\e[44m\e[93m \e[0m\e[0m│\n"
111 | ].join)
112 | end
113 |
114 | it "creates box without top & left borders and only color fill" do
115 | box = TTY::Box.frame(
116 | enable_color: true,
117 | width: 10, height: 4,
118 | border: {
119 | top: false,
120 | left: false
121 | },
122 | style: {
123 | fg: :bright_yellow,
124 | bg: :blue,
125 | border: {
126 | fg: :bright_yellow,
127 | bg: :blue
128 | }
129 | }
130 | )
131 |
132 | expect(box).to eq([
133 | "\e[44m\e[93m \e[0m\e[0m\e[44m\e[93m│\e[0m\e[0m\n",
134 | "\e[44m\e[93m \e[0m\e[0m\e[44m\e[93m│\e[0m\e[0m\n",
135 | "\e[44m\e[93m \e[0m\e[0m\e[44m\e[93m│\e[0m\e[0m\n",
136 | "\e[44m\e[93m────\e[0m\e[0m",
137 | "\e[44m\e[93m─────\e[0m\e[0m\e[44m\e[93m┘\e[0m\e[0m\n"
138 | ].join)
139 | end
140 | end
141 |
--------------------------------------------------------------------------------
/spec/unit/title_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe TTY::Box, ":title option" do
4 | it "allows to specify top border titles" do
5 | output = TTY::Box.frame(
6 | top: 0, left: 0,
7 | width: 35, height: 4,
8 | title: {
9 | top_left: "left",
10 | top_right: "right",
11 | top_center: "center"
12 | }
13 | )
14 |
15 | expect(output).to eq([
16 | "\e[1;1H┌left─────────center─────────right┐",
17 | "\e[2;1H│\e[2;35H│\e[3;1H│\e[3;35H│",
18 | "\e[4;1H└─────────────────────────────────┘"
19 | ].join)
20 | end
21 |
22 | it "allows to specify bottom border titles" do
23 | output = TTY::Box.frame(
24 | top: 0, left: 0,
25 | width: 35, height: 4,
26 | title: {
27 | bottom_left: "left",
28 | bottom_right: "right",
29 | bottom_center: "center"
30 | }
31 | )
32 |
33 | expect(output).to eq([
34 | "\e[1;1H┌─────────────────────────────────┐",
35 | "\e[2;1H│\e[2;35H│\e[3;1H│\e[3;35H│",
36 | "\e[4;1H└left─────────center─────────right┘"
37 | ].join)
38 | end
39 |
40 | it "allows the top title to be longer than the message" do
41 | output = TTY::Box.frame("BOO!",
42 | title: {
43 | top_left: " ⚠ WARNING "
44 | })
45 |
46 | expect(output).to eq([
47 | "┌ ⚠ WARNING ┐\n",
48 | "│BOO! │\n",
49 | "└───────────┘\n"
50 | ].join)
51 | end
52 |
53 | it "allows the bottom title to be longer than the message" do
54 | output = TTY::Box.frame("BOO!",
55 | title: {
56 | bottom_left: " ⚠ WARNING "
57 | })
58 |
59 | expect(output).to eq([
60 | "┌───────────┐\n",
61 | "│BOO! │\n",
62 | "└ ⚠ WARNING ┘\n"
63 | ].join)
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/tasks/console.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | desc "Load gem inside irb console"
4 | task :console do
5 | require "irb"
6 | require "irb/completion"
7 | require File.join(__FILE__, "../../lib/tty-box")
8 | ARGV.clear
9 | IRB.start
10 | end
11 | task c: %w[console]
12 |
--------------------------------------------------------------------------------
/tasks/coverage.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | desc "Measure code coverage"
4 | task :coverage do
5 | begin
6 | original, ENV["COVERAGE"] = ENV["COVERAGE"], "true"
7 | Rake::Task["spec"].invoke
8 | ensure
9 | ENV["COVERAGE"] = original
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/tasks/spec.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | begin
4 | require "rspec/core/rake_task"
5 |
6 | desc "Run all specs"
7 | RSpec::Core::RakeTask.new(:spec) do |task|
8 | task.pattern = "spec/{unit,integration}{,/*/**}/*_spec.rb"
9 | end
10 |
11 | namespace :spec do
12 | desc "Run unit specs"
13 | RSpec::Core::RakeTask.new(:unit) do |task|
14 | task.pattern = "spec/unit{,/*/**}/*_spec.rb"
15 | end
16 |
17 | desc "Run integration specs"
18 | RSpec::Core::RakeTask.new(:integration) do |task|
19 | task.pattern = "spec/integration{,/*/**}/*_spec.rb"
20 | end
21 |
22 | desc "Run performance specs"
23 | RSpec::Core::RakeTask.new(:perf) do |task|
24 | task.pattern = "spec/perf{,/*/**}/*_spec.rb"
25 | end
26 | end
27 | rescue LoadError
28 | %w[spec spec:unit spec:integration spec:perf].each do |name|
29 | task name do
30 | warn "In order to run #{name}, do `gem install rspec`"
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/tty-box.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "lib/tty/box/version"
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = "tty-box"
7 | spec.version = TTY::Box::VERSION
8 | spec.authors = ["Piotr Murach"]
9 | spec.email = ["piotr@piotrmurach.com"]
10 | spec.summary = %q{Draw various frames and boxes in the terminal window.}
11 | spec.description = %q{Draw various frames and boxes in the terminal window.}
12 | spec.homepage = "https://ttytoolkit.org"
13 | spec.license = "MIT"
14 | if spec.respond_to?(:metadata=)
15 | spec.metadata = {
16 | "allowed_push_host" => "https://rubygems.org",
17 | "bug_tracker_uri" => "https://github.com/piotrmurach/tty-box/issues",
18 | "changelog_uri" => "https://github.com/piotrmurach/tty-box/blob/master/CHANGELOG.md",
19 | "documentation_uri" => "https://www.rubydoc.info/gems/tty-box",
20 | "homepage_uri" => spec.homepage,
21 | "source_code_uri" => "https://github.com/piotrmurach/tty-box"
22 | }
23 | end
24 | spec.files = Dir["lib/**/*"]
25 | spec.extra_rdoc_files = ["README.md", "CHANGELOG.md", "LICENSE.txt"]
26 | spec.require_paths = ["lib"]
27 | spec.required_ruby_version = ">= 2.0.0"
28 |
29 | spec.add_dependency "pastel", "~> 0.8"
30 | spec.add_dependency "tty-cursor", "~> 0.7"
31 | spec.add_dependency "strings", "~> 0.2.0"
32 |
33 | spec.add_development_dependency "rake"
34 | spec.add_development_dependency "rspec", ">= 3.0"
35 | end
36 |
--------------------------------------------------------------------------------