1 ////////////////////////////////////////////////////////////////////////////// 2 // 3 // Copyright (c) 2004-2021 musikcube team 4 // 5 // All rights reserved. 6 // 7 // Redistribution and use in source and binary forms, with or without 8 // modification, are permitted provided that the following conditions are met: 9 // 10 // * Redistributions of source code must retain the above copyright notice, 11 // this list of conditions and the following disclaimer. 12 // 13 // * Redistributions in binary form must reproduce the above copyright 14 // notice, this list of conditions and the following disclaimer in the 15 // documentation and/or other materials provided with the distribution. 16 // 17 // * Neither the name of the author nor the names of other contributors may 18 // be used to endorse or promote products derived from this software 19 // without specific prior written permission. 20 // 21 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 24 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 25 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 26 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 27 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 28 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 29 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 30 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 // POSSIBILITY OF SUCH DAMAGE. 32 // 33 ////////////////////////////////////////////////////////////////////////////// 34 35 #include <stdafx.h> 36 #include <cursespp/curses_config.h> 37 #include <cursespp/Text.h> 38 39 #include <math.h> 40 41 #include <unordered_map> 42 #include <algorithm> 43 44 #define PAD(str, count) for (size_t i = 0; i < count; i++) { str += " "; } 45 46 namespace cursespp { 47 namespace text { Truncate(const std::string & str,size_t len)48 std::string Truncate(const std::string& str, size_t len) { 49 /* not a simple substr anymore, gotta deal with multi-byte 50 characters... */ 51 if (u8cols(str) > len) { 52 auto prev = str.begin(); 53 auto it = str.begin(); 54 auto end = str.end(); 55 56 size_t cols = 0; 57 while (cols <= len && it != str.end()) { 58 prev = it; 59 60 try { 61 utf8::next(it, end); 62 } 63 catch (...) { 64 /* invalid encoding, just treat as a single char */ 65 ++it; 66 } 67 68 cols += u8cols(std::string(prev, it)); 69 } 70 71 return std::string(str.begin(), prev); 72 } 73 74 return str; 75 } 76 Ellipsize(const std::string & str,size_t len)77 std::string Ellipsize(const std::string& str, size_t len) { 78 if (u8cols(str) > len) { 79 std::string trunc = Truncate(str, len - 2); 80 81 size_t tlen = u8cols(trunc); 82 for (size_t i = tlen; i < len; i++) { 83 trunc += "."; 84 } 85 86 return trunc; 87 } 88 89 return str; 90 } 91 Align(const std::string & str,TextAlign align,size_t cx)92 std::string Align(const std::string& str, TextAlign align, size_t cx) { 93 size_t len = u8cols(str); 94 95 if (len > cx) { 96 return Ellipsize(str, cx); 97 } 98 else if (align == AlignLeft) { 99 size_t pad = cx - len; 100 std::string left = str; 101 PAD(left, pad); 102 return left; 103 } 104 else { 105 size_t leftPad = (align == AlignRight) 106 ? (cx - len) 107 : (cx - len) / 2; 108 109 size_t rightPad = cx - (leftPad + len); 110 111 std::string padded; 112 PAD(padded, leftPad); 113 padded += str; 114 PAD(padded, rightPad); 115 return padded; 116 } 117 } 118 119 /* not rocket science, but stolen from http://stackoverflow.com/a/1493195 */ Split(const std::string & str,const std::string & delimiters,bool trimEmpty)120 std::vector<std::string> Split(const std::string& str, const std::string& delimiters, bool trimEmpty) { 121 using ContainerT = std::vector<std::string>; 122 ContainerT tokens; 123 std::string::size_type pos, lastPos = 0, length = str.length(); 124 125 using value_type = typename ContainerT::value_type; 126 using size_type = typename ContainerT::size_type; 127 128 while (lastPos < length + 1) { 129 pos = str.find_first_of(delimiters, lastPos); 130 if (pos == std::string::npos) { 131 pos = length; 132 } 133 134 if (pos != lastPos || !trimEmpty) { 135 tokens.push_back(value_type( 136 str.data() + lastPos, 137 (size_type) pos - lastPos)); 138 } 139 140 lastPos = pos + 1; 141 } 142 143 return tokens; 144 } 145 privateBreakLines(const std::string & line,size_t width,std::vector<std::string> & output)146 inline void privateBreakLines( 147 const std::string& line, 148 size_t width, 149 std::vector<std::string>& output) 150 { 151 size_t len = u8cols(line); 152 size_t count = (int)ceil((float)len / (float)width); 153 154 /* easy case: the line fits on a single line! */ 155 156 if (count <= 1) { 157 output.push_back(line); 158 } 159 160 /* difficult case: the line needs to be split multiple sub-lines to fit 161 the output display */ 162 163 else { 164 /* split by whitespace */ 165 166 std::vector<std::string> words = Split(line, " \t\v\f\r"); 167 168 /* this isn't super efficient, but let's find all words that are greater 169 than the width and break them into more sublines... it's possible to 170 do this more efficiently in the loop below this with a bunch of additional 171 logic, but let's keep things simple unless we run into performance 172 problems! */ 173 174 std::vector<std::string> sanitizedWords; 175 for (size_t i = 0; i < words.size(); i++) { 176 std::string word = words.at(i); 177 size_t len = u8cols(word); 178 179 /* this word is fine, it'll easily fit on its own line of necessary */ 180 181 if (width >= len) { 182 sanitizedWords.push_back(word); 183 } 184 185 /* otherwise, the word needs to be broken into multiple lines. */ 186 187 else { 188 std::string accum; 189 190 /* ugh, we gotta split on UTF8 characters, not actual characters. 191 this makes things a bit more difficult... we iterate over the string 192 one displayable character at a time, and break it apart into separate 193 lines as necessary. */ 194 195 std::string::iterator begin = word.begin(); 196 std::string::iterator end = word.begin(); 197 int count = 0; 198 bool done = false; 199 while (end != word.end()) { 200 utf8::unchecked::next(end); 201 ++count; 202 203 if (count == width || end == word.end()) { 204 sanitizedWords.push_back(std::string(begin, end)); 205 begin = end; 206 count = 0; 207 } 208 } 209 } 210 } 211 212 words.clear(); 213 214 /* now we have a bunch of tokenized words. let's string them together 215 into sequences that fit in the output window's width */ 216 217 std::string accum; 218 size_t accumLength = 0; 219 220 for (size_t i = 0; i < sanitizedWords.size(); i++) { 221 std::string word = sanitizedWords.at(i); 222 size_t wordLength = u8cols(word); 223 size_t extra = (i != 0); 224 225 /* we have enough space for this new word. accumulate it. */ 226 227 if (accumLength + extra + wordLength <= width) { 228 if (extra) { 229 accum += " "; 230 } 231 232 accum += word; 233 accumLength += wordLength + extra; 234 } 235 236 /* otherwise, flush the current line, and start a new one... */ 237 238 else { 239 if (accum.size()) { 240 output.push_back(accum); 241 } 242 243 /* special case -- if the word is the exactly length of the 244 width, just add it as a new line and reset... */ 245 246 if (wordLength == width) { 247 output.push_back(word); 248 accum = ""; 249 accumLength = 0; 250 } 251 252 /* otherwise, let's start accumulating a new line! */ 253 254 else { 255 accum = word; 256 accumLength = wordLength; 257 } 258 } 259 } 260 261 if (accum.size()) { 262 output.push_back(accum); 263 } 264 } 265 } 266 BreakLines(const std::string & line,size_t width)267 std::vector<std::string> BreakLines(const std::string& line, size_t width) { 268 std::vector<std::string> result; 269 270 if (width > 0) { 271 std::vector<std::string> split = Split(line, "\n"); 272 273 for (size_t i = 0; i < split.size(); i++) { 274 privateBreakLines(split.at(i), width, result); 275 } 276 } 277 278 return result; 279 } 280 } 281 282 namespace key { 283 static std::unordered_map<std::string, std::string> KEY_MAPPING = { 284 { "M-~", "M-`" }, 285 { "M-bquote", "M-`" }, 286 { "^@", "M-`" }, 287 { "M-comma", "M-," }, 288 { "M-stop", "M-." }, 289 { "^H", "KEY_BACKSPACE" }, 290 { "^?", "KEY_BACKSPACE" }, 291 { "M-^H", "M-KEY_BACKSPACE" }, 292 { "M-^?", "M-KEY_BACKSPACE" }, 293 { "M-bksp", "M-KEY_BACKSPACE" }, 294 { "^M", "KEY_ENTER" }, 295 { "M-^M", "M-enter" }, 296 { "kUP3", "M-up" }, 297 { "kDN3", "M-down" }, 298 { "M-KEY_UP", "M-up" }, 299 { "M-KEY_DOWN", "M-down" }, 300 { "kUP5", "CTL_UP" }, 301 { "kDN5", "CTL_DOWN" }, 302 #ifdef PDCURSES_WINCON 303 /* special bindings for the "Windows Terminal" app */ 304 { "KEY_A2", "KEY_UP" }, 305 { "KEY_C2", "KEY_DOWN" }, 306 { "KEY_B3", "KEY_RIGHT" }, 307 { "KEY_B1", "KEY_LEFT" }, 308 { "KEY_A3", "KEY_PPAGE" }, 309 { "KEY_C3", "KEY_NPAGE" }, 310 { "KEY_A1", "KEY_HOME" }, 311 { "KEY_C1", "KEY_END" }, 312 { "CTL_PAD4", "CTL_LEFT" }, 313 { "CTL_PAD6", "CTL_RIGHT" }, 314 { "CTL_PAD8", "CTL_UP" }, 315 { "CTL_PAD9", "CTL_DOWN" }, 316 #endif 317 }; 318 Normalize(const std::string & kn)319 std::string Normalize(const std::string& kn) { 320 auto it = KEY_MAPPING.find(kn); 321 return (it != KEY_MAPPING.end()) ? it->second : kn; 322 } 323 Read(int64_t ch)324 std::string Read(int64_t ch) { 325 std::string kn = keyname((int)ch); 326 327 /* convert +ESC to M- sequences */ 328 if (kn == "^[") { 329 int64_t next = getch(); 330 if (next != -1) { 331 kn = std::string("M-") + std::string(keyname((int)next)); 332 } 333 } 334 #ifdef WIN32 335 /* transform alt->meta for uniform handling */ 336 else if (kn.find("ALT_") == 0) { 337 std::transform(kn.begin(), kn.end(), kn.begin(), tolower); 338 kn.replace(0, 4, "M-"); 339 } 340 #endif 341 /* multi-byte UTF8 character */ 342 else if (ch >= 194 && ch <= 223) { 343 kn = ""; 344 kn += (char) ch; 345 kn += (char) getch(); 346 } 347 else if (ch >= 224 && ch <= 239) { 348 kn = ""; 349 kn += (char) ch; 350 kn += (char) getch(); 351 kn += (char) getch(); 352 } 353 else if (ch >= 240 && ch <= 244) { 354 kn = ""; 355 kn += (char) ch; 356 kn += (char) getch(); 357 kn += (char) getch(); 358 kn += (char) getch(); 359 } 360 361 kn = Normalize(kn); 362 363 #ifdef WIN32 364 /* seems like on Windows using PDCurses, if a non-English keyboard 365 is selected (like Russian) we receive a UTF16 character, not an 366 encoded UTF8 character. in this case, let's convert it to a UTF8 367 string and return that. */ 368 if (kn == "UNKNOWN KEY" && ch > 244) { 369 kn = u16to8(std::wstring(1, (wchar_t)ch)); 370 } 371 #endif 372 373 // std::cerr << "keyname: " << kn << std::endl; 374 // std::cerr << "ch: " << ch << std::endl; 375 376 return kn; 377 } 378 } 379 } 380