├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── assets ├── fltrdr.gif └── fltrdr.mp4 ├── build.sh ├── config ├── default ├── solarized-dark └── solarized-light ├── env.sh ├── install.sh └── src ├── fltrdr ├── fltrdr.cc ├── fltrdr.hh ├── tui.cc └── tui.hh ├── main.cc └── ob ├── algorithm.hh ├── color.hh ├── crypto.cc ├── crypto.hh ├── parg.hh ├── readline.cc ├── readline.hh ├── string.cc ├── string.hh ├── term.hh ├── text.hh └── timer.hh /.gitignore: -------------------------------------------------------------------------------- 1 | *.*.sw* 2 | build 3 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.8 FATAL_ERROR) 2 | 3 | set (TARGET "fltrdr") 4 | project (${TARGET}) 5 | 6 | find_program (CCACHE_FOUND ccache) 7 | if (CCACHE_FOUND) 8 | message ("ccache found") 9 | set_property (GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache) 10 | set_property (GLOBAL PROPERTY RULE_LAUNCH_LINK ccache) 11 | endif (CCACHE_FOUND) 12 | 13 | set (CMAKE_CXX_STANDARD 17) 14 | set (CMAKE_CXX_STANDARD_REQUIRED ON) 15 | 16 | set (CMAKE_CXX_FLAGS "-std=c++17 -fdiagnostics-color=auto") 17 | 18 | set (DEBUG_FLAGS "-g -Og -Wpedantic -Wall -Wextra -Wcast-align -Wcast-qual -Wctor-dtor-privacy -Wdisabled-optimization -Wformat=2 -Winit-self -Wlogical-op -Wmissing-declarations -Wmissing-include-dirs -Wnoexcept -Wold-style-cast -Woverloaded-virtual -Wredundant-decls -Wshadow -Wsign-conversion -Wsign-promo -Wstrict-null-sentinel -Wstrict-overflow=5 -Wswitch-default -Wundef -Wno-unused") 19 | set (DEBUG_LINK_FLAGS "-fprofile-arcs -ftest-coverage") 20 | 21 | set (RELEASE_FLAGS "-O3 -flto") 22 | set (RELEASE_LINK_FLAGS "-s -O3 -flto") 23 | 24 | set (CMAKE_CXX_FLAGS_DEBUG ${DEBUG_FLAGS}) 25 | set (CMAKE_EXE_LINKER_FLAGS_DEBUG ${DEBUG_LINK_FLAGS}) 26 | 27 | set (CMAKE_CXX_FLAGS_RELEASE ${RELEASE_FLAGS}) 28 | set (CMAKE_EXE_LINKER_FLAGS_RELEASE ${RELEASE_LINK_FLAGS}) 29 | 30 | message ("CMAKE_BUILD_TYPE is ${CMAKE_BUILD_TYPE}") 31 | 32 | set (SOURCES 33 | src/main.cc 34 | src/ob/crypto.cc 35 | src/ob/string.cc 36 | src/ob/readline.cc 37 | src/fltrdr/tui.cc 38 | src/fltrdr/fltrdr.cc 39 | ) 40 | 41 | add_executable ( 42 | ${TARGET} 43 | ${SOURCES} 44 | ) 45 | 46 | target_include_directories( 47 | ${TARGET} 48 | PRIVATE 49 | ./src 50 | ) 51 | 52 | target_link_libraries ( 53 | ${TARGET} 54 | stdc++fs 55 | crypto 56 | icuuc 57 | icui18n 58 | ) 59 | 60 | install ( 61 | TARGETS ${TARGET} 62 | DESTINATION bin 63 | ) 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Brett Robinson 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fltrdr 2 | A TUI text reader for the terminal. 3 | 4 | [![fltrdr](https://raw.githubusercontent.com/octobanana/fltrdr/master/assets/fltrdr.gif)](https://octobanana.com/software/fltrdr/blob/assets/fltrdr.mp4#file) 5 | 6 | Click the above gif to view a video of __Fltrdr__ in action. 7 | 8 | ## Contents 9 | * [About](#about) 10 | * [Features](#features) 11 | * [Usage](#usage) 12 | * [Terminal Compatibility](#terminal-compatibility) 13 | * [Pre-Build](#pre-build) 14 | * [Dependencies](#dependencies) 15 | * [Linked Libraries](#linked-libraries) 16 | * [Included Libraries](#included-libraries) 17 | * [Environment](#environment) 18 | * [macOS](#macos) 19 | * [Build](#build) 20 | * [Install](#install) 21 | * [Configuration](#configuration) 22 | * [License](#license) 23 | 24 | ## About 25 | __Fltrdr__, or *flat-reader*, is an interactive text reader for the terminal. 26 | It is *flat* in the sense that the reader is word-based. It creates a 27 | horizontal stream of words by ignoring all newline characters and removing 28 | sequential whitespace. 29 | Its purpose is to facilitate reading, scanning, and searching text. 30 | The program has a play mode that moves the reader forward one word at a time, 31 | along with a configurable words per minute (WPM) setting. 32 | It can be used to read text from either a file or stdin. 33 | 34 | ### Features 35 | * word-based text reader 36 | * *vi* inspired key-bindings 37 | * read UTF-8 encoded text from a file or stdin 38 | * play mode with variable speed controlled through WPM 39 | * in play mode, longer pauses occur on words that end in punctuation 40 | * focus point to align the current word 41 | * focus point tries to be smart about its placement, 42 | ignoring any surrounding punctuation or plurality 43 | * search forwards and backwards using regular expressions 44 | * command prompt to interact with the program 45 | * store your settings in a configuration file 46 | * choose how many words are shown at a time 47 | * colours can be customized 48 | * chars/symbols can be customized 49 | * show/hide parts of the interface 50 | * mouse support for play/pause and scrolling text 51 | * history file for command prompt inputs 52 | * history file for search prompt inputs 53 | * fuzzy search prompt input history based on current prompt input 54 | * load and save reader state to continue where you last left off 55 | 56 | ## Usage 57 | View the usage and help output with the `--help` or `-h` flag. 58 | The help output contains the documentation for the key bindings 59 | and commands for customization. 60 | 61 | ## Terminal Compatibility 62 | This program uses raw terminal control sequences to manipulate the terminal, 63 | such as moving the cursor, styling the output text, and clearing the screen. 64 | Although some of the control sequences used may not work as intended on all terminals, 65 | they should work fine on any modern terminal emulator. 66 | 67 | ## Pre-Build 68 | This section describes what environments this program may run on, 69 | any prior requirements or dependencies needed, 70 | and any third party libraries used. 71 | 72 | > #### Important 73 | > Any shell commands using relative paths are expected to be executed in the 74 | > root directory of this repository. 75 | 76 | ### Dependencies 77 | * __C++17__ compiler/library 78 | * __CMake__ >= 3.8 79 | * __ICU__ >= 62.1 80 | * __OpenSSL__ >= 1.1.0 81 | 82 | ### Linked Libraries 83 | * __stdc++fs__ (libstdc++fs) included in the C++17 Standard Library 84 | * __crypto__ (libcrypto) part of the OpenSSL Library 85 | * __icuuc__ (libicuuc) part of the ICU library 86 | * __icui18n__ (libicui18n) part of the ICU library 87 | 88 | ### Included Libraries 89 | * [__parg__](https://github.com/octobanana/parg): 90 | for parsing CLI args, included as `./src/ob/parg.hh` 91 | 92 | ### Environment 93 | * __Linux__ (supported) 94 | * __BSD__ (supported) 95 | * __macOS__ (supported) 96 | 97 | ### macOS 98 | Using a new version of __gcc__ or __llvm__ is __required__, as the default 99 | __Apple llvm compiler__ does __not__ support C++17 Standard Library features such as `std::filesystem`. 100 | 101 | A new compiler can be installed through a third-party package manager such as __Brew__. 102 | Assuming you have __Brew__ already installed, the following commands should install 103 | the latest __gcc__. 104 | 105 | ``` 106 | brew install gcc 107 | brew link gcc 108 | ``` 109 | 110 | The following line will then need to be appended to the CMakeLists.txt file. 111 | Replace the placeholder `` with the canonical path to the new __g++__ compiler binary. 112 | 113 | ``` 114 | set (CMAKE_CXX_COMPILER ) 115 | ``` 116 | 117 | ## Build 118 | The following shell command will build the project in release mode: 119 | ```sh 120 | ./build.sh 121 | ``` 122 | To build in debug mode, run the script with the `--debug` flag. 123 | 124 | ## Install 125 | The following shell command will install the project in release mode: 126 | ```sh 127 | ./install.sh 128 | ``` 129 | To install in debug mode, run the script with the `--debug` flag. 130 | 131 | ## Configuration 132 | 133 | Base Config Directory (BASE): `${HOME}/.fltrdr` 134 | State Directory: `BASE/state` 135 | History Directory: `BASE/history` 136 | Config File: `BASE/config` 137 | State Files: `BASE/state/` 138 | Search History File: `BASE/history/search` 139 | Command History File: `BASE/history/command` 140 | 141 | Use `--config=` to override the default config file. 142 | Use `--config-base=` to override the default base config directory. 143 | 144 | The base config directory and config file must be created by the user. 145 | The config file in the base config directory must be named `config`. 146 | It is a plain text file that can contain any of the 147 | commands listed in the __Commands__ section of the `--help` output. 148 | Each command must be on its own line. Lines that begin with the 149 | `#` character are treated as comments. 150 | 151 | If you want to permanently use a different base config directory, 152 | such as `~/.config/fltrdr`, add the following line to your shell profile: 153 | ```sh 154 | alias fltrdr="fltrdr --config-base ~/.config/fltrdr" 155 | ``` 156 | 157 | The following shell commands will create the base config directory 158 | in the default location and copy over the example config file: 159 | ```sh 160 | mkdir -pv ~/.fltrdr 161 | cp -uv ./config/default ~/.fltrdr/config 162 | ``` 163 | 164 | Several config file examples can be found in the `./config` directory. 165 | 166 | ## License 167 | This project is licensed under the MIT License. 168 | 169 | Copyright (c) 2019 [Brett Robinson](https://octobanana.com/) 170 | 171 | Permission is hereby granted, free of charge, to any person obtaining a copy 172 | of this software and associated documentation files (the "Software"), to deal 173 | in the Software without restriction, including without limitation the rights 174 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 175 | copies of the Software, and to permit persons to whom the Software is 176 | furnished to do so, subject to the following conditions: 177 | 178 | The above copyright notice and this permission notice shall be included in all 179 | copies or substantial portions of the Software. 180 | 181 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 182 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 183 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 184 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 185 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 186 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 187 | SOFTWARE. 188 | -------------------------------------------------------------------------------- /assets/fltrdr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/octobanana/fltrdr/cee8f45aadb6153640726a7492b12af393dcdca5/assets/fltrdr.gif -------------------------------------------------------------------------------- /assets/fltrdr.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/octobanana/fltrdr/cee8f45aadb6153640726a7492b12af393dcdca5/assets/fltrdr.mp4 -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | BUILD_TYPE="release" 5 | 6 | if [[ $# > 0 ]]; then 7 | if [[ $1 == "--debug" ]]; then 8 | BUILD_TYPE="debug" 9 | elif [[ $1 == "--release" ]]; then 10 | BUILD_TYPE="release" 11 | else 12 | printf "usage: ./build.sh [--debug|--release]\n"; 13 | exit 1 14 | fi 15 | fi 16 | 17 | # source environment variables 18 | source ./env.sh 19 | 20 | printf "\nBuilding ${APP} in ${BUILD_TYPE} mode\n" 21 | 22 | printf "\nCompiling ${APP}\n" 23 | mkdir -p build/${BUILD_TYPE} 24 | cd build/${BUILD_TYPE} 25 | cmake ../../ -DCMAKE_BUILD_TYPE=${BUILD_TYPE} 26 | time make 27 | -------------------------------------------------------------------------------- /config/default: -------------------------------------------------------------------------------- 1 | # fltrdr config 2 | # default 3 | # 4 | # refer to the 'Commands' section of the help output with 5 | # '--help' or '-h' to view all commands and their descriptions 6 | 7 | # ----------------------------------------------------------------------------- 8 | # General 9 | # ----------------------------------------------------------------------------- 10 | 11 | # set the reader speed in words per minute (wpm) 12 | # wpm <60-1200> 13 | wpm 250 14 | 15 | # set number of words visible to the left of the focused word 16 | # prev <0-60> 17 | prev 1 18 | 19 | # set number of words visible to the right of the focused word 20 | # next <0-60> 21 | next 1 22 | 23 | # set the offset of the focus point 24 | # 0 will center the focus point 25 | # 1-6 will make the focus point increasingly more left-oriented 26 | # offset <0-6> 27 | offset 2 28 | 29 | # ----------------------------------------------------------------------------- 30 | # Toggles 31 | # 32 | # set 33 | # ----------------------------------------------------------------------------- 34 | 35 | # toggle full text line visibility 36 | set view off 37 | 38 | # toggle progress bar visibility 39 | set progress on 40 | 41 | # toggle status bar visibility 42 | set status on 43 | 44 | # toggle border top and bottom visibility 45 | set border on 46 | 47 | # toggle border top visibility 48 | set border-top on 49 | 50 | # toggle border bottom visibility 51 | set border-bottom on 52 | 53 | # ----------------------------------------------------------------------------- 54 | # Symbols 55 | # 56 | # sym 57 | # ----------------------------------------------------------------------------- 58 | 59 | # border top/bottom line and border top/bottom mark symbol 60 | sym border ─ 61 | 62 | # border top line and border top mark symbol 63 | # sym border-top ─ 64 | 65 | # border top line symbol 66 | # sym border-top-line ─ 67 | 68 | # border top mark symbol 69 | sym border-top-mark ┬ 70 | 71 | # border bottom line and border bottom mark symbol 72 | # sym border-bottom ─ 73 | 74 | # border bottom line symbol 75 | # sym border-bottom-line ─ 76 | 77 | # border bottom mark symbol 78 | sym border-bottom-mark ┴ 79 | 80 | # progress bar and fill symbol 81 | sym progress ▁ 82 | 83 | # progress bar symbol 84 | # sym progress-bar ▁ 85 | 86 | # progress bar fill symbol 87 | # sym progress-fill ▁ 88 | 89 | # ----------------------------------------------------------------------------- 90 | # Styles 91 | # 92 | # style <#000-#fff|#000000-#ffffff|0-255|Colour|reverse|clear> 93 | # ----------------------------------------------------------------------------- 94 | 95 | # meta style, sets the primary colour of the program 96 | # style primary #4feae7 97 | 98 | # meta style, sets the secondary colour of the program 99 | # style secondary #2c323c 100 | 101 | # meta style, sets the colours of all input text shown in the reader 102 | # style text f0f0f0 103 | 104 | # set the background colour 105 | style background #1b1e24 106 | 107 | # set the text background colour shown to countdown 108 | # after play is pressed and before the reader begins 109 | style countdown #2c323c 110 | 111 | # set the colour of the border surrounding the reader 112 | style border #2c323c 113 | 114 | # set the colour of the command prompt symbol shown at the start of the line 115 | style prompt #2c323c 116 | 117 | # set the success status message colour 118 | style success #55ff00 119 | 120 | # set the error status message colour 121 | style error #ff5500 122 | 123 | # set the progress bar colour 124 | style progress-bar #2c323c 125 | 126 | # set progress bar fill colour 127 | style progress-fill #4feae7 128 | 129 | # set the colour of the text in the status bar 130 | style status-primary #2c323c 131 | 132 | # set the colour of the file path in the status bar 133 | style status-secondary #4feae7 134 | 135 | # set the background colour of the status bar 136 | style status-background #4feae7 137 | 138 | # set the colour of the current word in the reader 139 | style text-primary #f0f0f0 140 | 141 | # set the colour of the words shown on either side of the current word 142 | style text-secondary #c0c0c0 143 | 144 | # set the colour of the focus point character highlighted in the current word 145 | style text-highlight #4feae7 146 | 147 | # set the colour of the punctuation shown in the reader 148 | style text-punct #4feae7 149 | 150 | # set the colour of the quotes shown in the reader 151 | style text-quote #4feae7 152 | -------------------------------------------------------------------------------- /config/solarized-dark: -------------------------------------------------------------------------------- 1 | # fltrdr config 2 | # solarized-dark 3 | # 4 | # refer to the 'Commands' section of the help output with 5 | # '--help' or '-h' to view all commands and their descriptions 6 | 7 | wpm 250 8 | prev 1 9 | next 1 10 | offset 2 11 | 12 | set view off 13 | set progress on 14 | set status on 15 | set border on 16 | set border-top on 17 | set border-bottom on 18 | 19 | sym border ─ 20 | sym border-top-mark ┬ 21 | sym border-bottom-mark ┴ 22 | sym progress ▁ 23 | 24 | style background #002b36 25 | style countdown #073642 26 | style border #073642 27 | style prompt #839496 28 | style success #859900 29 | style error #dc322f 30 | 31 | style progress-bar #586e75 32 | style progress-fill #859900 33 | 34 | style status-primary #839496 35 | style status-secondary #d33682 36 | style status-background #073642 37 | 38 | style text-primary #fdf6e3 39 | style text-secondary #eee8d5 40 | style text-highlight #d33682 41 | style text-punct #2aa198 42 | style text-quote #6c71c4 43 | -------------------------------------------------------------------------------- /config/solarized-light: -------------------------------------------------------------------------------- 1 | # fltrdr config 2 | # solarized-light 3 | # 4 | # refer to the 'Commands' section of the help output with 5 | # '--help' or '-h' to view all commands and their descriptions 6 | 7 | wpm 250 8 | prev 1 9 | next 1 10 | offset 2 11 | 12 | set view off 13 | set progress on 14 | set status on 15 | set border on 16 | set border-top on 17 | set border-bottom on 18 | 19 | sym border ─ 20 | sym border-top-mark ┬ 21 | sym border-bottom-mark ┴ 22 | sym progress ▁ 23 | 24 | style background #fdf6e3 25 | style countdown #eee8d5 26 | style border #eee8d5 27 | style prompt #657b83 28 | style success #859900 29 | style error #dc322f 30 | 31 | style progress-bar #93a1a1 32 | style progress-fill #859900 33 | 34 | style status-primary #657b83 35 | style status-secondary #d33682 36 | style status-background #eee8d5 37 | 38 | style text-primary #002b36 39 | style text-secondary #073642 40 | style text-highlight #d33682 41 | style text-punct #2aa198 42 | style text-quote #6c71c4 43 | -------------------------------------------------------------------------------- /env.sh: -------------------------------------------------------------------------------- 1 | printf "\nSetting Environment Variables\n" 2 | 3 | export APP="fltrdr" 4 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | BUILD_TYPE="release" 5 | 6 | if [[ $# > 0 ]]; then 7 | if [[ $1 == "--debug" ]]; then 8 | BUILD_TYPE="debug" 9 | elif [[ $1 == "--release" ]]; then 10 | BUILD_TYPE="release" 11 | else 12 | printf "usage: ./install.sh [--debug|--release]\n"; 13 | exit 1 14 | fi 15 | fi 16 | 17 | # source environment variables 18 | source ./env.sh 19 | 20 | # build 21 | ./build.sh --${BUILD_TYPE} 22 | cd build/${BUILD_TYPE} 23 | 24 | # install 25 | printf "\nInstalling ${APP} in ${BUILD_TYPE} mode\n" 26 | if command -v sudo > /dev/null; then 27 | sudo make install 28 | elif command -v doas > /dev/null; then 29 | doas make install 30 | else 31 | printf "Don't know how to elevate privileges, bailing.\n" 32 | exit 1 33 | fi 34 | -------------------------------------------------------------------------------- /src/fltrdr/fltrdr.cc: -------------------------------------------------------------------------------- 1 | #include "fltrdr/fltrdr.hh" 2 | 3 | #include "ob/crypto.hh" 4 | #include "ob/string.hh" 5 | #include "ob/timer.hh" 6 | #include "ob/text.hh" 7 | #include "ob/term.hh" 8 | namespace aec = OB::Term::ANSI_Escape_Codes; 9 | 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | using namespace std::string_literals; 30 | 31 | void Fltrdr::init() 32 | { 33 | _ctx.pos = 0; 34 | _ctx.index = 1; 35 | 36 | _ctx.content_id.clear(); 37 | _ctx.text.clear(); 38 | _ctx.text.shrink_to_fit(); 39 | 40 | reset_timer(); 41 | reset_wpm_avg(); 42 | } 43 | 44 | bool Fltrdr::parse(std::istream& input) 45 | { 46 | init(); 47 | 48 | OB::Text::String word; 49 | std::size_t word_count {0}; 50 | 51 | while (input >> std::ws >> word) 52 | { 53 | if (word.size() > 1 && word.cols() == word.size() * 2) 54 | { 55 | for (auto const& e : word) 56 | { 57 | _ctx.text.str() += " " + std::string(e.str); 58 | ++word_count; 59 | } 60 | } 61 | else 62 | { 63 | _ctx.text.str() += " " + word.str(); 64 | ++word_count; 65 | } 66 | } 67 | 68 | if (word_count == 0) 69 | { 70 | _ctx.text.str() = " fltrdr"; 71 | ++word_count; 72 | 73 | _ctx.text.sync(); 74 | _ctx.index_max = word_count; 75 | _ctx.content_id = OB::Crypto::sha256(_ctx.text.str()).value_or(""); 76 | 77 | return false; 78 | } 79 | 80 | 81 | _ctx.text.sync(); 82 | _ctx.index_max = word_count; 83 | _ctx.content_id = OB::Crypto::sha256(_ctx.text.str()).value_or(""); 84 | 85 | return true; 86 | } 87 | 88 | std::string Fltrdr::content_id() 89 | { 90 | return _ctx.content_id; 91 | } 92 | 93 | bool Fltrdr::eof() 94 | { 95 | return _ctx.index >= _ctx.index_max; 96 | } 97 | 98 | void Fltrdr::begin() 99 | { 100 | set_index(_ctx.index_min); 101 | } 102 | 103 | void Fltrdr::end() 104 | { 105 | set_index(_ctx.index_max); 106 | } 107 | 108 | Fltrdr& Fltrdr::screen_size(std::size_t const width, std::size_t const height) 109 | { 110 | _ctx.width = width; 111 | _ctx.height = height; 112 | 113 | return *this; 114 | } 115 | 116 | OB::Text::View Fltrdr::buf_prev(std::size_t offset) 117 | { 118 | if (! _ctx.pos) 119 | { 120 | return {}; 121 | } 122 | 123 | auto const width = (_ctx.width / 2) - 1 - offset; 124 | auto size = static_cast(width - _ctx.prefix_width); 125 | 126 | if (size < 1) 127 | { 128 | return {}; 129 | } 130 | 131 | auto pos = _ctx.pos + 1; 132 | auto count = size; 133 | 134 | while (pos && count) 135 | { 136 | --pos; 137 | --count; 138 | } 139 | 140 | size = size - count; 141 | 142 | if (size < 1) 143 | { 144 | return {}; 145 | } 146 | 147 | auto const min = pos; 148 | 149 | if (_ctx.show_line) 150 | { 151 | return _ctx.text.substr(min, static_cast(size)); 152 | } 153 | 154 | pos = _ctx.pos; 155 | 156 | for (int i = 0; i < _ctx.show_prev; ++i) 157 | { 158 | pos = _ctx.text.rfind(" "s, static_cast(--pos)); 159 | 160 | if (pos <= min || pos == OB::Text::String::npos || pos == 0) 161 | { 162 | break; 163 | } 164 | } 165 | 166 | auto const start = pos < min ? min : pos; 167 | auto const len = _ctx.pos + 1 - start < static_cast(size) ? 168 | _ctx.pos + 1 - start : static_cast(size); 169 | 170 | return _ctx.text.substr(start, len); 171 | } 172 | 173 | OB::Text::View Fltrdr::buf_next(std::size_t offset) 174 | { 175 | auto pos = _ctx.text.find(" "s, static_cast(_ctx.pos + 1)); 176 | auto const start = pos; 177 | 178 | if (pos == OB::Text::String::npos) 179 | { 180 | return {}; 181 | } 182 | 183 | auto const width = (_ctx.width / 2) + 1 + offset; 184 | auto size = static_cast(width - (_ctx.word.size() - _ctx.prefix_width)); 185 | 186 | if (size < 1) 187 | { 188 | return {}; 189 | } 190 | 191 | if (_ctx.width % 2 != 0) 192 | { 193 | ++size; 194 | } 195 | 196 | auto p = pos; 197 | auto count = size; 198 | 199 | while (++count != size && ++p != _ctx.text.size() - 1); 200 | 201 | auto const max = static_cast(size); 202 | 203 | if (_ctx.show_line) 204 | { 205 | return _ctx.text.substr(pos, max); 206 | } 207 | 208 | for (int i = 0; i < _ctx.show_next; ++i) 209 | { 210 | pos = _ctx.text.find(" "s, ++pos); 211 | 212 | if (pos - start + 1 >= max || pos == OB::Text::String::npos) 213 | { 214 | pos = _ctx.text.size() - 1; 215 | 216 | break; 217 | } 218 | } 219 | 220 | auto const end = pos - start + 1; 221 | 222 | return _ctx.text.substr(start, end < max ? end : max); 223 | } 224 | 225 | void Fltrdr::set_focus_point() 226 | { 227 | std::size_t begin {0}; 228 | std::size_t end {_ctx.word.size()}; 229 | 230 | // check for punct at beginning of word 231 | for (auto i = _ctx.word.begin(); i != _ctx.word.end(); ++i) 232 | { 233 | if (! OB::Text::is_punct(OB::Text::to_int32(i->str))) 234 | { 235 | break; 236 | } 237 | 238 | ++begin; 239 | } 240 | 241 | // check for punct at end of word 242 | for (auto i = _ctx.word.rbegin(); i != _ctx.word.rend(); ++i) 243 | { 244 | if (! OB::Text::is_punct(OB::Text::to_int32(i->str))) 245 | { 246 | if (i->str == "s" && ++i != _ctx.word.rend() && 247 | OB::Text::is_punct(OB::Text::to_int32(i->str))) 248 | { 249 | end -= 2; 250 | } 251 | 252 | break; 253 | } 254 | 255 | --end; 256 | } 257 | 258 | auto size {end - begin}; 259 | if (size > _ctx.word.size() || size == 0) 260 | { 261 | size = _ctx.word.size(); 262 | } 263 | 264 | if (size < 13) 265 | { 266 | _ctx.focus_point = std::round(size * _ctx.focus); 267 | } 268 | else 269 | { 270 | _ctx.focus_point = 3; 271 | } 272 | 273 | if (_ctx.word.size() != size) 274 | { 275 | _ctx.focus_point += begin; 276 | } 277 | 278 | // calc display columns needed up to focus point position 279 | _ctx.prefix_width = _ctx.word.at(_ctx.focus_point).tcols; 280 | } 281 | 282 | void Fltrdr::set_line(std::size_t offset) 283 | { 284 | _ctx.line = {}; 285 | _ctx.prev.clear(); 286 | _ctx.next.clear(); 287 | 288 | current_word(); 289 | set_focus_point(); 290 | 291 | if (_ctx.show_line) 292 | { 293 | _ctx.prev = buf_prev(offset); 294 | _ctx.next = buf_next(offset); 295 | } 296 | else 297 | { 298 | if (_ctx.show_prev) 299 | { 300 | _ctx.prev = buf_prev(offset); 301 | } 302 | 303 | if (_ctx.show_next) 304 | { 305 | _ctx.next = buf_next(offset); 306 | } 307 | } 308 | 309 | auto const width_left = (_ctx.width / 2) - 1 - offset; 310 | auto const width_right = (_ctx.width / 2) + 1 + offset; 311 | 312 | auto pad_left {static_cast(width_left - _ctx.prefix_width - _ctx.prev.size())}; 313 | auto pad_right {static_cast(width_right - _ctx.word.size() + _ctx.prefix_width - _ctx.next.size())}; 314 | 315 | if (_ctx.width % 2 != 0) 316 | { 317 | ++pad_right; 318 | } 319 | 320 | if (pad_left < 0) 321 | { 322 | pad_left = 0; 323 | } 324 | 325 | if (pad_right < 0) 326 | { 327 | pad_right = 0; 328 | } 329 | 330 | 331 | _ctx.line.prev = OB::String::repeat(static_cast(pad_left), aec::space) + std::string(_ctx.prev.str()); 332 | 333 | _ctx.line.curr = _ctx.word; 334 | 335 | _ctx.line.next += std::string(_ctx.next.str()) + OB::String::repeat(static_cast(pad_right), aec::space); 336 | } 337 | 338 | Fltrdr::Line Fltrdr::get_line() 339 | { 340 | return _ctx.line; 341 | } 342 | 343 | void Fltrdr::prev_sentence() 344 | { 345 | if (_ctx.index == _ctx.index_min) 346 | { 347 | return; 348 | } 349 | 350 | prev_word(); 351 | if (_ctx.word.find_first_of(_ctx.sentence_end) != OB::Text::String::npos) 352 | { 353 | prev_word(); 354 | if (_ctx.word.find_first_of(_ctx.sentence_end) != OB::Text::String::npos) 355 | { 356 | next_word(); 357 | return; 358 | } 359 | } 360 | while (_ctx.index > _ctx.index_min) 361 | { 362 | prev_word(); 363 | if (_ctx.word.find_first_of(_ctx.sentence_end) != OB::Text::String::npos) 364 | { 365 | next_word(); 366 | break; 367 | } 368 | } 369 | } 370 | 371 | void Fltrdr::next_sentence() 372 | { 373 | if (_ctx.index == _ctx.index_max) 374 | { 375 | return; 376 | } 377 | 378 | while (_ctx.index < _ctx.index_max) 379 | { 380 | if (_ctx.word.find_first_of(_ctx.sentence_end) != OB::Text::String::npos) 381 | { 382 | next_word(); 383 | break; 384 | } 385 | next_word(); 386 | } 387 | } 388 | 389 | void Fltrdr::prev_chapter() 390 | { 391 | if (_ctx.index == _ctx.index_min) 392 | { 393 | return; 394 | } 395 | 396 | while (_ctx.index > _ctx.index_min) 397 | { 398 | prev_word(); 399 | if (_ctx.word.str() == "chapter") 400 | { 401 | break; 402 | } 403 | } 404 | } 405 | 406 | void Fltrdr::next_chapter() 407 | { 408 | if (_ctx.index == _ctx.index_max) 409 | { 410 | return; 411 | } 412 | 413 | while (_ctx.index < _ctx.index_max) 414 | { 415 | next_word(); 416 | if (_ctx.word.str() == "chapter") 417 | { 418 | break; 419 | } 420 | } 421 | } 422 | 423 | OB::Text::View Fltrdr::word() 424 | { 425 | return _ctx.word; 426 | } 427 | 428 | void Fltrdr::current_word() 429 | { 430 | auto const pos = _ctx.text.find(" "s, _ctx.pos + 1); 431 | if (pos != OB::Text::String::npos) 432 | { 433 | _ctx.word = _ctx.text.substr(_ctx.pos + 1, pos - _ctx.pos - 1); 434 | } 435 | else 436 | { 437 | _ctx.word = _ctx.text.substr(_ctx.pos + 1); 438 | } 439 | } 440 | 441 | bool Fltrdr::prev_word() 442 | { 443 | if (_ctx.index > _ctx.index_min) 444 | { 445 | --_ctx.index; 446 | 447 | auto const pos = _ctx.text.rfind(" "s, _ctx.pos - 1); 448 | if (pos != OB::Text::String::npos) 449 | { 450 | _ctx.pos = pos; 451 | current_word(); 452 | 453 | return true; 454 | } 455 | } 456 | 457 | return false; 458 | } 459 | 460 | bool Fltrdr::next_word() 461 | { 462 | if (_ctx.index < _ctx.index_max) 463 | { 464 | ++_ctx.index; 465 | 466 | auto const pos = _ctx.text.find(" "s, _ctx.pos + 1); 467 | if (pos != OB::Text::String::npos) 468 | { 469 | _ctx.pos = pos; 470 | current_word(); 471 | 472 | return true; 473 | } 474 | } 475 | 476 | return false; 477 | } 478 | 479 | void Fltrdr::set_index(std::size_t i) 480 | { 481 | if (i <= _ctx.index_min) 482 | { 483 | i = _ctx.index_min; 484 | } 485 | else if (i >= _ctx.index_max) 486 | { 487 | i = _ctx.index_max; 488 | } 489 | 490 | if (i < _ctx.index) 491 | { 492 | while (i != _ctx.index) 493 | { 494 | prev_word(); 495 | } 496 | } 497 | else if (i > _ctx.index) 498 | { 499 | while (i != _ctx.index) 500 | { 501 | next_word(); 502 | } 503 | } 504 | } 505 | 506 | std::size_t Fltrdr::get_index() 507 | { 508 | return _ctx.index; 509 | } 510 | 511 | int Fltrdr::get_wait() 512 | { 513 | bool punc {false}; 514 | 515 | // check for punct at end of word 516 | for (auto i = _ctx.word.rbegin(); i != _ctx.word.rend(); ++i) 517 | { 518 | if (OB::Text::is_punct(OB::Text::to_int32(i->str))) 519 | { 520 | switch (OB::Text::to_int32(i->str)) 521 | { 522 | case U',': case U'.': case U';': 523 | case U':': case U'?': case U'!': 524 | case U'…': 525 | punc = true; 526 | break; 527 | default: 528 | break; 529 | } 530 | 531 | if (punc) 532 | { 533 | break; 534 | } 535 | } 536 | else 537 | { 538 | break; 539 | } 540 | } 541 | 542 | auto const wait_std = static_cast((60000 / _ctx.wpm) * (1 + (_ctx.word.size() / 100 * 4.0))); 543 | 544 | // set ms 545 | if (punc) 546 | { 547 | _ctx.ms = wait_std * 2; 548 | } 549 | else 550 | { 551 | _ctx.ms = wait_std; 552 | } 553 | 554 | return _ctx.ms; 555 | } 556 | 557 | void Fltrdr::set_wpm_avg(int const i) 558 | { 559 | _ctx.wpm_avg = i; 560 | } 561 | 562 | int Fltrdr::get_wpm_avg() 563 | { 564 | return _ctx.wpm_avg; 565 | } 566 | 567 | void Fltrdr::calc_wpm_avg() 568 | { 569 | // calc average wpm 570 | _ctx.wpm_total += (60000 / _ctx.ms); 571 | _ctx.wpm_avg = _ctx.wpm_total / ++_ctx.wpm_count; 572 | } 573 | 574 | int Fltrdr::get_wpm() 575 | { 576 | return _ctx.wpm; 577 | } 578 | 579 | void Fltrdr::set_wpm(int const i) 580 | { 581 | _ctx.wpm = i; 582 | 583 | if (_ctx.wpm > _ctx.wpm_max) 584 | { 585 | _ctx.wpm = _ctx.wpm_max; 586 | } 587 | else if (_ctx.wpm < _ctx.wpm_min) 588 | { 589 | _ctx.wpm = _ctx.wpm_min; 590 | } 591 | } 592 | 593 | void Fltrdr::inc_wpm() 594 | { 595 | _ctx.wpm += _ctx.wpm_diff; 596 | 597 | if (_ctx.wpm > _ctx.wpm_max) 598 | { 599 | _ctx.wpm = _ctx.wpm_max; 600 | } 601 | } 602 | 603 | void Fltrdr::dec_wpm() 604 | { 605 | _ctx.wpm -= _ctx.wpm_diff; 606 | 607 | if (_ctx.wpm < _ctx.wpm_min) 608 | { 609 | _ctx.wpm = _ctx.wpm_min; 610 | } 611 | } 612 | 613 | std::string Fltrdr::get_stats() 614 | { 615 | std::ostringstream buf; 616 | 617 | buf 618 | << timer.str() << " " 619 | << _ctx.wpm_avg << "avg " 620 | << _ctx.wpm << "wpm " 621 | << _ctx.index << "w " 622 | << static_cast(_ctx.index / static_cast(_ctx.index_max) * 100) << "%"; 623 | 624 | return buf.str(); 625 | } 626 | 627 | void Fltrdr::set_show_line(bool const val) 628 | { 629 | _ctx.show_line = val; 630 | } 631 | 632 | bool Fltrdr::get_show_line() 633 | { 634 | return _ctx.show_line; 635 | } 636 | 637 | void Fltrdr::set_show_prev(int const val) 638 | { 639 | if (val >= _ctx.show_min && val <= _ctx.show_max) 640 | { 641 | _ctx.show_prev = val; 642 | } 643 | } 644 | 645 | int Fltrdr::get_show_prev() 646 | { 647 | return _ctx.show_prev; 648 | } 649 | 650 | void Fltrdr::set_show_next(int const val) 651 | { 652 | if (val >= _ctx.show_min && val <= _ctx.show_max) 653 | { 654 | _ctx.show_next = val; 655 | } 656 | } 657 | 658 | int Fltrdr::get_show_next() 659 | { 660 | return _ctx.show_next; 661 | } 662 | 663 | std::size_t Fltrdr::progress() 664 | { 665 | return static_cast(_ctx.index / static_cast(_ctx.index_max) * 100); 666 | } 667 | 668 | bool Fltrdr::search_forward() 669 | { 670 | if (_ctx.search.it.empty()) 671 | { 672 | return false; 673 | } 674 | 675 | bool last {false}; 676 | auto const index = get_index(); 677 | next_word(); 678 | 679 | for (auto const& e : _ctx.search.it) 680 | { 681 | auto const pos = _ctx.text.byte_to_char(e.pos); 682 | 683 | if (_ctx.pos < pos) 684 | { 685 | while (_ctx.pos < pos) 686 | { 687 | if (! next_word()) 688 | { 689 | last = true; 690 | 691 | break; 692 | } 693 | } 694 | 695 | break; 696 | } 697 | } 698 | 699 | auto const index_new = get_index(); 700 | 701 | if (index != _ctx.index_max) 702 | { 703 | if (! last || (index_new != index && index_new != _ctx.index_max)) 704 | { 705 | prev_word(); 706 | } 707 | } 708 | 709 | return true; 710 | } 711 | 712 | bool Fltrdr::search_backward() 713 | { 714 | if (_ctx.search.it.empty()) 715 | { 716 | return false; 717 | } 718 | 719 | std::size_t end {_ctx.search.it.size()}; 720 | 721 | for (std::size_t i = 0, j = 0; j < end; j = i, ++i) 722 | { 723 | if (i == end || _ctx.text.byte_to_char(_ctx.search.it.at(i).pos) > _ctx.pos) 724 | { 725 | auto const pos = _ctx.text.byte_to_char(_ctx.search.it.at(j).pos); 726 | 727 | while (_ctx.pos > pos) 728 | { 729 | if (! prev_word()) 730 | { 731 | break; 732 | } 733 | } 734 | 735 | break; 736 | } 737 | } 738 | 739 | return true; 740 | } 741 | 742 | bool Fltrdr::search_next() 743 | { 744 | return _ctx.search.forward ? search_forward() : search_backward(); 745 | } 746 | 747 | bool Fltrdr::search_prev() 748 | { 749 | return _ctx.search.forward ? search_backward() : search_forward(); 750 | } 751 | 752 | bool Fltrdr::search(std::string const& rx, bool forward) 753 | { 754 | try 755 | { 756 | _ctx.search.forward = forward; 757 | _ctx.search.rx = rx; 758 | _ctx.search.it.match(_ctx.search.rx, _ctx.text.str()); 759 | 760 | return search_next(); 761 | } 762 | catch (...) 763 | { 764 | _ctx.search.it.clear(); 765 | 766 | return false; 767 | } 768 | } 769 | 770 | void Fltrdr::reset_timer() 771 | { 772 | timer.reset(); 773 | } 774 | 775 | void Fltrdr::reset_wpm_avg() 776 | { 777 | _ctx.wpm_avg = 0; 778 | _ctx.wpm_count = 0; 779 | _ctx.wpm_total = 0; 780 | } 781 | -------------------------------------------------------------------------------- /src/fltrdr/fltrdr.hh: -------------------------------------------------------------------------------- 1 | #ifndef FLTRDR_HH 2 | #define FLTRDR_HH 3 | 4 | #include "ob/timer.hh" 5 | #include "ob/text.hh" 6 | #include "ob/term.hh" 7 | namespace aec = OB::Term::ANSI_Escape_Codes; 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | class Fltrdr 17 | { 18 | public: 19 | 20 | // current rendered line 21 | struct Line 22 | { 23 | std::string prev {}; 24 | std::string curr {}; 25 | std::string next {}; 26 | }; 27 | 28 | Fltrdr() = default; 29 | 30 | void init(); 31 | bool parse(std::istream& input); 32 | std::string content_id(); 33 | 34 | Fltrdr& screen_size(std::size_t const width, std::size_t const height); 35 | 36 | bool eof(); 37 | 38 | void begin(); 39 | void end(); 40 | 41 | OB::Text::View buf_prev(std::size_t offset = 0); 42 | OB::Text::View buf_next(std::size_t offset = 0); 43 | 44 | void set_focus_point(); 45 | 46 | void set_line(std::size_t offset = 0); 47 | Line get_line(); 48 | 49 | int get_wait(); 50 | 51 | void set_index(std::size_t i); 52 | std::size_t get_index(); 53 | 54 | int get_wpm(); 55 | void set_wpm(int const i); 56 | void inc_wpm(); 57 | void dec_wpm(); 58 | 59 | void set_wpm_avg(int const i); 60 | int get_wpm_avg(); 61 | 62 | void calc_wpm_avg(); 63 | std::string get_stats(); 64 | 65 | void set_show_line(bool const val); 66 | bool get_show_line(); 67 | 68 | void set_show_prev(int const val); 69 | int get_show_prev(); 70 | 71 | void set_show_next(int const val); 72 | int get_show_next(); 73 | 74 | std::size_t progress(); 75 | 76 | OB::Text::View word(); 77 | void current_word(); 78 | bool prev_word(); 79 | bool next_word(); 80 | 81 | void prev_sentence(); 82 | void next_sentence(); 83 | 84 | void prev_chapter(); 85 | void next_chapter(); 86 | 87 | bool search_next(); 88 | bool search_prev(); 89 | bool search(std::string const& rx, bool forward); 90 | 91 | void reset_timer(); 92 | void reset_wpm_avg(); 93 | 94 | OB::Timer timer; 95 | 96 | private: 97 | 98 | struct Ctx 99 | { 100 | // current terminal size 101 | std::size_t width {0}; 102 | std::size_t height {0}; 103 | 104 | // minimum terminal size 105 | std::size_t width_min {20}; 106 | 107 | // current word focus point 108 | double const focus {0.25}; 109 | std::size_t focus_point {0}; 110 | 111 | // current word display width in columns before focus point 112 | std::size_t prefix_width {0}; 113 | 114 | // text buffer 115 | OB::Text::String text; 116 | 117 | // sha256 hash of the text buffer 118 | std::string content_id; 119 | 120 | // current rendered line 121 | Line line; 122 | 123 | // text position of leading space to current word 124 | std::size_t pos {0}; 125 | 126 | // word index 127 | std::size_t index {1}; 128 | 129 | // min word index 130 | std::size_t index_min {1}; 131 | 132 | // max word index 133 | std::size_t index_max {1}; 134 | 135 | // current word 136 | OB::Text::View word; 137 | 138 | OB::Text::View prev; 139 | OB::Text::View next; 140 | 141 | // words per minute 142 | int const wpm_diff {10}; 143 | int const wpm_min {60}; 144 | int const wpm_max {1200}; 145 | int wpm {250}; 146 | int wpm_avg {0}; 147 | int wpm_count {0}; 148 | int wpm_total {0}; 149 | 150 | // wait time in milliseconds 151 | int ms {0}; 152 | 153 | // toggle prev and next buffer surrounding current word in line 154 | bool show_line {false}; 155 | int show_prev {1}; 156 | int show_next {1}; 157 | int show_min {0}; 158 | int show_max {60}; 159 | 160 | struct Search 161 | { 162 | OB::Text::Regex it; 163 | std::string rx; 164 | bool forward {true}; 165 | } search; 166 | 167 | // sentence end characters 168 | OB::Text::String sentence_end {".!?"}; 169 | } _ctx; 170 | 171 | bool search_forward(); 172 | bool search_backward(); 173 | }; 174 | 175 | #endif // FLTRDR_HH 176 | -------------------------------------------------------------------------------- /src/fltrdr/tui.hh: -------------------------------------------------------------------------------- 1 | #ifndef TUI_HH 2 | #define TUI_HH 3 | 4 | #include "fltrdr/fltrdr.hh" 5 | 6 | #include "ob/color.hh" 7 | #include "ob/readline.hh" 8 | #include "ob/string.hh" 9 | #include "ob/text.hh" 10 | #include "ob/term.hh" 11 | namespace aec = OB::Term::ANSI_Escape_Codes; 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include 25 | namespace fs = std::filesystem; 26 | 27 | class Tui 28 | { 29 | public: 30 | 31 | Tui(); 32 | 33 | Tui& init(fs::path const& path = {}); 34 | void base_config(fs::path const& path); 35 | void load_config(fs::path const& path); 36 | bool save_state(); 37 | bool load_state(); 38 | void load_hist_command(fs::path const& path); 39 | void load_hist_search(fs::path const& path); 40 | void run(); 41 | 42 | private: 43 | 44 | void get_input(int& wait); 45 | bool press_to_continue(std::string const& str = "ANY KEY", char32_t val = 0); 46 | 47 | std::optional> command(std::string const& input); 48 | void command_prompt(); 49 | 50 | void event_loop(); 51 | int screen_size(); 52 | 53 | void clear(); 54 | void refresh(); 55 | 56 | void draw(); 57 | void draw_content(); 58 | void draw_border_top(); 59 | void draw_border_bottom(); 60 | void draw_progress_bar(); 61 | void draw_status(); 62 | void draw_prompt_message(); 63 | void draw_keybuf(); 64 | 65 | void play(); 66 | void pause(); 67 | 68 | void set_wait(); 69 | void set_status(bool success, std::string const& msg); 70 | 71 | void search_forward(); 72 | void search_backward(); 73 | 74 | OB::Term::Mode _term_mode; 75 | bool const _colorterm; 76 | OB::Readline _readline; 77 | OB::Readline _readline_search; 78 | Fltrdr _fltrdr; 79 | 80 | struct Ctx 81 | { 82 | struct File 83 | { 84 | // file to read from 85 | fs::path path; 86 | 87 | // file name 88 | std::string name; 89 | } file; 90 | 91 | // base config directory 92 | fs::path base_config; 93 | 94 | // current terminal size 95 | std::size_t width {0}; 96 | std::size_t height {0}; 97 | 98 | // minimum terminal size 99 | std::size_t width_min {20}; 100 | std::size_t height_min {6}; 101 | 102 | // output buffer 103 | std::ostringstream buf; 104 | 105 | // control when to exit the event loop 106 | bool is_running {true}; 107 | 108 | // interval between reading a keypress 109 | int const input_interval {50}; 110 | 111 | // horizontal offset from center 112 | std::size_t offset {0}; 113 | int offset_value {2}; 114 | 115 | struct State 116 | { 117 | bool play {false}; 118 | 119 | int count_total {2}; 120 | int count_down {0}; 121 | bool counting_down {false}; 122 | 123 | int wait {250}; 124 | int refresh_rate {250}; 125 | } state; 126 | 127 | // status 128 | struct Status 129 | { 130 | std::string str; 131 | std::string mode {"FLTRDR"}; 132 | } status; 133 | 134 | // input key buffers 135 | OB::Text::Char32 key; 136 | std::vector keys; 137 | 138 | // command prompt 139 | struct Prompt 140 | { 141 | std::string str; 142 | int count {0}; 143 | int timeout {12}; 144 | } prompt; 145 | 146 | struct Show 147 | { 148 | bool border_top {true}; 149 | bool border_bottom {true}; 150 | bool progress {true}; 151 | bool status {true}; 152 | } show; 153 | 154 | struct Style 155 | { 156 | OB::Color bg {OB::Color::Type::bg}; 157 | 158 | OB::Color primary {OB::Color::Type::fg}; 159 | OB::Color secondary {OB::Color::Type::fg}; 160 | OB::Color background {"reverse", OB::Color::Type::bg}; 161 | 162 | OB::Color border {"white", OB::Color::Type::fg}; 163 | 164 | OB::Color countdown {"reverse", OB::Color::Type::bg}; 165 | 166 | OB::Color progress_bar {"white", OB::Color::Type::fg}; 167 | OB::Color progress_fill {OB::Color::Type::fg}; 168 | 169 | OB::Color prompt {OB::Color::Type::fg}; 170 | OB::Color prompt_status {OB::Color::Type::fg}; 171 | OB::Color success {"green", OB::Color::Type::fg}; 172 | OB::Color error {"red", OB::Color::Type::fg}; 173 | 174 | OB::Color word_primary {OB::Color::Type::fg}; 175 | OB::Color word_secondary {OB::Color::Type::fg}; 176 | OB::Color word_highlight {OB::Color::Type::fg}; 177 | OB::Color word_punct {OB::Color::Type::fg}; 178 | OB::Color word_quote {OB::Color::Type::fg}; 179 | } style; 180 | 181 | struct Sym 182 | { 183 | std::string border_top {" "}; 184 | std::string border_top_mark {" "}; 185 | 186 | std::string border_bottom {" "}; 187 | std::string border_bottom_mark {"^"}; 188 | 189 | std::string progress_bar {"_"}; 190 | std::string progress_fill {"_"}; 191 | } sym; 192 | } _ctx; 193 | }; 194 | 195 | #endif // TUI_HH 196 | -------------------------------------------------------------------------------- /src/main.cc: -------------------------------------------------------------------------------- 1 | #include "ob/parg.hh" 2 | using Parg = OB::Parg; 3 | 4 | #include "ob/term.hh" 5 | namespace aec = OB::Term::ANSI_Escape_Codes; 6 | 7 | #include "fltrdr/tui.hh" 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | namespace fs = std::filesystem; 20 | 21 | // prototypes 22 | int program_options(Parg& pg); 23 | 24 | int program_options(Parg& pg) 25 | { 26 | pg.name("fltrdr").version("0.3.1 (09.05.2019)"); 27 | pg.description("A TUI text reader for the terminal."); 28 | 29 | pg.usage("[--config-base ] [--config|-u ] []"); 30 | pg.usage("[--help|-h]"); 31 | pg.usage("[--version|-v]"); 32 | pg.usage("[--license]"); 33 | 34 | pg.info("Mouse Bindings", { 35 | "middle click\n toggle view", 36 | "right click\n toggle play/pause", 37 | "scroll up\n goto previous word", 38 | "scroll down\n goto next word", 39 | }); 40 | 41 | pg.info("Prompt Bindings", { 42 | "|\n exit the prompt", 43 | "\n submit the input", 44 | "\n clear the prompt", 45 | "|\n previous history value based on current input", 46 | "|\n next history value based on current input", 47 | "|\n move cursor left", 48 | "|\n move cursor right", 49 | "|\n move cursor to the start of the input", 50 | "|\n move cursor to the end of the input", 51 | "|\n delete character under the cursor or delete previous character if cursor is at the end of the input", 52 | "|\n delete previous character", 53 | }); 54 | 55 | pg.info("Key Bindings", { 56 | "q|Q|\n quit the program", 57 | "w\n save state", 58 | ":\n enter the command prompt", 59 | "/\n search forwards prompt", 60 | "?\n search backwards prompt", 61 | "\n pause or discard prompt", 62 | "\n toggle play/pause", 63 | "gg\n goto beginning", 64 | "G\n goto end", 65 | "v\n toggle view", 66 | "i\n increase show previous word", 67 | "I\n decrease show previous word", 68 | "o\n increase show next word", 69 | "O\n decrease show next word", 70 | "*\n search next current word", 71 | "#\n search previous current word", 72 | "n\n goto next search result", 73 | "N\n goto previous search result", 74 | "h|\n goto previous word", 75 | "l|\n goto next word", 76 | "H\n goto previous sentence", 77 | "L\n goto next sentence", 78 | "j|\n decrease wpm", 79 | "k|\n increase wpm", 80 | "J\n goto previous chapter", 81 | "K\n goto next chapter", 82 | }); 83 | 84 | pg.info("Commands", { 85 | "q|quit|Quit|exit\n quit the program", 86 | "w\n save state", 87 | "wq\n save state and quit the program", 88 | "open \n open file for reading", 89 | "wpm <60-1200>\n set wpm value", 90 | "goto <\\d+>\n goto specified word index", 91 | "prev <0-60>\n set number of previous words to show", 92 | "next <0-60>\n set number of next words to show", 93 | "offset <0-6>\n set offset of focus point from center", 94 | 95 | R"RAW( 96 | timer 97 | clear 98 | clear the timer 99 | <\d\dY:\d\dM:\d\dW:\d\dD:\d\dh:\d\dm:\d\ds> 100 | set the timer value)RAW", 101 | 102 | R"RAW( 103 | wpm-avg 104 | clear 105 | clear the wpm average 106 | <\d+> 107 | set the wpm average value)RAW", 108 | 109 | R"RAW( 110 | set 111 | view 112 | toggle full text line visibility 113 | progress 114 | toggle progress bar visibility 115 | status 116 | toggle status bar visibility 117 | border 118 | toggle border top and bottom visibility 119 | border-top 120 | toggle border top visibility 121 | border-bottom 122 | toggle border bottom visibility)RAW", 123 | 124 | R"RAW( 125 | sym 126 | progress 127 | progress bar and fill symbol 128 | progress-bar 129 | progress bar symbol 130 | progress-fill 131 | progress bar fill symbol 132 | border 133 | border top/bottom line and border top/bottom mark symbol 134 | border-top 135 | border top line and border top mark symbol 136 | border-top-mark 137 | border top mark symbol 138 | border-top-line 139 | border top line symbol 140 | border-bottom 141 | border bottom line and border bottom mark symbol 142 | border-bottom-mark 143 | border bottom mark symbol 144 | border-bottom-line 145 | border bottom line symbol)RAW", 146 | 147 | R"RAW( 148 | style <#000-#fff|#000000-#ffffff|0-255|Colour|reverse|clear> 149 | primary 150 | meta style, sets the primary colour of the program 151 | secondary 152 | meta style, sets the secondary colour of the program 153 | text 154 | meta style, sets the colours of all input text shown in the reader 155 | background 156 | set the background colour 157 | countdown 158 | set the text background colour shown to countdown 159 | after play is pressed and before the reader begins 160 | border 161 | set the colour of the border surrounding the reader 162 | prompt 163 | set the colour of the command prompt symbol shown at the start of the line 164 | success 165 | set the success status message colour 166 | error 167 | set the error status message colour 168 | progress-bar 169 | set the progress bar colour 170 | progress-fill 171 | set progress bar fill colour 172 | status-primary 173 | set the colour of the text in the status bar 174 | status-secondary 175 | set the colour of the file path in the status bar 176 | status-background 177 | set the background colour of the status bar 178 | text-primary 179 | set the colour of the current word in the reader 180 | text-secondary 181 | set the colour of the words shown on either side of the current word 182 | text-highlight 183 | set the colour of the focus point character highlighted in the current word 184 | text-punct 185 | set the colour of the punctuation shown in the reader 186 | text-quote 187 | set the colour of the quotes shown in the reader)RAW", 188 | }); 189 | 190 | pg.info("Colour", { 191 | "black [bright]", 192 | "red [bright]", 193 | "green [bright]", 194 | "yellow [bright]", 195 | "blue [bright]", 196 | "magenta [bright]", 197 | "cyan [bright]", 198 | "white [bright]", 199 | }); 200 | 201 | pg.info("Configuration", { 202 | R"RAW(Base Config Directory (BASE): '${HOME}/.fltrdr' 203 | State Directory: 'BASE/state' 204 | History Directory: 'BASE/history' 205 | Config File: 'BASE/config' 206 | State Files: 'BASE/state/' 207 | Search History File: 'BASE/history/search' 208 | Command History File: 'BASE/history/command' 209 | 210 | Use '--config=' to override the default config file. 211 | Use '--config-base=' to override the default base config directory. 212 | 213 | The base config directory and config file must be created by the user. 214 | The config file in the base config directory must be named 'config'. 215 | It is a plain text file that can contain any of the 216 | commands listed in the 'Commands' section of the '--help' output. 217 | Each command must be on its own line. Lines that begin with the 218 | '#' character are treated as comments.)RAW" 219 | }); 220 | 221 | pg.info("Examples", { 222 | "fltrdr", 223 | "fltrdr ", 224 | "cat | fltrdr", 225 | "fltrdr --config \"./path/to/config/file\"", 226 | "fltrdr --config-base \"~/.config/fltrdr\"", 227 | "fltrdr --help", 228 | "fltrdr --version", 229 | "fltrdr --license", 230 | }); 231 | 232 | pg.info("Exit Codes", {"0 -> normal", "1 -> error"}); 233 | 234 | pg.info("Repository", { 235 | "https://github.com/octobanana/fltrdr.git", 236 | }); 237 | 238 | pg.info("Homepage", { 239 | "https://octobanana.com/software/fltrdr", 240 | }); 241 | 242 | pg.author("Brett Robinson (octobanana) "); 243 | 244 | // general flags 245 | pg.set("help,h", "Print the help output."); 246 | pg.set("version,v", "Print the program version."); 247 | pg.set("license", "Print the program license."); 248 | 249 | // options 250 | pg.set("config,u", "", "file", "Use the commands in the config file 'file' for initialization.\n All other initializations are skipped. To skip all initializations,\n use the special name 'NONE'."); 251 | pg.set("config-base", "", "dir", "use 'dir' as the base config directory.\n To skip all initializations,\n use the special name 'NONE'."); 252 | 253 | pg.set_pos(); 254 | 255 | int status {pg.parse()}; 256 | 257 | if (status < 0) 258 | { 259 | std::cerr << "Usage:\n" << pg.usage() << "\n"; 260 | std::cerr << "Error: " << pg.error() << "\n"; 261 | 262 | auto const similar_names = pg.similar(); 263 | if (similar_names.size() > 0) 264 | { 265 | std::cerr 266 | << "did you mean:\n"; 267 | for (auto const& e : similar_names) 268 | { 269 | std::cerr 270 | << " --" << e << "\n"; 271 | } 272 | } 273 | 274 | return -1; 275 | } 276 | 277 | if (pg.get("help")) 278 | { 279 | std::cout << pg.help(); 280 | 281 | return 1; 282 | } 283 | 284 | if (pg.get("version")) 285 | { 286 | std::cout << pg.name() << " v" << pg.version() << "\n"; 287 | 288 | return 1; 289 | } 290 | 291 | if (pg.get("license")) 292 | { 293 | std::cout << R"(MIT License 294 | 295 | Copyright (c) 2019 Brett Robinson 296 | 297 | Permission is hereby granted, free of charge, to any person obtaining a copy 298 | of this software and associated documentation files (the "Software"), to deal 299 | in the Software without restriction, including without limitation the rights 300 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 301 | copies of the Software, and to permit persons to whom the Software is 302 | furnished to do so, subject to the following conditions: 303 | 304 | The above copyright notice and this permission notice shall be included in all 305 | copies or substantial portions of the Software. 306 | 307 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 308 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 309 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 310 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 311 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 312 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 313 | SOFTWARE.)" << "\n"; 314 | 315 | return 1; 316 | } 317 | 318 | return 0; 319 | } 320 | 321 | int main(int argc, char *argv[]) 322 | { 323 | Parg pg {argc, argv}; 324 | int pstatus {program_options(pg)}; 325 | if (pstatus > 0) return 0; 326 | if (pstatus < 0) return 1; 327 | 328 | std::ios_base::sync_with_stdio(false); 329 | 330 | try 331 | { 332 | Tui tui; 333 | 334 | if (! OB::Term::is_term(STDOUT_FILENO)) 335 | { 336 | throw std::runtime_error("stdout is not a tty"); 337 | } 338 | 339 | if (! OB::Term::is_term(STDIN_FILENO)) 340 | { 341 | // read from stdin 342 | tui.init("*stdin*"); 343 | 344 | // reset stdin 345 | int tty = open("/dev/tty", O_RDONLY); 346 | dup2(tty, STDIN_FILENO); 347 | close(tty); 348 | } 349 | else if (auto const file = pg.get_pos_vec(); ! file.empty()) 350 | { 351 | // read from file 352 | tui.init(file.at(0)); 353 | } 354 | else 355 | { 356 | // default 357 | tui.init(); 358 | } 359 | 360 | // load files 361 | { 362 | // determine base config directory 363 | // default to '~/.fltrdr' 364 | fs::path base_config_dir {pg.find("config-base") ? 365 | pg.get("config-base") : 366 | fs::path(OB::Term::env_var("HOME") + "/." + pg.name())}; 367 | 368 | if (base_config_dir != "NONE" && 369 | fs::exists(base_config_dir) && fs::is_directory(base_config_dir)) 370 | { 371 | // set base config directory 372 | tui.base_config(base_config_dir); 373 | 374 | // check/create default directories 375 | 376 | fs::path state_dir {base_config_dir / fs::path("state")}; 377 | 378 | if (! fs::exists(state_dir) || ! fs::is_directory(state_dir)) 379 | { 380 | fs::create_directory(state_dir); 381 | } 382 | 383 | fs::path history_dir {base_config_dir / fs::path("history")}; 384 | 385 | if (! fs::exists(history_dir) || ! fs::is_directory(history_dir)) 386 | { 387 | fs::create_directory(history_dir); 388 | } 389 | 390 | // load history files 391 | tui.load_hist_command(history_dir / fs::path("command")); 392 | tui.load_hist_search(history_dir / fs::path("search")); 393 | 394 | // load config file 395 | tui.load_config(pg.find("config") ? pg.get("config") : 396 | base_config_dir / fs::path("config")); 397 | 398 | // load content state if available 399 | tui.load_state(); 400 | } 401 | } 402 | 403 | // start event loop 404 | tui.run(); 405 | } 406 | catch(std::exception const& e) 407 | { 408 | std::cerr << "Error: " << e.what() << "\n"; 409 | 410 | return 1; 411 | } 412 | catch(...) 413 | { 414 | std::cerr << "Error: an unexpected error occurred\n"; 415 | 416 | return 1; 417 | } 418 | 419 | return 0; 420 | } 421 | -------------------------------------------------------------------------------- /src/ob/algorithm.hh: -------------------------------------------------------------------------------- 1 | #ifndef OB_ALGORITHM_HH 2 | #define OB_ALGORITHM_HH 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace OB::Algorithm 9 | { 10 | 11 | template 12 | void for_each(std::size_t const cn, Op const& op) 13 | { 14 | if (cn == 0) return; 15 | for (std::size_t i = 0; i < cn; ++i) 16 | { 17 | op(i); 18 | } 19 | } 20 | 21 | template 22 | void for_each(Cn const& cn, Op const& op) 23 | { 24 | if (cn.empty()) return; 25 | for (std::size_t i = 0; i < cn.size(); ++i) 26 | { 27 | op(cn[i]); 28 | } 29 | } 30 | 31 | template 32 | void for_each(Cn const& cn, Op1 const& op_n, Op2 const& op_l) 33 | { 34 | if (cn.empty()) return; 35 | for (std::size_t i = 0; i < cn.size() - 1; ++i) 36 | { 37 | op_n(cn[i]); 38 | } 39 | op_l(cn.back()); 40 | } 41 | 42 | template 43 | void for_each(std::size_t const& n, Op1 const& op_n, Op2 const& op_l) 44 | { 45 | if (n == 0) return; 46 | for (std::size_t i = 0; i < n - 1; ++i) 47 | { 48 | op_n(i); 49 | } 50 | op_l(n - 1); 51 | } 52 | 53 | } // namespace OB::Algorithm 54 | 55 | #endif // OB_ALGORITHM_HH 56 | -------------------------------------------------------------------------------- /src/ob/color.hh: -------------------------------------------------------------------------------- 1 | #ifndef OB_COLOR_HH 2 | #define OB_COLOR_HH 3 | 4 | #include "ob/string.hh" 5 | #include "ob/term.hh" 6 | namespace aec = OB::Term::ANSI_Escape_Codes; 7 | 8 | #include 9 | #include 10 | 11 | namespace OB 12 | { 13 | 14 | class Color 15 | { 16 | inline static const std::unordered_map color_fg { 17 | {"black", "\x1b[30m"}, 18 | {"black bright", "\x1b[90m"}, 19 | {"red", "\x1b[31m"}, 20 | {"red bright", "\x1b[91m"}, 21 | {"green", "\x1b[32m"}, 22 | {"green bright", "\x1b[92m"}, 23 | {"yellow", "\x1b[33m"}, 24 | {"yellow bright", "\x1b[93m"}, 25 | {"blue", "\x1b[34m"}, 26 | {"blue bright", "\x1b[94m"}, 27 | {"magenta", "\x1b[35m"}, 28 | {"magenta bright", "\x1b[95m"}, 29 | {"cyan", "\x1b[36m"}, 30 | {"cyan bright", "\x1b[96m"}, 31 | {"white", "\x1b[37m"}, 32 | {"white bright", "\x1b[97m"}, 33 | }; 34 | 35 | inline static const std::unordered_map color_bg { 36 | {"black", "\x1b[40m"}, 37 | {"black bright", "\x1b[100m"}, 38 | {"red", "\x1b[41m"}, 39 | {"red bright", "\x1b[101m"}, 40 | {"green", "\x1b[42m"}, 41 | {"green bright", "\x1b[102m"}, 42 | {"yellow", "\x1b[43m"}, 43 | {"yellow bright", "\x1b[103m"}, 44 | {"blue", "\x1b[44m"}, 45 | {"blue bright", "\x1b[104m"}, 46 | {"magenta", "\x1b[45m"}, 47 | {"magenta bright", "\x1b[105m"}, 48 | {"cyan", "\x1b[46m"}, 49 | {"cyan bright", "\x1b[106m"}, 50 | {"white", "\x1b[47m"}, 51 | {"white bright", "\x1b[107m"}, 52 | }; 53 | 54 | public: 55 | 56 | struct Type 57 | { 58 | enum value 59 | { 60 | bg = 0, 61 | fg 62 | }; 63 | }; 64 | 65 | Color() = default; 66 | 67 | Color(Type::value const fg) noexcept : 68 | _fg {static_cast(fg)} 69 | { 70 | } 71 | 72 | Color(std::string const& k, Type::value const fg = Type::value::fg) : 73 | _fg {static_cast(fg)} 74 | { 75 | key(k); 76 | } 77 | 78 | Color(Color&&) = default; 79 | Color(Color const&) = default; 80 | ~Color() = default; 81 | 82 | Color& operator=(Color&& rhs) = default; 83 | Color& operator=(Color const&) = default; 84 | 85 | Color& operator=(std::string const& k) 86 | { 87 | clear(); 88 | key(k); 89 | 90 | return *this; 91 | } 92 | 93 | operator bool() const 94 | { 95 | return _valid; 96 | } 97 | 98 | operator std::string() const 99 | { 100 | return _key; 101 | } 102 | 103 | friend std::ostream& operator<<(std::ostream& os, Color const& obj) 104 | { 105 | os << obj._value; 106 | 107 | return os; 108 | } 109 | 110 | friend std::string& operator+=(std::string& str, Color const& obj) 111 | { 112 | return str += obj._value; 113 | } 114 | 115 | bool is_valid() const 116 | { 117 | return _valid; 118 | } 119 | 120 | Color& clear() 121 | { 122 | _key = "clear"; 123 | _value.clear(); 124 | _valid = false; 125 | 126 | return *this; 127 | } 128 | 129 | Color& fg() 130 | { 131 | if (! _fg) 132 | { 133 | _fg = true; 134 | key(_key); 135 | } 136 | 137 | return *this; 138 | } 139 | 140 | Color& bg() 141 | { 142 | if (_fg) 143 | { 144 | _fg = false; 145 | key(_key); 146 | } 147 | 148 | return *this; 149 | } 150 | 151 | bool is_fg() const 152 | { 153 | return _fg; 154 | } 155 | 156 | bool is_bg() const 157 | { 158 | return ! _fg; 159 | } 160 | 161 | std::string key() const 162 | { 163 | return _key; 164 | } 165 | 166 | bool key(std::string const& k) 167 | { 168 | if (! k.empty()) 169 | { 170 | if (k == "clear") 171 | { 172 | // clear 173 | _key = k; 174 | _value = ""; 175 | _valid = true; 176 | } 177 | else if (k == "reverse") 178 | { 179 | // reverse 180 | _key = k; 181 | _value = "\x1b[7m"; 182 | _valid = true; 183 | } 184 | else 185 | { 186 | // 21-bit color 187 | if (k.at(0) == '#' && OB::String::assert_rx(k, 188 | std::regex("^#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?$"))) 189 | { 190 | _key = k; 191 | if (_fg) 192 | { 193 | _value = aec::fg_true(k); 194 | } 195 | else 196 | { 197 | _value = aec::bg_true(k); 198 | } 199 | _valid = true; 200 | } 201 | 202 | // 8-bit color 203 | else if (OB::String::assert_rx(k, std::regex("^[0-9]{1,3}$")) && 204 | std::stoi(k) >= 0 && std::stoi(k) <= 255) 205 | { 206 | _key = k; 207 | if (_fg) 208 | { 209 | _value = aec::fg_256(k); 210 | } 211 | else 212 | { 213 | _value = aec::bg_256(k); 214 | } 215 | _valid = true; 216 | } 217 | 218 | // 4-bit color 219 | else if (color_fg.find(k) != color_fg.end()) 220 | { 221 | _key = k; 222 | if (_fg) 223 | { 224 | _value = color_fg.at(k); 225 | } 226 | else 227 | { 228 | _value = color_bg.at(k); 229 | } 230 | _valid = true; 231 | } 232 | } 233 | } 234 | 235 | return _valid; 236 | } 237 | 238 | std::string value() const 239 | { 240 | return _value; 241 | } 242 | 243 | private: 244 | 245 | bool _valid {false}; 246 | bool _fg {true}; 247 | std::string _key {"clear"}; 248 | std::string _value; 249 | }; // Color 250 | 251 | } // namespace OB 252 | 253 | #endif // OB_COLOR_HH 254 | -------------------------------------------------------------------------------- /src/ob/crypto.cc: -------------------------------------------------------------------------------- 1 | #include "ob/crypto.hh" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace OB::Crypto 13 | { 14 | 15 | std::optional sha256(std::string_view const str) 16 | { 17 | SHA256_CTX ctx; 18 | 19 | if (! SHA256_Init(&ctx)) return {}; 20 | if (! SHA256_Update(&ctx, str.data(), str.size())) return {}; 21 | 22 | std::array digest; 23 | 24 | if (! SHA256_Final(digest.data(), &ctx)) return {}; 25 | 26 | std::ostringstream res; 27 | res << std::hex << std::setfill('0'); 28 | 29 | for (auto const& e : digest) 30 | { 31 | res << std::setw(2) << static_cast(e); 32 | } 33 | 34 | return res.str(); 35 | } 36 | 37 | } // namespace OB::Crypto 38 | -------------------------------------------------------------------------------- /src/ob/crypto.hh: -------------------------------------------------------------------------------- 1 | #ifndef OB_CRYPTO_HH 2 | #define OB_CRYPTO_HH 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace OB::Crypto 9 | { 10 | 11 | std::optional sha256(std::string_view const str); 12 | 13 | } // namespace OB::Crypto 14 | 15 | #endif // OB_CRYPTO_HH 16 | -------------------------------------------------------------------------------- /src/ob/parg.hh: -------------------------------------------------------------------------------- 1 | // 2 | // MIT License 3 | // 4 | // Copyright (c) 2018-2019 Brett Robinson 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // 24 | 25 | #ifndef OB_PARG_HH 26 | #define OB_PARG_HH 27 | 28 | #include "ob/string.hh" 29 | 30 | #include 31 | 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | 48 | namespace OB 49 | { 50 | 51 | class Parg 52 | { 53 | public: 54 | 55 | Parg() 56 | { 57 | } 58 | 59 | Parg(int _argc, char** _argv) 60 | { 61 | argc_ = _argc; 62 | argvf(_argv); 63 | } 64 | 65 | Parg& name(std::string const& _name) 66 | { 67 | name_ = _name; 68 | return *this; 69 | } 70 | 71 | std::string name() const 72 | { 73 | return name_; 74 | } 75 | 76 | Parg& version(std::string const& _version) 77 | { 78 | version_ = _version; 79 | return *this; 80 | } 81 | 82 | std::string version() const 83 | { 84 | return version_; 85 | } 86 | 87 | Parg& usage(std::string const& _usage) 88 | { 89 | usage_ += " " + name_ + " " + _usage + "\n"; 90 | return *this; 91 | } 92 | 93 | std::string usage() const 94 | { 95 | return usage_; 96 | } 97 | 98 | Parg& description(std::string const& _description) 99 | { 100 | description_ = _description; 101 | return *this; 102 | } 103 | 104 | std::string description() const 105 | { 106 | return description_; 107 | } 108 | 109 | Parg& info(std::string const& _title, std::vector const& _text) 110 | { 111 | info_.emplace_back(info_pair{_title, _text}); 112 | return *this; 113 | } 114 | 115 | Parg& author(std::string const& _author) 116 | { 117 | author_ = _author; 118 | return *this; 119 | } 120 | 121 | std::string author() const 122 | { 123 | return author_; 124 | } 125 | 126 | std::string help() const 127 | { 128 | std::stringstream out; 129 | if (! description_.empty()) 130 | { 131 | out << name_ << ":" << "\n"; 132 | std::stringstream ss; 133 | out << " " << description_ << "\n"; 134 | out << ss.str() << "\n"; 135 | } 136 | 137 | if (! usage_.empty()) 138 | { 139 | out << "Usage: " << "\n" 140 | << usage_ << "\n"; 141 | } 142 | 143 | if (! modes_.empty()) 144 | { 145 | out << "Flags: " << "\n" 146 | << modes_; 147 | } 148 | 149 | if (! options_.empty()) 150 | { 151 | out << "\nOptions: " << "\n" 152 | << options_; 153 | } 154 | 155 | if (! info_.empty()) 156 | { 157 | for (auto const& e : info_) 158 | { 159 | out << "\n" << e.title << ":" << "\n"; 160 | 161 | for (auto const& t : e.text) 162 | { 163 | out << " " << t << "\n"; 164 | } 165 | } 166 | } 167 | 168 | if (! author_.empty()) 169 | { 170 | out << "\nAuthor: " << "\n"; 171 | std::stringstream ss; 172 | ss << " " << author_ << "\n"; 173 | out << ss.str(); 174 | } 175 | 176 | return out.str(); 177 | } 178 | 179 | int parse() 180 | { 181 | if (is_stdin_) 182 | { 183 | pipe_stdin(); 184 | } 185 | status_ = parse_args(argc_, argv_); 186 | return status_; 187 | } 188 | 189 | int parse(int argc, char** argv) 190 | { 191 | if (is_stdin_) 192 | { 193 | pipe_stdin(); 194 | } 195 | argc_ = argc; 196 | argvf(argv); 197 | status_ = parse_args(argc_, argv_); 198 | return status_; 199 | } 200 | 201 | int parse(std::string const& str) 202 | { 203 | auto args = str_to_args(str); 204 | status_ = parse_args(args.size(), args); 205 | return status_; 206 | } 207 | 208 | std::vector str_to_args(std::string const& str) 209 | { 210 | std::vector args; 211 | 212 | std::string const backslash {"\\"}; 213 | 214 | // parse str into arg vector as if it was parsed by the shell 215 | for (std::size_t i = 0; i < str.size(); ++i) 216 | { 217 | std::string e {str.at(i)}; 218 | 219 | // default 220 | if (e.find_first_not_of(" \n\t\"'") != std::string::npos) 221 | { 222 | bool escaped {false}; 223 | args.emplace_back(""); 224 | for (;i < str.size(); ++i) 225 | { 226 | e = str.at(i); 227 | if (! escaped && e.find_first_of(" \n\t") != std::string::npos) 228 | { 229 | --i; // put back unmatched char 230 | break; 231 | } 232 | else if (e == backslash) 233 | { 234 | escaped = true; 235 | } 236 | else if (escaped) 237 | { 238 | args.back() += e; 239 | escaped = false; 240 | } 241 | else 242 | { 243 | args.back() += e; 244 | } 245 | } 246 | continue; 247 | } 248 | 249 | // whitespace 250 | else if (e.find_first_of(" \n\t") != std::string::npos) 251 | { 252 | for (;i < str.size(); ++i) 253 | { 254 | e = str.at(i); 255 | if (e.find_first_not_of(" \n\t") != std::string::npos) 256 | { 257 | --i; // put back unmatched char 258 | break; 259 | } 260 | } 261 | continue; 262 | } 263 | 264 | // string 265 | else if (e.find_first_of("\"'") != std::string::npos) 266 | { 267 | std::string quote {e}; 268 | bool escaped {false}; 269 | ++i; // skip start quote 270 | args.emplace_back(""); 271 | for (;i < str.size(); ++i) 272 | { 273 | e = str.at(i); 274 | if (! escaped && e == quote) 275 | { 276 | break; 277 | // skip end quote 278 | } 279 | else if (e == backslash) 280 | { 281 | escaped = true; 282 | } 283 | else if (escaped) 284 | { 285 | args.back() += e; 286 | escaped = false; 287 | } 288 | else 289 | { 290 | args.back() += e; 291 | } 292 | } 293 | } 294 | } 295 | return args; 296 | } 297 | 298 | void set(std::string const& _name, std::string const& _info) 299 | { 300 | // sets a flag 301 | std::string delim {","}; 302 | if (_name.find(delim) != std::string::npos) 303 | { 304 | // short and long 305 | bool has_short {false}; 306 | std::string _long; 307 | std::string _short; 308 | 309 | auto const names = delimit(_name, delim); 310 | assert(names.size() >= 1 && names.size() <= 2); 311 | _long = names.at(0); 312 | 313 | if (names.size() == 2) has_short = true; 314 | if (has_short) 315 | { 316 | // short name must be one char 317 | assert(names.at(1).size() == 1); 318 | _short = names.at(1); 319 | } 320 | 321 | flags_[_short] = _long; 322 | data_[_long].long_ = _long; 323 | data_[_long].short_ = _short; 324 | data_[_long].mode_ = true; 325 | data_[_long].value_ = "0"; 326 | modes_.append(" -" + _short + ", --" + _long + "\n"); 327 | } 328 | else 329 | { 330 | if (_name.size() == 1) 331 | { 332 | // short 333 | flags_[_name] = _name; 334 | data_[_name].long_ = _name; 335 | data_[_name].short_ = _name; 336 | data_[_name].mode_ = true; 337 | data_[_name].value_ = "0"; 338 | modes_.append(" -" + _name + "\n"); 339 | } 340 | else 341 | { 342 | // long 343 | data_[_name].long_ = _name; 344 | data_[_name].mode_ = true; 345 | data_[_name].value_ = "0"; 346 | modes_.append(" --" + _name + "\n"); 347 | } 348 | } 349 | 350 | std::stringstream out; 351 | out << " " << _info << "\n"; 352 | modes_.append(out.str()); 353 | } 354 | 355 | void set(std::string const& _name, std::string const& _default, 356 | std::string const& _arg, std::string const& _info) 357 | { 358 | // sets an option 359 | std::string delim {","}; 360 | if (_name.find(delim) != std::string::npos) 361 | { 362 | bool has_short {false}; 363 | std::string _long; 364 | std::string _short; 365 | 366 | auto const names = delimit(_name, delim); 367 | assert(names.size() >= 1 && names.size() <= 2); 368 | _long = names.at(0); 369 | 370 | if (names.size() == 2) has_short = true; 371 | if (has_short) 372 | { 373 | // short name must be one char 374 | assert(names.at(1).size() == 1); 375 | _short = names.at(1); 376 | } 377 | 378 | flags_[_short] = _long; 379 | data_[_long].long_ = _long; 380 | data_[_long].short_ = _short; 381 | data_[_long].mode_ = false; 382 | data_[_long].value_ = _default; 383 | options_.append(" -" + _short + ", --" + _long + "=<" + _arg + ">\n"); 384 | } 385 | else 386 | { 387 | if (_name.size() == 1) 388 | { 389 | // short 390 | flags_[_name] = _name; 391 | data_[_name].long_ = _name; 392 | data_[_name].short_ = _name; 393 | data_[_name].mode_ = false; 394 | data_[_name].value_ = _default; 395 | options_.append(" -" + _name + "=<" + _arg + ">\n"); 396 | } 397 | else 398 | { 399 | // long 400 | data_[_name].long_ = _name; 401 | data_[_name].mode_ = false; 402 | data_[_name].value_ = _default; 403 | options_.append(" --" + _name + "=<" + _arg + ">\n"); 404 | } 405 | } 406 | 407 | std::stringstream out; 408 | out << " " << _info << "\n"; 409 | options_.append(out.str()); 410 | } 411 | 412 | template || 415 | std::is_same_v, 416 | int> = 0> 417 | T get(std::string const& _key) 418 | { 419 | if (data_.find(_key) == data_.end()) 420 | { 421 | throw std::logic_error("parg get '" + _key + "' is not defined"); 422 | } 423 | return data_[_key].value_; 424 | } 425 | 426 | template, 429 | int> = 0> 430 | T get(std::string const& _key) 431 | { 432 | if (data_.find(_key) == data_.end()) 433 | { 434 | throw std::logic_error("parg get '" + _key + "' is not defined"); 435 | } 436 | std::stringstream ss; 437 | ss << data_[_key].value_; 438 | T val; 439 | ss >> val; 440 | return val; 441 | } 442 | 443 | bool find(std::string const& _key) const 444 | { 445 | // key must exist 446 | if (data_.find(_key) == data_.end()) return false; 447 | return data_.at(_key).seen_; 448 | } 449 | 450 | Parg& set_pos(bool const _positional = true) 451 | { 452 | is_positional_ = _positional; 453 | return *this; 454 | } 455 | 456 | std::string get_pos() const 457 | { 458 | std::string str; 459 | if (positional_vec_.empty()) 460 | { 461 | return str; 462 | } 463 | for (auto const& e : positional_vec_) 464 | { 465 | str += e + " "; 466 | } 467 | str.pop_back(); 468 | return str; 469 | } 470 | 471 | std::vector get_pos_vec() const 472 | { 473 | return positional_vec_; 474 | } 475 | 476 | Parg& set_stdin(bool const _stdin = true) 477 | { 478 | is_stdin_ = _stdin; 479 | return *this; 480 | } 481 | 482 | std::string get_stdin() const 483 | { 484 | return stdin_; 485 | } 486 | 487 | int status() const 488 | { 489 | return status_; 490 | } 491 | 492 | std::string error() const 493 | { 494 | return error_; 495 | } 496 | 497 | std::vector const& similar() const 498 | { 499 | return similar_; 500 | } 501 | 502 | std::size_t flags_found() const 503 | { 504 | std::size_t count {0}; 505 | 506 | for (auto const& e : data_) 507 | { 508 | if (e.second.mode_ && e.second.seen_) 509 | { 510 | ++count; 511 | } 512 | } 513 | 514 | return count; 515 | } 516 | 517 | std::size_t options_found() const 518 | { 519 | std::size_t count {0}; 520 | 521 | for (auto const& e : data_) 522 | { 523 | if (! e.second.mode_ && e.second.seen_) 524 | { 525 | ++count; 526 | } 527 | } 528 | 529 | return count; 530 | } 531 | 532 | struct Option 533 | { 534 | std::string short_; 535 | std::string long_; 536 | bool mode_; 537 | std::string value_; 538 | bool seen_ {false}; 539 | }; 540 | 541 | struct info_pair 542 | { 543 | std::string title; 544 | std::vector text; 545 | }; 546 | 547 | private: 548 | 549 | int argc_ {0}; 550 | std::vector argv_; 551 | std::string name_; 552 | std::string version_; 553 | std::string usage_; 554 | std::string description_; 555 | std::string modes_; 556 | std::string options_; 557 | int options_indent_ {0}; 558 | std::vector info_; 559 | std::string author_; 560 | std::map data_; 561 | std::map flags_; 562 | bool is_positional_ {false}; 563 | std::string positional_; 564 | std::vector positional_vec_; 565 | std::string stdin_; 566 | bool is_stdin_ {false}; 567 | int status_ {0}; 568 | std::string error_; 569 | std::vector similar_; 570 | 571 | void argvf(char** _argv) 572 | { 573 | // removes first arg 574 | if (argc_ < 1) return; 575 | for (int i = 1; i < argc_; ++i) 576 | { 577 | argv_.emplace_back(_argv[i]); 578 | } 579 | // std::cerr << "argv: " << i << " -> " << argv_.at(i) << std::endl; 580 | --argc_; 581 | } 582 | 583 | int pipe_stdin() 584 | { 585 | if (! isatty(STDIN_FILENO)) 586 | { 587 | stdin_.assign((std::istreambuf_iterator(std::cin)), 588 | (std::istreambuf_iterator())); 589 | return 0; 590 | } 591 | stdin_ = ""; 592 | return -1; 593 | } 594 | 595 | std::vector delimit(std::string const& str, std::string const& delim) const 596 | { 597 | std::vector vtok; 598 | std::size_t start {0}; 599 | std::size_t end = str.find(delim); 600 | while (end != std::string::npos) { 601 | vtok.emplace_back(str.substr(start, end - start)); 602 | start = end + delim.length(); 603 | end = str.find(delim, start); 604 | } 605 | vtok.emplace_back(str.substr(start, end)); 606 | return vtok; 607 | } 608 | 609 | int parse_args(int _argc, std::vector const& _argv) 610 | { 611 | if (_argc < 1) return 1; 612 | 613 | bool dashdash {false}; 614 | 615 | // loop through arg vector 616 | for (int i = 0; i < _argc; ++i) 617 | { 618 | std::string const& tmp {_argv.at(static_cast(i))}; 619 | // std::cerr << "ARG: " << i << " -> " << tmp << std::endl; 620 | 621 | if (dashdash) 622 | { 623 | positional_vec_.emplace_back(tmp); 624 | continue; 625 | } 626 | 627 | if (tmp.size() > 1 && tmp.at(0) == '-' && tmp.at(1) != '-') 628 | { 629 | // short 630 | // std::cerr << "SHORT: " << tmp << std::endl; 631 | 632 | std::string c {tmp.at(1)}; 633 | if (flags_.find(c) != flags_.end() && !(data_.at(flags_.at(c)).mode_)) 634 | { 635 | // short arg 636 | // std::cerr << "SHORT: arg -> " << c << std::endl; 637 | 638 | if (data_.at(flags_.at(c)).seen_) 639 | { 640 | // error 641 | error_ = "flag '-" + c + "' has already been seen"; 642 | return -1; 643 | } 644 | 645 | if (tmp.size() > 2 && tmp.at(2) != '=') 646 | { 647 | data_.at(flags_.at(c)).value_ = tmp.substr(2, tmp.size() - 1); 648 | data_.at(flags_.at(c)).seen_ = true; 649 | } 650 | else if (tmp.size() > 3 && tmp.at(2) == '=') 651 | { 652 | data_.at(flags_.at(c)).value_ = tmp.substr(3, tmp.size() - 1); 653 | data_.at(flags_.at(c)).seen_ = true; 654 | } 655 | else if (i + 1 < _argc) 656 | { 657 | data_.at(flags_.at(c)).value_ = _argv.at(static_cast(i + 1)); 658 | data_.at(flags_.at(c)).seen_ = true; 659 | ++i; 660 | } 661 | else 662 | { 663 | // error 664 | error_ = "flag '-" + c + "' requires an arg"; 665 | return -1; 666 | } 667 | } 668 | else 669 | { 670 | // short mode 671 | for (std::size_t j = 1; j < tmp.size(); ++j) 672 | { 673 | std::string s {tmp.at(j)}; 674 | 675 | if (flags_.find(s) != flags_.end() && data_.at(flags_.at(s)).mode_) 676 | { 677 | // std::cerr << "SHORT: mode -> " << s << std::endl; 678 | 679 | if (data_.at(flags_.at(s)).seen_) 680 | { 681 | // error 682 | error_ = "flag '-" + s + "' has already been seen"; 683 | return -1; 684 | } 685 | 686 | data_.at(flags_.at(s)).value_ = "1"; 687 | data_.at(flags_.at(s)).seen_ = true; 688 | } 689 | else 690 | { 691 | // error 692 | error_ = "invalid flag '" + tmp + "'"; 693 | // find_similar(s); 694 | 695 | return -1; 696 | } 697 | } 698 | } 699 | } 700 | else if (tmp.size() > 2 && tmp.at(0) == '-' && tmp.at(1) == '-') 701 | { 702 | // long || -- 703 | // std::cerr << "LONG: " << tmp << std::endl; 704 | std::string c {tmp.substr(2, tmp.size() - 1)}; 705 | std::string a; 706 | 707 | auto const delim = c.find("="); 708 | if (delim != std::string::npos) 709 | { 710 | c = tmp.substr(2, delim); 711 | a = tmp.substr(3 + delim, tmp.size() - 1); 712 | } 713 | 714 | if (data_.find(c) != data_.end()) 715 | { 716 | if (data_.at(c).seen_) 717 | { 718 | // error 719 | error_ = "option '--" + c + "' has already been seen"; 720 | return -1; 721 | } 722 | 723 | if (data_.at(c).mode_ && a.size() == 0) 724 | { 725 | // std::cerr << "LONG: mode -> " << c << std::endl; 726 | data_.at(c).value_ = "1"; 727 | data_.at(c).seen_ = true; 728 | } 729 | else 730 | { 731 | // std::cerr << "LONG: arg -> " << c << std::endl; 732 | if (a.size() > 0) 733 | { 734 | data_.at(c).value_ = a; 735 | data_.at(c).seen_ = true; 736 | } 737 | else if (i + 1 < _argc) 738 | { 739 | data_.at(c).value_ = _argv.at(static_cast(i + 1)); 740 | data_.at(c).seen_ = true; 741 | ++i; 742 | } 743 | else 744 | { 745 | // error 746 | error_ = "option '--" + c + "' requires an arg"; 747 | return -1; 748 | } 749 | } 750 | } 751 | else 752 | { 753 | // error 754 | error_ = "invalid option '" + tmp + "'"; 755 | find_similar(c); 756 | return -1; 757 | } 758 | } 759 | else if (tmp.size() > 0 && is_positional_) 760 | { 761 | // positional 762 | // std::cerr << "POS: " << tmp << std::endl; 763 | if (tmp == "--") 764 | { 765 | dashdash = true; 766 | } 767 | else 768 | { 769 | positional_vec_.emplace_back(tmp); 770 | } 771 | } 772 | else 773 | { 774 | // error 775 | error_ = "no match for '" + tmp + "'"; 776 | find_similar(tmp); 777 | return -1; 778 | } 779 | } 780 | 781 | return 0; 782 | } 783 | 784 | void find_similar(std::string const& name) 785 | { 786 | int const weight_max {8}; 787 | std::vector> dist; 788 | 789 | for (auto const& [key, val] : data_) 790 | { 791 | int weight {0}; 792 | 793 | if (OB::String::starts_with(val.long_, name)) 794 | { 795 | weight = 0; 796 | } 797 | else 798 | { 799 | weight = OB::String::damerau_levenshtein(name, val.long_, 1, 2, 3, 0); 800 | } 801 | 802 | if (weight < weight_max) 803 | { 804 | dist.emplace_back(weight, val.long_); 805 | } 806 | } 807 | 808 | std::sort(dist.begin(), dist.end(), 809 | [](auto const& lhs, auto const& rhs) 810 | { 811 | return (lhs.first == rhs.first) ? 812 | (lhs.second.size() < rhs.second.size()) : 813 | (lhs.first < rhs.first); 814 | }); 815 | 816 | for (auto const& [key, val] : dist) 817 | { 818 | similar_.emplace_back(val); 819 | } 820 | 821 | size_t const similar_max {3}; 822 | if (similar_.size() > similar_max) 823 | { 824 | similar_.erase(similar_.begin() + similar_max, similar_.end()); 825 | } 826 | } 827 | 828 | }; // class Parg 829 | 830 | } // namespace OB 831 | 832 | #endif // OB_PARG_HH 833 | -------------------------------------------------------------------------------- /src/ob/readline.cc: -------------------------------------------------------------------------------- 1 | #include "ob/readline.hh" 2 | 3 | #include "ob/string.hh" 4 | #include "ob/text.hh" 5 | #include "ob/term.hh" 6 | namespace aec = OB::Term::ANSI_Escape_Codes; 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include 23 | namespace fs = std::filesystem; 24 | 25 | namespace OB 26 | { 27 | 28 | Readline& Readline::style(std::string const& style) 29 | { 30 | _style.input = style; 31 | 32 | return *this; 33 | } 34 | 35 | Readline& Readline::prompt(std::string const& str, std::string const& style) 36 | { 37 | _prompt.str = str; 38 | _style.prompt = style; 39 | _prompt.fmt = aec::wrap(str, style); 40 | 41 | return *this; 42 | } 43 | 44 | void Readline::refresh() 45 | { 46 | _prompt.lhs = _prompt.fmt; 47 | _prompt.rhs.clear(); 48 | 49 | if (_input.str.cols() + 2 > _width) 50 | { 51 | std::size_t pos {_input.off + _input.idx - 1}; 52 | std::size_t cols {0}; 53 | 54 | for (; pos != OB::Text::String::npos && cols < _width - 2; --pos) 55 | { 56 | cols += _input.str.at(pos).cols; 57 | } 58 | 59 | if (pos == OB::Text::String::npos) 60 | { 61 | pos = 0; 62 | } 63 | 64 | std::size_t end {_input.off + _input.idx - pos}; 65 | _input.fmt.str(_input.str.substr(pos, end)); 66 | 67 | if (_input.fmt.cols() > _width - 2) 68 | { 69 | while (_input.fmt.cols() > _width - 2) 70 | { 71 | _input.fmt.erase(0, 1); 72 | } 73 | 74 | _input.cur = _input.fmt.cols(); 75 | 76 | if (_input.cur == OB::Text::String::npos) 77 | { 78 | _input.cur = 0; 79 | } 80 | 81 | _prompt.lhs = aec::wrap("<", _style.prompt); 82 | } 83 | else 84 | { 85 | _input.cur = _input.fmt.cols(); 86 | 87 | if (_input.cur == OB::Text::String::npos) 88 | { 89 | _input.cur = 0; 90 | } 91 | 92 | while (_input.fmt.cols() <= _width - 2) 93 | { 94 | _input.fmt.append(std::string(_input.str.at(end++).str)); 95 | } 96 | 97 | _input.fmt.erase(_input.fmt.size() - 1, 1); 98 | } 99 | 100 | if (_input.off + _input.idx < _input.str.size()) 101 | { 102 | _prompt.rhs = aec::wrap( 103 | OB::String::repeat(_width - _input.fmt.cols() - 2, " ") + ">", 104 | _style.prompt); 105 | } 106 | } 107 | else 108 | { 109 | _input.fmt = _input.str; 110 | _input.cur = _input.fmt.cols(0, _input.idx); 111 | 112 | if (_input.cur == OB::Text::String::npos) 113 | { 114 | _input.cur = 0; 115 | } 116 | } 117 | 118 | std::cout 119 | << aec::cursor_hide 120 | << aec::cr 121 | << aec::erase_line 122 | << _style.input 123 | << OB::String::repeat(_width, " ") 124 | << aec::cr 125 | << _prompt.lhs 126 | << _style.input 127 | << _input.fmt 128 | << aec::clear 129 | << _prompt.rhs 130 | << aec::cursor_set(_input.cur + 2, _height) 131 | << aec::cursor_show 132 | << std::flush; 133 | } 134 | 135 | std::string Readline::operator()(bool& is_running) 136 | { 137 | // update width and height of terminal 138 | OB::Term::size(_width, _height); 139 | 140 | // reset input struct 141 | _input = {}; 142 | 143 | // input key as 32-bit char 144 | char32_t ch {0}; 145 | 146 | // input key as utf8 string 147 | // contains 1-4 bytes 148 | std::string utf8; 149 | 150 | bool loop {true}; 151 | bool save_input {true}; 152 | bool clear_input {false}; 153 | auto wait {std::chrono::milliseconds(50)}; 154 | 155 | std::cout 156 | << aec::cr 157 | << aec::erase_line 158 | << _style.input 159 | << OB::String::repeat(_width, " ") 160 | << aec::cr 161 | << _prompt.fmt 162 | << std::flush; 163 | 164 | while (loop && is_running) 165 | { 166 | std::this_thread::sleep_for(wait); 167 | 168 | while ((ch = OB::Term::get_key(&utf8)) > 0) 169 | { 170 | switch (ch) 171 | { 172 | case OB::Term::Key::escape: 173 | { 174 | // exit the command prompt 175 | loop = false; 176 | clear_input = true; 177 | 178 | break; 179 | } 180 | 181 | case OB::Term::Key::tab: 182 | { 183 | // TODO add tab completion 184 | 185 | break; 186 | } 187 | 188 | case OB::Term::ctrl_key('c'): 189 | { 190 | // exit the command prompt 191 | loop = false; 192 | save_input = false; 193 | clear_input = true; 194 | 195 | break; 196 | } 197 | 198 | case OB::Term::ctrl_key('u'): 199 | { 200 | edit_clear(); 201 | 202 | break; 203 | } 204 | 205 | case OB::Term::Key::newline: 206 | { 207 | // submit the input string 208 | loop = false; 209 | 210 | break; 211 | } 212 | 213 | case OB::Term::Key::up: 214 | case OB::Term::ctrl_key('p'): 215 | { 216 | hist_prev(); 217 | 218 | break; 219 | } 220 | 221 | case OB::Term::Key::down: 222 | case OB::Term::ctrl_key('n'): 223 | { 224 | hist_next(); 225 | 226 | break; 227 | } 228 | 229 | case OB::Term::Key::right: 230 | case OB::Term::ctrl_key('f'): 231 | { 232 | curs_right(); 233 | 234 | break; 235 | } 236 | 237 | case OB::Term::Key::left: 238 | case OB::Term::ctrl_key('b'): 239 | { 240 | curs_left(); 241 | 242 | break; 243 | } 244 | 245 | case OB::Term::Key::end: 246 | case OB::Term::ctrl_key('e'): 247 | { 248 | curs_end(); 249 | 250 | break; 251 | } 252 | 253 | case OB::Term::Key::home: 254 | case OB::Term::ctrl_key('a'): 255 | { 256 | curs_begin(); 257 | 258 | break; 259 | } 260 | 261 | case OB::Term::Key::delete_: 262 | case OB::Term::ctrl_key('d'): 263 | { 264 | loop = edit_delete(); 265 | 266 | break; 267 | } 268 | 269 | case OB::Term::Key::backspace: 270 | case OB::Term::ctrl_key('h'): 271 | { 272 | loop = edit_backspace(); 273 | 274 | break; 275 | } 276 | 277 | default: 278 | { 279 | if (ch < 0xF0000 && (ch == OB::Term::Key::space || OB::Text::is_graph(static_cast(ch)))) 280 | { 281 | edit_insert(utf8); 282 | } 283 | 284 | break; 285 | } 286 | } 287 | } 288 | } 289 | 290 | auto res = normalize(_input.str); 291 | 292 | if (save_input) 293 | { 294 | hist_push(res); 295 | hist_save(res); 296 | } 297 | 298 | if (clear_input) 299 | { 300 | res.clear(); 301 | } 302 | 303 | return res; 304 | } 305 | 306 | void Readline::curs_begin() 307 | { 308 | // move cursor to start of line 309 | 310 | if (_input.idx || _input.off) 311 | { 312 | _input.idx = 0; 313 | _input.off = 0; 314 | 315 | refresh(); 316 | } 317 | } 318 | 319 | void Readline::curs_end() 320 | { 321 | // move cursor to end of line 322 | 323 | if (_input.str.empty()) 324 | { 325 | return; 326 | } 327 | 328 | if (_input.off + _input.idx < _input.str.size()) 329 | { 330 | if (_input.str.cols() + 2 > _width) 331 | { 332 | _input.off = _input.str.size() - _width + 2; 333 | _input.idx = _width - 2; 334 | } 335 | else 336 | { 337 | _input.idx = _input.str.size(); 338 | } 339 | 340 | refresh(); 341 | } 342 | } 343 | 344 | void Readline::curs_left() 345 | { 346 | // move cursor left 347 | 348 | if (_input.off || _input.idx) 349 | { 350 | if (_input.off) 351 | { 352 | --_input.off; 353 | } 354 | else 355 | { 356 | --_input.idx; 357 | } 358 | 359 | refresh(); 360 | } 361 | } 362 | 363 | void Readline::curs_right() 364 | { 365 | // move cursor right 366 | 367 | if (_input.off + _input.idx < _input.str.size()) 368 | { 369 | if (_input.idx + 2 < _width) 370 | { 371 | ++_input.idx; 372 | } 373 | else 374 | { 375 | ++_input.off; 376 | } 377 | 378 | refresh(); 379 | } 380 | } 381 | 382 | void Readline::edit_insert(std::string const& str) 383 | { 384 | // insert or append char to input buffer 385 | 386 | auto size {_input.str.size()}; 387 | _input.str.insert(_input.off + _input.idx, str); 388 | 389 | if (size != _input.str.size()) 390 | { 391 | if (_input.idx + 2 < _width) 392 | { 393 | ++_input.idx; 394 | } 395 | else 396 | { 397 | ++_input.off; 398 | } 399 | } 400 | 401 | refresh(); 402 | 403 | hist_reset(); 404 | } 405 | 406 | void Readline::edit_clear() 407 | { 408 | // clear line 409 | 410 | _input.idx = 0; 411 | _input.off = 0; 412 | _input.str.clear(); 413 | 414 | refresh(); 415 | 416 | hist_reset(); 417 | } 418 | 419 | bool Readline::edit_delete() 420 | { 421 | // erase char under cursor 422 | 423 | if (_input.str.empty()) 424 | { 425 | _input.str.clear(); 426 | 427 | return false; 428 | } 429 | 430 | if (_input.off + _input.idx < _input.str.size()) 431 | { 432 | if (_input.idx + 2 < _width) 433 | { 434 | _input.str.erase(_input.off + _input.idx, 1); 435 | } 436 | else 437 | { 438 | _input.str.erase(_input.idx, 1); 439 | } 440 | 441 | refresh(); 442 | 443 | hist_reset(); 444 | } 445 | else if (_input.off || _input.idx) 446 | { 447 | if (_input.off) 448 | { 449 | _input.str.erase(_input.off + _input.idx - 1, 1); 450 | --_input.off; 451 | } 452 | else 453 | { 454 | --_input.idx; 455 | _input.str.erase(_input.idx, 1); 456 | } 457 | 458 | refresh(); 459 | 460 | hist_reset(); 461 | } 462 | 463 | return true; 464 | } 465 | 466 | bool Readline::edit_backspace() 467 | { 468 | // erase char behind cursor 469 | 470 | if (_input.str.empty()) 471 | { 472 | _input.str.clear(); 473 | 474 | return false; 475 | } 476 | 477 | _input.str.erase(_input.off + _input.idx - 1, 1); 478 | 479 | if (_input.off || _input.idx) 480 | { 481 | if (_input.off) 482 | { 483 | --_input.off; 484 | } 485 | else if (_input.idx) 486 | { 487 | --_input.idx; 488 | } 489 | 490 | refresh(); 491 | 492 | hist_reset(); 493 | } 494 | 495 | return true; 496 | } 497 | 498 | void Readline::hist_prev() 499 | { 500 | // cycle backwards in history 501 | 502 | if (_history().empty() && _history.search().empty()) 503 | { 504 | return; 505 | } 506 | 507 | bool bounds {_history.search().empty() ? 508 | (_history.idx < _history().size() - 1) : 509 | (_history.idx < _history.search().size() - 1)}; 510 | 511 | if (bounds || _history.idx == History::npos) 512 | { 513 | if (_history.idx == History::npos) 514 | { 515 | _input.buf = _input.str; 516 | 517 | if (! _input.buf.empty()) 518 | { 519 | hist_search(_input.buf); 520 | } 521 | } 522 | 523 | ++_history.idx; 524 | 525 | if (_history.search().empty()) 526 | { 527 | // normal search 528 | _input.str = _history().at(_history.idx); 529 | } 530 | else 531 | { 532 | // fuzzy search 533 | _input.str = _history().at(_history.search().at(_history.idx).idx); 534 | } 535 | 536 | if (_input.str.size() + 1 >= _width) 537 | { 538 | _input.off = _input.str.size() - _width + 2; 539 | _input.idx = _width - 2; 540 | } 541 | else 542 | { 543 | _input.off = 0; 544 | _input.idx = _input.str.size(); 545 | } 546 | 547 | refresh(); 548 | } 549 | } 550 | 551 | void Readline::hist_next() 552 | { 553 | // cycle forwards in history 554 | 555 | if (_history.idx != History::npos) 556 | { 557 | --_history.idx; 558 | 559 | if (_history.idx == History::npos) 560 | { 561 | _input.str = _input.buf; 562 | } 563 | else if (_history.search().empty()) 564 | { 565 | // normal search 566 | _input.str = _history().at(_history.idx); 567 | } 568 | else 569 | { 570 | // fuzzy search 571 | _input.str = _history().at(_history.search().at(_history.idx).idx); 572 | } 573 | 574 | if (_input.str.size() + 1 >= _width) 575 | { 576 | _input.off = _input.str.size() - _width + 2; 577 | _input.idx = _width - 2; 578 | } 579 | else 580 | { 581 | _input.off = 0; 582 | _input.idx = _input.str.size(); 583 | } 584 | 585 | refresh(); 586 | } 587 | } 588 | 589 | void Readline::hist_reset() 590 | { 591 | _history.search.clear(); 592 | _history.idx = History::npos; 593 | } 594 | 595 | void Readline::hist_search(std::string const& str) 596 | { 597 | _history.search.clear(); 598 | 599 | OB::Text::String input {OB::Text::normalize_foldcase( 600 | std::regex_replace(OB::Text::trim(str), std::regex("\\s+"), 601 | " ", std::regex_constants::match_not_null))}; 602 | 603 | if (input.empty()) 604 | { 605 | return; 606 | } 607 | 608 | std::size_t idx {0}; 609 | std::size_t count {0}; 610 | std::size_t weight {0}; 611 | std::string prev_hist {" "}; 612 | std::string prev_input {" "}; 613 | OB::Text::String hist; 614 | 615 | for (std::size_t i = 0; i < _history().size(); ++i) 616 | { 617 | hist.str(OB::Text::normalize_foldcase(_history().at(i))); 618 | 619 | if (hist.size() <= input.size()) 620 | { 621 | continue; 622 | } 623 | 624 | idx = 0; 625 | count = 0; 626 | weight = 0; 627 | prev_hist = " "; 628 | prev_input = " "; 629 | 630 | for (std::size_t j = 0, seq = 0; j < hist.size(); ++j) 631 | { 632 | if (idx < input.size() && 633 | hist.at(j).str == input.at(idx).str) 634 | { 635 | ++seq; 636 | count += 1; 637 | 638 | if (seq > 1) 639 | { 640 | count += 1; 641 | } 642 | 643 | if (prev_hist == " " && prev_input == " ") 644 | { 645 | count += 1; 646 | } 647 | 648 | prev_input = input.at(idx).str; 649 | ++idx; 650 | 651 | // short circuit to keep history order 652 | // comment out to search according to closest match 653 | if (idx == input.size()) 654 | { 655 | break; 656 | } 657 | } 658 | else 659 | { 660 | seq = 0; 661 | weight += 2; 662 | 663 | if (prev_input == " ") 664 | { 665 | weight += 1; 666 | } 667 | } 668 | 669 | prev_hist = hist.at(j).str; 670 | } 671 | 672 | if (idx != input.size()) 673 | { 674 | continue; 675 | } 676 | 677 | while (count && weight) 678 | { 679 | --count; 680 | --weight; 681 | } 682 | 683 | _history.search().emplace_back(weight, i); 684 | } 685 | 686 | std::sort(_history.search().begin(), _history.search().end(), 687 | [](auto const& lhs, auto const& rhs) 688 | { 689 | // default to history order if score is equal 690 | return lhs.score == rhs.score ? lhs.idx < rhs.idx : lhs.score < rhs.score; 691 | }); 692 | } 693 | 694 | void Readline::hist_push(std::string const& str) 695 | { 696 | if (! str.empty() && ! (! _history().empty() && _history().back() == str)) 697 | { 698 | if (auto pos = std::find(_history().begin(), _history().end(), str); pos != _history().end()) 699 | { 700 | _history().erase(pos); 701 | } 702 | 703 | _history().emplace_front(str); 704 | } 705 | 706 | hist_reset(); 707 | } 708 | 709 | void Readline::hist_load(fs::path const& path) 710 | { 711 | if (! path.empty()) 712 | { 713 | std::ifstream ifile {path}; 714 | 715 | if (ifile && ifile.is_open()) 716 | { 717 | std::string line; 718 | 719 | while (std::getline(ifile, line)) 720 | { 721 | hist_push(line); 722 | } 723 | } 724 | 725 | hist_open(path); 726 | } 727 | } 728 | 729 | void Readline::hist_save(std::string const& str) 730 | { 731 | if (_history.file.is_open()) 732 | { 733 | _history.file 734 | << str << "\n" 735 | << std::flush; 736 | } 737 | } 738 | 739 | void Readline::hist_open(fs::path const& path) 740 | { 741 | _history.file.open(path, std::ios::app); 742 | 743 | if (! _history.file.is_open()) 744 | { 745 | throw std::runtime_error("could not open file '" + path.string() + "'"); 746 | } 747 | } 748 | 749 | std::string Readline::normalize(std::string const& str) const 750 | { 751 | // trim leading and trailing whitespace 752 | // collapse sequential whitespace 753 | return std::regex_replace(OB::Text::trim(str), std::regex("\\s+"), 754 | " ", std::regex_constants::match_not_null); 755 | } 756 | 757 | } // namespace OB 758 | -------------------------------------------------------------------------------- /src/ob/readline.hh: -------------------------------------------------------------------------------- 1 | #ifndef OB_READLINE_HH 2 | #define OB_READLINE_HH 3 | 4 | #include "ob/text.hh" 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | namespace fs = std::filesystem; 15 | 16 | namespace OB 17 | { 18 | 19 | class Readline 20 | { 21 | public: 22 | 23 | Readline() = default; 24 | 25 | Readline& style(std::string const& style = {}); 26 | Readline& prompt(std::string const& str, std::string const& style = {}); 27 | 28 | std::string operator()(bool& is_running); 29 | 30 | void hist_push(std::string const& str); 31 | void hist_load(fs::path const& path); 32 | 33 | private: 34 | 35 | void refresh(); 36 | 37 | void curs_begin(); 38 | void curs_end(); 39 | void curs_left(); 40 | void curs_right(); 41 | 42 | void edit_insert(std::string const& str); 43 | void edit_clear(); 44 | bool edit_delete(); 45 | bool edit_backspace(); 46 | 47 | void hist_prev(); 48 | void hist_next(); 49 | void hist_reset(); 50 | void hist_search(std::string const& str); 51 | void hist_open(fs::path const& path); 52 | void hist_save(std::string const& str); 53 | 54 | std::string normalize(std::string const& str) const; 55 | 56 | // width and height of the terminal 57 | std::size_t _width {0}; 58 | std::size_t _height {0}; 59 | 60 | struct Style 61 | { 62 | std::string prompt; 63 | std::string input; 64 | } _style; 65 | 66 | struct Prompt 67 | { 68 | std::string lhs; 69 | std::string rhs; 70 | std::string fmt; 71 | std::string str {":"}; 72 | } _prompt; 73 | 74 | struct Input 75 | { 76 | std::size_t off {0}; 77 | std::size_t idx {0}; 78 | std::size_t cur {0}; 79 | std::string buf; 80 | OB::Text::String str; 81 | OB::Text::String fmt; 82 | } _input; 83 | 84 | struct History 85 | { 86 | static std::size_t constexpr npos {std::numeric_limits::max()}; 87 | 88 | struct Search 89 | { 90 | struct Result 91 | { 92 | Result(std::size_t s, std::size_t i): 93 | score {s}, 94 | idx {i} 95 | { 96 | } 97 | 98 | std::size_t score {0}; 99 | std::size_t idx {0}; 100 | }; 101 | 102 | using value_type = std::deque; 103 | 104 | value_type& operator()() 105 | { 106 | return val; 107 | } 108 | 109 | bool empty() 110 | { 111 | return val.empty(); 112 | } 113 | 114 | void clear() 115 | { 116 | idx = History::npos; 117 | val.clear(); 118 | } 119 | 120 | std::size_t idx {0}; 121 | value_type val; 122 | } search; 123 | 124 | using value_type = std::deque; 125 | 126 | value_type& operator()() 127 | { 128 | return val; 129 | } 130 | 131 | value_type val; 132 | std::size_t idx {npos}; 133 | 134 | std::ofstream file; 135 | } _history; 136 | }; 137 | 138 | } // namespace OB 139 | 140 | #endif // OB_READLINE_HH 141 | -------------------------------------------------------------------------------- /src/ob/string.cc: -------------------------------------------------------------------------------- 1 | #include "ob/string.hh" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace OB::String 14 | { 15 | 16 | std::vector split(std::string const& str, 17 | std::string const& delim, std::size_t size) 18 | { 19 | std::vector vtok; 20 | std::size_t start {0}; 21 | auto end = str.find(delim); 22 | 23 | while ((size-- > 0) && (end != std::string::npos)) 24 | { 25 | vtok.emplace_back(str.substr(start, end - start)); 26 | start = end + delim.size(); 27 | end = str.find(delim, start); 28 | } 29 | 30 | vtok.emplace_back(str.substr(start, end)); 31 | 32 | return vtok; 33 | } 34 | 35 | std::string lowercase(std::string const& str) 36 | { 37 | auto const to_lower = [](char& c) 38 | { 39 | if (c >= 'A' && c <= 'Z') 40 | { 41 | c += 'a' - 'A'; 42 | } 43 | 44 | return c; 45 | }; 46 | 47 | std::string res {str}; 48 | 49 | for (char& c : res) 50 | { 51 | c = to_lower(c); 52 | } 53 | 54 | return res; 55 | } 56 | 57 | std::string trim(std::string str) 58 | { 59 | auto start = str.find_first_not_of(" \t\n\r\f\v"); 60 | 61 | if (start != std::string::npos) 62 | { 63 | auto end = str.find_last_not_of(" \t\n\r\f\v"); 64 | str = str.substr(start, end - start + 1); 65 | 66 | return str; 67 | } 68 | 69 | return {}; 70 | } 71 | 72 | bool assert_rx(std::string const& str, std::regex rx) 73 | { 74 | std::smatch m; 75 | 76 | if (std::regex_match(str, m, rx, std::regex_constants::match_not_null)) 77 | { 78 | return true; 79 | } 80 | 81 | return false; 82 | } 83 | 84 | std::optional> match(std::string const& str, std::regex rx) 85 | { 86 | std::smatch m; 87 | 88 | if (std::regex_match(str, m, rx, std::regex_constants::match_not_null)) 89 | { 90 | std::vector v; 91 | 92 | for (auto const& e : m) 93 | { 94 | v.emplace_back(std::string(e)); 95 | } 96 | 97 | return v; 98 | } 99 | 100 | return {}; 101 | } 102 | 103 | std::string repeat(std::size_t const num, std::string const& str) 104 | { 105 | if (num == 0) 106 | { 107 | return {}; 108 | } 109 | 110 | if (num == 1) 111 | { 112 | return str; 113 | } 114 | 115 | std::string res; 116 | res.reserve(str.size() * num); 117 | 118 | for (std::size_t i {0}; i < num; ++i) 119 | { 120 | res += str; 121 | } 122 | 123 | return res; 124 | } 125 | 126 | bool starts_with(std::string const& str, std::string const& val) 127 | { 128 | if (str.empty() || str.size() < val.size()) 129 | { 130 | return false; 131 | } 132 | 133 | if (str.compare(0, val.size(), val) == 0) 134 | { 135 | return true; 136 | } 137 | 138 | return false; 139 | } 140 | 141 | std::size_t damerau_levenshtein(std::string const& lhs, std::string const& rhs, 142 | std::size_t const weight_insert, std::size_t const weight_substitute, 143 | std::size_t const weight_delete, std::size_t const weight_transpose) 144 | { 145 | if (lhs == rhs) 146 | { 147 | return 0; 148 | } 149 | 150 | std::string_view lhsv {lhs}; 151 | std::string_view rhsv {rhs}; 152 | 153 | bool swapped {false}; 154 | if (lhsv.size() > rhsv.size()) 155 | { 156 | swapped = true; 157 | std::swap(lhsv, rhsv); 158 | } 159 | 160 | for (std::size_t i = 0; i < lhsv.size(); ++i) 161 | { 162 | if (lhsv.at(i) != rhsv.at(i)) 163 | { 164 | if (i) 165 | { 166 | lhsv.substr(i); 167 | rhsv.substr(i); 168 | } 169 | 170 | break; 171 | } 172 | } 173 | 174 | for (std::size_t i = 0; i < lhsv.size(); ++i) 175 | { 176 | if (lhsv.at(lhsv.size() - 1 - i) != rhsv.at(rhsv.size() - 1 - i)) 177 | { 178 | if (i) 179 | { 180 | lhsv.substr(0, lhsv.size() - 1 - i); 181 | rhsv.substr(0, rhsv.size() - 1 - i); 182 | } 183 | 184 | break; 185 | } 186 | } 187 | 188 | if (swapped) 189 | { 190 | std::swap(lhsv, rhsv); 191 | } 192 | 193 | if (lhsv.empty()) 194 | { 195 | return rhsv.size() * weight_insert; 196 | } 197 | 198 | if (rhsv.empty()) 199 | { 200 | return lhsv.size() * weight_delete; 201 | } 202 | 203 | std::vector v0 (rhsv.size() + 1, 0); 204 | std::vector v1 (rhsv.size() + 1, 0); 205 | std::vector v2 (rhsv.size() + 1, 0); 206 | 207 | for (std::size_t i = 0; i <= rhsv.size(); ++i) 208 | { 209 | v1.at(i) = i * weight_insert; 210 | } 211 | 212 | for (std::size_t i = 0; i < lhsv.size(); ++i) 213 | { 214 | v2.at(0) = (i + 1) * weight_delete; 215 | for (std::size_t j = 0; j < rhsv.size(); j++) 216 | { 217 | v2.at(j + 1) = std::min( 218 | // deletion 219 | v1.at(j + 1) + weight_delete, 220 | std::min( 221 | // insertion 222 | v2.at(j) + weight_insert, 223 | // substitution 224 | v1.at(j) + (weight_substitute * (lhsv.at(i) != rhsv.at(j))))); 225 | 226 | if (i > 0 && j > 0 && 227 | (lhsv.at(i - 1) == rhsv.at(j)) && 228 | (lhsv.at(i) == rhsv.at(j - 1))) 229 | { 230 | v2.at(j + 1) = std::min( 231 | v0.at(j + 1), 232 | // transposition 233 | v0.at(j - 1) + weight_transpose); 234 | } 235 | } 236 | 237 | std::swap(v0, v1); 238 | std::swap(v1, v2); 239 | } 240 | 241 | return v1.at(rhsv.size()); 242 | } 243 | 244 | } // namespace OB::String 245 | -------------------------------------------------------------------------------- /src/ob/string.hh: -------------------------------------------------------------------------------- 1 | #ifndef OB_STRING_HH 2 | #define OB_STRING_HH 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace OB::String 13 | { 14 | 15 | std::vector split(std::string const& str, std::string const& delim, 16 | std::size_t size = std::numeric_limits::max()); 17 | 18 | std::string lowercase(std::string const& str); 19 | 20 | std::string trim(std::string str); 21 | 22 | bool assert_rx(std::string const& str, std::regex rx); 23 | 24 | std::optional> match(std::string const& str, std::regex rx); 25 | 26 | std::string repeat(std::size_t const num, std::string const& str); 27 | 28 | bool starts_with(std::string const& str, std::string const& val); 29 | 30 | std::size_t damerau_levenshtein(std::string const& lhs, std::string const& rhs, 31 | std::size_t const weight_insert = 1, std::size_t const weight_substitute = 1, 32 | std::size_t const weight_delete = 1, std::size_t const weight_transpose = 1); 33 | 34 | } // namespace OB::String 35 | 36 | #endif // OB_STRING_HH 37 | -------------------------------------------------------------------------------- /src/ob/term.hh: -------------------------------------------------------------------------------- 1 | #ifndef OB_TERM_HH 2 | #define OB_TERM_HH 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | namespace OB::Term 30 | { 31 | 32 | namespace Key 33 | { 34 | enum 35 | { 36 | null = 0, 37 | bell = 7, 38 | tab = 9, 39 | newline = 10, 40 | enter = 13, 41 | escape = 27, 42 | space = 32, 43 | backspace = 127, 44 | 45 | up = 0xF0000, 46 | down, 47 | left, 48 | right, 49 | home, 50 | end, 51 | delete_, 52 | insert, 53 | page_up, 54 | page_down 55 | }; 56 | } 57 | 58 | namespace Mouse 59 | { 60 | enum 61 | { 62 | null = 0, 63 | btn_release = 0xF1000, 64 | btn1_press, 65 | btn1_release, 66 | btn2_press, 67 | btn2_release, 68 | btn3_press, 69 | btn3_release, 70 | scroll_up, 71 | scroll_down 72 | }; 73 | } 74 | 75 | inline int constexpr ctrl_key(int const c) 76 | { 77 | return (c & 0x1F); 78 | } 79 | 80 | inline char32_t utf8_to_char32(std::string_view str) 81 | { 82 | if (str.empty()) 83 | { 84 | return 0; 85 | } 86 | 87 | if ((str.at(0) & 0x80) == 0) 88 | { 89 | return static_cast(str.at(0)); 90 | } 91 | else if ((str.at(0) & 0xE0) == 0xC0 && str.size() == 2) 92 | { 93 | return (static_cast(str[0] & 0x1F) << 6) | 94 | static_cast(str[1] & 0x3F); 95 | } 96 | else if ((str.at(0) & 0xF0) == 0xE0 && str.size() == 3) 97 | { 98 | return (static_cast(str[0] & 0x0F) << 12) | 99 | (static_cast(str[1] & 0x3F) << 6) | 100 | static_cast(str[2] & 0x3F); 101 | } 102 | else if ((str.at(0) & 0xF8) == 0xF0 && str.size() == 4) 103 | { 104 | return (static_cast(str[0] & 0x07) << 18) | 105 | (static_cast(str[1] & 0x3F) << 12) | 106 | (static_cast(str[2] & 0x3F) << 6) | 107 | static_cast(str[3] & 0x3F); 108 | } 109 | 110 | return 0; 111 | } 112 | 113 | inline char32_t get_key(std::string* str = nullptr) 114 | { 115 | // NOTE term mode should be in raw state before call to this func 116 | 117 | char key[4] {0}; 118 | int ec = read(STDIN_FILENO, &key[0], 1); 119 | 120 | if ((ec == -1) && (errno != EAGAIN)) 121 | { 122 | throw std::runtime_error("read failed"); 123 | } 124 | 125 | if (ec == 0) 126 | { 127 | return Key::null; 128 | } 129 | 130 | // utf-8 multibyte code point 131 | if (key[0] & 0x80) 132 | { 133 | std::size_t bytes {0}; 134 | 135 | for (; bytes < 3; ++bytes) 136 | { 137 | if (! (key[0] & (0x80 >> (bytes + 1)))) 138 | { 139 | break; 140 | } 141 | } 142 | 143 | if ((ec = read(STDIN_FILENO, &key[1], bytes)) != static_cast(bytes)) 144 | { 145 | if ((ec == -1) && (errno != EAGAIN)) 146 | { 147 | throw std::runtime_error("read failed"); 148 | } 149 | 150 | return static_cast(key[0]); 151 | } 152 | 153 | ++bytes; 154 | 155 | if (str != nullptr) 156 | { 157 | str->assign(&key[0], bytes); 158 | } 159 | 160 | return utf8_to_char32(std::string_view(&key[0], bytes)); 161 | } 162 | 163 | // utf-8 single-byte code point 164 | if (str != nullptr) 165 | { 166 | str->assign(&key[0], 1); 167 | } 168 | 169 | // esc / esc sequence 170 | if (key[0] == Key::escape) 171 | { 172 | char seq[3] {0}; 173 | 174 | if ((ec = read(STDIN_FILENO, &seq[0], 1)) != 1) 175 | { 176 | if ((ec == -1) && (errno != EAGAIN)) 177 | { 178 | throw std::runtime_error("read failed"); 179 | } 180 | 181 | return static_cast(key[0]); 182 | } 183 | 184 | if ((ec = read(STDIN_FILENO, &seq[1], 1)) != 1) 185 | { 186 | if ((ec == -1) && (errno != EAGAIN)) 187 | { 188 | throw std::runtime_error("read failed"); 189 | } 190 | 191 | return static_cast(key[0]); 192 | } 193 | 194 | if (seq[0] == '[') 195 | { 196 | if (seq[1] >= '0' && seq[1] <= '9') 197 | { 198 | if ((ec = read(STDIN_FILENO, &seq[2], 1)) != 1) 199 | { 200 | if ((ec == -1) && (errno != EAGAIN)) 201 | { 202 | throw std::runtime_error("read failed"); 203 | } 204 | 205 | return static_cast(key[0]); 206 | } 207 | 208 | if (seq[2] == '~') 209 | { 210 | switch (seq[1]) 211 | { 212 | case '1': 213 | { 214 | return Key::home; 215 | } 216 | 217 | case '2': 218 | { 219 | return Key::insert; 220 | } 221 | 222 | case '3': 223 | { 224 | return Key::delete_; 225 | } 226 | 227 | case '4': 228 | { 229 | return Key::end; 230 | } 231 | 232 | case '5': 233 | { 234 | return Key::page_up; 235 | } 236 | 237 | case '6': 238 | { 239 | return Key::page_down; 240 | } 241 | 242 | default: 243 | { 244 | return static_cast(key[0]); 245 | } 246 | } 247 | } 248 | } 249 | else 250 | { 251 | switch (seq[1]) 252 | { 253 | case 'A': 254 | { 255 | return Key::up; 256 | } 257 | 258 | case 'B': 259 | { 260 | return Key::down; 261 | } 262 | 263 | case 'C': 264 | { 265 | return Key::right; 266 | } 267 | 268 | case 'D': 269 | { 270 | return Key::left; 271 | } 272 | 273 | case '<': 274 | { 275 | // 1000;1006 mouse event 276 | 277 | std::size_t constexpr buf_size {64}; 278 | char mouse[buf_size] {0}; 279 | 280 | for (std::size_t i = 0; i < buf_size; ++i) 281 | { 282 | if ((ec = read(STDIN_FILENO, &mouse[i], 1)) != 1) 283 | { 284 | if ((ec == -1) && (errno != EAGAIN)) 285 | { 286 | throw std::runtime_error("read failed"); 287 | } 288 | 289 | return static_cast(key[0]); 290 | } 291 | 292 | if (mouse[i] >= 0x40 && mouse[i] <= 0x7E) 293 | { 294 | if (str != nullptr) 295 | { 296 | str->assign(mouse, i + 1); 297 | } 298 | 299 | switch (mouse[i]) 300 | { 301 | case 'm': 302 | { 303 | switch (mouse[0]) 304 | { 305 | case '0': 306 | { 307 | return Mouse::btn1_release; 308 | } 309 | 310 | case '1': 311 | { 312 | return Mouse::btn2_release; 313 | } 314 | 315 | case '2': 316 | { 317 | return Mouse::btn3_release; 318 | } 319 | 320 | default: 321 | { 322 | break; 323 | } 324 | } 325 | 326 | break; 327 | } 328 | 329 | case 'M': 330 | { 331 | switch (mouse[0]) 332 | { 333 | case '0': 334 | { 335 | return Mouse::btn1_press; 336 | } 337 | 338 | case '1': 339 | { 340 | return Mouse::btn2_press; 341 | } 342 | 343 | case '2': 344 | { 345 | return Mouse::btn3_press; 346 | } 347 | 348 | case '6': 349 | { 350 | switch (mouse[1]) 351 | { 352 | case '4': 353 | { 354 | return Mouse::scroll_up; 355 | } 356 | 357 | case '5': 358 | { 359 | return Mouse::scroll_down; 360 | } 361 | 362 | default: 363 | { 364 | break; 365 | } 366 | } 367 | 368 | break; 369 | } 370 | 371 | default: 372 | { 373 | break; 374 | } 375 | } 376 | 377 | break; 378 | } 379 | 380 | default: 381 | { 382 | break; 383 | } 384 | } 385 | 386 | break; 387 | } 388 | } 389 | 390 | return static_cast(key[0]); 391 | } 392 | 393 | case 'M': 394 | { 395 | // 1000 mouse event 396 | 397 | char mouse[3] {0}; 398 | 399 | if ((ec = read(STDIN_FILENO, &mouse[0], 3)) != 3) 400 | { 401 | if ((ec == -1) && (errno != EAGAIN)) 402 | { 403 | throw std::runtime_error("read failed"); 404 | } 405 | 406 | return static_cast(key[0]); 407 | } 408 | 409 | if (str != nullptr) 410 | { 411 | str->assign(mouse, 3); 412 | } 413 | 414 | switch (mouse[0] & 0x03) 415 | { 416 | case 0: 417 | { 418 | if (mouse[0] & 0x40) 419 | { 420 | return Mouse::scroll_up; 421 | } 422 | 423 | return Mouse::btn1_press; 424 | } 425 | 426 | case 1: 427 | { 428 | if (mouse[0] & 0x40) 429 | { 430 | return Mouse::scroll_down; 431 | } 432 | 433 | return Mouse::btn2_press; 434 | } 435 | 436 | case 2: 437 | { 438 | return Mouse::btn3_press; 439 | } 440 | 441 | case 3: 442 | { 443 | return Mouse::btn_release; 444 | } 445 | 446 | default: 447 | { 448 | break; 449 | } 450 | } 451 | 452 | return Mouse::null; 453 | } 454 | 455 | default: 456 | { 457 | return static_cast(key[0]); 458 | } 459 | } 460 | } 461 | } 462 | } 463 | 464 | return static_cast(key[0]); 465 | } 466 | 467 | class Stdin 468 | { 469 | public: 470 | 471 | Stdin() 472 | { 473 | _fp = std::fopen("/dev/tty", "r"); 474 | if (! _fp) 475 | { 476 | throw std::runtime_error("could not open stdin"); 477 | } 478 | 479 | _fd = fileno(_fp); 480 | } 481 | 482 | ~Stdin() 483 | { 484 | if (_fp) 485 | { 486 | std::fclose(_fp); 487 | } 488 | } 489 | 490 | int fd() 491 | { 492 | return _fd; 493 | } 494 | 495 | FILE* fp() 496 | { 497 | return _fp; 498 | } 499 | 500 | private: 501 | 502 | int _fd {0}; 503 | FILE* _fp {NULL}; 504 | }; 505 | 506 | class Mode 507 | { 508 | public: 509 | 510 | Mode() 511 | { 512 | if (tcgetattr(_stdin.fd(), &_old) == -1) 513 | { 514 | throw std::runtime_error("tcgetattr failed"); 515 | } 516 | } 517 | 518 | ~Mode() 519 | { 520 | if (! _cooked) 521 | { 522 | set_cooked(); 523 | } 524 | } 525 | 526 | operator bool() 527 | { 528 | return _cooked; 529 | } 530 | 531 | void set_cooked() 532 | { 533 | if (_cooked) 534 | { 535 | return; 536 | } 537 | 538 | if (tcsetattr(_stdin.fd(), TCSAFLUSH, &_old) == -1) 539 | { 540 | throw std::runtime_error("tcsetattr failed"); 541 | } 542 | 543 | _cooked = true; 544 | } 545 | 546 | void set_raw() 547 | { 548 | if (! _cooked) 549 | { 550 | return; 551 | } 552 | 553 | _raw = _old; 554 | // _raw.c_iflag &= static_cast(~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON)); 555 | // _raw.c_oflag &= static_cast(~(OPOST)); 556 | _raw.c_lflag &= static_cast(~(ECHO | ECHONL | ICANON | ISIG | IEXTEN)); 557 | _raw.c_cflag &= static_cast(~(CSIZE | PARENB)); 558 | _raw.c_cflag |= static_cast(CS8); 559 | _raw.c_cc[VMIN] = _min; 560 | _raw.c_cc[VTIME] = _timeout; 561 | 562 | if (tcsetattr(_stdin.fd(), TCSAFLUSH, &_raw) == -1) 563 | { 564 | throw std::runtime_error("tcsetattr failed"); 565 | } 566 | 567 | _cooked = false; 568 | } 569 | 570 | void set_timeout(int val = 0) 571 | { 572 | if (_timeout == val) 573 | { 574 | return; 575 | } 576 | 577 | _timeout = val; 578 | 579 | if (_cooked) 580 | { 581 | return; 582 | } 583 | 584 | _raw.c_cc[VTIME] = _timeout; 585 | 586 | if (tcsetattr(_stdin.fd(), TCSAFLUSH, &_raw) == -1) 587 | { 588 | throw std::runtime_error("tcsetattr failed"); 589 | } 590 | } 591 | 592 | int get_timeout() 593 | { 594 | return _timeout; 595 | } 596 | 597 | void set_min(int val = 0) 598 | { 599 | if (_min == val) 600 | { 601 | return; 602 | } 603 | 604 | _min = val; 605 | 606 | if (_cooked) 607 | { 608 | return; 609 | } 610 | 611 | _raw.c_cc[VMIN] = _min; 612 | 613 | if (tcsetattr(_stdin.fd(), TCSAFLUSH, &_raw) == -1) 614 | { 615 | throw std::runtime_error("tcsetattr failed"); 616 | } 617 | } 618 | 619 | int get_min() 620 | { 621 | return _min; 622 | } 623 | 624 | private: 625 | 626 | Stdin _stdin; 627 | termios _old; 628 | termios _raw; 629 | bool _cooked {true}; 630 | int _timeout {0}; 631 | int _min {0}; 632 | }; // class Mode 633 | 634 | inline bool press_to_continue(std::string const& str = "ANY KEY", char32_t val = 0) 635 | { 636 | std::cerr 637 | << "Press " << str << " to continue"; 638 | 639 | Mode _term_mode; 640 | _term_mode.set_min(1); 641 | _term_mode.set_raw(); 642 | 643 | bool res {false}; 644 | char32_t key {0}; 645 | if ((key = get_key()) > 0) 646 | { 647 | res = (val == 0 ? true : val == key); 648 | } 649 | 650 | _term_mode.set_cooked(); 651 | 652 | std::cerr 653 | << "\n"; 654 | 655 | return res; 656 | } 657 | 658 | inline std::string env_var(std::string const& str) 659 | { 660 | std::string res; 661 | 662 | if (char const* envar = std::getenv(str.data())) 663 | { 664 | res = envar; 665 | } 666 | 667 | return res; 668 | } 669 | 670 | inline bool is_term(std::size_t fd_) 671 | { 672 | switch (fd_) 673 | { 674 | case STDIN_FILENO: return isatty(STDIN_FILENO); 675 | case STDOUT_FILENO: return isatty(STDOUT_FILENO); 676 | case STDERR_FILENO: return isatty(STDERR_FILENO); 677 | default: return false; 678 | } 679 | } 680 | 681 | inline bool is_colorterm() 682 | { 683 | auto const colorterm = env_var("COLORTERM"); 684 | 685 | if (colorterm == "truecolor" || colorterm == "24bit") 686 | { 687 | return true; 688 | } 689 | 690 | return false; 691 | } 692 | 693 | inline int width(std::size_t& width_, std::size_t fd_ = STDOUT_FILENO) 694 | { 695 | if (fd_ != STDIN_FILENO && fd_ != STDOUT_FILENO && fd_ != STDERR_FILENO) 696 | { 697 | return -1; 698 | } 699 | 700 | winsize w; 701 | ioctl(fd_, TIOCGWINSZ, &w); 702 | 703 | width_ = w.ws_col; 704 | 705 | return 0; 706 | } 707 | 708 | inline int height(std::size_t& height_, std::size_t fd_ = STDOUT_FILENO) 709 | { 710 | if (fd_ != STDIN_FILENO && fd_ != STDOUT_FILENO && fd_ != STDERR_FILENO) 711 | { 712 | return -1; 713 | } 714 | 715 | winsize w; 716 | ioctl(fd_, TIOCGWINSZ, &w); 717 | 718 | height_ = w.ws_row; 719 | 720 | return 0; 721 | } 722 | 723 | inline int size(std::size_t& width_, std::size_t& height_, std::size_t fd_ = STDOUT_FILENO) 724 | { 725 | if (fd_ != STDIN_FILENO && fd_ != STDOUT_FILENO && fd_ != STDERR_FILENO) 726 | { 727 | return -1; 728 | } 729 | 730 | winsize w; 731 | ioctl(fd_, TIOCGWINSZ, &w); 732 | 733 | width_ = w.ws_col; 734 | height_ = w.ws_row; 735 | 736 | return 0; 737 | } 738 | 739 | namespace ANSI_Escape_Codes 740 | { 741 | 742 | // constants 743 | std::string const space {" "}; 744 | 745 | // standard escaped characters 746 | std::string const cr {"\r"}; 747 | std::string const nl {"\n"}; 748 | std::string const crnl {cr + nl}; 749 | std::string const tab {"\t"}; 750 | std::string const alert {"\a"}; 751 | std::string const backspace {"\b"}; 752 | std::string const backslash {"\\"}; 753 | 754 | // escape code sequence 755 | std::string const esc {"\x1b"}; 756 | 757 | // reset terminal 758 | std::string const reset {esc + "c"}; 759 | 760 | // clear all attributes 761 | std::string const clear {esc + "[0m"}; 762 | 763 | // mouse 764 | std::string const mouse_enable {esc + "[?1000;1006h"}; 765 | std::string const mouse_disable {esc + "[?1000;1006l"}; 766 | 767 | // style 768 | std::string const bold {esc + "[1m"}; 769 | std::string const dim {esc + "[2m"}; 770 | std::string const italic {esc + "[3m"}; 771 | std::string const underline {esc + "[4m"}; 772 | std::string const blink {esc + "[5m"}; 773 | std::string const rblink {esc + "[6m"}; 774 | std::string const reverse {esc + "[7m"}; 775 | std::string const conceal {esc + "[8m"}; 776 | std::string const cross {esc + "[9m"}; 777 | 778 | // TODO add negative of each style attr 779 | std::string const nbold {esc + "[22m"}; 780 | 781 | // screen 782 | std::string const screen_push {esc + "[?1049h"}; 783 | std::string const screen_pop {esc + "[?1049l"}; 784 | std::string const screen_clear {esc + "[2J"}; 785 | 786 | // scroll 787 | std::string const scroll_up {esc + "M"}; 788 | std::string const scroll_down {esc + "D"}; 789 | 790 | // erase 791 | std::string const erase_end {esc + "[K"}; 792 | std::string const erase_start {esc + "[1K"}; 793 | std::string const erase_line {esc + "[2K"}; 794 | std::string const erase_down {esc + "[J"}; 795 | std::string const erase_up {esc + "[1J"}; 796 | 797 | // cursor visibility 798 | std::string const cursor_hide {esc + "[?25l"}; 799 | std::string const cursor_show {esc + "[?25h"}; 800 | 801 | // cursor movement 802 | std::string const cursor_home {esc + "[H"}; 803 | std::string const cursor_up {esc + "[1A"}; 804 | std::string const cursor_down {esc + "[1B"}; 805 | std::string const cursor_right {esc + "[1C"}; 806 | std::string const cursor_left {esc + "[1D"}; 807 | 808 | // cursor position 809 | std::string const cursor_save {esc + "7"}; 810 | std::string const cursor_load {esc + "8"}; 811 | 812 | // foreground color 813 | std::string const fg_black {esc + "[30m"}; 814 | std::string const fg_red {esc + "[31m"}; 815 | std::string const fg_green {esc + "[32m"}; 816 | std::string const fg_yellow {esc + "[33m"}; 817 | std::string const fg_blue {esc + "[34m"}; 818 | std::string const fg_magenta {esc + "[35m"}; 819 | std::string const fg_cyan {esc + "[36m"}; 820 | std::string const fg_white {esc + "[37m"}; 821 | 822 | // background color 823 | std::string const bg_black {esc + "[40m"}; 824 | std::string const bg_red {esc + "[41m"}; 825 | std::string const bg_green {esc + "[42m"}; 826 | std::string const bg_yellow {esc + "[43m"}; 827 | std::string const bg_blue {esc + "[44m"}; 828 | std::string const bg_magenta {esc + "[45m"}; 829 | std::string const bg_cyan {esc + "[46m"}; 830 | std::string const bg_white {esc + "[47m"}; 831 | 832 | // foreground color bright 833 | std::string const fg_black_bright {esc + "[90m"}; 834 | std::string const fg_red_bright {esc + "[91m"}; 835 | std::string const fg_green_bright {esc + "[92m"}; 836 | std::string const fg_yellow_bright {esc + "[99m"}; 837 | std::string const fg_blue_bright {esc + "[94m"}; 838 | std::string const fg_magenta_bright {esc + "[95m"}; 839 | std::string const fg_cyan_bright {esc + "[96m"}; 840 | std::string const fg_white_bright {esc + "[97m"}; 841 | 842 | // background color bright 843 | std::string const bg_black_bright {esc + "[100m"}; 844 | std::string const bg_red_bright {esc + "[101m"}; 845 | std::string const bg_green_bright {esc + "[102m"}; 846 | std::string const bg_yellow_bright {esc + "[103m"}; 847 | std::string const bg_blue_bright {esc + "[104m"}; 848 | std::string const bg_magenta_bright {esc + "[105m"}; 849 | std::string const bg_cyan_bright {esc + "[106m"}; 850 | std::string const bg_white_bright {esc + "[107m"}; 851 | 852 | // box drawing 853 | // TODO add box drawing chars 854 | 855 | inline std::string str_to_fg_color(std::string const& str_, bool bright_ = false) 856 | { 857 | if ("black" == str_) 858 | { 859 | if (bright_) 860 | { 861 | return fg_black_bright; 862 | } 863 | else 864 | { 865 | return fg_black; 866 | } 867 | } 868 | else if ("red" == str_) 869 | { 870 | if (bright_) 871 | { 872 | return fg_red_bright; 873 | } 874 | else 875 | { 876 | return fg_red; 877 | } 878 | } 879 | else if ("green" == str_) 880 | { 881 | if (bright_) 882 | { 883 | return fg_green_bright; 884 | } 885 | else 886 | { 887 | return fg_green; 888 | } 889 | } 890 | else if ("yellow" == str_) 891 | { 892 | if (bright_) 893 | { 894 | return fg_yellow_bright; 895 | } 896 | else 897 | { 898 | return fg_yellow; 899 | } 900 | } 901 | else if ("blue" == str_) 902 | { 903 | if (bright_) 904 | { 905 | return fg_blue_bright; 906 | } 907 | else 908 | { 909 | return fg_blue; 910 | } 911 | } 912 | else if ("magenta" == str_) 913 | { 914 | if (bright_) 915 | { 916 | return fg_magenta_bright; 917 | } 918 | else 919 | { 920 | return fg_magenta; 921 | } 922 | } 923 | else if ("cyan" == str_) 924 | { 925 | if (bright_) 926 | { 927 | return fg_cyan_bright; 928 | } 929 | else 930 | { 931 | return fg_cyan; 932 | } 933 | } 934 | else if ("white" == str_) 935 | { 936 | if (bright_) 937 | { 938 | return fg_white_bright; 939 | } 940 | else 941 | { 942 | return fg_white; 943 | } 944 | } 945 | else 946 | { 947 | return {}; 948 | } 949 | } 950 | 951 | inline std::string str_to_bg_color(std::string const& str_, bool bright_ = false) 952 | { 953 | if ("black" == str_) 954 | { 955 | if (bright_) 956 | { 957 | return bg_black_bright; 958 | } 959 | else 960 | { 961 | return bg_black; 962 | } 963 | } 964 | else if ("red" == str_) 965 | { 966 | if (bright_) 967 | { 968 | return bg_red_bright; 969 | } 970 | else 971 | { 972 | return bg_red; 973 | } 974 | } 975 | else if ("green" == str_) 976 | { 977 | if (bright_) 978 | { 979 | return bg_green_bright; 980 | } 981 | else 982 | { 983 | return bg_green; 984 | } 985 | } 986 | else if ("yellow" == str_) 987 | { 988 | if (bright_) 989 | { 990 | return bg_yellow_bright; 991 | } 992 | else 993 | { 994 | return bg_yellow; 995 | } 996 | } 997 | else if ("blue" == str_) 998 | { 999 | if (bright_) 1000 | { 1001 | return bg_blue_bright; 1002 | } 1003 | else 1004 | { 1005 | return bg_blue; 1006 | } 1007 | } 1008 | else if ("magenta" == str_) 1009 | { 1010 | if (bright_) 1011 | { 1012 | return bg_magenta_bright; 1013 | } 1014 | else 1015 | { 1016 | return bg_magenta; 1017 | } 1018 | } 1019 | else if ("cyan" == str_) 1020 | { 1021 | if (bright_) 1022 | { 1023 | return bg_cyan_bright; 1024 | } 1025 | else 1026 | { 1027 | return bg_cyan; 1028 | } 1029 | } 1030 | else if ("white" == str_) 1031 | { 1032 | if (bright_) 1033 | { 1034 | return bg_white_bright; 1035 | } 1036 | else 1037 | { 1038 | return bg_white; 1039 | } 1040 | } 1041 | else 1042 | { 1043 | return {}; 1044 | } 1045 | } 1046 | 1047 | inline std::string fg_256(std::string const& str_) 1048 | { 1049 | auto const n = std::stoi(str_); 1050 | if (n < 0 || n > 256) return {}; 1051 | std::stringstream ss; 1052 | ss << esc << "[38;5;" << str_ << "m"; 1053 | 1054 | return ss.str(); 1055 | } 1056 | 1057 | inline std::string bg_256(std::string const& str_) 1058 | { 1059 | auto const n = std::stoi(str_); 1060 | if (n < 0 || n > 256) return {}; 1061 | std::stringstream ss; 1062 | ss << esc << "[48;5;" << str_ << "m"; 1063 | 1064 | return ss.str(); 1065 | } 1066 | 1067 | inline std::string htoi(std::string const& str_) 1068 | { 1069 | std::stringstream ss; 1070 | ss << str_; 1071 | unsigned int n; 1072 | ss >> std::hex >> n; 1073 | 1074 | return std::to_string(n); 1075 | } 1076 | 1077 | inline bool valid_hstr(std::string& str_) 1078 | { 1079 | std::smatch m; 1080 | std::regex rx {"^#?((?:[0-9a-fA-F]{3}){1,2})$"}; 1081 | 1082 | if (std::regex_match(str_, m, rx, std::regex_constants::match_not_null)) 1083 | { 1084 | std::string hstr {m[1]}; 1085 | 1086 | if (hstr.size() == 3) 1087 | { 1088 | std::stringstream ss; 1089 | ss << hstr[0] << hstr[0] << hstr[1] << hstr[1] << hstr[2] << hstr[2]; 1090 | hstr = ss.str(); 1091 | } 1092 | 1093 | str_ = hstr; 1094 | 1095 | return true; 1096 | } 1097 | 1098 | return false; 1099 | } 1100 | 1101 | inline std::string fg_true(std::string str_) 1102 | { 1103 | if (! valid_hstr(str_)) 1104 | { 1105 | return {}; 1106 | } 1107 | 1108 | std::string const h1 {str_.substr(0, 2)}; 1109 | std::string const h2 {str_.substr(2, 2)}; 1110 | std::string const h3 {str_.substr(4, 2)}; 1111 | 1112 | std::stringstream ss; ss 1113 | << esc << "[38;2;" 1114 | << htoi(h1) << ";" 1115 | << htoi(h2) << ";" 1116 | << htoi(h3) << "m"; 1117 | 1118 | return ss.str(); 1119 | } 1120 | 1121 | inline std::string bg_true(std::string str_) 1122 | { 1123 | if (! valid_hstr(str_)) 1124 | { 1125 | return {}; 1126 | } 1127 | 1128 | std::string const h1 {str_.substr(0, 2)}; 1129 | std::string const h2 {str_.substr(2, 2)}; 1130 | std::string const h3 {str_.substr(4, 2)}; 1131 | 1132 | std::stringstream ss; ss 1133 | << esc << "[48;2;" 1134 | << htoi(h1) << ";" 1135 | << htoi(h2) << ";" 1136 | << htoi(h3) << "m"; 1137 | 1138 | return ss.str(); 1139 | } 1140 | 1141 | inline std::string cursor_set(std::size_t x_, std::size_t y_) 1142 | { 1143 | std::stringstream ss; 1144 | ss << esc << "[" << y_ << ";" << x_ << "H"; 1145 | 1146 | return ss.str(); 1147 | } 1148 | 1149 | inline int cursor_get(std::size_t& x_, std::size_t& y_, bool mode_ = true) 1150 | { 1151 | Term::Mode mode; 1152 | 1153 | if (mode_) 1154 | { 1155 | mode.set_raw(); 1156 | } 1157 | 1158 | std::cout << (esc + "[6n") << std::flush; 1159 | 1160 | char buf[32]; 1161 | std::uint8_t i {0}; 1162 | 1163 | // attempt to read response for up to 1 second 1164 | for (int retry = 20; i < sizeof(buf) - 1; ++i) 1165 | { 1166 | while (retry-- > 0 && read(STDIN_FILENO, &buf[i], 1) != 1) 1167 | { 1168 | std::this_thread::sleep_for(std::chrono::milliseconds(50)); 1169 | } 1170 | 1171 | if (buf[i] == 'R') 1172 | { 1173 | break; 1174 | } 1175 | } 1176 | 1177 | buf[i] = '\0'; 1178 | if (buf[0] != '\x1b' || buf[1] != '[') 1179 | { 1180 | return -1; 1181 | } 1182 | 1183 | int x; 1184 | int y; 1185 | if (std::sscanf(&buf[2], "%d;%d", &y, &x) != 2) 1186 | { 1187 | return -1; 1188 | } 1189 | 1190 | x_ = static_cast(x); 1191 | y_ = static_cast(y); 1192 | 1193 | return 0; 1194 | } 1195 | 1196 | template 1197 | std::string wrap(T const val_, std::string const attr_, bool color_ = true) 1198 | { 1199 | std::stringstream ss; 1200 | 1201 | if (color_) 1202 | { 1203 | ss 1204 | << attr_ 1205 | << val_ 1206 | << clear; 1207 | } 1208 | else 1209 | { 1210 | ss << val_; 1211 | } 1212 | 1213 | return ss.str(); 1214 | } 1215 | 1216 | template 1217 | std::string wrap(T const val_, std::vector const attr_, bool color_ = true) 1218 | { 1219 | std::stringstream ss; 1220 | 1221 | if (color_) 1222 | { 1223 | for (auto const& e : attr_) 1224 | { 1225 | ss << e; 1226 | } 1227 | ss << val_ << clear; 1228 | } 1229 | else 1230 | { 1231 | ss << val_ << clear; 1232 | } 1233 | 1234 | return ss.str(); 1235 | } 1236 | 1237 | } // namespace ANSI_Escape_Codes 1238 | 1239 | class ostream : public std::ostream 1240 | { 1241 | protected: 1242 | 1243 | class streambuf : public std::streambuf 1244 | { 1245 | using string = std::basic_string; 1246 | 1247 | public: 1248 | 1249 | explicit streambuf(std::streambuf* streambuf_, 1250 | std::size_t width_ = std::numeric_limits::max()) : 1251 | _streambuf {streambuf_}, 1252 | _width {width_} 1253 | { 1254 | } 1255 | 1256 | ~streambuf() 1257 | { 1258 | flush(); 1259 | } 1260 | 1261 | streambuf& flush() 1262 | { 1263 | if (! _buffer.empty()) 1264 | { 1265 | if (_size) 1266 | { 1267 | if (! _white_space && ! _buffer.empty() && _buffer.back() == ' ') 1268 | { 1269 | _buffer.erase(_buffer.size() - 1); 1270 | } 1271 | 1272 | _streambuf->sputn(_prefix.data(), static_cast(_prefix.size())); 1273 | _streambuf->sputn(_buffer.data(), static_cast(_buffer.size())); 1274 | } 1275 | else 1276 | { 1277 | _streambuf->sputn(_buffer.data(), static_cast(_buffer.size())); 1278 | } 1279 | 1280 | _streambuf->sputc('\n'); 1281 | } 1282 | 1283 | _size = 0; 1284 | _buffer.clear(); 1285 | _esc_seq.clear(); 1286 | 1287 | return *this; 1288 | } 1289 | 1290 | streambuf& push(std::size_t val_) 1291 | { 1292 | flush(); 1293 | 1294 | _level += val_; 1295 | 1296 | _prefix = string(_level * _indent, ' '); 1297 | 1298 | return *this; 1299 | } 1300 | 1301 | streambuf& pop(std::size_t val_) 1302 | { 1303 | flush(); 1304 | 1305 | if (_level > val_) 1306 | { 1307 | _level -= val_; 1308 | } 1309 | else 1310 | { 1311 | _level = 0; 1312 | } 1313 | 1314 | _prefix = string(_level * _indent, ' '); 1315 | 1316 | return *this; 1317 | } 1318 | 1319 | streambuf& width(std::size_t val_) 1320 | { 1321 | _width = val_; 1322 | 1323 | return *this; 1324 | } 1325 | 1326 | streambuf& escape_codes(bool val_) 1327 | { 1328 | _escape_codes = val_; 1329 | _esc_seq.clear(); 1330 | 1331 | return *this; 1332 | } 1333 | 1334 | streambuf& line_wrap(bool val_) 1335 | { 1336 | _line_wrap = val_; 1337 | 1338 | return *this; 1339 | } 1340 | 1341 | streambuf& first_wrap(bool val_) 1342 | { 1343 | _first_wrap = val_; 1344 | 1345 | return *this; 1346 | } 1347 | 1348 | streambuf& auto_wrap(bool val_) 1349 | { 1350 | _auto_wrap = val_; 1351 | _prefix.clear(); 1352 | 1353 | return *this; 1354 | } 1355 | 1356 | streambuf& word_break(bool val_) 1357 | { 1358 | _word_break = val_; 1359 | 1360 | return *this; 1361 | } 1362 | 1363 | streambuf& white_space(bool val_) 1364 | { 1365 | _white_space = val_; 1366 | 1367 | return *this; 1368 | } 1369 | 1370 | streambuf& indent(std::size_t val_) 1371 | { 1372 | _indent = val_; 1373 | _prefix = string(_level * _indent, ' '); 1374 | 1375 | return *this; 1376 | } 1377 | 1378 | streambuf& level(std::size_t val_) 1379 | { 1380 | _level = val_; 1381 | _prefix = string(_level * _indent, ' '); 1382 | 1383 | return *this; 1384 | } 1385 | 1386 | protected: 1387 | 1388 | int_type overflow(int_type ch_) 1389 | { 1390 | if (traits_type::eq_int_type(traits_type::eof(), ch_)) 1391 | { 1392 | return traits_type::not_eof(ch_); 1393 | } 1394 | 1395 | // handle auto wrap prefix 1396 | if (ch_ != ' ' && ch_ != '\t') 1397 | { 1398 | _is_prefix = false; 1399 | } 1400 | 1401 | // handle ansi escape codes 1402 | if (! _esc_seq.empty()) 1403 | { 1404 | if (_esc_seq.size() == 1) 1405 | { 1406 | if (ch_ != '[' && ch_ != '(' && ch_ != ')' && ch_ != '#') 1407 | { 1408 | _esc_seq += ch_; 1409 | if (_escape_codes) 1410 | { 1411 | _buffer += _esc_seq; 1412 | } 1413 | _esc_seq.clear(); 1414 | } 1415 | else if (ch_ == '[' || ch_ == '(' || ch_ == ')' || ch_ == '#') 1416 | { 1417 | _esc_seq += ch_; 1418 | } 1419 | else if (ch_ == std::isalpha(static_cast(ch_))) 1420 | { 1421 | _esc_seq += ch_; 1422 | if (_escape_codes) 1423 | { 1424 | _buffer += _esc_seq; 1425 | } 1426 | _esc_seq.clear(); 1427 | } 1428 | else 1429 | { 1430 | _esc_seq.clear(); 1431 | } 1432 | } 1433 | else if (_esc_seq.size() == 2 && _esc_seq.back() == '#') 1434 | { 1435 | _esc_seq += ch_; 1436 | if (_escape_codes) 1437 | { 1438 | _buffer += _esc_seq; 1439 | } 1440 | _esc_seq.clear(); 1441 | } 1442 | else if (std::isalpha(static_cast(ch_))) 1443 | { 1444 | _esc_seq += ch_; 1445 | if (_escape_codes) 1446 | { 1447 | _buffer += _esc_seq; 1448 | } 1449 | _esc_seq.clear(); 1450 | } 1451 | else 1452 | { 1453 | _esc_seq += ch_; 1454 | } 1455 | 1456 | return ch_; 1457 | } 1458 | 1459 | switch (ch_) 1460 | { 1461 | case '\t': 1462 | { 1463 | if (! _first_wrap && _level == 0) 1464 | { 1465 | // don't wrap first line when level is 0 1466 | // block left intentionally empty 1467 | } 1468 | else if (_line_wrap && (_size + _indent >= _width - _prefix.size())) 1469 | { 1470 | if (auto pos = _buffer.find_last_of(" "); 1471 | _word_break && pos != string::npos) 1472 | { 1473 | _streambuf->sputn(_prefix.data(), static_cast(_prefix.size())); 1474 | _streambuf->sputn(_buffer.data(), static_cast(pos)); 1475 | 1476 | _size = _buffer.size() - pos - 1; 1477 | _buffer = _buffer.substr(pos + 1); 1478 | } 1479 | else 1480 | { 1481 | _streambuf->sputn(_prefix.data(), static_cast(_prefix.size())); 1482 | _streambuf->sputn(_buffer.data(), static_cast(_buffer.size())); 1483 | 1484 | _size = 0; 1485 | _buffer.clear(); 1486 | } 1487 | 1488 | _streambuf->sputc('\n'); 1489 | } 1490 | 1491 | if (_auto_wrap && _is_prefix) 1492 | { 1493 | _level = 1; 1494 | _prefix += string(_indent, ' '); 1495 | } 1496 | else if (_white_space || _buffer.empty()) 1497 | { 1498 | _size += _indent; 1499 | _buffer += string(_indent, ' '); 1500 | } 1501 | else if (! _buffer.empty() && _buffer.back() != ' ') 1502 | { 1503 | _size += _indent; 1504 | _buffer += string(_indent, ' '); 1505 | } 1506 | 1507 | return ch_; 1508 | } 1509 | 1510 | case '\x1b': 1511 | { 1512 | _esc_seq.clear(); 1513 | _esc_seq += ch_; 1514 | 1515 | return ch_; 1516 | } 1517 | 1518 | case '\a': 1519 | { 1520 | _buffer += ch_; 1521 | 1522 | return ch_; 1523 | } 1524 | 1525 | case '\b': 1526 | { 1527 | --_size; 1528 | _buffer += ch_; 1529 | 1530 | return ch_; 1531 | } 1532 | 1533 | case '\n': 1534 | case '\r': 1535 | { 1536 | if (! _white_space && ! _buffer.empty() && _buffer.back() == ' ') 1537 | { 1538 | _buffer.erase(_buffer.size() - 1); 1539 | } 1540 | 1541 | _streambuf->sputn(_prefix.data(), static_cast(_prefix.size())); 1542 | _streambuf->sputn(_buffer.data(), static_cast(_buffer.size())); 1543 | _streambuf->sputc(ch_); 1544 | 1545 | _size = 0; 1546 | _buffer.clear(); 1547 | 1548 | if (_auto_wrap) 1549 | { 1550 | _level = 0; 1551 | _is_prefix = true; 1552 | _prefix.clear(); 1553 | } 1554 | 1555 | return ch_; 1556 | } 1557 | 1558 | case ' ': 1559 | { 1560 | if (! _first_wrap && _level == 0) 1561 | { 1562 | // don't wrap first line when level is 0 1563 | // block left intentionally empty 1564 | } 1565 | else if (_line_wrap && (_size + 1 + _prefix.size() >= _width)) 1566 | { 1567 | ++_size; 1568 | _buffer += " "; 1569 | 1570 | if (auto pos = _buffer.find_last_of(" "); 1571 | _word_break && pos != string::npos) 1572 | { 1573 | _streambuf->sputn(_prefix.data(), static_cast(_prefix.size())); 1574 | _streambuf->sputn(_buffer.data(), static_cast(pos)); 1575 | 1576 | _size = _buffer.size() - pos - 1; 1577 | _buffer = _buffer.substr(pos + 1); 1578 | } 1579 | else 1580 | { 1581 | _streambuf->sputn(_prefix.data(), static_cast(_prefix.size())); 1582 | _streambuf->sputn(_buffer.data(), static_cast(_buffer.size())); 1583 | 1584 | _size = 0; 1585 | _buffer.clear(); 1586 | } 1587 | 1588 | _streambuf->sputc('\n'); 1589 | 1590 | return ch_; 1591 | } 1592 | 1593 | if (_auto_wrap && _is_prefix) 1594 | { 1595 | _level = 1; 1596 | _prefix += " "; 1597 | } 1598 | else if (_white_space) 1599 | { 1600 | ++_size; 1601 | _buffer += " "; 1602 | } 1603 | else if (! _buffer.empty() && _buffer.back() != ' ') 1604 | { 1605 | ++_size; 1606 | _buffer += " "; 1607 | } 1608 | 1609 | return ch_; 1610 | } 1611 | 1612 | default: 1613 | { 1614 | if (! _first_wrap && _level == 0) 1615 | { 1616 | // don't wrap first line when level is 0 1617 | // block left intentionally empty 1618 | } 1619 | else if (_line_wrap && (_size + _prefix.size() >= _width)) 1620 | { 1621 | if (auto pos = _buffer.find_last_of(" "); 1622 | _word_break && pos != string::npos) 1623 | { 1624 | _streambuf->sputn(_prefix.data(), static_cast(_prefix.size())); 1625 | _streambuf->sputn(_buffer.data(), static_cast(pos)); 1626 | 1627 | _size = _buffer.size() - pos - 1; 1628 | _buffer = _buffer.substr(pos + 1); 1629 | } 1630 | else 1631 | { 1632 | _streambuf->sputn(_prefix.data(), static_cast(_prefix.size())); 1633 | _streambuf->sputn(_buffer.data(), static_cast(_buffer.size())); 1634 | 1635 | _size = 0; 1636 | _buffer.clear(); 1637 | } 1638 | 1639 | _streambuf->sputc('\n'); 1640 | } 1641 | 1642 | ++_size; 1643 | _buffer += ch_; 1644 | 1645 | return ch_; 1646 | } 1647 | } 1648 | } 1649 | 1650 | // output streambuf 1651 | std::streambuf* _streambuf; 1652 | 1653 | // maximum output width 1654 | std::size_t _width; 1655 | 1656 | // number of spaces to indent 1657 | std::size_t _indent {2}; 1658 | 1659 | // indentation level 1660 | std::size_t _level {0}; 1661 | 1662 | // stream should wrap around at output width 1663 | bool _line_wrap {true}; 1664 | 1665 | // stream should wrap around at output width when level=0 1666 | bool _first_wrap {true}; 1667 | 1668 | // auto calc indent when wrapping line 1669 | bool _auto_wrap {false}; 1670 | 1671 | // stream should break words on wrap 1672 | bool _word_break {true}; 1673 | 1674 | // stream should preserve whitespace 1675 | bool _white_space {true}; 1676 | 1677 | // stream should output escape codes 1678 | bool _escape_codes {true}; 1679 | 1680 | // used to calc the indent for auto wrap 1681 | bool _is_prefix {true}; 1682 | 1683 | // size of buffer minus special chars 1684 | std::size_t _size {0}; 1685 | 1686 | // prefix string prepended onto start of a new line 1687 | string _prefix {""}; 1688 | 1689 | // buffer string for temporary storage of chars 1690 | string _buffer {""}; 1691 | 1692 | // buffer string for escape sequences 1693 | string _esc_seq {""}; 1694 | }; // class streambuf 1695 | 1696 | public: 1697 | 1698 | explicit ostream(std::ostream& os_, std::size_t indent_width_ = 2, 1699 | std::size_t output_width_ = std::numeric_limits::max()) : 1700 | std::ostream {&_stream}, 1701 | _stream {os_.rdbuf(), output_width_} 1702 | { 1703 | indent(indent_width_); 1704 | } 1705 | 1706 | ostream& flush() 1707 | { 1708 | _stream.flush(); 1709 | std::ostream::flush(); 1710 | 1711 | return *this; 1712 | } 1713 | 1714 | ostream& push(std::size_t val_ = 1) 1715 | { 1716 | _stream.push(val_); 1717 | std::ostream::flush(); 1718 | 1719 | return *this; 1720 | } 1721 | 1722 | ostream& pop(std::size_t val_ = 1) 1723 | { 1724 | _stream.pop(val_); 1725 | std::ostream::flush(); 1726 | 1727 | return *this; 1728 | } 1729 | 1730 | ostream& width(std::size_t val_ = std::numeric_limits::max()) 1731 | { 1732 | _stream.width(val_); 1733 | 1734 | return *this; 1735 | } 1736 | 1737 | ostream& escape_codes(bool val_ = true) 1738 | { 1739 | _stream.escape_codes(val_); 1740 | 1741 | return *this; 1742 | } 1743 | 1744 | ostream& line_wrap(bool val_ = true) 1745 | { 1746 | _stream.line_wrap(val_); 1747 | 1748 | return *this; 1749 | } 1750 | 1751 | ostream& first_wrap(bool val_ = true) 1752 | { 1753 | _stream.first_wrap(val_); 1754 | 1755 | return *this; 1756 | } 1757 | 1758 | ostream& auto_wrap(bool val_ = true) 1759 | { 1760 | _stream.auto_wrap(val_); 1761 | 1762 | return *this; 1763 | } 1764 | 1765 | ostream& word_break(bool val_ = true) 1766 | { 1767 | _stream.word_break(val_); 1768 | 1769 | return *this; 1770 | } 1771 | 1772 | ostream& white_space(bool val_ = true) 1773 | { 1774 | _stream.white_space(val_); 1775 | 1776 | return *this; 1777 | } 1778 | 1779 | ostream& indent(std::size_t val_ = 2) 1780 | { 1781 | _stream.indent(val_); 1782 | 1783 | return *this; 1784 | } 1785 | 1786 | ostream& level(std::size_t val_ = 0) 1787 | { 1788 | _stream.level(val_); 1789 | 1790 | return *this; 1791 | } 1792 | 1793 | protected: 1794 | 1795 | streambuf _stream; 1796 | }; // class ostream 1797 | 1798 | namespace iomanip 1799 | { 1800 | 1801 | class flush 1802 | { 1803 | public: 1804 | 1805 | friend Term::ostream& operator<<(Term::ostream& os_, flush const&) 1806 | { 1807 | os_.flush(); 1808 | 1809 | return os_; 1810 | } 1811 | 1812 | friend Term::ostream& operator<<(std::ostream& os_, flush const&) 1813 | { 1814 | auto& derived = dynamic_cast(os_); 1815 | derived.flush(); 1816 | 1817 | return derived; 1818 | } 1819 | }; // class flush 1820 | 1821 | class push 1822 | { 1823 | public: 1824 | 1825 | explicit push(std::size_t val_ = 1) : 1826 | _val {val_} 1827 | { 1828 | } 1829 | 1830 | friend Term::ostream& operator<<(Term::ostream& os_, push const& obj_) 1831 | { 1832 | os_.push(obj_._val); 1833 | 1834 | return os_; 1835 | } 1836 | 1837 | friend Term::ostream& operator<<(std::ostream& os_, push const& obj_) 1838 | { 1839 | auto& derived = dynamic_cast(os_); 1840 | derived.push(obj_._val); 1841 | 1842 | return derived; 1843 | } 1844 | 1845 | private: 1846 | 1847 | std::size_t _val; 1848 | }; // class push 1849 | 1850 | class pop 1851 | { 1852 | public: 1853 | 1854 | explicit pop(std::size_t val_ = 1) : 1855 | _val {val_} 1856 | { 1857 | } 1858 | 1859 | friend Term::ostream& operator<<(Term::ostream& os_, pop const& obj_) 1860 | { 1861 | os_.pop(obj_._val); 1862 | 1863 | return os_; 1864 | } 1865 | 1866 | friend Term::ostream& operator<<(std::ostream& os_, pop const& obj_) 1867 | { 1868 | auto& derived = dynamic_cast(os_); 1869 | derived.pop(obj_._val); 1870 | 1871 | return derived; 1872 | } 1873 | 1874 | private: 1875 | 1876 | std::size_t _val; 1877 | }; // class pop 1878 | 1879 | class line_wrap 1880 | { 1881 | public: 1882 | 1883 | explicit line_wrap(bool val_ = true) : 1884 | _val {val_} 1885 | { 1886 | } 1887 | 1888 | friend Term::ostream& operator<<(Term::ostream& os_, line_wrap const& obj_) 1889 | { 1890 | os_.line_wrap(obj_._val); 1891 | 1892 | return os_; 1893 | } 1894 | 1895 | friend Term::ostream& operator<<(std::ostream& os_, line_wrap const& obj_) 1896 | { 1897 | auto& derived = dynamic_cast(os_); 1898 | derived.line_wrap(obj_._val); 1899 | 1900 | return derived; 1901 | } 1902 | 1903 | private: 1904 | 1905 | bool _val; 1906 | }; // class line_wrap 1907 | 1908 | class first_wrap 1909 | { 1910 | public: 1911 | 1912 | explicit first_wrap(bool val_ = true) : 1913 | _val {val_} 1914 | { 1915 | } 1916 | 1917 | friend Term::ostream& operator<<(Term::ostream& os_, first_wrap const& obj_) 1918 | { 1919 | os_.first_wrap(obj_._val); 1920 | 1921 | return os_; 1922 | } 1923 | 1924 | friend Term::ostream& operator<<(std::ostream& os_, first_wrap const& obj_) 1925 | { 1926 | auto& derived = dynamic_cast(os_); 1927 | derived.first_wrap(obj_._val); 1928 | 1929 | return derived; 1930 | } 1931 | 1932 | private: 1933 | 1934 | bool _val; 1935 | }; // class first_wrap 1936 | 1937 | class word_break 1938 | { 1939 | public: 1940 | 1941 | explicit word_break(bool val_ = true) : 1942 | _val {val_} 1943 | { 1944 | } 1945 | 1946 | friend Term::ostream& operator<<(Term::ostream& os_, word_break const& obj_) 1947 | { 1948 | os_.word_break(obj_._val); 1949 | 1950 | return os_; 1951 | } 1952 | 1953 | friend Term::ostream& operator<<(std::ostream& os_, word_break const& obj_) 1954 | { 1955 | auto& derived = dynamic_cast(os_); 1956 | derived.word_break(obj_._val); 1957 | 1958 | return derived; 1959 | } 1960 | 1961 | private: 1962 | 1963 | bool _val; 1964 | }; // class word_break 1965 | 1966 | class white_space 1967 | { 1968 | public: 1969 | 1970 | explicit white_space(bool val_ = true) : 1971 | _val {val_} 1972 | { 1973 | } 1974 | 1975 | friend Term::ostream& operator<<(Term::ostream& os_, white_space const& obj_) 1976 | { 1977 | os_.white_space(obj_._val); 1978 | 1979 | return os_; 1980 | } 1981 | 1982 | friend Term::ostream& operator<<(std::ostream& os_, white_space const& obj_) 1983 | { 1984 | auto& derived = dynamic_cast(os_); 1985 | derived.white_space(obj_._val); 1986 | 1987 | return derived; 1988 | } 1989 | 1990 | private: 1991 | 1992 | bool _val; 1993 | }; // class white_space 1994 | 1995 | class escape_codes 1996 | { 1997 | public: 1998 | 1999 | explicit escape_codes(bool val_ = true) : 2000 | _val {val_} 2001 | { 2002 | } 2003 | 2004 | friend Term::ostream& operator<<(Term::ostream& os_, escape_codes const& obj_) 2005 | { 2006 | os_.escape_codes(obj_._val); 2007 | 2008 | return os_; 2009 | } 2010 | 2011 | friend Term::ostream& operator<<(std::ostream& os_, escape_codes const& obj_) 2012 | { 2013 | auto& derived = dynamic_cast(os_); 2014 | derived.escape_codes(obj_._val); 2015 | 2016 | return derived; 2017 | } 2018 | 2019 | private: 2020 | 2021 | bool _val; 2022 | }; // class escape_codes 2023 | 2024 | class width 2025 | { 2026 | public: 2027 | 2028 | explicit width(bool val_ = true) : 2029 | _val {val_} 2030 | { 2031 | } 2032 | 2033 | friend Term::ostream& operator<<(Term::ostream& os_, width const& obj_) 2034 | { 2035 | os_.width(obj_._val); 2036 | 2037 | return os_; 2038 | } 2039 | 2040 | friend Term::ostream& operator<<(std::ostream& os_, width const& obj_) 2041 | { 2042 | auto& derived = dynamic_cast(os_); 2043 | derived.width(obj_._val); 2044 | 2045 | return derived; 2046 | } 2047 | 2048 | private: 2049 | 2050 | bool _val; 2051 | }; // class width 2052 | 2053 | class indent 2054 | { 2055 | public: 2056 | 2057 | explicit indent(bool val_ = true) : 2058 | _val {val_} 2059 | { 2060 | } 2061 | 2062 | friend Term::ostream& operator<<(Term::ostream& os_, indent const& obj_) 2063 | { 2064 | os_.indent(obj_._val); 2065 | 2066 | return os_; 2067 | } 2068 | 2069 | friend Term::ostream& operator<<(std::ostream& os_, indent const& obj_) 2070 | { 2071 | auto& derived = dynamic_cast(os_); 2072 | derived.indent(obj_._val); 2073 | 2074 | return derived; 2075 | } 2076 | 2077 | private: 2078 | 2079 | bool _val; 2080 | }; // class indent 2081 | 2082 | class level 2083 | { 2084 | public: 2085 | 2086 | explicit level(bool val_ = true) : 2087 | _val {val_} 2088 | { 2089 | } 2090 | 2091 | friend Term::ostream& operator<<(Term::ostream& os_, level const& obj_) 2092 | { 2093 | os_.level(obj_._val); 2094 | 2095 | return os_; 2096 | } 2097 | 2098 | friend Term::ostream& operator<<(std::ostream& os_, level const& obj_) 2099 | { 2100 | auto& derived = dynamic_cast(os_); 2101 | derived.level(obj_._val); 2102 | 2103 | return derived; 2104 | } 2105 | 2106 | private: 2107 | 2108 | bool _val; 2109 | }; // class level 2110 | 2111 | } // namespace iomanip 2112 | 2113 | } // namespace OB::Term 2114 | 2115 | #endif // OB_TERM_HH 2116 | -------------------------------------------------------------------------------- /src/ob/text.hh: -------------------------------------------------------------------------------- 1 | #ifndef OB_TEXT_HH 2 | #define OB_TEXT_HH 3 | 4 | #define U_CHARSET_IS_UTF8 1 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | namespace OB::Text 30 | { 31 | 32 | class View 33 | { 34 | public: 35 | 36 | using size_type = std::size_t; 37 | using char_type = char; 38 | using string = std::basic_string; 39 | using string_view = std::basic_string_view; 40 | using brk_iter = icu::BreakIterator; 41 | using locale = icu::Locale; 42 | 43 | struct Ctx 44 | { 45 | Ctx(size_type bytes_, size_type tcols_, size_type cols_, string_view str_) : 46 | bytes {bytes_}, 47 | tcols {tcols_}, 48 | cols {cols_}, 49 | str {str_} 50 | { 51 | } 52 | 53 | friend std::ostream& operator<<(std::ostream& os, Ctx const& obj) 54 | { 55 | os << obj.str; 56 | 57 | return os; 58 | } 59 | 60 | operator string() 61 | { 62 | return string(str); 63 | } 64 | 65 | operator string_view() 66 | { 67 | return str; 68 | } 69 | 70 | size_type bytes {0}; 71 | size_type tcols {0}; 72 | size_type cols {0}; 73 | string_view str {}; 74 | }; // struct Ctx 75 | 76 | using value_type = std::vector; 77 | using iterator = typename value_type::iterator; 78 | using const_iterator = typename value_type::const_iterator; 79 | using reverse_iterator = typename value_type::reverse_iterator; 80 | using const_reverse_iterator = typename value_type::const_reverse_iterator; 81 | 82 | static auto constexpr iter_end {icu::BreakIterator::DONE}; 83 | static size_type constexpr npos {std::numeric_limits::max()}; 84 | 85 | View() = default; 86 | View(View&&) = default; 87 | View(View const&) = default; 88 | 89 | View(string_view str) 90 | { 91 | this->str(str); 92 | } 93 | 94 | ~View() = default; 95 | 96 | View& operator=(View&&) = default; 97 | View& operator=(View const&) = default; 98 | 99 | View& operator=(string_view str) 100 | { 101 | this->str(str); 102 | 103 | return *this; 104 | } 105 | 106 | friend std::ostream& operator<<(std::ostream& os, View const& obj) 107 | { 108 | os << obj.str(); 109 | 110 | return os; 111 | } 112 | 113 | operator string() 114 | { 115 | return string(str()); 116 | } 117 | 118 | View& str(string_view str) 119 | { 120 | _cols = 0; 121 | _bytes = 0; 122 | 123 | _view.clear(); 124 | // _view.shrink_to_fit(); 125 | 126 | if (str.empty()) 127 | { 128 | return *this; 129 | } 130 | 131 | UErrorCode ec = U_ZERO_ERROR; 132 | 133 | std::unique_ptr text ( 134 | utext_openUTF8(nullptr, str.data(), static_cast(str.size()), &ec), 135 | utext_close); 136 | 137 | if (U_FAILURE(ec)) 138 | { 139 | throw std::runtime_error("failed to create utext"); 140 | } 141 | 142 | std::unique_ptr iter {brk_iter::createCharacterInstance( 143 | locale::getDefault(), ec)}; 144 | 145 | if (U_FAILURE(ec)) 146 | { 147 | throw std::runtime_error("failed to create break iterator"); 148 | } 149 | 150 | iter->setText(text.get(), ec); 151 | 152 | if (U_FAILURE(ec)) 153 | { 154 | throw std::runtime_error("failed to set break iterator text"); 155 | } 156 | 157 | // get size of iterator 158 | size_type size {0}; 159 | while (iter->next() != iter_end) 160 | { 161 | ++size; 162 | } 163 | 164 | // reserve array size 165 | _view.reserve(size); 166 | 167 | size = 0; 168 | UChar32 uch; 169 | int width {0}; 170 | size_type cols {0}; 171 | auto begin = iter->first(); 172 | auto end = iter->next(); 173 | 174 | while (end != iter_end) 175 | { 176 | // get column width 177 | uch = utext_char32At(text.get(), begin); 178 | width = u_getIntPropertyValue(uch, UCHAR_EAST_ASIAN_WIDTH); 179 | if (width == U_EA_FULLWIDTH || width == U_EA_WIDE) 180 | { 181 | // full width 182 | cols = 2; 183 | } 184 | else 185 | { 186 | // half width 187 | cols = 1; 188 | } 189 | 190 | // get string size 191 | size = static_cast(end - begin); 192 | 193 | // add character context to array 194 | _view.emplace_back(_bytes, _cols, cols, string_view(str.data() + (_bytes * sizeof(char_type)), size)); 195 | 196 | // increase total column count 197 | _cols += cols; 198 | 199 | // increase total byte count 200 | _bytes += size; 201 | 202 | // increase iterators 203 | begin = end; 204 | end = iter->next(); 205 | } 206 | 207 | return *this; 208 | } 209 | 210 | string_view str() const 211 | { 212 | if (_view.empty()) 213 | { 214 | return {}; 215 | } 216 | 217 | return string_view(_view.at(0).str.data(), _bytes); 218 | } 219 | 220 | value_type const& view() const 221 | { 222 | return _view; 223 | } 224 | 225 | size_type byte_to_char(size_type pos) const 226 | { 227 | if (pos >= _bytes) 228 | { 229 | return npos; 230 | } 231 | 232 | auto const it = std::lower_bound(_view.crbegin(), _view.crend(), pos, 233 | [](auto const& lhs, auto const& rhs) { 234 | return lhs.bytes > rhs; 235 | }); 236 | 237 | if (it != _view.crend()) 238 | { 239 | return static_cast(std::distance(_view.cbegin(), it.base()) - 1); 240 | } 241 | 242 | return npos; 243 | } 244 | 245 | size_type char_to_byte(size_type pos) const 246 | { 247 | if (pos >= _view.size()) 248 | { 249 | return npos; 250 | } 251 | 252 | auto const& ctx = _view.at(pos); 253 | 254 | return ctx.bytes; 255 | } 256 | 257 | Ctx& operator[](size_type pos) 258 | { 259 | return _view[pos]; 260 | } 261 | 262 | Ctx const& operator[](size_type pos) const 263 | { 264 | return _view[pos]; 265 | } 266 | 267 | Ctx& at(size_type pos) 268 | { 269 | return _view.at(pos); 270 | } 271 | 272 | Ctx const& at(size_type pos) const 273 | { 274 | return _view.at(pos); 275 | } 276 | 277 | Ctx& front() 278 | { 279 | return _view.front(); 280 | } 281 | 282 | Ctx const& front() const 283 | { 284 | return _view.front(); 285 | } 286 | 287 | Ctx& back() 288 | { 289 | return _view.back(); 290 | } 291 | 292 | Ctx const& back() const 293 | { 294 | return _view.back(); 295 | } 296 | 297 | string_view substr(size_type pos, size_type size = npos) const 298 | { 299 | if (pos >= _view.size()) 300 | { 301 | return {}; 302 | } 303 | 304 | if (size == npos) 305 | { 306 | size = _view.size(); 307 | } 308 | else 309 | { 310 | size += pos; 311 | } 312 | 313 | size_type count {0}; 314 | 315 | for (size_type i = pos; i < size && i < _view.size(); ++i) 316 | { 317 | count += _view.at(i).str.size(); 318 | } 319 | 320 | return string_view(_view.at(pos).str.data(), count); 321 | } 322 | 323 | size_type find(string const& str, size_type pos = npos) const 324 | { 325 | if (pos == npos) 326 | { 327 | pos = 0; 328 | } 329 | 330 | if (pos >= _view.size()) 331 | { 332 | return npos; 333 | } 334 | 335 | 336 | for (size_type i = pos; i < _view.size(); ++i) 337 | { 338 | if (str == _view.at(i).str) 339 | { 340 | return i; 341 | } 342 | } 343 | 344 | return npos; 345 | } 346 | 347 | size_type rfind(string const& str, size_type pos = npos) const 348 | { 349 | if (pos == npos) 350 | { 351 | pos = _view.size() - 1; 352 | } 353 | 354 | if (pos == 0 || pos >= _view.size()) 355 | { 356 | return npos; 357 | } 358 | 359 | for (size_type i = pos; i != npos; --i) 360 | { 361 | if (str == _view.at(i).str) 362 | { 363 | return i; 364 | } 365 | } 366 | 367 | return npos; 368 | } 369 | 370 | size_type find_first_of(View const& str, size_type pos = npos) const 371 | { 372 | if (pos == npos) 373 | { 374 | pos = 0; 375 | } 376 | 377 | if (pos >= _view.size()) 378 | { 379 | return npos; 380 | } 381 | 382 | for (size_type i = pos; i < _view.size(); ++i) 383 | { 384 | auto const& lhs = _view.at(i).str; 385 | 386 | for (size_type j = 0; j < str.size(); ++j) 387 | { 388 | if (lhs == str.at(j).str) 389 | { 390 | return i; 391 | } 392 | } 393 | } 394 | 395 | return npos; 396 | } 397 | 398 | size_type rfind_first_of(View const& str, size_type pos = npos) const 399 | { 400 | if (pos == npos) 401 | { 402 | pos = _view.size() - 1; 403 | } 404 | 405 | if (pos == 0 || pos >= _view.size()) 406 | { 407 | return npos; 408 | } 409 | 410 | for (size_type i = pos; i != npos; --i) 411 | { 412 | auto const& lhs = _view.at(i).str; 413 | 414 | for (size_type j = 0; j < str.size(); ++j) 415 | { 416 | if (lhs == str.at(j).str) 417 | { 418 | return i; 419 | } 420 | } 421 | } 422 | 423 | return npos; 424 | } 425 | 426 | bool empty() const 427 | { 428 | return _view.empty(); 429 | } 430 | 431 | View& clear() 432 | { 433 | _view.clear(); 434 | _bytes = 0; 435 | _cols = 0; 436 | 437 | return *this; 438 | } 439 | 440 | View& shrink_to_fit() 441 | { 442 | _view.shrink_to_fit(); 443 | 444 | return *this; 445 | } 446 | 447 | size_type size() const 448 | { 449 | return _view.size(); 450 | } 451 | 452 | size_type length() const 453 | { 454 | return _view.size(); 455 | } 456 | 457 | size_type bytes() const 458 | { 459 | return _bytes; 460 | } 461 | 462 | size_type bytes(size_type pos, size_type size = npos) const 463 | { 464 | if (pos >= _view.size()) 465 | { 466 | return npos; 467 | } 468 | 469 | if (size == npos) 470 | { 471 | size = _view.size(); 472 | } 473 | else 474 | { 475 | size += pos; 476 | } 477 | 478 | size_type count {0}; 479 | 480 | for (size_type i = pos; i < size && i < _view.size(); ++i) 481 | { 482 | count += _view.at(i).str.size(); 483 | } 484 | 485 | return count; 486 | } 487 | 488 | size_type cols() const 489 | { 490 | return _cols; 491 | } 492 | 493 | size_type cols(size_type pos, size_type size = npos) const 494 | { 495 | if (pos >= _view.size()) 496 | { 497 | return npos; 498 | } 499 | 500 | if (size == npos) 501 | { 502 | size = _view.size(); 503 | } 504 | else 505 | { 506 | size += pos; 507 | } 508 | 509 | size_type count {0}; 510 | 511 | for (size_type i = pos; i < size && i < _view.size(); ++i) 512 | { 513 | count += _view.at(i).cols; 514 | } 515 | 516 | return count; 517 | } 518 | 519 | string_view colstr(size_type pos, size_type size = npos) const 520 | { 521 | if (pos >= _view.size()) 522 | { 523 | return {}; 524 | } 525 | 526 | if (size == npos) 527 | { 528 | size = _view.size(); 529 | } 530 | else 531 | { 532 | size += pos; 533 | } 534 | 535 | size_type count {0}; 536 | 537 | for (size_type i = pos, cols = 0; i < _view.size(); ++i) 538 | { 539 | auto const& ctx = _view.at(i); 540 | 541 | if (cols + ctx.cols > size) 542 | { 543 | break; 544 | } 545 | 546 | cols += ctx.cols; 547 | count += ctx.str.size(); 548 | } 549 | 550 | return string_view(_view.at(pos).str.data(), count); 551 | } 552 | 553 | string_view rcolstr(size_type pos, size_type size = npos) const 554 | { 555 | if (empty()) 556 | { 557 | return {}; 558 | } 559 | 560 | if (pos >= _view.size()) 561 | { 562 | pos = _view.size() - 1; 563 | } 564 | 565 | if (size == npos) 566 | { 567 | size = _view.size() - pos; 568 | } 569 | 570 | size_type count {0}; 571 | 572 | for (size_type cols = 0; pos != npos; --pos) 573 | { 574 | auto const& ctx = _view.at(pos); 575 | 576 | if (cols + ctx.cols > size) 577 | { 578 | ++pos; 579 | 580 | break; 581 | } 582 | 583 | cols += ctx.cols; 584 | count += ctx.str.size(); 585 | } 586 | 587 | if (pos == npos) 588 | { 589 | ++pos; 590 | } 591 | 592 | return string_view(_view.at(pos).str.data(), count); 593 | } 594 | 595 | iterator begin() 596 | { 597 | return _view.begin(); 598 | } 599 | 600 | const_iterator cbegin() const 601 | { 602 | return _view.cbegin(); 603 | } 604 | 605 | reverse_iterator rbegin() 606 | { 607 | return _view.rbegin(); 608 | } 609 | 610 | const_reverse_iterator crbegin() const 611 | { 612 | return _view.crbegin(); 613 | } 614 | 615 | iterator end() 616 | { 617 | return _view.end(); 618 | } 619 | 620 | const_iterator cend() const 621 | { 622 | return _view.cend(); 623 | } 624 | 625 | reverse_iterator rend() 626 | { 627 | return _view.rend(); 628 | } 629 | 630 | const_reverse_iterator crend() const 631 | { 632 | return _view.crend(); 633 | } 634 | 635 | private: 636 | 637 | // array of contexts mapping the string 638 | value_type _view; 639 | 640 | // number of columns needed to display the string 641 | size_type _cols {0}; 642 | 643 | // number of bytes in the string 644 | size_type _bytes {0}; 645 | }; // class View 646 | 647 | class String 648 | { 649 | public: 650 | 651 | using size_type = std::size_t; 652 | using char_type = char; 653 | using string = std::basic_string; 654 | using string_view = std::basic_string_view; 655 | using Ctx = View::Ctx; 656 | 657 | using iterator = View::iterator; 658 | using const_iterator = View::const_iterator; 659 | using reverse_iterator = View::reverse_iterator; 660 | using const_reverse_iterator = View::const_reverse_iterator; 661 | 662 | static size_type constexpr npos {std::numeric_limits::max()}; 663 | 664 | String(string const& str = {}): 665 | _str {str}, 666 | _view {_str} 667 | { 668 | } 669 | 670 | String(String&& obj) 671 | { 672 | _str = std::move(obj._str); 673 | sync(); 674 | } 675 | 676 | String(String const& obj) 677 | { 678 | _str = obj._str; 679 | sync(); 680 | } 681 | 682 | ~String() = default; 683 | 684 | String& operator=(String&& obj) 685 | { 686 | _str = std::move(obj._str); 687 | sync(); 688 | 689 | return *this; 690 | } 691 | 692 | String& operator=(String const& obj) 693 | { 694 | _str = obj._str; 695 | sync(); 696 | 697 | return *this; 698 | } 699 | 700 | String& operator=(string_view str) 701 | { 702 | _str = string(str); 703 | sync(); 704 | 705 | return *this; 706 | } 707 | 708 | String& operator=(string const& str) 709 | { 710 | _str = str; 711 | sync(); 712 | 713 | return *this; 714 | } 715 | 716 | template 717 | String& operator<<(T const& obj) 718 | { 719 | std::ostringstream os; 720 | os << obj; 721 | append(os.str()); 722 | 723 | return *this; 724 | } 725 | 726 | friend std::ostream& operator<<(std::ostream& os, String const& obj) 727 | { 728 | os << obj._str; 729 | 730 | return os; 731 | } 732 | 733 | friend std::istream& operator>>(std::istream& is, String& obj) 734 | { 735 | if (is >> obj._str) 736 | { 737 | obj.sync(); 738 | } 739 | else 740 | { 741 | is.setstate(std::ios::failbit); 742 | } 743 | 744 | return is; 745 | } 746 | 747 | operator string() 748 | { 749 | return _str; 750 | } 751 | 752 | operator string_view() 753 | { 754 | return string_view(_str.data(), _str.size()); 755 | } 756 | 757 | operator View() 758 | { 759 | return _view; 760 | } 761 | 762 | string& str() 763 | { 764 | return _str; 765 | } 766 | 767 | string const& str() const 768 | { 769 | return _str; 770 | } 771 | 772 | String& str(string_view str) 773 | { 774 | _str = str; 775 | sync(); 776 | 777 | return *this; 778 | } 779 | 780 | View& view() 781 | { 782 | return _view; 783 | } 784 | 785 | View const& view() const 786 | { 787 | return _view; 788 | } 789 | 790 | String& sync() 791 | { 792 | _view.str(_str); 793 | 794 | return *this; 795 | } 796 | 797 | size_type byte_to_char(size_type pos) const 798 | { 799 | return _view.byte_to_char(pos); 800 | } 801 | 802 | size_type char_to_byte(size_type pos) const 803 | { 804 | return _view.char_to_byte(pos); 805 | } 806 | 807 | String& append(string const& val) 808 | { 809 | _str.append(val); 810 | sync(); 811 | 812 | return *this; 813 | } 814 | 815 | String& insert(size_type pos, string const& val) 816 | { 817 | pos = _view.char_to_byte(pos); 818 | if (pos == npos) 819 | { 820 | pos = _str.size(); 821 | } 822 | 823 | _str.insert(pos, val); 824 | sync(); 825 | 826 | return *this; 827 | } 828 | 829 | String& erase(size_type pos, size_type size) 830 | { 831 | auto const get_pos = ([this, &pos]() { 832 | auto const bpos = _view.char_to_byte(pos); 833 | if (pos == npos) 834 | { 835 | return _str.size(); 836 | } 837 | return bpos; 838 | })(); 839 | 840 | _str.erase(get_pos, _view.substr(pos, size).size()); 841 | sync(); 842 | 843 | return *this; 844 | } 845 | 846 | String& replace(size_type pos, size_type size, string const& val) 847 | { 848 | erase(pos, size); 849 | insert(pos, val); 850 | 851 | return *this; 852 | } 853 | 854 | char const* data() const 855 | { 856 | return _str.data(); 857 | } 858 | 859 | char* data() 860 | { 861 | return _str.data(); 862 | } 863 | 864 | char const* c_str() const 865 | { 866 | return _str.data(); 867 | } 868 | 869 | String& reserve(size_type size) 870 | { 871 | _str.reserve(size); 872 | 873 | return *this; 874 | } 875 | 876 | size_type capacity() const 877 | { 878 | return _str.capacity(); 879 | } 880 | 881 | size_type max_size() const 882 | { 883 | return _str.max_size(); 884 | } 885 | 886 | Ctx& operator[](size_type pos) 887 | { 888 | return _view[pos]; 889 | } 890 | 891 | Ctx const& operator[](size_type pos) const 892 | { 893 | return _view[pos]; 894 | } 895 | 896 | Ctx& at(size_type pos) 897 | { 898 | return _view.at(pos); 899 | } 900 | 901 | Ctx const& at(size_type pos) const 902 | { 903 | return _view.at(pos); 904 | } 905 | 906 | Ctx& front() 907 | { 908 | return _view.front(); 909 | } 910 | 911 | Ctx const& front() const 912 | { 913 | return _view.front(); 914 | } 915 | 916 | Ctx& back() 917 | { 918 | return _view.back(); 919 | } 920 | 921 | Ctx const& back() const 922 | { 923 | return _view.back(); 924 | } 925 | 926 | string_view substr(size_type pos, size_type size = npos) const 927 | { 928 | return _view.substr(pos, size); 929 | } 930 | 931 | size_type find(string const& str, size_type pos = npos) const 932 | { 933 | return _view.find(str, pos); 934 | } 935 | 936 | size_type rfind(string const& str, size_type pos = npos) const 937 | { 938 | return _view.rfind(str, pos); 939 | } 940 | 941 | size_type find_first_of(View const& str, size_type pos = npos) const 942 | { 943 | return _view.find_first_of(str, pos); 944 | } 945 | 946 | size_type rfind_first_of(View const& str, size_type pos = npos) const 947 | { 948 | return _view.rfind_first_of(str, pos); 949 | } 950 | 951 | size_type empty() const 952 | { 953 | return _str.empty(); 954 | } 955 | 956 | String& clear() 957 | { 958 | _view.clear(); 959 | _str.clear(); 960 | 961 | return *this; 962 | } 963 | 964 | String& shrink_to_fit() 965 | { 966 | _view.shrink_to_fit(); 967 | _str.shrink_to_fit(); 968 | 969 | return *this; 970 | } 971 | 972 | size_type size() const 973 | { 974 | return _view.size(); 975 | } 976 | 977 | size_type length() const 978 | { 979 | return _view.length(); 980 | } 981 | 982 | size_type bytes() const 983 | { 984 | return _view.bytes(); 985 | } 986 | 987 | size_type bytes(size_type pos, size_type size = npos) const 988 | { 989 | return _view.bytes(pos, size); 990 | } 991 | 992 | size_type cols() const 993 | { 994 | return _view.cols(); 995 | } 996 | 997 | size_type cols(size_type pos, size_type size = npos) const 998 | { 999 | return _view.cols(pos, size); 1000 | } 1001 | 1002 | string_view colstr(size_type pos, size_type size = npos) const 1003 | { 1004 | return _view.colstr(pos, size); 1005 | } 1006 | 1007 | string_view rcolstr(size_type pos, size_type size = npos) const 1008 | { 1009 | return _view.rcolstr(pos, size); 1010 | } 1011 | 1012 | iterator begin() 1013 | { 1014 | return _view.begin(); 1015 | } 1016 | 1017 | const_iterator cbegin() const 1018 | { 1019 | return _view.cbegin(); 1020 | } 1021 | 1022 | reverse_iterator rbegin() 1023 | { 1024 | return _view.rbegin(); 1025 | } 1026 | 1027 | const_reverse_iterator crbegin() const 1028 | { 1029 | return _view.crbegin(); 1030 | } 1031 | 1032 | iterator end() 1033 | { 1034 | return _view.end(); 1035 | } 1036 | 1037 | const_iterator cend() const 1038 | { 1039 | return _view.cend(); 1040 | } 1041 | 1042 | reverse_iterator rend() 1043 | { 1044 | return _view.rend(); 1045 | } 1046 | 1047 | const_reverse_iterator crend() const 1048 | { 1049 | return _view.crend(); 1050 | } 1051 | 1052 | private: 1053 | 1054 | string _str; 1055 | View _view; 1056 | }; // class String 1057 | 1058 | class Char32 1059 | { 1060 | public: 1061 | 1062 | Char32() = default; 1063 | 1064 | Char32(char32_t val_, std::string const& str_) : 1065 | val {val_}, 1066 | str {str_} 1067 | { 1068 | } 1069 | 1070 | friend std::ostream& operator<<(std::ostream& os, Char32 const& obj) 1071 | { 1072 | os << obj.str; 1073 | 1074 | return os; 1075 | } 1076 | 1077 | Char32& clear() 1078 | { 1079 | val = 0; 1080 | str.clear(); 1081 | 1082 | return *this; 1083 | } 1084 | 1085 | char32_t val {0}; 1086 | std::string str; 1087 | }; // class Char32 1088 | 1089 | class Regex 1090 | { 1091 | public: 1092 | 1093 | using size_type = std::size_t; 1094 | using char_type = char; 1095 | using string = std::basic_string; 1096 | using string_view = std::basic_string_view; 1097 | using regex = icu::RegexMatcher; 1098 | 1099 | struct Match 1100 | { 1101 | friend std::ostream& operator<<(std::ostream& os, Match const& obj) 1102 | { 1103 | os << obj.str; 1104 | 1105 | return os; 1106 | } 1107 | 1108 | size_type pos {0}; 1109 | size_type size {0}; 1110 | string_view str; 1111 | std::vector group; 1112 | }; // struct Match 1113 | 1114 | using value_type = std::vector; 1115 | using iterator = typename value_type::iterator; 1116 | using const_iterator = typename value_type::const_iterator; 1117 | using reverse_iterator = typename value_type::reverse_iterator; 1118 | using const_reverse_iterator = typename value_type::const_reverse_iterator; 1119 | 1120 | Regex() = default; 1121 | Regex(Regex&&) = default; 1122 | Regex(Regex const&) = default; 1123 | 1124 | Regex(string_view rx, string_view str) 1125 | { 1126 | match(rx, str); 1127 | } 1128 | 1129 | ~Regex() = default; 1130 | 1131 | Regex& operator=(Regex&&) = default; 1132 | Regex& operator=(Regex const&) = default; 1133 | 1134 | Regex& match(string_view rx, string_view str) 1135 | { 1136 | _str.clear(); 1137 | _str.shrink_to_fit(); 1138 | 1139 | if (str.empty()) 1140 | { 1141 | return *this; 1142 | } 1143 | 1144 | UErrorCode ec = U_ZERO_ERROR; 1145 | 1146 | std::unique_ptr urx ( 1147 | utext_openUTF8(nullptr, rx.data(), static_cast(rx.size()), &ec), 1148 | utext_close); 1149 | 1150 | if (U_FAILURE(ec)) 1151 | { 1152 | throw std::runtime_error("failed to create utext"); 1153 | } 1154 | 1155 | std::unique_ptr ustr ( 1156 | utext_openUTF8(nullptr, str.data(), static_cast(str.size()), &ec), 1157 | utext_close); 1158 | 1159 | if (U_FAILURE(ec)) 1160 | { 1161 | throw std::runtime_error("failed to create utext"); 1162 | } 1163 | 1164 | std::unique_ptr iter {new regex(urx.get(), UREGEX_CASE_INSENSITIVE, ec)}; 1165 | 1166 | if (! U_SUCCESS(ec)) 1167 | { 1168 | throw std::runtime_error("failed to create regex matcher"); 1169 | } 1170 | 1171 | iter->reset(ustr.get()); 1172 | 1173 | size_type size {0}; 1174 | std::int32_t count {0}; 1175 | std::int32_t begin {0}; 1176 | std::int32_t end {0}; 1177 | 1178 | while (iter->find()) 1179 | { 1180 | count = static_cast(iter->groupCount()); 1181 | 1182 | begin = iter->start(ec); 1183 | 1184 | if (U_FAILURE(ec)) 1185 | { 1186 | throw std::runtime_error("failed to get regex matcher start"); 1187 | } 1188 | 1189 | end = iter->end(ec); 1190 | 1191 | if (U_FAILURE(ec)) 1192 | { 1193 | throw std::runtime_error("failed to get regex matcher end"); 1194 | } 1195 | 1196 | size = static_cast(end - begin); 1197 | 1198 | Match match; 1199 | match.pos = static_cast(begin); 1200 | match.size = static_cast(count); 1201 | match.str = string_view(str.data() + begin, size); 1202 | 1203 | for (std::int32_t i = 1; i <= count; ++i) 1204 | { 1205 | begin = iter->start(i, ec); 1206 | 1207 | if (U_FAILURE(ec)) 1208 | { 1209 | throw std::runtime_error("failed to get regex matcher group start"); 1210 | } 1211 | 1212 | end = iter->end(i, ec); 1213 | 1214 | if (U_FAILURE(ec)) 1215 | { 1216 | throw std::runtime_error("failed to get regex matcher group end"); 1217 | } 1218 | 1219 | size = static_cast(end - begin); 1220 | 1221 | match.group.emplace_back(string_view(str.data() + begin, size)); 1222 | } 1223 | 1224 | _str.emplace_back(match); 1225 | } 1226 | 1227 | return *this; 1228 | } 1229 | 1230 | value_type const& get() const 1231 | { 1232 | return _str; 1233 | } 1234 | 1235 | Match& at(size_type pos) 1236 | { 1237 | return _str.at(pos); 1238 | } 1239 | 1240 | Match const& at(size_type pos) const 1241 | { 1242 | return _str.at(pos); 1243 | } 1244 | 1245 | bool empty() const 1246 | { 1247 | return _str.empty(); 1248 | } 1249 | 1250 | Regex& clear() 1251 | { 1252 | _str.clear(); 1253 | 1254 | return *this; 1255 | } 1256 | 1257 | Regex& shrink_to_fit() 1258 | { 1259 | _str.shrink_to_fit(); 1260 | 1261 | return *this; 1262 | } 1263 | 1264 | size_type size() const 1265 | { 1266 | return _str.size(); 1267 | } 1268 | 1269 | size_type length() const 1270 | { 1271 | return _str.size(); 1272 | } 1273 | 1274 | iterator begin() 1275 | { 1276 | return _str.begin(); 1277 | } 1278 | 1279 | const_iterator cbegin() const 1280 | { 1281 | return _str.cbegin(); 1282 | } 1283 | 1284 | reverse_iterator rbegin() 1285 | { 1286 | return _str.rbegin(); 1287 | } 1288 | 1289 | const_reverse_iterator crbegin() const 1290 | { 1291 | return _str.crbegin(); 1292 | } 1293 | 1294 | iterator end() 1295 | { 1296 | return _str.end(); 1297 | } 1298 | 1299 | const_iterator cend() const 1300 | { 1301 | return _str.cend(); 1302 | } 1303 | 1304 | reverse_iterator rend() 1305 | { 1306 | return _str.rend(); 1307 | } 1308 | 1309 | const_reverse_iterator crend() const 1310 | { 1311 | return _str.crend(); 1312 | } 1313 | 1314 | private: 1315 | 1316 | value_type _str; 1317 | }; // class Regex 1318 | 1319 | inline std::string lowercase(std::string_view const str) 1320 | { 1321 | icu::UnicodeString ustr {icu::UnicodeString::fromUTF8(icu::StringPiece(str.data(), str.size()))}; 1322 | std::string res; 1323 | ustr.toLower().toUTF8String(res); 1324 | 1325 | return res; 1326 | } 1327 | 1328 | inline std::string uppercase(std::string_view const str) 1329 | { 1330 | icu::UnicodeString ustr {icu::UnicodeString::fromUTF8(icu::StringPiece(str.data(), str.size()))}; 1331 | std::string res; 1332 | ustr.toUpper().toUTF8String(res); 1333 | 1334 | return res; 1335 | } 1336 | 1337 | inline std::string foldcase(std::string_view const str) 1338 | { 1339 | icu::UnicodeString ustr {icu::UnicodeString::fromUTF8(icu::StringPiece(str.data(), str.size()))}; 1340 | std::string res; 1341 | ustr.foldCase().toUTF8String(res); 1342 | 1343 | return res; 1344 | } 1345 | 1346 | inline std::string trim(std::string_view const str) 1347 | { 1348 | icu::UnicodeString ustr {icu::UnicodeString::fromUTF8(icu::StringPiece(str.data(), str.size()))}; 1349 | std::string res; 1350 | ustr.trim().toUTF8String(res); 1351 | 1352 | return res; 1353 | } 1354 | 1355 | inline std::int32_t compare(std::string_view const lhs, std::string_view const rhs) 1356 | { 1357 | UErrorCode ec = U_ZERO_ERROR; 1358 | 1359 | std::unique_ptr coll {icu::Collator::createInstance(ec)}; 1360 | 1361 | if (U_FAILURE(ec)) 1362 | { 1363 | throw std::runtime_error("failed to create collator"); 1364 | } 1365 | 1366 | std::int32_t res {coll->compareUTF8( 1367 | icu::StringPiece(lhs.data(), lhs.size()), 1368 | icu::StringPiece(rhs.data(), rhs.size()), 1369 | ec)}; 1370 | 1371 | if (U_FAILURE(ec)) 1372 | { 1373 | throw std::runtime_error("failed to collate text"); 1374 | } 1375 | 1376 | return res; 1377 | } 1378 | 1379 | inline std::string normalize(std::string_view const str) 1380 | { 1381 | UErrorCode ec = U_ZERO_ERROR; 1382 | 1383 | auto const norm = icu::Normalizer2::getNFKCInstance(ec); 1384 | 1385 | if (U_FAILURE(ec)) 1386 | { 1387 | throw std::runtime_error("failed to create normalizer"); 1388 | } 1389 | 1390 | std::string res; 1391 | icu::StringByteSink bytesink (&res, str.size()); 1392 | 1393 | norm->normalizeUTF8( 1394 | 0, 1395 | icu::StringPiece(str.data(), str.size()), 1396 | bytesink, 1397 | NULL, 1398 | ec); 1399 | 1400 | if (U_FAILURE(ec)) 1401 | { 1402 | throw std::runtime_error("failed to normalize text"); 1403 | } 1404 | 1405 | return res; 1406 | } 1407 | 1408 | inline std::string normalize_foldcase(std::string_view const str) 1409 | { 1410 | UErrorCode ec = U_ZERO_ERROR; 1411 | 1412 | auto const norm = icu::Normalizer2::getNFKCCasefoldInstance(ec); 1413 | 1414 | if (U_FAILURE(ec)) 1415 | { 1416 | throw std::runtime_error("failed to create normalizer"); 1417 | } 1418 | 1419 | std::string res; 1420 | icu::StringByteSink bytesink (&res, str.size()); 1421 | 1422 | norm->normalizeUTF8( 1423 | 0, 1424 | icu::StringPiece(str.data(), str.size()), 1425 | bytesink, 1426 | NULL, 1427 | ec); 1428 | 1429 | if (U_FAILURE(ec)) 1430 | { 1431 | throw std::runtime_error("failed to normalize text"); 1432 | } 1433 | 1434 | return res; 1435 | } 1436 | 1437 | inline std::int32_t to_int32(std::string_view const str) 1438 | { 1439 | if (str.empty()) 1440 | { 1441 | return 0; 1442 | } 1443 | 1444 | if ((str.at(0) & 0x80) == 0) 1445 | { 1446 | return static_cast(str.at(0)); 1447 | } 1448 | else if ((str.at(0) & 0xE0) == 0xC0 && str.size() == 2) 1449 | { 1450 | return (static_cast(str[0] & 0x1F) << 6) | 1451 | static_cast(str[1] & 0x3F); 1452 | } 1453 | else if ((str.at(0) & 0xF0) == 0xE0 && str.size() == 3) 1454 | { 1455 | return (static_cast(str[0] & 0x0F) << 12) | 1456 | (static_cast(str[1] & 0x3F) << 6) | 1457 | static_cast(str[2] & 0x3F); 1458 | } 1459 | else if ((str.at(0) & 0xF8) == 0xF0 && str.size() == 4) 1460 | { 1461 | return (static_cast(str[0] & 0x07) << 18) | 1462 | (static_cast(str[1] & 0x3F) << 12) | 1463 | (static_cast(str[2] & 0x3F) << 6) | 1464 | static_cast(str[3] & 0x3F); 1465 | } 1466 | 1467 | return 0; 1468 | } 1469 | 1470 | inline bool is_quote(std::int32_t const ch) 1471 | { 1472 | switch(ch) 1473 | { 1474 | case U'"': case U'\'': case U'«': case U'»': case U'‘': case U'’': 1475 | case U'‚': case U'‛': case U'“': case U'”': case U'„': case U'‟': 1476 | case U'‹': case U'›': case U'❛': case U'❜': case U'❝': case U'❞': 1477 | case U'❟': case U'❮': case U'❯': case U'⹂': case U'「': case U'」': 1478 | case U'『': case U'』': case U'〝': case U'〞': case U'〟': case U'"': 1479 | return true; 1480 | 1481 | default: 1482 | return false; 1483 | } 1484 | } 1485 | 1486 | inline bool is_upper(std::int32_t const ch) 1487 | { 1488 | return u_isupper(ch); 1489 | } 1490 | 1491 | inline bool is_lower(std::int32_t const ch) 1492 | { 1493 | return u_islower(ch); 1494 | } 1495 | 1496 | inline bool is_punct(std::int32_t const ch) 1497 | { 1498 | return u_ispunct(ch); 1499 | } 1500 | 1501 | inline bool is_digit(std::int32_t const ch) 1502 | { 1503 | return u_isdigit(ch); 1504 | } 1505 | 1506 | inline bool is_alpha(std::int32_t const ch) 1507 | { 1508 | return u_isalpha(ch); 1509 | } 1510 | 1511 | inline bool is_alnum(std::int32_t const ch) 1512 | { 1513 | return u_isalnum(ch); 1514 | } 1515 | 1516 | inline bool is_xdigit(std::int32_t const ch) 1517 | { 1518 | return u_isxdigit(ch); 1519 | } 1520 | 1521 | inline bool is_blank(std::int32_t const ch) 1522 | { 1523 | return u_isblank(ch); 1524 | } 1525 | 1526 | inline bool is_space(std::int32_t const ch) 1527 | { 1528 | return u_isspace(ch); 1529 | } 1530 | 1531 | inline bool is_whitespace(std::int32_t const ch) 1532 | { 1533 | return u_isWhitespace(ch); 1534 | } 1535 | 1536 | inline bool is_ctrl(std::int32_t const ch) 1537 | { 1538 | return u_iscntrl(ch); 1539 | } 1540 | 1541 | inline bool is_title(std::int32_t const ch) 1542 | { 1543 | return u_istitle(ch); 1544 | } 1545 | 1546 | inline bool is_graph(std::int32_t const ch) 1547 | { 1548 | return u_isgraph(ch); 1549 | } 1550 | 1551 | inline bool is_defined(std::int32_t const ch) 1552 | { 1553 | return u_isdefined(ch); 1554 | } 1555 | 1556 | inline bool is_isoctrl(std::int32_t const ch) 1557 | { 1558 | return u_isISOControl(ch); 1559 | } 1560 | 1561 | inline bool is_print(std::int32_t const ch) 1562 | { 1563 | return u_isprint(ch); 1564 | } 1565 | 1566 | inline std::int32_t to_title(std::int32_t const ch) 1567 | { 1568 | return u_totitle(ch); 1569 | } 1570 | 1571 | inline std::int32_t to_upper(std::int32_t const ch) 1572 | { 1573 | return u_toupper(ch); 1574 | } 1575 | 1576 | inline std::int32_t to_lower(std::int32_t const ch) 1577 | { 1578 | return u_tolower(ch); 1579 | } 1580 | 1581 | } // namespace OB::Text 1582 | 1583 | #endif // OB_TEXT_HH 1584 | -------------------------------------------------------------------------------- /src/ob/timer.hh: -------------------------------------------------------------------------------- 1 | #ifndef OB_TIMER_HH 2 | #define OB_TIMER_HH 3 | 4 | #include "ob/string.hh" 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace OB 14 | { 15 | 16 | class Timer 17 | { 18 | public: 19 | 20 | Timer() = default; 21 | 22 | operator bool() 23 | { 24 | return _is_running; 25 | } 26 | 27 | Timer& start() 28 | { 29 | _is_running = true; 30 | _start = std::chrono::high_resolution_clock::now(); 31 | 32 | return *this; 33 | } 34 | 35 | Timer& stop() 36 | { 37 | update(); 38 | _is_running = false; 39 | 40 | return *this; 41 | } 42 | 43 | Timer& reset() 44 | { 45 | _is_running = false; 46 | _total = {}; 47 | 48 | return *this; 49 | } 50 | 51 | template 52 | T time() 53 | { 54 | if (_is_running) 55 | { 56 | update(); 57 | } 58 | 59 | return std::chrono::duration_cast(_total); 60 | } 61 | 62 | std::string str() 63 | { 64 | if (_is_running) 65 | { 66 | update(); 67 | } 68 | 69 | long int const diff {std::chrono::time_point_cast(_total).time_since_epoch().count()}; 70 | 71 | return seconds_to_string(diff); 72 | } 73 | 74 | void str(std::string const& str) 75 | { 76 | reset(); 77 | _total = std::chrono::time_point(string_to_seconds(str)); 78 | } 79 | 80 | private: 81 | 82 | void update() 83 | { 84 | auto const stop = std::chrono::high_resolution_clock::now(); 85 | 86 | if (stop > _start) 87 | { 88 | _total += (stop - _start); 89 | } 90 | 91 | _start = stop; 92 | } 93 | 94 | std::chrono::seconds string_to_seconds(std::string const& str) 95 | { 96 | long int constexpr t_second {1}; 97 | long int constexpr t_minute {t_second * 60}; 98 | long int constexpr t_hour {t_minute * 60}; 99 | long int constexpr t_day {t_hour * 24}; 100 | long int constexpr t_week {t_day * 7}; 101 | long int constexpr t_month (t_day * 30.4); 102 | long int constexpr t_year {t_month * 12}; 103 | 104 | auto const pstr = OB::String::match(str, 105 | std::regex("^(?:(\\d+)Y:)?(?:(\\d+)M:)?(?:(\\d+)W:)?(?:(\\d+)D:)?(?:(\\d+)h:)?(?:(\\d+)m:)?(?:(\\d+)s)$")); 106 | 107 | if (! pstr) 108 | { 109 | return static_cast(0); 110 | } 111 | 112 | auto const t = pstr.value(); 113 | long int sec {0}; 114 | 115 | if (! t.at(1).empty()) 116 | { 117 | sec += std::stol(t.at(1)) * t_year; 118 | } 119 | 120 | if (! t.at(2).empty()) 121 | { 122 | sec += std::stol(t.at(2)) * t_month; 123 | } 124 | 125 | if (! t.at(3).empty()) 126 | { 127 | sec += std::stol(t.at(3)) * t_week; 128 | } 129 | 130 | if (! t.at(4).empty()) 131 | { 132 | sec += std::stol(t.at(4)) * t_day; 133 | } 134 | 135 | if (! t.at(5).empty()) 136 | { 137 | sec += std::stol(t.at(5)) * t_hour; 138 | } 139 | 140 | if (! t.at(6).empty()) 141 | { 142 | sec += std::stol(t.at(6)) * t_minute; 143 | } 144 | 145 | if (! t.at(7).empty()) 146 | { 147 | sec += std::stol(t.at(7)) * t_second; 148 | } 149 | 150 | return static_cast(sec); 151 | } 152 | 153 | std::string seconds_to_string(long int sec) 154 | { 155 | std::string res; 156 | 157 | long int constexpr t_second {1}; 158 | long int constexpr t_minute {t_second * 60}; 159 | long int constexpr t_hour {t_minute * 60}; 160 | long int constexpr t_day {t_hour * 24}; 161 | long int constexpr t_week {t_day * 7}; 162 | long int constexpr t_month (t_day * 30.4); 163 | long int constexpr t_year {t_month * 12}; 164 | 165 | auto const fuzzy_string = [&](long int const time_ref, std::string const time_str) 166 | { 167 | auto const t = (sec / time_ref); 168 | sec -= (t * time_ref); 169 | res += std::to_string(t) + time_str + ":"; 170 | }; 171 | 172 | if (sec >= t_year) 173 | { 174 | fuzzy_string(t_year, "Y"); 175 | } 176 | if (sec >= t_month) 177 | { 178 | fuzzy_string(t_month, "M"); 179 | } 180 | if (sec >= t_week) 181 | { 182 | fuzzy_string(t_week, "W"); 183 | } 184 | if (sec >= t_day) 185 | { 186 | fuzzy_string(t_day, "D"); 187 | } 188 | if (sec >= t_hour) 189 | { 190 | fuzzy_string(t_hour, "h"); 191 | } 192 | if (sec >= t_minute) 193 | { 194 | fuzzy_string(t_minute, "m"); 195 | } 196 | if (sec >= t_second) 197 | { 198 | fuzzy_string(t_second, "s"); 199 | } 200 | else 201 | { 202 | res += "0s:"; 203 | } 204 | 205 | res.pop_back(); 206 | 207 | return res; 208 | } 209 | 210 | bool _is_running {false}; 211 | std::chrono::time_point _start; 212 | std::chrono::time_point _total; 213 | }; // class Timer 214 | 215 | } // namespace OB 216 | 217 | #endif // OB_TIMER_HH 218 | --------------------------------------------------------------------------------