├── .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 | TTY Toolkit logo 3 |
4 | 5 | # TTY::Box [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter] 6 | 7 | [![Gem Version](https://badge.fury.io/rb/tty-box.svg)][gem] 8 | [![Actions CI](https://github.com/piotrmurach/tty-box/workflows/CI/badge.svg?branch=master)][gh_actions_ci] 9 | [![Build status](https://ci.appveyor.com/api/projects/status/h9b88fk5xpya3fh1?svg=true)][appveyor] 10 | [![Maintainability](https://api.codeclimate.com/v1/badges/dfac05073e1549e9dbb6/maintainability)][codeclimate] 11 | [![Coverage Status](https://coveralls.io/repos/github/piotrmurach/tty-box/badge.svg)][coverage] 12 | [![Inline docs](http://inch-ci.org/github/piotrmurach/tty-box.svg?branch=master)][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 | ![Box drawing](https://github.com/piotrmurach/tty-box/blob/master/assets/tty-box-drawing.png) 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 | ![Box messages](https://github.com/piotrmurach/tty-box/blob/master/assets/tty-box-messages.png) 416 | 417 | #### 2.8.1 info 418 | 419 | To draw an information type box around your content use `info`: 420 | 421 | ```ruby 422 | box = TTY::Box.info("Deploying application") 423 | ``` 424 | 425 | And then print: 426 | 427 | ```ruby 428 | print box 429 | # => 430 | # ╔ ℹ INFO ═══════════════╗ 431 | # ║ ║ 432 | # ║ Deploying application ║ 433 | # ║ ║ 434 | # ╚═══════════════════════╝ 435 | ``` 436 | 437 | #### 2.8.2 warn 438 | 439 | To draw a warning type box around your content use `warn`: 440 | 441 | ```ruby 442 | box = TTY::Box.warn("Deploying application") 443 | ``` 444 | 445 | And then print: 446 | 447 | ```ruby 448 | print box 449 | # => 450 | # ╔ ⚠ WARNING ════════════╗ 451 | # ║ ║ 452 | # ║ Deploying application ║ 453 | # ║ ║ 454 | # ╚═══════════════════════╝ 455 | ``` 456 | 457 | #### 2.8.3 success 458 | 459 | To draw a success type box around your content use `success`: 460 | 461 | ```ruby 462 | box = TTY::Box.success("Deploying application") 463 | ``` 464 | 465 | And then print: 466 | 467 | ```ruby 468 | print box 469 | # => 470 | # ╔ ✔ OK ═════════════════╗ 471 | # ║ ║ 472 | # ║ Deploying application ║ 473 | # ║ ║ 474 | # ╚═══════════════════════╝ 475 | ``` 476 | 477 | #### 2.8.4 error 478 | 479 | To draw an error type box around your content use `error`: 480 | 481 | ```ruby 482 | box = TTY::Box.error("Deploying application") 483 | ``` 484 | 485 | And then print: 486 | 487 | ```ruby 488 | print box 489 | # => 490 | # ╔ ⨯ ERROR ══════════════╗ 491 | # ║ ║ 492 | # ║ Deploying application ║ 493 | # ║ ║ 494 | # ╚═══════════════════════╝ 495 | ``` 496 | 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 | --------------------------------------------------------------------------------