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
13 * @brief Console/terminal paging functionality.
16 /* FIXME: Temporary HACK before we cleanly support UNICODE functions */
22 // #include <winnls.h>
23 #include <wincon.h> // Console APIs (only if kernel32 support included)
24 #include <winnls.h> // for WideCharToMultiByte
33 #define CON_STREAM_WRITE ConStreamWrite
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)
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)
52 INT ret
= WideCharToMultiByte(nCodePage
, 0, &ch
, 1, NULL
, 0, NULL
, NULL
);
61 * @brief Retrieves a new text line, or continue fetching the current one.
63 * @remark Manages setting Pager's CurrentLine, ichCurr, iEndLine, and the
64 * line cache (CachedLine, cchCachedLine). Other functions must not
65 * modify these values.
69 IN OUT PCON_PAGER Pager
,
73 SIZE_T ich
= Pager
->ich
;
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.
87 // INVESTIGATE: Do that only if (ichStart >= iEndLine) ??
88 if (Pager
->CurrentLine
)
90 // ASSERT(Pager->CurrentLine == Pager->CachedLine);
91 if (Pager
->CachedLine
)
93 HeapFree(GetProcessHeap(), 0, (PVOID
)Pager
->CachedLine
);
94 Pager
->CachedLine
= NULL
;
95 Pager
->cchCachedLine
= 0;
98 Pager
->CurrentLine
= NULL
;
101 /* Nothing else to read if we are past the end of the buffer */
104 /* If we have a pending cached line, terminate it now */
105 if (Pager
->CachedLine
)
108 /* Otherwise, bail out */
112 /* Start a new line, or continue an existing one */
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
)
119 if (TextBuff
[ich
] == TEXT('\n'))
127 cchLine
= (ich
- ichStart
);
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.
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.
143 bCacheLine
= ((Pager
->dwFlags
& CON_PAGER_CACHE_INCOMPLETE_LINE
) &&
144 (ich
>= cch
) && (TextBuff
[ich
- 1] != TEXT('\n')));
146 /* Allocate, or re-allocate, the cached line buffer */
147 if (bCacheLine
&& !Pager
->CachedLine
)
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;
154 if (!Pager
->CachedLine
)
156 SetLastError(ERROR_NOT_ENOUGH_MEMORY
);
160 else if (Pager
->CachedLine
)
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
));
168 HeapFree(GetProcessHeap(), 0, (PVOID
)Pager
->CachedLine
);
169 Pager
->CachedLine
= NULL
;
170 Pager
->cchCachedLine
= 0;
172 SetLastError(ERROR_NOT_ENOUGH_MEMORY
);
175 Pager
->CachedLine
= ptr
;
177 if (Pager
->CachedLine
)
179 /* Copy/append the text to the cached line buffer */
180 RtlCopyMemory((PVOID
)&Pager
->CachedLine
[Pager
->cchCachedLine
],
182 cchLine
* sizeof(TCHAR
));
183 Pager
->cchCachedLine
+= cchLine
;
187 /* The line is currently incomplete, don't proceed further for now */
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. */
195 /* We are starting a new line */
197 if (Pager
->CachedLine
)
199 Pager
->iEndLine
= Pager
->cchCachedLine
;
200 Pager
->CurrentLine
= Pager
->CachedLine
;
204 Pager
->iEndLine
= cchLine
;
205 Pager
->CurrentLine
= &TextBuff
[ichStart
];
208 /* Increase only when we have got a NEWLINE */
209 if ((Pager
->iEndLine
> 0) && (Pager
->CurrentLine
[Pager
->iEndLine
- 1] == TEXT('\n')))
216 * @brief Does the main paging work: fetching text lines and displaying them.
224 const DWORD PageColumns
= Pager
->PageColumns
;
225 const DWORD ScrollRows
= Pager
->ScrollRows
;
227 BOOL bFinitePaging
= ((PageColumns
> 0) && (Pager
->PageRows
> 0));
228 LONG nTabWidth
= Pager
->nTabWidth
;
234 DWORD iColumn
= Pager
->iColumn
;
236 UINT nCodePage
= GetConsoleOutputCP();
237 BOOL IsCJK
= IsCJKCodePage(nCodePage
);
238 UINT nWidthOfChar
= 1;
239 BOOL IsDoubleWidthCharTrailing
= FALSE
;
241 /* Normalize the tab width: if negative or too large,
242 * cap it to the number of columns. */
243 if (PageColumns
> 0) // if (bFinitePaging)
246 nTabWidth
= PageColumns
- 1;
248 nTabWidth
= min(nTabWidth
, PageColumns
- 1);
252 /* If no column width is known, default to 8 spaces if the
253 * original value is negative; otherwise keep the current one. */
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
;
266 /* Stop now if we have displayed more page lines than requested */
267 if (bFinitePaging
&& (Pager
->iLine
>= ScrollRows
))
270 if (!Line
|| (ichStart
>= iEndLine
))
272 /* Start a new line */
273 if (!GetNextLine(Pager
, TextBuff
, cch
))
276 Line
= Pager
->CurrentLine
;
277 ichStart
= Pager
->ichCurr
;
278 iEndLine
= Pager
->iEndLine
;
282 /* Continue displaying the current line */
285 // ASSERT(Line && ((ichStart < iEndLine) || (ichStart == iEndLine && iEndLine == 0)));
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
))
291 iColumn
= Pager
->iColumn
;
293 /* Done with this line; start a new one */
294 Pager
->nSpacePending
= 0; // And reset any pending space.
298 // else: Continue displaying the line.
301 /* Print out any pending TAB expansion */
302 if (Pager
->nSpacePending
> 0)
305 while (Pager
->nSpacePending
> 0)
307 /* Print filling spaces */
308 CON_STREAM_WRITE(Pager
->Screen
->Stream
, TEXT(" "), 1);
309 --(Pager
->nSpacePending
);
312 /* Check whether we are going across the column */
313 if ((PageColumns
> 0) && (iColumn
% PageColumns
== 0))
315 // Pager->nSpacePending = 0; // <-- This is the mode of most text editors...
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);
323 /* Restart at the character */
324 // ASSERT(ichStart == ich);
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
)
335 /* NEWLINE character */
336 if (Line
[ich
] == TEXT('\n'))
338 /* We should stop now */
339 // ASSERT(ich == iEndLine - 1);
344 if (Line
[ich
] == TEXT('\t') &&
345 (Pager
->dwFlags
& CON_PAGER_EXPAND_TABS
))
347 /* We should stop now */
351 /* FORM-FEED character */
352 if (Line
[ich
] == TEXT('\f') &&
353 (Pager
->dwFlags
& CON_PAGER_EXPAND_FF
))
355 /* We should stop now */
359 /* Other character - Handle double-width for CJK */
362 nWidthOfChar
= GetWidthOfCharCJK(nCodePage
, Line
[ich
]);
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
)
368 IsDoubleWidthCharTrailing
= (nWidthOfChar
== 2) &&
369 ((iColumn
+ 1) % PageColumns
== 0);
370 if (IsDoubleWidthCharTrailing
)
372 /* Reserve this character for the next line */
373 ++iColumn
; // Count a blank instead.
374 /* We should stop now */
379 iColumn
+= nWidthOfChar
;
381 /* Check whether we are going across the column */
382 if ((PageColumns
> 0) && (iColumn
% PageColumns
== 0))
389 /* Output the pending line segment */
390 if (ich
- ichStart
> 0)
391 CON_STREAM_WRITE(Pager
->Screen
->Stream
, &Line
[ichStart
], ich
- ichStart
);
393 /* Have we finished the line segment? */
396 /* Restart at the character */
401 /* Handle special characters */
403 /* NEWLINE character */
404 if (Line
[ich
] == TEXT('\n'))
406 // ASSERT(ich == iEndLine - 1);
408 /* Reposition the cursor to the next line, first column */
409 CON_STREAM_WRITE(Pager
->Screen
->Stream
, TEXT("\n"), 1);
414 /* Done with this line; start a new one */
415 Pager
->nSpacePending
= 0; // And reset any pending space.
421 if (Line
[ich
] == TEXT('\t') &&
422 (Pager
->dwFlags
& CON_PAGER_EXPAND_TABS
))
424 /* Perform TAB expansion, unless the tab width is zero */
432 /* Reset the number of spaces needed to develop this TAB character */
433 Pager
->nSpacePending
= nTabWidth
- (iColumn
% nTabWidth
);
437 /* FORM-FEED character */
438 if (Line
[ich
] == TEXT('\f') &&
439 (Pager
->dwFlags
& CON_PAGER_EXPAND_FF
))
443 /* Clear until the end of the page */
444 while (Pager
->iLine
< ScrollRows
)
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))
451 /* Only one blank line displayed, that counts in the line count */
457 CON_STREAM_WRITE(Pager
->Screen
->Stream
, TEXT("\n"), 1);
464 /* Just output a FORM-FEED and a NEWLINE */
465 CON_STREAM_WRITE(Pager
->Screen
->Stream
, TEXT("\f\n"), 2);
470 Pager
->nSpacePending
= 0; // And reset any pending space.
472 /* Skip and restart past the character */
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
)
481 IsDoubleWidthCharTrailing
= FALSE
; // Reset the flag.
482 CON_STREAM_WRITE(Pager
->Screen
->Stream
, TEXT(" "), 1);
483 /* Fall back below */
486 /* Are we wrapping the line? */
487 if ((PageColumns
> 0) && (iColumn
% PageColumns
== 0))
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);
496 /* Restart at the character */
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.
507 Pager
->ichCurr
= ichStart
;
508 Pager
->iColumn
= iColumn
;
509 // INVESTIGATE: Can we get rid of CurrentLine here? // if (ichStart >= iEndLine) ...
511 /* Return TRUE if we displayed all the required lines; FALSE otherwise */
512 if (bFinitePaging
&& (Pager
->iLine
>= ScrollRows
))
514 Pager
->iLine
= 0; /* Reset the count of lines being printed */
525 * @name ConWritePaging
526 * Pages the contents of a user-specified character buffer on the screen.
529 * Pager object that describes where the paged output is issued.
531 * @param[in] PagePrompt
532 * A user-specific callback, called when a page has been displayed.
534 * @param[in] StartPaging
535 * Set to TRUE for initializing the paging operation; FALSE during paging.
538 * Pointer to the character buffer whose contents are to be paged.
541 * Length of the character buffer pointed by @p szStr, specified
542 * in number of characters.
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).
551 IN PAGE_PROMPT PagePrompt
,
556 CONSOLE_SCREEN_BUFFER_INFO csbi
;
559 /* Parameters validation */
563 /* Get the size of the visual screen that can be printed to */
564 bIsConsole
= ConGetScreenInfo(Pager
->Screen
, &csbi
);
567 /* Calculate the console screen extent */
568 Pager
->PageColumns
= csbi
.dwSize
.X
;
569 Pager
->PageRows
= csbi
.srWindow
.Bottom
- csbi
.srWindow
.Top
+ 1;
573 /* We assume it's a file handle */
574 Pager
->PageColumns
= 0;
580 if (bIsConsole
&& (Pager
->PageRows
>= 2))
582 /* Reset to display one page by default */
583 Pager
->ScrollRows
= Pager
->PageRows
- 1;
587 /* File output, or single line: all lines are displayed at once; reset to a default value */
588 Pager
->ScrollRows
= 0;
591 /* Reset the internal data buffer */
592 Pager
->CachedLine
= NULL
;
593 Pager
->cchCachedLine
= 0;
595 /* Reset the paging state */
596 Pager
->CurrentLine
= NULL
;
599 Pager
->nSpacePending
= 0;
605 /* Reset the reading index in the user-provided source buffer */
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
)
612 /* No cached line, bail out now */
613 if (len
== 0 || szStr
== NULL
)
617 while (ConPagerWorker(Pager
, szStr
, len
))
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))
624 /* Reset to display one page by default */
625 Pager
->ScrollRows
= Pager
->PageRows
- 1;
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
))
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
))
637 Pager
->PageColumns
= csbi
.dwSize
.X
;
638 Pager
->PageRows
= csbi
.srWindow
.Bottom
- csbi
.srWindow
.Top
+ 1;
648 IN PAGE_PROMPT PagePrompt
,
654 /* Return if no string has been given */
659 return ConWritePaging(Pager
, PagePrompt
, StartPaging
, szStr
, len
);
665 IN PAGE_PROMPT PagePrompt
,
667 IN HINSTANCE hInstance OPTIONAL
,
673 Len
= K32LoadStringW(hInstance
, uID
, (PWSTR
)&szStr
, 0);
675 return ConWritePaging(Pager
, PagePrompt
, StartPaging
, szStr
, Len
);
683 IN PAGE_PROMPT PagePrompt
,
687 return ConResPagingEx(Pager
, PagePrompt
, StartPaging
,
688 NULL
/*GetModuleHandleW(NULL)*/, uID
);