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