]>
Commit | Line | Data |
---|---|---|
84da4a30 DH |
1 | /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ |
2 | ||
3 | /*** | |
4 | This file is part of systemd. | |
5 | ||
6 | Copyright (C) 2014 David Herrmann <dh.herrmann@gmail.com> | |
7 | ||
8 | systemd is free software; you can redistribute it and/or modify it | |
9 | under the terms of the GNU Lesser General Public License as published by | |
10 | the Free Software Foundation; either version 2.1 of the License, or | |
11 | (at your option) any later version. | |
12 | ||
13 | systemd is distributed in the hope that it will be useful, but | |
14 | WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
16 | Lesser General Public License for more details. | |
17 | ||
18 | You should have received a copy of the GNU Lesser General Public License | |
19 | along with systemd; If not, see <http://www.gnu.org/licenses/>. | |
20 | ***/ | |
21 | ||
22 | /* | |
23 | * Terminal Page/Line/Cell/Char Handling | |
24 | * This file implements page handling of a terminal. It is split into pages, | |
25 | * lines, cells and characters. Each object is independent of the next upper | |
26 | * object. | |
27 | * | |
28 | * The Terminal layer keeps each line of a terminal separate and dynamically | |
29 | * allocated. This allows us to move lines from main-screen to history-buffers | |
30 | * very fast. Same is true for scrolling, top/bottom borders and other buffer | |
31 | * operations. | |
32 | * | |
33 | * While lines are dynamically allocated, cells are not. This would be a waste | |
34 | * of memory and causes heavy fragmentation. Furthermore, cells are moved much | |
35 | * less frequently than lines so the performance-penalty is pretty small. | |
36 | * However, to support combining-characters, we have to initialize and cleanup | |
37 | * cells properly and cannot just release the underlying memory. Therefore, | |
38 | * cells are treated as proper objects despite being allocated in arrays. | |
39 | * | |
40 | * Each cell has a set of attributes and a stored character. This is usually a | |
41 | * single Unicode character stored as 32bit UCS-4 char. However, we need to | |
42 | * support Unicode combining-characters, therefore this gets more complicated. | |
43 | * Characters themselves are represented by a "term_char_t" object. It | |
44 | * should be treated as a normal integer and passed by value. The | |
06b643e7 | 45 | * surrounding struct is just to hide the internals. A term-char can contain a |
84da4a30 DH |
46 | * base character together with up to 2 combining-chars in a single integer. |
47 | * Only if you need more combining-chars (very unlikely!) a term-char is a | |
48 | * pointer to an allocated storage. This requires you to always free term-char | |
49 | * objects once no longer used (even though this is a no-op most of the time). | |
50 | * Furthermore, term-char objects are not ref-counted so you must duplicate them | |
51 | * in case you want to store it somewhere and retain a copy yourself. By | |
52 | * convention, all functions that take a term-char object will not duplicate | |
53 | * it but implicitly take ownership of the passed value. It's up to the caller | |
54 | * to duplicate it beforehand, in case it wants to retain a copy. | |
55 | * | |
56 | * If it turns out, that more than 2 comb-chars become common in specific | |
57 | * languages, we can try to optimize this. One idea is to ref-count allocated | |
58 | * characters and store them in a hash-table (like gnome's libvte3 does). This | |
59 | * way we will never have two allocated chars for the same content. Or we can | |
60 | * simply put two uint64_t into a "term_char_t". This will slow down operations | |
61 | * on systems that don't need that many comb-chars, but avoid the dynamic | |
62 | * allocations on others. | |
63 | * Anyhow, until we have proper benchmarks, we will keep the current code. It | |
64 | * seems to compete very well with other solutions so far. | |
28622e8f DH |
65 | * |
66 | * The page-layer is a one-dimensional array of lines. Considering that each | |
67 | * line is a one-dimensional array of cells, the page layer provides the | |
68 | * two-dimensional cell-page required for terminals. The page itself only | |
69 | * operates on lines. All cell-related operations are forwarded to the correct | |
70 | * line. | |
71 | * A page does not contain any cursor tracking. It only provides the raw | |
72 | * operations to shuffle lines and modify the page. | |
84da4a30 DH |
73 | */ |
74 | ||
75 | #include <stdbool.h> | |
76 | #include <stdint.h> | |
77 | #include <stdlib.h> | |
78 | #include <wchar.h> | |
79 | #include "macro.h" | |
80 | #include "term-internal.h" | |
81 | #include "util.h" | |
82 | ||
83 | /* maximum UCS-4 character */ | |
84 | #define CHAR_UCS4_MAX (0x10ffff) | |
85 | /* mask for valid UCS-4 characters (21bit) */ | |
86 | #define CHAR_UCS4_MASK (0x1fffff) | |
87 | /* UCS-4 replacement character */ | |
88 | #define CHAR_UCS4_REPLACEMENT (0xfffd) | |
89 | ||
90 | /* real storage behind "term_char_t" in case it's not packed */ | |
91 | typedef struct term_character { | |
92 | uint8_t n; | |
93 | uint32_t codepoints[]; | |
94 | } term_character; | |
95 | ||
96 | /* | |
97 | * char_pack() takes 3 UCS-4 values and packs them into a term_char_t object. | |
98 | * Note that UCS-4 chars only take 21 bits, so we still have the LSB as marker. | |
99 | * We set it to 1 so others can distinguish it from pointers. | |
100 | */ | |
101 | static inline term_char_t char_pack(uint32_t v1, uint32_t v2, uint32_t v3) { | |
102 | uint64_t packed, u1, u2, u3; | |
103 | ||
104 | u1 = v1; | |
105 | u2 = v2; | |
106 | u3 = v3; | |
107 | ||
108 | packed = 0x01; | |
109 | packed |= (u1 & (uint64_t)CHAR_UCS4_MASK) << 43; | |
110 | packed |= (u2 & (uint64_t)CHAR_UCS4_MASK) << 22; | |
111 | packed |= (u3 & (uint64_t)CHAR_UCS4_MASK) << 1; | |
112 | ||
113 | return TERM_CHAR_INIT(packed); | |
114 | } | |
115 | ||
116 | #define char_pack1(_v1) char_pack2((_v1), CHAR_UCS4_MAX + 1) | |
117 | #define char_pack2(_v1, _v2) char_pack3((_v1), (_v2), CHAR_UCS4_MAX + 1) | |
118 | #define char_pack3(_v1, _v2, _v3) char_pack((_v1), (_v2), (_v3)) | |
119 | ||
120 | /* | |
121 | * char_unpack() is the inverse of char_pack(). It extracts the 3 stored UCS-4 | |
122 | * characters and returns them. Note that this does not validate the passed | |
123 | * term_char_t. That's the responsibility of the caller. | |
124 | * This returns the number of characters actually packed. This obviously is a | |
125 | * number between 0 and 3 (inclusive). | |
126 | */ | |
127 | static inline uint8_t char_unpack(term_char_t packed, uint32_t *out_v1, uint32_t *out_v2, uint32_t *out_v3) { | |
128 | uint32_t v1, v2, v3; | |
129 | ||
130 | v1 = (packed._value >> 43) & (uint64_t)CHAR_UCS4_MASK; | |
131 | v2 = (packed._value >> 22) & (uint64_t)CHAR_UCS4_MASK; | |
132 | v3 = (packed._value >> 1) & (uint64_t)CHAR_UCS4_MASK; | |
133 | ||
134 | if (out_v1) | |
135 | *out_v1 = v1; | |
136 | if (out_v2) | |
137 | *out_v2 = v2; | |
138 | if (out_v3) | |
139 | *out_v3 = v3; | |
140 | ||
141 | return (v1 > CHAR_UCS4_MAX) ? 0 : | |
142 | ((v2 > CHAR_UCS4_MAX) ? 1 : | |
143 | ((v3 > CHAR_UCS4_MAX) ? 2 : | |
144 | 3)); | |
145 | } | |
146 | ||
147 | /* cast a term_char_t to a term_character* */ | |
148 | static inline term_character *char_to_ptr(term_char_t ch) { | |
149 | return (term_character*)(unsigned long)ch._value; | |
150 | } | |
151 | ||
152 | /* cast a term_character* to a term_char_t */ | |
153 | static inline term_char_t char_from_ptr(term_character *c) { | |
154 | return TERM_CHAR_INIT((unsigned long)c); | |
155 | } | |
156 | ||
157 | /* | |
158 | * char_alloc() allocates a properly aligned term_character object and returns | |
159 | * a pointer to it. NULL is returned on allocation errors. The object will have | |
160 | * enough room for @n following UCS-4 chars. | |
161 | * Note that we allocate (n+1) characters and set the last one to 0 in case | |
162 | * anyone prints this string for debugging. | |
163 | */ | |
164 | static term_character *char_alloc(uint8_t n) { | |
165 | term_character *c; | |
166 | int r; | |
167 | ||
168 | r = posix_memalign((void**)&c, | |
169 | MAX(sizeof(void*), (size_t)2), | |
170 | sizeof(*c) + sizeof(*c->codepoints) * (n + 1)); | |
171 | if (r) | |
172 | return NULL; | |
173 | ||
174 | c->n = n; | |
175 | c->codepoints[n] = 0; | |
176 | ||
177 | return c; | |
178 | } | |
179 | ||
180 | /* | |
181 | * char_free() frees the memory allocated via char_alloc(). It is safe to call | |
182 | * this on any term_char_t, only allocated characters are freed. | |
183 | */ | |
184 | static inline void char_free(term_char_t ch) { | |
185 | if (term_char_is_allocated(ch)) | |
186 | free(char_to_ptr(ch)); | |
187 | } | |
188 | ||
189 | /* | |
190 | * This appends @append_ucs4 to the existing character @base and returns | |
191 | * it as a new character. In case that's not possible, @base is returned. The | |
192 | * caller can use term_char_same() to test whether the returned character was | |
193 | * freshly allocated or not. | |
194 | */ | |
195 | static term_char_t char_build(term_char_t base, uint32_t append_ucs4) { | |
196 | /* soft-limit for combining-chars; hard-limit is currently 255 */ | |
197 | const size_t climit = 64; | |
198 | term_character *c; | |
199 | uint32_t buf[3], *t; | |
200 | uint8_t n; | |
201 | ||
202 | /* ignore invalid UCS-4 */ | |
203 | if (append_ucs4 > CHAR_UCS4_MAX) | |
204 | return base; | |
205 | ||
206 | if (term_char_is_null(base)) { | |
207 | return char_pack1(append_ucs4); | |
208 | } else if (!term_char_is_allocated(base)) { | |
209 | /* unpack and try extending the packed character */ | |
210 | n = char_unpack(base, &buf[0], &buf[1], &buf[2]); | |
211 | ||
212 | switch (n) { | |
213 | case 0: | |
214 | return char_pack1(append_ucs4); | |
215 | case 1: | |
216 | if (climit < 2) | |
217 | return base; | |
218 | ||
219 | return char_pack2(buf[0], append_ucs4); | |
220 | case 2: | |
221 | if (climit < 3) | |
222 | return base; | |
223 | ||
224 | return char_pack3(buf[0], buf[1], append_ucs4); | |
225 | default: | |
226 | /* fallthrough */ | |
227 | break; | |
228 | } | |
229 | ||
230 | /* already fully packed, we need to allocate a new one */ | |
231 | t = buf; | |
232 | } else { | |
233 | /* already an allocated type, we need to allocate a new one */ | |
234 | c = char_to_ptr(base); | |
235 | t = c->codepoints; | |
236 | n = c->n; | |
237 | } | |
238 | ||
239 | /* bail out if soft-limit is reached */ | |
240 | if (n >= climit) | |
241 | return base; | |
242 | ||
243 | /* allocate new char */ | |
244 | c = char_alloc(n + 1); | |
245 | if (!c) | |
246 | return base; | |
247 | ||
248 | memcpy(c->codepoints, t, sizeof(*t) * n); | |
249 | c->codepoints[n] = append_ucs4; | |
250 | ||
251 | return char_from_ptr(c); | |
252 | } | |
253 | ||
254 | /** | |
255 | * term_char_set() - Reset character to a single UCS-4 character | |
256 | * @previous: term-char to reset | |
257 | * @append_ucs4: UCS-4 char to set | |
258 | * | |
259 | * This frees all resources in @previous and re-initializes it to @append_ucs4. | |
260 | * The new char is returned. | |
261 | * | |
262 | * Usually, this is used like this: | |
263 | * obj->ch = term_char_set(obj->ch, ucs4); | |
264 | * | |
265 | * Returns: The previous character reset to @append_ucs4. | |
266 | */ | |
267 | term_char_t term_char_set(term_char_t previous, uint32_t append_ucs4) { | |
268 | char_free(previous); | |
269 | return char_build(TERM_CHAR_NULL, append_ucs4); | |
270 | } | |
271 | ||
272 | /** | |
273 | * term_char_merge() - Merge UCS-4 char at the end of an existing char | |
274 | * @base: existing term-char | |
275 | * @append_ucs4: UCS-4 character to append | |
276 | * | |
277 | * This appends @append_ucs4 to @base and returns the result. @base is | |
278 | * invalidated by this function and must no longer be used. The returned value | |
279 | * replaces the old one. | |
280 | * | |
281 | * Usually, this is used like this: | |
282 | * obj->ch = term_char_merge(obj->ch, ucs4); | |
283 | * | |
284 | * Returns: The new merged character. | |
285 | */ | |
286 | term_char_t term_char_merge(term_char_t base, uint32_t append_ucs4) { | |
287 | term_char_t ch; | |
288 | ||
289 | ch = char_build(base, append_ucs4); | |
290 | if (!term_char_same(ch, base)) | |
291 | term_char_free(base); | |
292 | ||
293 | return ch; | |
294 | } | |
295 | ||
296 | /** | |
297 | * term_char_dup() - Duplicate character | |
298 | * @ch: character to duplicate | |
299 | * | |
300 | * This duplicates a term-character. In case the character is not allocated, | |
301 | * nothing is done. Otherwise, the underlying memory is copied and returned. You | |
302 | * need to call term_char_free() on the returned character to release it again. | |
303 | * On allocation errors, a replacement character is returned. Therefore, the | |
304 | * caller can safely assume that this function always succeeds. | |
305 | * | |
306 | * Returns: The duplicated term-character. | |
307 | */ | |
308 | term_char_t term_char_dup(term_char_t ch) { | |
309 | term_character *c, *newc; | |
310 | ||
311 | if (!term_char_is_allocated(ch)) | |
312 | return ch; | |
313 | ||
314 | c = char_to_ptr(ch); | |
315 | newc = char_alloc(c->n); | |
316 | if (!newc) | |
317 | return char_pack1(CHAR_UCS4_REPLACEMENT); | |
318 | ||
319 | memcpy(newc->codepoints, c->codepoints, sizeof(*c->codepoints) * c->n); | |
320 | return char_from_ptr(newc); | |
321 | } | |
322 | ||
323 | /** | |
324 | * term_char_dup_append() - Duplicate tsm-char with UCS-4 character appended | |
325 | * @base: existing term-char | |
326 | * @append_ucs4: UCS-4 character to append | |
327 | * | |
328 | * This is similar to term_char_merge(), but it returns a separately allocated | |
329 | * character. That is, @base will stay valid after this returns and is not | |
330 | * touched. In case the append-operation fails, @base is duplicated and | |
331 | * returned. That is, the returned char is always independent of @base. | |
332 | * | |
333 | * Returns: Newly allocated character with @append_ucs4 appended to @base. | |
334 | */ | |
335 | term_char_t term_char_dup_append(term_char_t base, uint32_t append_ucs4) { | |
336 | term_char_t ch; | |
337 | ||
338 | ch = char_build(base, append_ucs4); | |
339 | if (term_char_same(ch, base)) | |
340 | ch = term_char_dup(base); | |
341 | ||
342 | return ch; | |
343 | } | |
344 | ||
345 | /** | |
346 | * term_char_resolve() - Retrieve the UCS-4 string for a term-char | |
347 | * @ch: character to resolve | |
348 | * @s: storage for size of string or NULL | |
349 | * @b: storage for string or NULL | |
350 | * | |
351 | * This takes a term-character and returns the UCS-4 string associated with it. | |
352 | * In case @ch is not allocated, the string is stored in @b (in case @b is NULL | |
353 | * static storage is used). Otherwise, a pointer to the allocated storage is | |
354 | * returned. | |
355 | * | |
356 | * The returned string is only valid as long as @ch and @b are valid. The string | |
357 | * is zero-terminated and can safely be printed via long-character printf(). | |
358 | * The length of the string excluding the zero-character is returned in @s. | |
359 | * | |
360 | * This never returns NULL. Even if the size is 0, this points to a buffer of at | |
361 | * least a zero-terminator. | |
362 | * | |
363 | * Returns: The UCS-4 string-representation of @ch, and its size in @s. | |
364 | */ | |
365 | const uint32_t *term_char_resolve(term_char_t ch, size_t *s, term_charbuf_t *b) { | |
366 | static term_charbuf_t static_b; | |
367 | term_character *c; | |
368 | uint32_t *cache; | |
369 | size_t len; | |
370 | ||
371 | if (b) | |
372 | cache = b->buf; | |
373 | else | |
374 | cache = static_b.buf; | |
375 | ||
376 | if (term_char_is_null(ch)) { | |
377 | len = 0; | |
378 | cache[0] = 0; | |
379 | } else if (term_char_is_allocated(ch)) { | |
380 | c = char_to_ptr(ch); | |
381 | len = c->n; | |
382 | cache = c->codepoints; | |
383 | } else { | |
384 | len = char_unpack(ch, &cache[0], &cache[1], &cache[2]); | |
385 | cache[len] = 0; | |
386 | } | |
387 | ||
388 | if (s) | |
389 | *s = len; | |
390 | ||
391 | return cache; | |
392 | } | |
393 | ||
394 | /** | |
395 | * term_char_lookup_width() - Lookup cell-width of a character | |
396 | * @ch: character to return cell-width for | |
397 | * | |
398 | * This is an equivalent of wcwidth() for term_char_t. It can deal directly | |
399 | * with UCS-4 and combining-characters and avoids the mess that is wchar_t and | |
400 | * locale handling. | |
401 | * | |
402 | * Returns: 0 for unprintable characters, >0 for everything else. | |
403 | */ | |
404 | unsigned int term_char_lookup_width(term_char_t ch) { | |
405 | term_charbuf_t b; | |
406 | const uint32_t *str; | |
407 | unsigned int max; | |
408 | size_t i, len; | |
409 | int r; | |
410 | ||
411 | max = 0; | |
412 | str = term_char_resolve(ch, &len, &b); | |
413 | ||
414 | for (i = 0; i < len; ++i) { | |
415 | /* | |
416 | * Oh god, C99 locale handling strikes again: wcwidth() expects | |
417 | * wchar_t, but there is no way for us to know the | |
418 | * internal encoding of wchar_t. Moreover, it is nearly | |
419 | * impossible to convert UCS-4 into wchar_t (except for iconv, | |
420 | * which is way too much overhead). | |
421 | * Therefore, we use our own copy of wcwidth(). Lets just hope | |
422 | * that glibc will one day export it's internal UCS-4 and UTF-8 | |
423 | * helpers for direct use. | |
424 | */ | |
425 | assert_cc(sizeof(wchar_t) >= 4); | |
426 | r = mk_wcwidth((wchar_t)str[i]); | |
427 | if (r > 0 && (unsigned int)r > max) | |
428 | max = r; | |
429 | } | |
430 | ||
431 | return max; | |
432 | } | |
433 | ||
434 | /** | |
435 | * term_cell_init() - Initialize a new cell | |
436 | * @cell: cell to initialize | |
437 | * @ch: character to set on the cell or TERM_CHAR_NULL | |
438 | * @cwidth: character width of @ch | |
439 | * @attr: attributes to set on the cell or NULL | |
440 | * @age: age to set on the cell or TERM_AGE_NULL | |
441 | * | |
442 | * This initializes a new cell. The backing-memory of the cell must be allocated | |
443 | * by the caller beforehand. The caller is responsible to destroy the cell via | |
444 | * term_cell_destroy() before freeing the backing-memory. | |
445 | * | |
446 | * It is safe (and supported!) to use: | |
447 | * zero(*c); | |
448 | * instead of: | |
449 | * term_cell_init(c, TERM_CHAR_NULL, NULL, TERM_AGE_NULL); | |
450 | * | |
451 | * Note that this call takes ownership of @ch. If you want to use it yourself | |
452 | * after this call, you need to duplicate it before calling this. | |
453 | */ | |
454 | static void term_cell_init(term_cell *cell, term_char_t ch, unsigned int cwidth, const term_attr *attr, term_age_t age) { | |
455 | assert(cell); | |
456 | ||
457 | cell->ch = ch; | |
458 | cell->cwidth = cwidth; | |
459 | cell->age = age; | |
460 | ||
461 | if (attr) | |
462 | memcpy(&cell->attr, attr, sizeof(*attr)); | |
463 | else | |
464 | zero(cell->attr); | |
465 | } | |
466 | ||
467 | /** | |
468 | * term_cell_destroy() - Destroy previously initialized cell | |
469 | * @cell: cell to destroy or NULL | |
470 | * | |
471 | * This releases all resources associated with a cell. The backing memory is | |
472 | * kept as-is. It's the responsibility of the caller to manage it. | |
473 | * | |
474 | * You must not call any other cell operations on this cell after this call | |
475 | * returns. You must re-initialize the cell via term_cell_init() before you can | |
476 | * use it again. | |
477 | * | |
478 | * If @cell is NULL, this is a no-op. | |
479 | */ | |
480 | static void term_cell_destroy(term_cell *cell) { | |
481 | if (!cell) | |
482 | return; | |
483 | ||
484 | term_char_free(cell->ch); | |
485 | } | |
486 | ||
487 | /** | |
488 | * term_cell_set() - Change contents of a cell | |
489 | * @cell: cell to modify | |
490 | * @ch: character to set on the cell or cell->ch | |
491 | * @cwidth: character width of @ch or cell->cwidth | |
492 | * @attr: attributes to set on the cell or NULL | |
493 | * @age: age to set on the cell or cell->age | |
494 | * | |
495 | * This changes the contents of a cell. It can be used to change the character, | |
496 | * attributes and age. To keep the current character, pass cell->ch as @ch. To | |
497 | * reset the current attributes, pass NULL. To keep the current age, pass | |
498 | * cell->age. | |
499 | * | |
500 | * This call takes ownership of @ch. You need to duplicate it first, in case you | |
501 | * want to use it for your own purposes after this call. | |
502 | * | |
503 | * The cell must have been initialized properly before calling this. See | |
504 | * term_cell_init(). | |
505 | */ | |
506 | static void term_cell_set(term_cell *cell, term_char_t ch, unsigned int cwidth, const term_attr *attr, term_age_t age) { | |
507 | assert(cell); | |
508 | ||
509 | if (!term_char_same(ch, cell->ch)) { | |
510 | term_char_free(cell->ch); | |
511 | cell->ch = ch; | |
512 | } | |
513 | ||
514 | cell->cwidth = cwidth; | |
515 | cell->age = age; | |
516 | ||
517 | if (attr) | |
518 | memcpy(&cell->attr, attr, sizeof(*attr)); | |
519 | else | |
520 | zero(cell->attr); | |
521 | } | |
522 | ||
523 | /** | |
524 | * term_cell_append() - Append a combining-char to a cell | |
525 | * @cell: cell to modify | |
526 | * @ucs4: UCS-4 character to append to the cell | |
527 | * @age: new age to set on the cell or cell->age | |
528 | * | |
529 | * This appends a combining-character to a cell. No validation of the UCS-4 | |
530 | * character is done, so this can be used to append any character. Additionally, | |
531 | * this can update the age of the cell. | |
532 | * | |
533 | * The cell must have been initialized properly before calling this. See | |
534 | * term_cell_init(). | |
535 | */ | |
536 | static void term_cell_append(term_cell *cell, uint32_t ucs4, term_age_t age) { | |
537 | assert(cell); | |
538 | ||
539 | cell->ch = term_char_merge(cell->ch, ucs4); | |
540 | cell->age = age; | |
541 | } | |
542 | ||
543 | /** | |
544 | * term_cell_init_n() - Initialize an array of cells | |
545 | * @cells: pointer to an array of cells to initialize | |
546 | * @n: number of cells | |
547 | * @attr: attributes to set on all cells or NULL | |
548 | * @age: age to set on all cells | |
549 | * | |
550 | * This is the same as term_cell_init() but initializes an array of cells. | |
551 | * Furthermore, this always sets the character to TERM_CHAR_NULL. | |
552 | * If you want to set a specific characters on all cells, you need to hard-code | |
553 | * this loop and duplicate the character for each cell. | |
554 | */ | |
555 | static void term_cell_init_n(term_cell *cells, unsigned int n, const term_attr *attr, term_age_t age) { | |
556 | for ( ; n > 0; --n, ++cells) | |
557 | term_cell_init(cells, TERM_CHAR_NULL, 0, attr, age); | |
558 | } | |
559 | ||
560 | /** | |
561 | * term_cell_destroy_n() - Destroy an array of cells | |
562 | * @cells: pointer to an array of cells to destroy | |
563 | * @n: number of cells | |
564 | * | |
565 | * This is the same as term_cell_destroy() but destroys an array of cells. | |
566 | */ | |
567 | static void term_cell_destroy_n(term_cell *cells, unsigned int n) { | |
568 | for ( ; n > 0; --n, ++cells) | |
569 | term_cell_destroy(cells); | |
570 | } | |
571 | ||
572 | /** | |
573 | * term_cell_clear_n() - Clear contents of an array of cells | |
574 | * @cells: pointer to an array of cells to modify | |
575 | * @n: number of cells | |
576 | * @attr: attributes to set on all cells or NULL | |
577 | * @age: age to set on all cells | |
578 | * | |
579 | * This is the same as term_cell_set() but operates on an array of cells. Note | |
580 | * that all characters are always set to TERM_CHAR_NULL, unlike term_cell_set() | |
581 | * which takes the character as argument. | |
582 | * If you want to set a specific characters on all cells, you need to hard-code | |
583 | * this loop and duplicate the character for each cell. | |
584 | */ | |
585 | static void term_cell_clear_n(term_cell *cells, unsigned int n, const term_attr *attr, term_age_t age) { | |
586 | for ( ; n > 0; --n, ++cells) | |
587 | term_cell_set(cells, TERM_CHAR_NULL, 0, attr, age); | |
588 | } | |
589 | ||
590 | /** | |
591 | * term_line_new() - Allocate a new line | |
592 | * @out: place to store pointer to new line | |
593 | * | |
594 | * This allocates and initialized a new line. The line is unlinked and | |
595 | * independent of any page. It can be used for any purpose. The initial | |
596 | * cell-count is set to 0. | |
597 | * | |
598 | * The line has to be freed via term_line_free() once it's no longer needed. | |
599 | * | |
600 | * Returns: 0 on success, negative error code on failure. | |
601 | */ | |
602 | int term_line_new(term_line **out) { | |
603 | _term_line_free_ term_line *line = NULL; | |
604 | ||
605 | assert_return(out, -EINVAL); | |
606 | ||
607 | line = new0(term_line, 1); | |
608 | if (!line) | |
609 | return -ENOMEM; | |
610 | ||
611 | *out = line; | |
612 | line = NULL; | |
613 | return 0; | |
614 | } | |
615 | ||
616 | /** | |
617 | * term_line_free() - Free a line | |
618 | * @line: line to free or NULL | |
619 | * | |
620 | * This frees a line that was previously allocated via term_line_free(). All its | |
621 | * cells are released, too. | |
622 | * | |
623 | * If @line is NULL, this is a no-op. | |
624 | */ | |
625 | term_line *term_line_free(term_line *line) { | |
626 | if (!line) | |
627 | return NULL; | |
628 | ||
629 | term_cell_destroy_n(line->cells, line->n_cells); | |
630 | free(line->cells); | |
631 | free(line); | |
632 | ||
633 | return NULL; | |
634 | } | |
635 | ||
636 | /** | |
637 | * term_line_reserve() - Pre-allocate cells for a line | |
638 | * @line: line to pre-allocate cells for | |
639 | * @width: numbers of cells the line shall have pre-allocated | |
640 | * @attr: attribute for all allocated cells or NULL | |
641 | * @age: current age for all modifications | |
642 | * @protect_width: width to protect from erasure | |
643 | * | |
644 | * This pre-allocates cells for this line. Please note that @width is the number | |
645 | * of cells the line is guaranteed to have allocated after this call returns. | |
646 | * It's not the number of cells that are added, neither is it the new width of | |
647 | * the line. | |
648 | * | |
649 | * This function never frees memory. That is, reducing the line-width will | |
650 | * always succeed, same is true for increasing the width to a previously set | |
651 | * width. | |
652 | * | |
653 | * @attr and @age are used to initialize new cells. Additionally, any | |
654 | * existing cell outside of the protected area specified by @protect_width are | |
655 | * cleared and reset with @attr and @age. | |
656 | * | |
657 | * Returns: 0 on success, negative error code on failure. | |
658 | */ | |
659 | int term_line_reserve(term_line *line, unsigned int width, const term_attr *attr, term_age_t age, unsigned int protect_width) { | |
660 | unsigned int min_width; | |
661 | term_cell *t; | |
662 | ||
663 | assert_return(line, -EINVAL); | |
664 | ||
665 | /* reset existing cells if required */ | |
666 | min_width = MIN(line->n_cells, width); | |
667 | if (min_width > protect_width) | |
668 | term_cell_clear_n(line->cells + protect_width, | |
669 | min_width - protect_width, | |
670 | attr, | |
671 | age); | |
672 | ||
673 | /* allocate new cells if required */ | |
674 | ||
675 | if (width > line->n_cells) { | |
676 | t = realloc_multiply(line->cells, sizeof(*t), width); | |
677 | if (!t) | |
678 | return -ENOMEM; | |
679 | ||
680 | if (!attr && !age) | |
681 | memzero(t + line->n_cells, | |
682 | sizeof(*t) * (width - line->n_cells)); | |
683 | else | |
684 | term_cell_init_n(t + line->n_cells, | |
685 | width - line->n_cells, | |
686 | attr, | |
687 | age); | |
688 | ||
689 | line->cells = t; | |
690 | line->n_cells = width; | |
691 | } | |
692 | ||
693 | line->fill = MIN(line->fill, protect_width); | |
694 | ||
695 | return 0; | |
696 | } | |
697 | ||
698 | /** | |
699 | * term_line_set_width() - Change width of a line | |
700 | * @line: line to modify | |
701 | * @width: new width | |
702 | * | |
703 | * This changes the actual width of a line. It is the caller's responsibility | |
704 | * to use term_line_reserve() to make sure enough space is allocated. If @width | |
705 | * is greater than the allocated size, it is cropped. | |
706 | * | |
707 | * This does not modify any cells. Use term_line_reserve() or term_line_erase() | |
708 | * to clear any newly added cells. | |
709 | * | |
710 | * NOTE: The fill state is cropped at line->width. Therefore, if you increase | |
711 | * the line-width afterwards, but there is a multi-cell character at the | |
712 | * end of the line that got cropped, then the fill-state will _not_ be | |
713 | * adjusted. | |
714 | * This means, the fill-state always includes the cells up to the start | |
715 | * of the right-most character, but it might or might not cover it until | |
716 | * its end. This should be totally fine, though. You should never access | |
717 | * multi-cell tails directly, anyway. | |
718 | */ | |
719 | void term_line_set_width(term_line *line, unsigned int width) { | |
720 | assert(line); | |
721 | ||
722 | if (width > line->n_cells) | |
723 | width = line->n_cells; | |
724 | ||
725 | line->width = width; | |
726 | line->fill = MIN(line->fill, width); | |
727 | } | |
728 | ||
729 | /** | |
730 | * line_insert() - Insert characters and move existing cells to the right | |
731 | * @from: position to insert cells at | |
732 | * @num: number of cells to insert | |
733 | * @head_char: character that is set on the first cell | |
734 | * @head_cwidth: character-length of @head_char | |
735 | * @attr: attribute for all inserted cells or NULL | |
736 | * @age: current age for all modifications | |
737 | * | |
738 | * The INSERT operation (or writes with INSERT_MODE) writes data at a specific | |
739 | * position on a line and shifts the existing cells to the right. Cells that are | |
740 | * moved beyond the right hand border are discarded. | |
741 | * | |
742 | * This helper contains the actual INSERT implementation which is independent of | |
743 | * the data written. It works on cells, not on characters. The first cell is set | |
744 | * to @head_char, all others are reset to TERM_CHAR_NULL. See each caller for a | |
745 | * more detailed description. | |
746 | */ | |
747 | static inline void line_insert(term_line *line, unsigned int from, unsigned int num, term_char_t head_char, unsigned int head_cwidth, const term_attr *attr, term_age_t age) { | |
748 | unsigned int i, rem, move; | |
749 | ||
750 | if (from >= line->width) | |
751 | return; | |
752 | if (from + num < from || from + num > line->width) | |
753 | num = line->width - from; | |
754 | if (!num) | |
755 | return; | |
756 | ||
757 | move = line->width - from - num; | |
758 | rem = MIN(num, move); | |
759 | ||
760 | if (rem > 0) { | |
761 | /* | |
762 | * Make room for @num cells; shift cells to the right if | |
763 | * required. @rem is the number of remaining cells that we will | |
764 | * knock off on the right and overwrite during the right shift. | |
765 | * | |
766 | * For INSERT_MODE, @num/@rem are usually 1 or 2, @move is 50% | |
767 | * of the line on average. Therefore, the actual move is quite | |
768 | * heavy and we can safely invalidate cells manually instead of | |
769 | * the whole line. | |
770 | * However, for INSERT operations, any parameters are | |
771 | * possible. But we cannot place any assumption on its usage | |
772 | * across applications, so we just handle it the same as | |
773 | * INSERT_MODE and do per-cell invalidation. | |
774 | */ | |
775 | ||
776 | /* destroy cells that are knocked off on the right */ | |
777 | term_cell_destroy_n(line->cells + line->width - rem, rem); | |
778 | ||
779 | /* move remaining bulk of cells */ | |
780 | memmove(line->cells + from + num, | |
781 | line->cells + from, | |
782 | sizeof(*line->cells) * move); | |
783 | ||
784 | /* invalidate cells */ | |
785 | for (i = 0; i < move; ++i) | |
786 | line->cells[from + num + i].age = age; | |
787 | ||
788 | /* initialize fresh head-cell */ | |
789 | term_cell_init(line->cells + from, | |
790 | head_char, | |
791 | head_cwidth, | |
792 | attr, | |
793 | age); | |
794 | ||
795 | /* initialize fresh tail-cells */ | |
796 | term_cell_init_n(line->cells + from + 1, | |
797 | num - 1, | |
798 | attr, | |
799 | age); | |
800 | ||
801 | /* adjust fill-state */ | |
802 | DISABLE_WARNING_SHADOW; | |
803 | line->fill = MIN(line->width, | |
804 | MAX(line->fill + num, | |
805 | from + num)); | |
806 | REENABLE_WARNING; | |
807 | } else { | |
808 | /* modify head-cell */ | |
809 | term_cell_set(line->cells + from, | |
810 | head_char, | |
811 | head_cwidth, | |
812 | attr, | |
813 | age); | |
814 | ||
815 | /* reset tail-cells */ | |
816 | term_cell_clear_n(line->cells + from + 1, | |
817 | num - 1, | |
818 | attr, | |
819 | age); | |
820 | ||
821 | /* adjust fill-state */ | |
822 | line->fill = line->width; | |
823 | } | |
824 | } | |
825 | ||
826 | /** | |
827 | * term_line_write() - Write to a single, specific cell | |
828 | * @line: line to write to | |
829 | * @pos_x: x-position of cell in @line to write to | |
830 | * @ch: character to write to the cell | |
831 | * @cwidth: character width of @ch | |
832 | * @attr: attributes to set on the cell or NULL | |
833 | * @age: current age for all modifications | |
834 | * @insert_mode: true if INSERT-MODE is enabled | |
835 | * | |
836 | * This writes to a specific cell in a line. The cell is addressed by its | |
837 | * X-position @pos_x. If that cell does not exist, this is a no-op. | |
838 | * | |
839 | * @ch and @attr are set on this cell. | |
840 | * | |
841 | * If @insert_mode is true, this inserts the character instead of overwriting | |
842 | * existing data (existing data is now moved to the right before writing). | |
843 | * | |
844 | * This function is the low-level handler of normal writes to a terminal. | |
845 | */ | |
846 | void term_line_write(term_line *line, unsigned int pos_x, term_char_t ch, unsigned int cwidth, const term_attr *attr, term_age_t age, bool insert_mode) { | |
847 | unsigned int len; | |
848 | ||
849 | assert(line); | |
850 | ||
851 | if (pos_x >= line->width) | |
852 | return; | |
853 | ||
854 | len = MAX(1U, cwidth); | |
855 | if (pos_x + len < pos_x || pos_x + len > line->width) | |
856 | len = line->width - pos_x; | |
857 | if (!len) | |
858 | return; | |
859 | ||
860 | if (insert_mode) { | |
861 | /* Use line_insert() to insert the character-head and fill | |
862 | * the remains with NULLs. */ | |
863 | line_insert(line, pos_x, len, ch, cwidth, attr, age); | |
864 | } else { | |
865 | /* modify head-cell */ | |
866 | term_cell_set(line->cells + pos_x, ch, cwidth, attr, age); | |
867 | ||
868 | /* reset tail-cells */ | |
869 | term_cell_clear_n(line->cells + pos_x + 1, | |
870 | len - 1, | |
871 | attr, | |
872 | age); | |
873 | ||
874 | /* adjust fill-state */ | |
875 | DISABLE_WARNING_SHADOW; | |
876 | line->fill = MIN(line->width, | |
877 | MAX(line->fill, | |
878 | pos_x + len)); | |
879 | REENABLE_WARNING; | |
880 | } | |
881 | } | |
882 | ||
883 | /** | |
884 | * term_line_insert() - Insert empty cells | |
885 | * @line: line to insert empty cells into | |
886 | * @from: x-position where to insert cells | |
887 | * @num: number of cells to insert | |
888 | * @attr: attributes to set on the cells or NULL | |
889 | * @age: current age for all modifications | |
890 | * | |
891 | * This inserts @num empty cells at position @from in line @line. All existing | |
892 | * cells to the right are shifted to make room for the new cells. Cells that get | |
893 | * pushed beyond the right hand border are discarded. | |
894 | */ | |
895 | void term_line_insert(term_line *line, unsigned int from, unsigned int num, const term_attr *attr, term_age_t age) { | |
896 | /* use line_insert() to insert @num empty cells */ | |
897 | return line_insert(line, from, num, TERM_CHAR_NULL, 0, attr, age); | |
898 | } | |
899 | ||
900 | /** | |
901 | * term_line_delete() - Delete cells from line | |
902 | * @line: line to delete cells from | |
903 | * @from: position to delete cells at | |
904 | * @num: number of cells to delete | |
905 | * @attr: attributes to set on any new cells | |
906 | * @age: current age for all modifications | |
907 | * | |
908 | * Delete cells from a line. All cells to the right of the deleted cells are | |
909 | * shifted to the left to fill the empty space. New cells appearing on the right | |
910 | * hand border are cleared and initialized with @attr. | |
911 | */ | |
912 | void term_line_delete(term_line *line, unsigned int from, unsigned int num, const term_attr *attr, term_age_t age) { | |
913 | unsigned int rem, move, i; | |
914 | ||
915 | assert(line); | |
916 | ||
917 | if (from >= line->width) | |
918 | return; | |
919 | if (from + num < from || from + num > line->width) | |
920 | num = line->width - from; | |
921 | if (!num) | |
922 | return; | |
923 | ||
924 | /* destroy and move as many upfront as possible */ | |
925 | move = line->width - from - num; | |
926 | rem = MIN(num, move); | |
927 | if (rem > 0) { | |
928 | /* destroy to be removed cells */ | |
929 | term_cell_destroy_n(line->cells + from, rem); | |
930 | ||
931 | /* move tail upfront */ | |
932 | memmove(line->cells + from, | |
933 | line->cells + from + num, | |
934 | sizeof(*line->cells) * move); | |
935 | ||
936 | /* invalidate copied cells */ | |
937 | for (i = 0; i < move; ++i) | |
938 | line->cells[from + i].age = age; | |
939 | ||
940 | /* initialize tail that was moved away */ | |
941 | term_cell_init_n(line->cells + line->width - rem, | |
942 | rem, | |
943 | attr, | |
944 | age); | |
945 | ||
946 | /* reset remaining cells in case the move was too small */ | |
947 | if (num > move) | |
948 | term_cell_clear_n(line->cells + from + move, | |
949 | num - move, | |
950 | attr, | |
951 | age); | |
952 | } else { | |
953 | /* reset cells */ | |
954 | term_cell_clear_n(line->cells + from, | |
955 | num, | |
956 | attr, | |
957 | age); | |
958 | } | |
959 | ||
960 | /* adjust fill-state */ | |
961 | if (from + num < line->fill) | |
962 | line->fill -= num; | |
963 | else if (from < line->fill) | |
964 | line->fill = from; | |
965 | } | |
966 | ||
967 | /** | |
968 | * term_line_append_combchar() - Append combining char to existing cell | |
969 | * @line: line to modify | |
970 | * @pos_x: position of cell to append combining char to | |
971 | * @ucs4: combining character to append | |
972 | * @age: current age for all modifications | |
973 | * | |
974 | * Unicode allows trailing combining characters, which belong to the | |
975 | * char in front of them. The caller is responsible of detecting | |
976 | * combining characters and calling term_line_append_combchar() instead of | |
977 | * term_line_write(). This simply appends the char to the correct cell then. | |
978 | * If the cell is not in the visible area, this call is skipped. | |
979 | * | |
980 | * Note that control-sequences are not 100% compatible with combining | |
981 | * characters as they require delayed parsing. However, we must handle | |
982 | * control-sequences immediately. Therefore, there might be trailing | |
983 | * combining chars that should be discarded by the parser. | |
984 | * However, to prevent programming errors, we're also being pedantic | |
985 | * here and discard weirdly placed combining chars. This prevents | |
986 | * situations were invalid content is parsed into the terminal and you | |
987 | * might end up with cells containing only combining chars. | |
988 | * | |
989 | * Long story short: To get combining-characters working with old-fashioned | |
990 | * terminal-emulation, we parse them exclusively for direct cell-writes. Other | |
991 | * combining-characters are usually simply discarded and ignored. | |
992 | */ | |
993 | void term_line_append_combchar(term_line *line, unsigned int pos_x, uint32_t ucs4, term_age_t age) { | |
994 | assert(line); | |
995 | ||
996 | if (pos_x >= line->width) | |
997 | return; | |
998 | ||
999 | /* Unused cell? Skip appending any combining chars then. */ | |
1000 | if (term_char_is_null(line->cells[pos_x].ch)) | |
1001 | return; | |
1002 | ||
1003 | term_cell_append(line->cells + pos_x, ucs4, age); | |
1004 | } | |
1005 | ||
1006 | /** | |
1007 | * term_line_erase() - Erase parts of a line | |
1008 | * @line: line to modify | |
1009 | * @from: position to start the erase | |
1010 | * @num: number of cells to erase | |
1011 | * @attr: attributes to initialize erased cells with | |
1012 | * @age: current age for all modifications | |
1013 | * @keep_protected: true if protected cells should be kept | |
1014 | * | |
06b643e7 | 1015 | * This is the standard erase operation. It clears all cells in the targeted |
84da4a30 DH |
1016 | * area and re-initializes them. Cells to the right are not shifted left, you |
1017 | * must use DELETE to achieve that. Cells outside the visible area are skipped. | |
1018 | * | |
1019 | * If @keep_protected is true, protected cells will not be erased. | |
1020 | */ | |
1021 | void term_line_erase(term_line *line, unsigned int from, unsigned int num, const term_attr *attr, term_age_t age, bool keep_protected) { | |
1022 | term_cell *cell; | |
1023 | unsigned int i, last_protected; | |
1024 | ||
1025 | assert(line); | |
1026 | ||
1027 | if (from >= line->width) | |
1028 | return; | |
1029 | if (from + num < from || from + num > line->width) | |
1030 | num = line->width - from; | |
1031 | if (!num) | |
1032 | return; | |
1033 | ||
1034 | last_protected = 0; | |
1035 | for (i = 0; i < num; ++i) { | |
1036 | cell = line->cells + from + i; | |
1037 | if (keep_protected && cell->attr.protect) { | |
1038 | /* only count protected-cells inside the fill-region */ | |
1039 | if (from + i < line->fill) | |
1040 | last_protected = from + i; | |
1041 | ||
1042 | continue; | |
1043 | } | |
1044 | ||
1045 | term_cell_set(cell, TERM_CHAR_NULL, 0, attr, age); | |
1046 | } | |
1047 | ||
1048 | /* Adjust fill-state. This is a bit tricks, we can only adjust it in | |
1049 | * case the erase-region starts inside the fill-region and ends at the | |
1050 | * tail or beyond the fill-region. Otherwise, the current fill-state | |
1051 | * stays as it was. | |
1052 | * Furthermore, we must account for protected cells. The loop above | |
1053 | * ensures that protected-cells are only accounted for if they're | |
1054 | * inside the fill-region. */ | |
1055 | if (from < line->fill && from + num >= line->fill) | |
1056 | line->fill = MAX(from, last_protected); | |
1057 | } | |
1058 | ||
1059 | /** | |
1060 | * term_line_reset() - Reset a line | |
1061 | * @line: line to reset | |
1062 | * @attr: attributes to initialize all cells with | |
1063 | * @age: current age for all modifications | |
1064 | * | |
1065 | * This resets all visible cells of a line and sets their attributes and ages | |
1066 | * to @attr and @age. This is equivalent to erasing a whole line via | |
1067 | * term_line_erase(). | |
1068 | */ | |
1069 | void term_line_reset(term_line *line, const term_attr *attr, term_age_t age) { | |
1070 | assert(line); | |
1071 | ||
1072 | return term_line_erase(line, 0, line->width, attr, age, 0); | |
1073 | } | |
1074 | ||
1075 | /** | |
1076 | * term_line_link() - Link line in front of a list | |
1077 | * @line: line to link | |
1078 | * @first: member pointing to first entry | |
1079 | * @last: member pointing to last entry | |
1080 | * | |
1081 | * This links a line into a list of lines. The line is inserted at the front and | |
1082 | * must not be linked, yet. See the TERM_LINE_LINK() macro for an easier usage of | |
1083 | * this. | |
1084 | */ | |
1085 | void term_line_link(term_line *line, term_line **first, term_line **last) { | |
1086 | assert(line); | |
1087 | assert(first); | |
1088 | assert(last); | |
1089 | assert(!line->lines_prev); | |
1090 | assert(!line->lines_next); | |
1091 | ||
1092 | line->lines_prev = NULL; | |
1093 | line->lines_next = *first; | |
1094 | if (*first) | |
1095 | (*first)->lines_prev = line; | |
1096 | else | |
1097 | *last = line; | |
1098 | *first = line; | |
1099 | } | |
1100 | ||
1101 | /** | |
1102 | * term_line_link_tail() - Link line at tail of a list | |
1103 | * @line: line to link | |
1104 | * @first: member pointing to first entry | |
1105 | * @last: member pointing to last entry | |
1106 | * | |
1107 | * Same as term_line_link() but links the line at the tail. | |
1108 | */ | |
1109 | void term_line_link_tail(term_line *line, term_line **first, term_line **last) { | |
1110 | assert(line); | |
1111 | assert(first); | |
1112 | assert(last); | |
1113 | assert(!line->lines_prev); | |
1114 | assert(!line->lines_next); | |
1115 | ||
1116 | line->lines_next = NULL; | |
1117 | line->lines_prev = *last; | |
1118 | if (*last) | |
1119 | (*last)->lines_next = line; | |
1120 | else | |
1121 | *first = line; | |
1122 | *last = line; | |
1123 | } | |
1124 | ||
1125 | /** | |
1126 | * term_line_unlink() - Unlink line from a list | |
1127 | * @line: line to unlink | |
1128 | * @first: member pointing to first entry | |
1129 | * @last: member pointing to last entry | |
1130 | * | |
1131 | * This unlinks a previously linked line. See TERM_LINE_UNLINK() for an easier to | |
1132 | * use macro. | |
1133 | */ | |
1134 | void term_line_unlink(term_line *line, term_line **first, term_line **last) { | |
1135 | assert(line); | |
1136 | assert(first); | |
1137 | assert(last); | |
1138 | ||
1139 | if (line->lines_prev) | |
1140 | line->lines_prev->lines_next = line->lines_next; | |
1141 | else | |
1142 | *first = line->lines_next; | |
1143 | if (line->lines_next) | |
1144 | line->lines_next->lines_prev = line->lines_prev; | |
1145 | else | |
1146 | *last = line->lines_prev; | |
1147 | ||
1148 | line->lines_prev = NULL; | |
1149 | line->lines_next = NULL; | |
1150 | } | |
28622e8f DH |
1151 | |
1152 | /** | |
1153 | * term_page_new() - Allocate new page | |
1154 | * @out: storage for pointer to new page | |
1155 | * | |
1156 | * Allocate a new page. The initial dimensions are 0/0. | |
1157 | * | |
1158 | * Returns: 0 on success, negative error code on failure. | |
1159 | */ | |
1160 | int term_page_new(term_page **out) { | |
1161 | _term_page_free_ term_page *page = NULL; | |
1162 | ||
1163 | assert_return(out, -EINVAL); | |
1164 | ||
1165 | page = new0(term_page, 1); | |
1166 | if (!page) | |
1167 | return -ENOMEM; | |
1168 | ||
1169 | *out = page; | |
1170 | page = NULL; | |
1171 | return 0; | |
1172 | } | |
1173 | ||
1174 | /** | |
1175 | * term_page_free() - Free page | |
1176 | * @page: page to free or NULL | |
1177 | * | |
1178 | * Free a previously allocated page and all associated data. If @page is NULL, | |
1179 | * this is a no-op. | |
1180 | * | |
1181 | * Returns: NULL | |
1182 | */ | |
1183 | term_page *term_page_free(term_page *page) { | |
1184 | unsigned int i; | |
1185 | ||
1186 | if (!page) | |
1187 | return NULL; | |
1188 | ||
1189 | for (i = 0; i < page->n_lines; ++i) | |
1190 | term_line_free(page->lines[i]); | |
1191 | ||
1192 | free(page->line_cache); | |
1193 | free(page->lines); | |
1194 | free(page); | |
1195 | ||
1196 | return NULL; | |
1197 | } | |
1198 | ||
1199 | /** | |
1200 | * term_page_get_cell() - Return pointer to requested cell | |
1201 | * @page: page to operate on | |
1202 | * @x: x-position of cell | |
1203 | * @y: y-position of cell | |
1204 | * | |
1205 | * This returns a pointer to the cell at position @x/@y. You're free to modify | |
1206 | * this cell as much as you like. However, once you call any other function on | |
1207 | * the page, you must drop the pointer to the cell. | |
1208 | * | |
1209 | * Returns: Pointer to the cell or NULL if out of the visible area. | |
1210 | */ | |
1211 | term_cell *term_page_get_cell(term_page *page, unsigned int x, unsigned int y) { | |
1212 | assert_return(page, NULL); | |
1213 | ||
1214 | if (x >= page->width) | |
1215 | return NULL; | |
1216 | if (y >= page->height) | |
1217 | return NULL; | |
1218 | ||
1219 | return &page->lines[y]->cells[x]; | |
1220 | } | |
1221 | ||
1222 | /** | |
1223 | * page_scroll_up() - Scroll up | |
1224 | * @page: page to operate on | |
1225 | * @new_width: width to use for any new line moved into the visible area | |
1226 | * @num: number of lines to scroll up | |
1227 | * @attr: attributes to set on new lines | |
1228 | * @age: age to use for all modifications | |
1229 | * @history: history to use for old lines or NULL | |
1230 | * | |
1231 | * This scrolls the scroll-region by @num lines. New lines are cleared and reset | |
1232 | * with the given attributes. Old lines are moved into the history if non-NULL. | |
1233 | * If a new line is allocated, moved from the history buffer or moved from | |
1234 | * outside the visible region into the visible region, this call makes sure it | |
1235 | * has at least @width cells allocated. If a possible memory-allocation fails, | |
1236 | * the previous line is reused. This has the side effect, that it will not be | |
1237 | * linked into the history buffer. | |
1238 | * | |
1239 | * If the scroll-region is empty, this is a no-op. | |
1240 | */ | |
1241 | static void page_scroll_up(term_page *page, unsigned int new_width, unsigned int num, const term_attr *attr, term_age_t age, term_history *history) { | |
1242 | term_line *line, **cache; | |
1243 | unsigned int i; | |
1244 | int r; | |
1245 | ||
1246 | assert(page); | |
1247 | ||
1248 | if (num > page->scroll_num) | |
1249 | num = page->scroll_num; | |
1250 | if (num < 1) | |
1251 | return; | |
1252 | ||
1253 | /* Better safe than sorry: avoid under-allocating lines, even when | |
1254 | * resizing. */ | |
1255 | new_width = MAX(new_width, page->width); | |
1256 | ||
1257 | cache = page->line_cache; | |
1258 | ||
1259 | /* Try moving lines into history and allocate new lines for each moved | |
1260 | * line. In case allocation fails, or if we have no history, reuse the | |
1261 | * line. | |
1262 | * We keep the lines in the line-cache so we can safely move the | |
1263 | * remaining lines around. */ | |
1264 | for (i = 0; i < num; ++i) { | |
1265 | line = page->lines[page->scroll_idx + i]; | |
1266 | ||
1267 | r = -EAGAIN; | |
1268 | if (history) { | |
1269 | r = term_line_new(&cache[i]); | |
1270 | if (r >= 0) { | |
1271 | r = term_line_reserve(cache[i], | |
1272 | new_width, | |
1273 | attr, | |
1274 | age, | |
1275 | 0); | |
1276 | if (r < 0) | |
1277 | term_line_free(cache[i]); | |
1278 | else | |
1279 | term_line_set_width(cache[i], page->width); | |
1280 | } | |
1281 | } | |
1282 | ||
1283 | if (r >= 0) { | |
1284 | term_history_push(history, line); | |
1285 | } else { | |
1286 | cache[i] = line; | |
1287 | term_line_reset(line, attr, age); | |
1288 | } | |
1289 | } | |
1290 | ||
1291 | if (num < page->scroll_num) { | |
1292 | memmove(page->lines + page->scroll_idx, | |
1293 | page->lines + page->scroll_idx + num, | |
1294 | sizeof(*page->lines) * (page->scroll_num - num)); | |
1295 | ||
1296 | /* update age of moved lines */ | |
1297 | for (i = 0; i < page->scroll_num - num; ++i) | |
1298 | page->lines[page->scroll_idx + i]->age = age; | |
1299 | } | |
1300 | ||
1301 | /* copy remaining lines from cache; age is already updated */ | |
1302 | memcpy(page->lines + page->scroll_idx + page->scroll_num - num, | |
1303 | cache, | |
1304 | sizeof(*cache) * num); | |
1305 | ||
1306 | /* update fill */ | |
1307 | page->scroll_fill -= MIN(page->scroll_fill, num); | |
1308 | } | |
1309 | ||
1310 | /** | |
1311 | * page_scroll_down() - Scroll down | |
1312 | * @page: page to operate on | |
1313 | * @new_width: width to use for any new line moved into the visible area | |
1314 | * @num: number of lines to scroll down | |
1315 | * @attr: attributes to set on new lines | |
1316 | * @age: age to use for all modifications | |
1317 | * @history: history to use for new lines or NULL | |
1318 | * | |
1319 | * This scrolls the scroll-region by @num lines. New lines are retrieved from | |
1320 | * the history or cleared if the history is empty or NULL. | |
1321 | * | |
1322 | * Usually, scroll-down implies that new lines are cleared. Therefore, you're | |
1323 | * highly encouraged to set @history to NULL. However, if you resize a terminal, | |
1324 | * you might want to include history-lines in the new area. In that case, you | |
1325 | * should set @history to non-NULL. | |
1326 | * | |
1327 | * If a new line is allocated, moved from the history buffer or moved from | |
1328 | * outside the visible region into the visible region, this call makes sure it | |
1329 | * has at least @width cells allocated. If a possible memory-allocation fails, | |
1330 | * the previous line is reused. This will have the side-effect that lines from | |
1331 | * the history will not get visible on-screen but kept in history. | |
1332 | * | |
1333 | * If the scroll-region is empty, this is a no-op. | |
1334 | */ | |
1335 | static void page_scroll_down(term_page *page, unsigned int new_width, unsigned int num, const term_attr *attr, term_age_t age, term_history *history) { | |
1336 | term_line *line, **cache, *t; | |
1337 | unsigned int i, last_idx; | |
1338 | ||
1339 | assert(page); | |
1340 | ||
1341 | if (num > page->scroll_num) | |
1342 | num = page->scroll_num; | |
1343 | if (num < 1) | |
1344 | return; | |
1345 | ||
1346 | /* Better safe than sorry: avoid under-allocating lines, even when | |
1347 | * resizing. */ | |
1348 | new_width = MAX(new_width, page->width); | |
1349 | ||
1350 | cache = page->line_cache; | |
1351 | last_idx = page->scroll_idx + page->scroll_num - 1; | |
1352 | ||
1353 | /* Try pulling out lines from history; if history is empty or if no | |
1354 | * history is given, we reuse the to-be-removed lines. Otherwise, those | |
1355 | * lines are released. */ | |
1356 | for (i = 0; i < num; ++i) { | |
1357 | line = page->lines[last_idx - i]; | |
1358 | ||
1359 | t = NULL; | |
1360 | if (history) | |
1361 | t = term_history_pop(history, new_width, attr, age); | |
1362 | ||
1363 | if (t) { | |
1364 | cache[num - 1 - i] = t; | |
1365 | term_line_free(line); | |
1366 | } else { | |
1367 | cache[num - 1 - i] = line; | |
1368 | term_line_reset(line, attr, age); | |
1369 | } | |
1370 | } | |
1371 | ||
1372 | if (num < page->scroll_num) { | |
1373 | memmove(page->lines + page->scroll_idx + num, | |
1374 | page->lines + page->scroll_idx, | |
1375 | sizeof(*page->lines) * (page->scroll_num - num)); | |
1376 | ||
1377 | /* update age of moved lines */ | |
1378 | for (i = 0; i < page->scroll_num - num; ++i) | |
1379 | page->lines[page->scroll_idx + num + i]->age = age; | |
1380 | } | |
1381 | ||
1382 | /* copy remaining lines from cache; age is already updated */ | |
1383 | memcpy(page->lines + page->scroll_idx, | |
1384 | cache, | |
1385 | sizeof(*cache) * num); | |
1386 | ||
1387 | /* update fill; but only if there's already content in it */ | |
1388 | if (page->scroll_fill > 0) | |
1389 | page->scroll_fill = MIN(page->scroll_num, | |
1390 | page->scroll_fill + num); | |
1391 | } | |
1392 | ||
1393 | /** | |
1394 | * page_reserve() - Reserve page area | |
1395 | * @page: page to modify | |
1396 | * @cols: required columns (width) | |
1397 | * @rows: required rows (height) | |
1398 | * @attr: attributes for newly allocated cells | |
1399 | * @age: age to set on any modified cells | |
1400 | * | |
1401 | * This allocates the required amount of lines and cells to guarantee that the | |
1402 | * page has at least the demanded dimensions of @cols x @rows. Note that this | |
1403 | * never shrinks the page-memory. We keep cells allocated for performance | |
1404 | * reasons. | |
1405 | * | |
1406 | * Additionally to allocating lines, this also clears any newly added cells so | |
1407 | * you can safely change the size afterwards without clearing new cells. | |
1408 | * | |
1409 | * Note that you must be careful what operations you call on the page between | |
1410 | * page_reserve() and updating page->width/height. Any newly allocated line (or | |
1411 | * shifted line) might not meet your new width/height expectations. | |
1412 | * | |
1413 | * Returns: 0 on success, negative error code on failure. | |
1414 | */ | |
1415 | int term_page_reserve(term_page *page, unsigned int cols, unsigned int rows, const term_attr *attr, term_age_t age) { | |
1416 | _term_line_free_ term_line *line = NULL; | |
1417 | unsigned int i, min_lines; | |
1418 | term_line **t; | |
1419 | int r; | |
1420 | ||
1421 | assert_return(page, -EINVAL); | |
1422 | ||
1423 | /* | |
1424 | * First make sure the first MIN(page->n_lines, rows) lines have at | |
1425 | * least the required width of @cols. This does not modify any visible | |
1426 | * cells in the existing @page->width x @page->height area, therefore, | |
1427 | * we can safely bail out afterwards in case anything else fails. | |
1428 | * Note that lines in between page->height and page->n_lines might be | |
1429 | * shorter than page->width. Hence, we need to resize them all, but we | |
1430 | * can skip some of them for better performance. | |
1431 | */ | |
1432 | min_lines = MIN(page->n_lines, rows); | |
1433 | for (i = 0; i < min_lines; ++i) { | |
1434 | /* lines below page->height have at least page->width cells */ | |
1435 | if (cols < page->width && i < page->height) | |
1436 | continue; | |
1437 | ||
1438 | r = term_line_reserve(page->lines[i], | |
1439 | cols, | |
1440 | attr, | |
1441 | age, | |
1442 | (i < page->height) ? page->width : 0); | |
1443 | if (r < 0) | |
1444 | return r; | |
1445 | } | |
1446 | ||
1447 | /* | |
1448 | * We now know the first @min_lines lines have at least width @cols and | |
1449 | * are prepared for resizing. We now only have to allocate any | |
1450 | * additional lines below @min_lines in case @rows is greater than | |
1451 | * page->n_lines. | |
1452 | */ | |
1453 | if (rows > page->n_lines) { | |
1454 | t = realloc_multiply(page->lines, sizeof(*t), rows); | |
1455 | if (!t) | |
1456 | return -ENOMEM; | |
1457 | page->lines = t; | |
1458 | ||
1459 | t = realloc_multiply(page->line_cache, sizeof(*t), rows); | |
1460 | if (!t) | |
1461 | return -ENOMEM; | |
1462 | page->line_cache = t; | |
1463 | ||
1464 | while (page->n_lines < rows) { | |
1465 | r = term_line_new(&line); | |
1466 | if (r < 0) | |
1467 | return r; | |
1468 | ||
1469 | r = term_line_reserve(line, cols, attr, age, 0); | |
1470 | if (r < 0) | |
1471 | return r; | |
1472 | ||
1473 | page->lines[page->n_lines++] = line; | |
1474 | line = NULL; | |
1475 | } | |
1476 | } | |
1477 | ||
1478 | return 0; | |
1479 | } | |
1480 | ||
1481 | /** | |
1482 | * term_page_resize() - Resize page | |
1483 | * @page: page to modify | |
1484 | * @cols: number of columns (width) | |
1485 | * @rows: number of rows (height) | |
1486 | * @attr: attributes for newly allocated cells | |
1487 | * @age: age to set on any modified cells | |
1488 | * @history: history buffer to use for new/old lines or NULL | |
1489 | * | |
1490 | * This changes the visible dimensions of a page. You must have called | |
1491 | * term_page_reserve() beforehand, otherwise, this will fail. | |
1492 | * | |
1493 | * Returns: 0 on success, negative error code on failure. | |
1494 | */ | |
1495 | void term_page_resize(term_page *page, unsigned int cols, unsigned int rows, const term_attr *attr, term_age_t age, term_history *history) { | |
1496 | unsigned int i, num, empty, max, old_height; | |
1497 | term_line *line; | |
1498 | ||
1499 | assert(page); | |
1500 | assert(page->n_lines >= rows); | |
1501 | ||
1502 | old_height = page->height; | |
1503 | ||
1504 | if (rows < old_height) { | |
1505 | /* | |
1506 | * If we decrease the terminal-height, we emulate a scroll-up. | |
1507 | * This way, existing data from the scroll-area is moved into | |
1508 | * the history, making space at the bottom to reduce the screen | |
1509 | * height. In case the scroll-fill indicates empty lines, we | |
1510 | * reduce the amount of scrolled lines. | |
1511 | * Once scrolled, we have to move the lower margin from below | |
1512 | * the scroll area up so it is preserved. | |
1513 | */ | |
1514 | ||
1515 | /* move lines to history if scroll region is filled */ | |
1516 | num = old_height - rows; | |
1517 | empty = page->scroll_num - page->scroll_fill; | |
1518 | if (num > empty) | |
1519 | page_scroll_up(page, | |
1520 | cols, | |
1521 | num - empty, | |
1522 | attr, | |
1523 | age, | |
1524 | history); | |
1525 | ||
1526 | /* move lower margin up; drop its lines if not enough space */ | |
1527 | num = LESS_BY(old_height, page->scroll_idx + page->scroll_num); | |
1528 | max = LESS_BY(rows, page->scroll_idx); | |
1529 | num = MIN(num, max); | |
1530 | if (num > 0) { | |
1531 | unsigned int top, bottom; | |
1532 | ||
1533 | top = rows - num; | |
1534 | bottom = page->scroll_idx + page->scroll_num; | |
1535 | ||
1536 | /* might overlap; must run topdown, not bottomup */ | |
1537 | for (i = 0; i < num; ++i) { | |
1538 | line = page->lines[top + i]; | |
1539 | page->lines[top + i] = page->lines[bottom + i]; | |
1540 | page->lines[bottom + i] = line; | |
1541 | } | |
1542 | } | |
1543 | ||
1544 | /* update vertical extents */ | |
1545 | page->height = rows; | |
1546 | page->scroll_idx = MIN(page->scroll_idx, rows); | |
1547 | page->scroll_num -= MIN(page->scroll_num, old_height - rows); | |
1548 | /* fill is already up-to-date or 0 due to scroll-up */ | |
1549 | } else if (rows > old_height) { | |
1550 | /* | |
1551 | * If we increase the terminal-height, we emulate a scroll-down | |
1552 | * and fetch new lines from the history. | |
1553 | * New lines are always accounted to the scroll-region. Thus we | |
1554 | * have to preserve the lower margin first, by moving it down. | |
1555 | */ | |
1556 | ||
1557 | /* move lower margin down */ | |
1558 | num = LESS_BY(old_height, page->scroll_idx + page->scroll_num); | |
1559 | if (num > 0) { | |
1560 | unsigned int top, bottom; | |
1561 | ||
1562 | top = page->scroll_idx + page->scroll_num; | |
1563 | bottom = top + (rows - old_height); | |
1564 | ||
1565 | /* might overlap; must run bottomup, not topdown */ | |
1566 | for (i = num; i-- > 0; ) { | |
1567 | line = page->lines[top + i]; | |
1568 | page->lines[top + i] = page->lines[bottom + i]; | |
1569 | page->lines[bottom + i] = line; | |
1570 | } | |
1571 | } | |
1572 | ||
1573 | /* update vertical extents */ | |
1574 | page->height = rows; | |
1575 | page->scroll_num = MIN(LESS_BY(rows, page->scroll_idx), | |
1576 | page->scroll_num + (rows - old_height)); | |
1577 | ||
1578 | /* check how many lines can be received from history */ | |
1579 | if (history) | |
1580 | num = term_history_peek(history, | |
1581 | rows - old_height, | |
1582 | cols, | |
1583 | attr, | |
1584 | age); | |
1585 | else | |
1586 | num = 0; | |
1587 | ||
1588 | /* retrieve new lines from history if available */ | |
1589 | if (num > 0) | |
1590 | page_scroll_down(page, | |
1591 | cols, | |
1592 | num, | |
1593 | attr, | |
1594 | age, | |
1595 | history); | |
1596 | } | |
1597 | ||
1598 | /* set horizontal extents */ | |
1599 | page->width = cols; | |
1600 | for (i = 0; i < page->height; ++i) | |
1601 | term_line_set_width(page->lines[i], cols); | |
1602 | } | |
1603 | ||
1604 | /** | |
1605 | * term_page_write() - Write to a single cell | |
1606 | * @page: page to operate on | |
1607 | * @pos_x: x-position of cell to write to | |
1608 | * @pos_y: y-position of cell to write to | |
1609 | * @ch: character to write | |
1610 | * @cwidth: character-width of @ch | |
1611 | * @attr: attributes to set on the cell or NULL | |
1612 | * @age: age to use for all modifications | |
1613 | * @insert_mode: true if INSERT-MODE is enabled | |
1614 | * | |
1615 | * This writes a character to a specific cell. If the cell is beyond bounds, | |
1616 | * this is a no-op. @attr and @age are used to update the cell. @flags can be | |
1617 | * used to alter the behavior of this function. | |
1618 | * | |
1619 | * This is a wrapper around term_line_write(). | |
1620 | * | |
1621 | * This call does not wrap around lines. That is, this only operates on a single | |
1622 | * line. | |
1623 | */ | |
1624 | void term_page_write(term_page *page, unsigned int pos_x, unsigned int pos_y, term_char_t ch, unsigned int cwidth, const term_attr *attr, term_age_t age, bool insert_mode) { | |
1625 | assert(page); | |
1626 | ||
1627 | if (pos_y >= page->height) | |
1628 | return; | |
1629 | ||
1630 | term_line_write(page->lines[pos_y], pos_x, ch, cwidth, attr, age, insert_mode); | |
1631 | } | |
1632 | ||
1633 | /** | |
1634 | * term_page_insert_cells() - Insert cells into a line | |
1635 | * @page: page to operate on | |
1636 | * @from_x: x-position where to insert new cells | |
1637 | * @from_y: y-position where to insert new cells | |
1638 | * @num: number of cells to insert | |
1639 | * @attr: attributes to set on new cells or NULL | |
1640 | * @age: age to use for all modifications | |
1641 | * | |
1642 | * This inserts new cells into a given line. This is a wrapper around | |
1643 | * term_line_insert(). | |
1644 | * | |
1645 | * This call does not wrap around lines. That is, this only operates on a single | |
1646 | * line. | |
1647 | */ | |
1648 | void term_page_insert_cells(term_page *page, unsigned int from_x, unsigned int from_y, unsigned int num, const term_attr *attr, term_age_t age) { | |
1649 | assert(page); | |
1650 | ||
1651 | if (from_y >= page->height) | |
1652 | return; | |
1653 | ||
1654 | term_line_insert(page->lines[from_y], from_x, num, attr, age); | |
1655 | } | |
1656 | ||
1657 | /** | |
1658 | * term_page_delete_cells() - Delete cells from a line | |
1659 | * @page: page to operate on | |
1660 | * @from_x: x-position where to delete cells | |
1661 | * @from_y: y-position where to delete cells | |
1662 | * @num: number of cells to delete | |
1663 | * @attr: attributes to set on new cells or NULL | |
1664 | * @age: age to use for all modifications | |
1665 | * | |
1666 | * This deletes cells from a given line. This is a wrapper around | |
1667 | * term_line_delete(). | |
1668 | * | |
1669 | * This call does not wrap around lines. That is, this only operates on a single | |
1670 | * line. | |
1671 | */ | |
1672 | void term_page_delete_cells(term_page *page, unsigned int from_x, unsigned int from_y, unsigned int num, const term_attr *attr, term_age_t age) { | |
1673 | assert(page); | |
1674 | ||
1675 | if (from_y >= page->height) | |
1676 | return; | |
1677 | ||
1678 | term_line_delete(page->lines[from_y], from_x, num, attr, age); | |
1679 | } | |
1680 | ||
1681 | /** | |
1682 | * term_page_append_combchar() - Append combining-character to a cell | |
1683 | * @page: page to operate on | |
1684 | * @pos_x: x-position of target cell | |
1685 | * @pos_y: y-position of target cell | |
1686 | * @ucs4: combining character to append | |
1687 | * @age: age to use for all modifications | |
1688 | * | |
1689 | * This appends a combining-character to a specific cell. This is a wrapper | |
1690 | * around term_line_append_combchar(). | |
1691 | */ | |
1692 | void term_page_append_combchar(term_page *page, unsigned int pos_x, unsigned int pos_y, uint32_t ucs4, term_age_t age) { | |
1693 | assert(page); | |
1694 | ||
1695 | if (pos_y >= page->height) | |
1696 | return; | |
1697 | ||
1698 | term_line_append_combchar(page->lines[pos_y], pos_x, ucs4, age); | |
1699 | } | |
1700 | ||
1701 | /** | |
1702 | * term_page_erase() - Erase parts of a page | |
1703 | * @page: page to operate on | |
1704 | * @from_x: x-position where to start erasure (inclusive) | |
1705 | * @from_y: y-position where to start erasure (inclusive) | |
1706 | * @to_x: x-position where to stop erasure (inclusive) | |
1707 | * @to_y: y-position where to stop erasure (inclusive) | |
1708 | * @attr: attributes to set on cells | |
1709 | * @age: age to use for all modifications | |
1710 | * @keep_protected: true if protected cells should be kept | |
1711 | * | |
1712 | * This erases all cells starting at @from_x/@from_y up to @to_x/@to_y. Note | |
1713 | * that this wraps around line-boundaries so lines between @from_y and @to_y | |
1714 | * are cleared entirely. | |
1715 | * | |
1716 | * Lines outside the visible area are left untouched. | |
1717 | */ | |
1718 | void term_page_erase(term_page *page, unsigned int from_x, unsigned int from_y, unsigned int to_x, unsigned int to_y, const term_attr *attr, term_age_t age, bool keep_protected) { | |
1719 | unsigned int i, from, to; | |
1720 | ||
1721 | assert(page); | |
1722 | ||
1723 | for (i = from_y; i <= to_y && i < page->height; ++i) { | |
1724 | from = 0; | |
1725 | to = page->width; | |
1726 | ||
1727 | if (i == from_y) | |
1728 | from = from_x; | |
1729 | if (i == to_y) | |
1730 | to = to_x; | |
1731 | ||
1732 | term_line_erase(page->lines[i], | |
1733 | from, | |
1734 | LESS_BY(to, from), | |
1735 | attr, | |
1736 | age, | |
1737 | keep_protected); | |
1738 | } | |
1739 | } | |
1740 | ||
1741 | /** | |
1742 | * term_page_reset() - Reset page | |
1743 | * @page: page to modify | |
1744 | * @attr: attributes to set on cells | |
1745 | * @age: age to use for all modifications | |
1746 | * | |
1747 | * This erases the whole visible page. See term_page_erase(). | |
1748 | */ | |
1749 | void term_page_reset(term_page *page, const term_attr *attr, term_age_t age) { | |
1750 | assert(page); | |
1751 | ||
1752 | return term_page_erase(page, | |
1753 | 0, 0, | |
1754 | page->width - 1, page->height - 1, | |
1755 | attr, | |
1756 | age, | |
1757 | 0); | |
1758 | } | |
1759 | ||
1760 | /** | |
1761 | * term_page_set_scroll_region() - Set scroll region | |
1762 | * @page: page to operate on | |
1763 | * @idx: start-index of scroll region | |
1764 | * @num: number of lines in scroll region | |
1765 | * | |
1766 | * This sets the scroll region of a page. Whenever an operation needs to scroll | |
1767 | * lines, it scrolls them inside of that region. Lines outside the region are | |
1768 | * left untouched. In case a scroll-operation is targeted outside of this | |
1769 | * region, it will implicitly get a scroll-region of only one line (i.e., no | |
1770 | * scroll region at all). | |
1771 | * | |
1772 | * Note that the scroll-region is clipped to the current page-extents. Growing | |
1773 | * or shrinking the page always accounts new/old lines to the scroll region and | |
1774 | * moves top/bottom margins accordingly so they're preserved. | |
1775 | */ | |
1776 | void term_page_set_scroll_region(term_page *page, unsigned int idx, unsigned int num) { | |
1777 | assert(page); | |
1778 | ||
1779 | if (page->height < 1) { | |
1780 | page->scroll_idx = 0; | |
1781 | page->scroll_num = 0; | |
1782 | } else { | |
1783 | page->scroll_idx = MIN(idx, page->height - 1); | |
1784 | page->scroll_num = MIN(num, page->height - page->scroll_idx); | |
1785 | } | |
1786 | } | |
1787 | ||
1788 | /** | |
1789 | * term_page_scroll_up() - Scroll up | |
1790 | * @page: page to operate on | |
1791 | * @num: number of lines to scroll up | |
1792 | * @attr: attributes to set on new lines | |
1793 | * @age: age to use for all modifications | |
1794 | * @history: history to use for old lines or NULL | |
1795 | * | |
1796 | * This scrolls the scroll-region by @num lines. New lines are cleared and reset | |
1797 | * with the given attributes. Old lines are moved into the history if non-NULL. | |
1798 | * | |
1799 | * If the scroll-region is empty, this is a no-op. | |
1800 | */ | |
1801 | void term_page_scroll_up(term_page *page, unsigned int num, const term_attr *attr, term_age_t age, term_history *history) { | |
1802 | page_scroll_up(page, page->width, num, attr, age, history); | |
1803 | } | |
1804 | ||
1805 | /** | |
1806 | * term_page_scroll_down() - Scroll down | |
1807 | * @page: page to operate on | |
1808 | * @num: number of lines to scroll down | |
1809 | * @attr: attributes to set on new lines | |
1810 | * @age: age to use for all modifications | |
1811 | * @history: history to use for new lines or NULL | |
1812 | * | |
1813 | * This scrolls the scroll-region by @num lines. New lines are retrieved from | |
1814 | * the history or cleared if the history is empty or NULL. | |
1815 | * | |
1816 | * Usually, scroll-down implies that new lines are cleared. Therefore, you're | |
1817 | * highly encouraged to set @history to NULL. However, if you resize a terminal, | |
1818 | * you might want to include history-lines in the new area. In that case, you | |
1819 | * should set @history to non-NULL. | |
1820 | * | |
1821 | * If the scroll-region is empty, this is a no-op. | |
1822 | */ | |
1823 | void term_page_scroll_down(term_page *page, unsigned int num, const term_attr *attr, term_age_t age, term_history *history) { | |
1824 | page_scroll_down(page, page->width, num, attr, age, history); | |
1825 | } | |
1826 | ||
1827 | /** | |
1828 | * term_page_insert_lines() - Insert new lines | |
1829 | * @page: page to operate on | |
1830 | * @pos_y: y-position where to insert new lines | |
1831 | * @num: number of lines to insert | |
1832 | * @attr: attributes to set on new lines | |
1833 | * @age: age to use for all modifications | |
1834 | * | |
1835 | * This inserts @num new lines at position @pos_y. If @pos_y is beyond | |
1836 | * boundaries or @num is 0, this is a no-op. | |
1837 | * All lines below @pos_y are moved down to make space for the new lines. Lines | |
1838 | * on the bottom are dropped. Note that this only moves lines above or inside | |
1839 | * the scroll-region. If @pos_y is below the scroll-region, a scroll-region of | |
1840 | * one line is implied (which means the line is simply cleared). | |
1841 | */ | |
1842 | void term_page_insert_lines(term_page *page, unsigned int pos_y, unsigned int num, const term_attr *attr, term_age_t age) { | |
1843 | unsigned int scroll_idx, scroll_num; | |
1844 | ||
1845 | assert(page); | |
1846 | ||
1847 | if (pos_y >= page->height) | |
1848 | return; | |
1849 | if (num >= page->height) | |
1850 | num = page->height; | |
1851 | ||
1852 | /* remember scroll-region */ | |
1853 | scroll_idx = page->scroll_idx; | |
1854 | scroll_num = page->scroll_num; | |
1855 | ||
1856 | /* set scroll-region temporarily so we can reuse scroll_down() */ | |
1857 | { | |
1858 | page->scroll_idx = pos_y; | |
1859 | if (pos_y >= scroll_idx + scroll_num) | |
1860 | page->scroll_num = 1; | |
1861 | else if (pos_y >= scroll_idx) | |
1862 | page->scroll_num -= pos_y - scroll_idx; | |
1863 | else | |
1864 | page->scroll_num += scroll_idx - pos_y; | |
1865 | ||
1866 | term_page_scroll_down(page, num, attr, age, NULL); | |
1867 | } | |
1868 | ||
1869 | /* reset scroll-region */ | |
1870 | page->scroll_idx = scroll_idx; | |
1871 | page->scroll_num = scroll_num; | |
1872 | } | |
1873 | ||
1874 | /** | |
1875 | * term_page_delete_lines() - Delete lines | |
1876 | * @page: page to operate on | |
1877 | * @pos_y: y-position where to delete lines | |
1878 | * @num: number of lines to delete | |
1879 | * @attr: attributes to set on new lines | |
1880 | * @age: age to use for all modifications | |
1881 | * | |
1882 | * This deletes @num lines at position @pos_y. If @pos_y is beyond boundaries or | |
1883 | * @num is 0, this is a no-op. | |
1884 | * All lines below @pos_y are moved up into the newly made space. New lines | |
1885 | * on the bottom are clear. Note that this only moves lines above or inside | |
1886 | * the scroll-region. If @pos_y is below the scroll-region, a scroll-region of | |
1887 | * one line is implied (which means the line is simply cleared). | |
1888 | */ | |
1889 | void term_page_delete_lines(term_page *page, unsigned int pos_y, unsigned int num, const term_attr *attr, term_age_t age) { | |
1890 | unsigned int scroll_idx, scroll_num; | |
1891 | ||
1892 | assert(page); | |
1893 | ||
1894 | if (pos_y >= page->height) | |
1895 | return; | |
1896 | if (num >= page->height) | |
1897 | num = page->height; | |
1898 | ||
1899 | /* remember scroll-region */ | |
1900 | scroll_idx = page->scroll_idx; | |
1901 | scroll_num = page->scroll_num; | |
1902 | ||
1903 | /* set scroll-region temporarily so we can reuse scroll_up() */ | |
1904 | { | |
1905 | page->scroll_idx = pos_y; | |
1906 | if (pos_y >= scroll_idx + scroll_num) | |
1907 | page->scroll_num = 1; | |
1908 | else if (pos_y > scroll_idx) | |
1909 | page->scroll_num -= pos_y - scroll_idx; | |
1910 | else | |
1911 | page->scroll_num += scroll_idx - pos_y; | |
1912 | ||
1913 | term_page_scroll_up(page, num, attr, age, NULL); | |
1914 | } | |
1915 | ||
1916 | /* reset scroll-region */ | |
1917 | page->scroll_idx = scroll_idx; | |
1918 | page->scroll_num = scroll_num; | |
1919 | } | |
1920 | ||
1921 | /** | |
1922 | * term_history_new() - Create new history object | |
1923 | * @out: storage for pointer to new history | |
1924 | * | |
1925 | * Create a new history object. Histories are used to store scrollback-lines | |
1926 | * from VTE pages. You're highly recommended to set a history-limit on | |
1927 | * history->max_lines and trim it via term_history_trim(), otherwise history | |
1928 | * allocations are unlimited. | |
1929 | * | |
1930 | * Returns: 0 on success, negative error code on failure. | |
1931 | */ | |
1932 | int term_history_new(term_history **out) { | |
1933 | _term_history_free_ term_history *history = NULL; | |
1934 | ||
1935 | assert_return(out, -EINVAL); | |
1936 | ||
1937 | history = new0(term_history, 1); | |
1938 | if (!history) | |
1939 | return -ENOMEM; | |
1940 | ||
1941 | history->max_lines = 4096; | |
1942 | ||
1943 | *out = history; | |
1944 | history = NULL; | |
1945 | return 0; | |
1946 | } | |
1947 | ||
1948 | /** | |
1949 | * term_history_free() - Free history | |
1950 | * @history: history to free | |
1951 | * | |
1952 | * Clear and free history. You must not access the object afterwards. | |
1953 | * | |
1954 | * Returns: NULL | |
1955 | */ | |
1956 | term_history *term_history_free(term_history *history) { | |
1957 | if (!history) | |
1958 | return NULL; | |
1959 | ||
1960 | term_history_clear(history); | |
1961 | free(history); | |
1962 | return NULL; | |
1963 | } | |
1964 | ||
1965 | /** | |
1966 | * term_history_clear() - Clear history | |
1967 | * @history: history to clear | |
1968 | * | |
1969 | * Remove all linked lines from a history and reset it to its initial state. | |
1970 | */ | |
1971 | void term_history_clear(term_history *history) { | |
1972 | return term_history_trim(history, 0); | |
1973 | } | |
1974 | ||
1975 | /** | |
1976 | * term_history_trim() - Trim history | |
1977 | * @history: history to trim | |
1978 | * @max: maximum number of lines to be left in history | |
1979 | * | |
1980 | * This removes lines from the history until it is smaller than @max. Lines are | |
1981 | * removed from the top. | |
1982 | */ | |
1983 | void term_history_trim(term_history *history, unsigned int max) { | |
1984 | term_line *line; | |
1985 | ||
1986 | if (!history) | |
1987 | return; | |
1988 | ||
1989 | while (history->n_lines > max && (line = history->lines_first)) { | |
1990 | TERM_LINE_UNLINK(line, history); | |
1991 | term_line_free(line); | |
1992 | --history->n_lines; | |
1993 | } | |
1994 | } | |
1995 | ||
1996 | /** | |
1997 | * term_history_push() - Push line into history | |
1998 | * @history: history to work on | |
1999 | * @line: line to push into history | |
2000 | * | |
2001 | * This pushes a line into the given history. It is linked at the tail. In case | |
2002 | * the history is limited, the top-most line might be freed. | |
2003 | */ | |
2004 | void term_history_push(term_history *history, term_line *line) { | |
2005 | assert(history); | |
2006 | assert(line); | |
2007 | ||
2008 | TERM_LINE_LINK_TAIL(line, history); | |
2009 | if (history->max_lines > 0 && history->n_lines >= history->max_lines) { | |
2010 | line = history->lines_first; | |
2011 | TERM_LINE_UNLINK(line, history); | |
2012 | term_line_free(line); | |
2013 | } else { | |
2014 | ++history->n_lines; | |
2015 | } | |
2016 | } | |
2017 | ||
2018 | /** | |
2019 | * term_history_pop() - Retrieve last line from history | |
2020 | * @history: history to work on | |
2021 | * @new_width: width to reserve and set on the line | |
2022 | * @attr: attributes to use for cell reservation | |
2023 | * @age: age to use for cell reservation | |
2024 | * | |
2025 | * This unlinks the last linked line of the history and returns it. This also | |
2026 | * makes sure the line has the given width pre-allocated (see | |
2027 | * term_line_reserve()). If the pre-allocation fails, this returns NULL, so it | |
2028 | * is treated like there's no line in history left. This simplifies | |
2029 | * history-handling on the caller's side in case of allocation errors. No need | |
2030 | * to throw lines away just because the reservation failed. We can keep them in | |
2031 | * history safely, and make them available as scrollback. | |
2032 | * | |
2033 | * Returns: Line from history or NULL | |
2034 | */ | |
2035 | term_line *term_history_pop(term_history *history, unsigned int new_width, const term_attr *attr, term_age_t age) { | |
2036 | term_line *line; | |
2037 | int r; | |
2038 | ||
2039 | assert_return(history, NULL); | |
2040 | ||
2041 | line = history->lines_last; | |
2042 | if (!line) | |
2043 | return NULL; | |
2044 | ||
2045 | r = term_line_reserve(line, new_width, attr, age, line->width); | |
2046 | if (r < 0) | |
2047 | return NULL; | |
2048 | ||
2049 | term_line_set_width(line, new_width); | |
2050 | TERM_LINE_UNLINK(line, history); | |
2051 | --history->n_lines; | |
2052 | ||
2053 | return line; | |
2054 | } | |
2055 | ||
2056 | /** | |
2057 | * term_history_peek() - Return number of available history-lines | |
2058 | * @history: history to work on | |
2059 | * @max: maximum number of lines to look at | |
2060 | * @reserve_width: width to reserve on the line | |
2061 | * @attr: attributes to use for cell reservation | |
2062 | * @age: age to use for cell reservation | |
2063 | * | |
2064 | * This returns the number of available lines in the history given as @history. | |
2065 | * It returns at most @max. For each line that is looked at, the line is | |
2066 | * verified to have at least @reserve_width cells. Valid cells are preserved, | |
2067 | * new cells are initialized with @attr and @age. In case an allocation fails, | |
2068 | * we bail out and return the number of lines that are valid so far. | |
2069 | * | |
2070 | * Usually, this function should be used before running a loop on | |
2071 | * term_history_pop(). This function guarantees that term_history_pop() (with | |
2072 | * the same arguments) will succeed at least the returned number of times. | |
2073 | * | |
2074 | * Returns: Number of valid lines that can be received via term_history_pop(). | |
2075 | */ | |
2076 | unsigned int term_history_peek(term_history *history, unsigned int max, unsigned int reserve_width, const term_attr *attr, term_age_t age) { | |
2077 | unsigned int num; | |
2078 | term_line *line; | |
2079 | int r; | |
2080 | ||
2081 | assert(history); | |
2082 | ||
2083 | num = 0; | |
2084 | line = history->lines_last; | |
2085 | ||
2086 | while (num < max && line) { | |
2087 | r = term_line_reserve(line, reserve_width, attr, age, line->width); | |
2088 | if (r < 0) | |
2089 | break; | |
2090 | ||
2091 | ++num; | |
2092 | line = line->lines_prev; | |
2093 | } | |
2094 | ||
2095 | return num; | |
2096 | } |