xref: /reactos/sdk/lib/conutils/pager.c (revision ea6e7740)
1 /*
2  * PROJECT:     ReactOS Console Utilities Library
3  * LICENSE:     GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+)
4  * PURPOSE:     Console/terminal paging functionality.
5  * COPYRIGHT:   Copyright 2017-2021 Hermes Belusca-Maito
6  *              Copyright 2021 Katayama Hirofumi MZ
7  */
8 
9 /**
10  * @file    pager.c
11  * @ingroup ConUtils
12  *
13  * @brief   Console/terminal paging functionality.
14  **/
15 
16 /* FIXME: Temporary HACK before we cleanly support UNICODE functions */
17 #define UNICODE
18 #define _UNICODE
19 
20 #include <windef.h>
21 #include <winbase.h>
22 // #include <winnls.h>
23 #include <wincon.h>  // Console APIs (only if kernel32 support included)
24 #include <winnls.h> // for WideCharToMultiByte
25 #include <strsafe.h>
26 
27 #include "conutils.h"
28 #include "stream.h"
29 #include "screen.h"
30 #include "pager.h"
31 
32 // Temporary HACK
33 #define CON_STREAM_WRITE    ConStreamWrite
34 
35 #define CP_SHIFTJIS 932  // Japanese Shift-JIS
36 #define CP_HANGUL   949  // Korean Hangul/Wansung
37 #define CP_JOHAB    1361 // Korean Johab
38 #define CP_GB2312   936  // Chinese Simplified (GB2312)
39 #define CP_BIG5     950  // Chinese Traditional (Big5)
40 
41 /* IsFarEastCP(CodePage) */
42 #define IsCJKCodePage(CodePage) \
43     ((CodePage) == CP_SHIFTJIS || (CodePage) == CP_HANGUL || \
44   /* (CodePage) == CP_JOHAB || */ \
45      (CodePage) == CP_BIG5     || (CodePage) == CP_GB2312)
46 
47 static inline INT
48 GetWidthOfCharCJK(
49     IN UINT nCodePage,
50     IN WCHAR ch)
51 {
52     INT ret = WideCharToMultiByte(nCodePage, 0, &ch, 1, NULL, 0, NULL, NULL);
53     if (ret == 0)
54         ret = 1;
55     else if (ret > 2)
56         ret = 2;
57     return ret;
58 }
59 
60 /**
61  * @brief   Retrieves a new text line, or continue fetching the current one.
62  *
63  * @remark  Manages setting Pager's CurrentLine, ichCurr, iEndLine, and the
64  *          line cache (CachedLine, cchCachedLine). Other functions must not
65  *          modify these values.
66  **/
67 static BOOL
68 GetNextLine(
69     IN OUT PCON_PAGER Pager,
70     IN PCTCH TextBuff,
71     IN SIZE_T cch)
72 {
73     SIZE_T ich = Pager->ich;
74     SIZE_T ichStart;
75     SIZE_T cchLine;
76     BOOL bCacheLine;
77 
78     Pager->ichCurr = 0;
79     Pager->iEndLine = 0;
80 
81     /*
82      * If we already had an existing line, then we can safely start a new one
83      * and getting rid of any current cached line. Otherwise, we don't have
84      * a current line and we may be caching a new one, in which case, continue
85      * caching it until it becomes complete.
86      */
87     // INVESTIGATE: Do that only if (ichStart >= iEndLine) ??
88     if (Pager->CurrentLine)
89     {
90         // ASSERT(Pager->CurrentLine == Pager->CachedLine);
91         if (Pager->CachedLine)
92         {
93             HeapFree(GetProcessHeap(), 0, (PVOID)Pager->CachedLine);
94             Pager->CachedLine = NULL;
95             Pager->cchCachedLine = 0;
96         }
97 
98         Pager->CurrentLine = NULL;
99     }
100 
101     /* Nothing else to read if we are past the end of the buffer */
102     if (ich >= cch)
103     {
104         /* If we have a pending cached line, terminate it now */
105         if (Pager->CachedLine)
106             goto TerminateLine;
107 
108         /* Otherwise, bail out */
109         return FALSE;
110     }
111 
112     /* Start a new line, or continue an existing one */
113     ichStart = ich;
114 
115     /* Find where this line ends, looking for a NEWLINE character.
116      * (NOTE: We cannot use strchr because the buffer is not NULL-terminated) */
117     for (; ich < cch; ++ich)
118     {
119         if (TextBuff[ich] == TEXT('\n'))
120         {
121             ++ich;
122             break;
123         }
124     }
125     Pager->ich = ich;
126 
127     cchLine = (ich - ichStart);
128 
129     //
130     // FIXME: Impose a maximum string limit when the line is cached, in order
131     // not to potentially grow memory indefinitely. When the limit is reached,
132     // terminate the line.
133     //
134 
135     /*
136      * If we have stopped because we have exhausted the text buffer
137      * and we have not found an end-of-line character, this may mean
138      * that the text line spans across different text buffers. If we
139      * have been told so, cache this line: we will complete it during
140      * the next call(s) and only then, display it.
141      * Otherwise, consider the line to be terminated now.
142      */
143     bCacheLine = ((Pager->dwFlags & CON_PAGER_CACHE_INCOMPLETE_LINE) &&
144                   (ich >= cch) && (TextBuff[ich - 1] != TEXT('\n')));
145 
146     /* Allocate, or re-allocate, the cached line buffer */
147     if (bCacheLine && !Pager->CachedLine)
148     {
149         /* We start caching, allocate the cached line buffer */
150         Pager->CachedLine = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,
151                                       cchLine * sizeof(TCHAR));
152         Pager->cchCachedLine = 0;
153 
154         if (!Pager->CachedLine)
155         {
156             SetLastError(ERROR_NOT_ENOUGH_MEMORY);
157             return FALSE;
158         }
159     }
160     else if (Pager->CachedLine)
161     {
162         /* We continue caching, re-allocate the cached line buffer */
163         PVOID ptr = HeapReAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,
164                                 (PVOID)Pager->CachedLine,
165                                 (Pager->cchCachedLine + cchLine) * sizeof(TCHAR));
166         if (!ptr)
167         {
168             HeapFree(GetProcessHeap(), 0, (PVOID)Pager->CachedLine);
169             Pager->CachedLine = NULL;
170             Pager->cchCachedLine = 0;
171 
172             SetLastError(ERROR_NOT_ENOUGH_MEMORY);
173             return FALSE;
174         }
175         Pager->CachedLine = ptr;
176     }
177     if (Pager->CachedLine)
178     {
179         /* Copy/append the text to the cached line buffer */
180         RtlCopyMemory((PVOID)&Pager->CachedLine[Pager->cchCachedLine],
181                       &TextBuff[ichStart],
182                       cchLine * sizeof(TCHAR));
183         Pager->cchCachedLine += cchLine;
184     }
185     if (bCacheLine)
186     {
187         /* The line is currently incomplete, don't proceed further for now */
188         return FALSE;
189     }
190 
191 TerminateLine:
192     /* The line should be complete now. If we have an existing cached line,
193      * it has been completed by appending the remaining text to it. */
194 
195     /* We are starting a new line */
196     Pager->ichCurr = 0;
197     if (Pager->CachedLine)
198     {
199         Pager->iEndLine = Pager->cchCachedLine;
200         Pager->CurrentLine = Pager->CachedLine;
201     }
202     else
203     {
204         Pager->iEndLine = cchLine;
205         Pager->CurrentLine = &TextBuff[ichStart];
206     }
207 
208     /* Increase only when we have got a NEWLINE */
209     if ((Pager->iEndLine > 0) && (Pager->CurrentLine[Pager->iEndLine - 1] == TEXT('\n')))
210         Pager->lineno++;
211 
212     return TRUE;
213 }
214 
215 /**
216  * @brief   Does the main paging work: fetching text lines and displaying them.
217  **/
218 static BOOL
219 ConPagerWorker(
220     IN PCON_PAGER Pager,
221     IN PCTCH TextBuff,
222     IN DWORD cch)
223 {
224     const DWORD PageColumns = Pager->PageColumns;
225     const DWORD ScrollRows = Pager->ScrollRows;
226 
227     BOOL bFinitePaging = ((PageColumns > 0) && (Pager->PageRows > 0));
228     LONG nTabWidth = Pager->nTabWidth;
229 
230     PCTCH Line;
231     SIZE_T ich;
232     SIZE_T ichStart;
233     SIZE_T iEndLine;
234     DWORD iColumn = Pager->iColumn;
235 
236     UINT nCodePage = GetConsoleOutputCP();
237     BOOL IsCJK = IsCJKCodePage(nCodePage);
238     UINT nWidthOfChar = 1;
239     BOOL IsDoubleWidthCharTrailing = FALSE;
240 
241     /* Normalize the tab width: if negative or too large,
242      * cap it to the number of columns. */
243     if (PageColumns > 0) // if (bFinitePaging)
244     {
245         if (nTabWidth < 0)
246             nTabWidth = PageColumns - 1;
247         else
248             nTabWidth = min(nTabWidth, PageColumns - 1);
249     }
250     else
251     {
252         /* If no column width is known, default to 8 spaces if the
253          * original value is negative; otherwise keep the current one. */
254         if (nTabWidth < 0)
255             nTabWidth = 8;
256     }
257 
258 
259     /* Continue displaying the previous line, if any, or start a new one */
260     Line = Pager->CurrentLine;
261     ichStart = Pager->ichCurr;
262     iEndLine = Pager->iEndLine;
263 
264 ProcessLine:
265 
266     /* Stop now if we have displayed more page lines than requested */
267     if (bFinitePaging && (Pager->iLine >= ScrollRows))
268         goto End;
269 
270     if (!Line || (ichStart >= iEndLine))
271     {
272         /* Start a new line */
273         if (!GetNextLine(Pager, TextBuff, cch))
274             goto End;
275 
276         Line = Pager->CurrentLine;
277         ichStart = Pager->ichCurr;
278         iEndLine = Pager->iEndLine;
279     }
280     else
281     {
282         /* Continue displaying the current line */
283     }
284 
285     // ASSERT(Line && ((ichStart < iEndLine) || (ichStart == iEndLine && iEndLine == 0)));
286 
287     /* Determine whether this line segment (from the current position till the end) should be displayed */
288     Pager->iColumn = iColumn;
289     if (Pager->PagerLine && Pager->PagerLine(Pager, &Line[ichStart], iEndLine - ichStart))
290     {
291         iColumn = Pager->iColumn;
292 
293         /* Done with this line; start a new one */
294         Pager->nSpacePending = 0; // And reset any pending space.
295         ichStart = iEndLine;
296         goto ProcessLine;
297     }
298     // else: Continue displaying the line.
299 
300 
301     /* Print out any pending TAB expansion */
302     if (Pager->nSpacePending > 0)
303     {
304 ExpandTab:
305         while (Pager->nSpacePending > 0)
306         {
307             /* Print filling spaces */
308             CON_STREAM_WRITE(Pager->Screen->Stream, TEXT(" "), 1);
309             --(Pager->nSpacePending);
310             ++iColumn;
311 
312             /* Check whether we are going across the column */
313             if ((PageColumns > 0) && (iColumn % PageColumns == 0))
314             {
315                 // Pager->nSpacePending = 0; // <-- This is the mode of most text editors...
316 
317                 /* Reposition the cursor to the next line, first column */
318                 if (!bFinitePaging || (PageColumns < Pager->Screen->csbi.dwSize.X))
319                     CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\n"), 1);
320 
321                 Pager->iLine++;
322 
323                 /* Restart at the character */
324                 // ASSERT(ichStart == ich);
325                 goto ProcessLine;
326             }
327         }
328     }
329 
330 
331     /* Find, within this line segment (starting from its
332      * beginning), until where we can print to the page. */
333     for (ich = ichStart; ich < iEndLine; ++ich)
334     {
335         /* NEWLINE character */
336         if (Line[ich] == TEXT('\n'))
337         {
338             /* We should stop now */
339             // ASSERT(ich == iEndLine - 1);
340             break;
341         }
342 
343         /* TAB character */
344         if (Line[ich] == TEXT('\t') &&
345             (Pager->dwFlags & CON_PAGER_EXPAND_TABS))
346         {
347             /* We should stop now */
348             break;
349         }
350 
351         /* FORM-FEED character */
352         if (Line[ich] == TEXT('\f') &&
353             (Pager->dwFlags & CON_PAGER_EXPAND_FF))
354         {
355             /* We should stop now */
356             break;
357         }
358 
359         /* Other character - Handle double-width for CJK */
360 
361         if (IsCJK)
362             nWidthOfChar = GetWidthOfCharCJK(nCodePage, Line[ich]);
363 
364         /* Care about CJK character presentation only when outputting
365          * to a device where the number of columns is known. */
366         if ((PageColumns > 0) && IsCJK)
367         {
368             IsDoubleWidthCharTrailing = (nWidthOfChar == 2) &&
369                                         ((iColumn + 1) % PageColumns == 0);
370             if (IsDoubleWidthCharTrailing)
371             {
372                 /* Reserve this character for the next line */
373                 ++iColumn; // Count a blank instead.
374                 /* We should stop now */
375                 break;
376             }
377         }
378 
379         iColumn += nWidthOfChar;
380 
381         /* Check whether we are going across the column */
382         if ((PageColumns > 0) && (iColumn % PageColumns == 0))
383         {
384             ++ich;
385             break;
386         }
387     }
388 
389     /* Output the pending line segment */
390     if (ich - ichStart > 0)
391         CON_STREAM_WRITE(Pager->Screen->Stream, &Line[ichStart], ich - ichStart);
392 
393     /* Have we finished the line segment? */
394     if (ich >= iEndLine)
395     {
396         /* Restart at the character */
397         ichStart = ich;
398         goto ProcessLine;
399     }
400 
401     /* Handle special characters */
402 
403     /* NEWLINE character */
404     if (Line[ich] == TEXT('\n'))
405     {
406         // ASSERT(ich == iEndLine - 1);
407 
408         /* Reposition the cursor to the next line, first column */
409         CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\n"), 1);
410 
411         Pager->iLine++;
412         iColumn = 0;
413 
414         /* Done with this line; start a new one */
415         Pager->nSpacePending = 0; // And reset any pending space.
416         ichStart = iEndLine;
417         goto ProcessLine;
418     }
419 
420     /* TAB character */
421     if (Line[ich] == TEXT('\t') &&
422         (Pager->dwFlags & CON_PAGER_EXPAND_TABS))
423     {
424         /* Perform TAB expansion, unless the tab width is zero */
425         if (nTabWidth == 0)
426         {
427             ichStart = ++ich;
428             goto ProcessLine;
429         }
430 
431         ichStart = ++ich;
432         /* Reset the number of spaces needed to develop this TAB character */
433         Pager->nSpacePending = nTabWidth - (iColumn % nTabWidth);
434         goto ExpandTab;
435     }
436 
437     /* FORM-FEED character */
438     if (Line[ich] == TEXT('\f') &&
439         (Pager->dwFlags & CON_PAGER_EXPAND_FF))
440     {
441         if (bFinitePaging)
442         {
443             /* Clear until the end of the page */
444             while (Pager->iLine < ScrollRows)
445             {
446                 /* Call the user paging function in order to know
447                  * whether we need to output the blank lines. */
448                 Pager->iColumn = iColumn;
449                 if (Pager->PagerLine && Pager->PagerLine(Pager, TEXT("\n"), 1))
450                 {
451                     /* Only one blank line displayed, that counts in the line count */
452                     Pager->iLine++;
453                     break;
454                 }
455                 else
456                 {
457                     CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\n"), 1);
458                     Pager->iLine++;
459                 }
460             }
461         }
462         else
463         {
464             /* Just output a FORM-FEED and a NEWLINE */
465             CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\f\n"), 2);
466             Pager->iLine++;
467         }
468 
469         iColumn = 0;
470         Pager->nSpacePending = 0; // And reset any pending space.
471 
472         /* Skip and restart past the character */
473         ichStart = ++ich;
474         goto ProcessLine;
475     }
476 
477     /* If we output a double-width character that goes across the column,
478      * fill with blank and display the character on the next line. */
479     if (IsDoubleWidthCharTrailing)
480     {
481         IsDoubleWidthCharTrailing = FALSE; // Reset the flag.
482         CON_STREAM_WRITE(Pager->Screen->Stream, TEXT(" "), 1);
483         /* Fall back below */
484     }
485 
486     /* Are we wrapping the line? */
487     if ((PageColumns > 0) && (iColumn % PageColumns == 0))
488     {
489         /* Reposition the cursor to the next line, first column */
490         if (!bFinitePaging || (PageColumns < Pager->Screen->csbi.dwSize.X))
491             CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\n"), 1);
492 
493         Pager->iLine++;
494     }
495 
496     /* Restart at the character */
497     ichStart = ich;
498     goto ProcessLine;
499 
500 
501 End:
502     /*
503      * We are exiting, either because we displayed all the required lines
504      * (iLine >= ScrollRows), or, because we don't have more data to display.
505      */
506 
507     Pager->ichCurr = ichStart;
508     Pager->iColumn = iColumn;
509     // INVESTIGATE: Can we get rid of CurrentLine here? // if (ichStart >= iEndLine) ...
510 
511     /* Return TRUE if we displayed all the required lines; FALSE otherwise */
512     if (bFinitePaging && (Pager->iLine >= ScrollRows))
513     {
514         Pager->iLine = 0; /* Reset the count of lines being printed */
515         return TRUE;
516     }
517     else
518     {
519         return FALSE;
520     }
521 }
522 
523 
524 /**
525  * @name ConWritePaging
526  *     Pages the contents of a user-specified character buffer on the screen.
527  *
528  * @param[in]   Pager
529  *     Pager object that describes where the paged output is issued.
530  *
531  * @param[in]   PagePrompt
532  *     A user-specific callback, called when a page has been displayed.
533  *
534  * @param[in]   StartPaging
535  *     Set to TRUE for initializing the paging operation; FALSE during paging.
536  *
537  * @param[in]   szStr
538  *     Pointer to the character buffer whose contents are to be paged.
539  *
540  * @param[in]   len
541  *     Length of the character buffer pointed by @p szStr, specified
542  *     in number of characters.
543  *
544  * @return
545  *     TRUE when all the contents of the character buffer has been displayed;
546  *     FALSE if the paging operation has been stopped (controlled via @p PagePrompt).
547  **/
548 BOOL
549 ConWritePaging(
550     IN PCON_PAGER Pager,
551     IN PAGE_PROMPT PagePrompt,
552     IN BOOL StartPaging,
553     IN PCTCH szStr,
554     IN DWORD len)
555 {
556     CONSOLE_SCREEN_BUFFER_INFO csbi;
557     BOOL bIsConsole;
558 
559     /* Parameters validation */
560     if (!Pager)
561         return FALSE;
562 
563     /* Get the size of the visual screen that can be printed to */
564     bIsConsole = ConGetScreenInfo(Pager->Screen, &csbi);
565     if (bIsConsole)
566     {
567         /* Calculate the console screen extent */
568         Pager->PageColumns = csbi.dwSize.X;
569         Pager->PageRows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
570     }
571     else
572     {
573         /* We assume it's a file handle */
574         Pager->PageColumns = 0;
575         Pager->PageRows = 0;
576     }
577 
578     if (StartPaging)
579     {
580         if (bIsConsole && (Pager->PageRows >= 2))
581         {
582             /* Reset to display one page by default */
583             Pager->ScrollRows = Pager->PageRows - 1;
584         }
585         else
586         {
587             /* File output, or single line: all lines are displayed at once; reset to a default value */
588             Pager->ScrollRows = 0;
589         }
590 
591         /* Reset the internal data buffer */
592         Pager->CachedLine = NULL;
593         Pager->cchCachedLine = 0;
594 
595         /* Reset the paging state */
596         Pager->CurrentLine = NULL;
597         Pager->ichCurr = 0;
598         Pager->iEndLine = 0;
599         Pager->nSpacePending = 0;
600         Pager->iColumn = 0;
601         Pager->iLine = 0;
602         Pager->lineno = 0;
603     }
604 
605     /* Reset the reading index in the user-provided source buffer */
606     Pager->ich = 0;
607 
608     /* Run the pager even when the user-provided source buffer is
609      * empty, in case we need to flush any remaining cached line. */
610     if (!Pager->CachedLine)
611     {
612         /* No cached line, bail out now */
613         if (len == 0 || szStr == NULL)
614             return TRUE;
615     }
616 
617     while (ConPagerWorker(Pager, szStr, len))
618     {
619         /* Prompt the user only when we display to a console and the screen
620          * is not too small: at least one line for the actual paged text and
621          * one line for the prompt. */
622         if (bIsConsole && (Pager->PageRows >= 2))
623         {
624             /* Reset to display one page by default */
625             Pager->ScrollRows = Pager->PageRows - 1;
626 
627             /* Prompt the user; give him some values for statistics */
628             // FIXME: Doesn't reflect what's currently being displayed.
629             if (!PagePrompt(Pager, Pager->ich, len))
630                 return FALSE;
631         }
632 
633         /* If we display to a console, recalculate its screen extent
634          * in case the user has redimensioned it during the prompt. */
635         if (bIsConsole && ConGetScreenInfo(Pager->Screen, &csbi))
636         {
637             Pager->PageColumns = csbi.dwSize.X;
638             Pager->PageRows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
639         }
640     }
641 
642     return TRUE;
643 }
644 
645 BOOL
646 ConPutsPaging(
647     IN PCON_PAGER Pager,
648     IN PAGE_PROMPT PagePrompt,
649     IN BOOL StartPaging,
650     IN PCTSTR szStr)
651 {
652     DWORD len;
653 
654     /* Return if no string has been given */
655     if (szStr == NULL)
656         return TRUE;
657 
658     len = wcslen(szStr);
659     return ConWritePaging(Pager, PagePrompt, StartPaging, szStr, len);
660 }
661 
662 BOOL
663 ConResPagingEx(
664     IN PCON_PAGER Pager,
665     IN PAGE_PROMPT PagePrompt,
666     IN BOOL StartPaging,
667     IN HINSTANCE hInstance OPTIONAL,
668     IN UINT uID)
669 {
670     INT Len;
671     PCWSTR szStr = NULL;
672 
673     Len = K32LoadStringW(hInstance, uID, (PWSTR)&szStr, 0);
674     if (szStr && Len)
675         return ConWritePaging(Pager, PagePrompt, StartPaging, szStr, Len);
676     else
677         return TRUE;
678 }
679 
680 BOOL
681 ConResPaging(
682     IN PCON_PAGER Pager,
683     IN PAGE_PROMPT PagePrompt,
684     IN BOOL StartPaging,
685     IN UINT uID)
686 {
687     return ConResPagingEx(Pager, PagePrompt, StartPaging,
688                           NULL /*GetModuleHandleW(NULL)*/, uID);
689 }
690 
691 /* EOF */
692