/* * PROJECT: ReactOS Console Utilities Library * LICENSE: GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+) * PURPOSE: Console/terminal paging functionality. * COPYRIGHT: Copyright 2017-2021 Hermes Belusca-Maito * Copyright 2021 Katayama Hirofumi MZ */ /** * @file pager.c * @ingroup ConUtils * * @brief Console/terminal paging functionality. **/ /* FIXME: Temporary HACK before we cleanly support UNICODE functions */ #define UNICODE #define _UNICODE #include #include // #include #include // Console APIs (only if kernel32 support included) #include // for WideCharToMultiByte #include #include "conutils.h" #include "stream.h" #include "screen.h" #include "pager.h" // Temporary HACK #define CON_STREAM_WRITE ConStreamWrite #define CP_SHIFTJIS 932 // Japanese Shift-JIS #define CP_HANGUL 949 // Korean Hangul/Wansung #define CP_JOHAB 1361 // Korean Johab #define CP_GB2312 936 // Chinese Simplified (GB2312) #define CP_BIG5 950 // Chinese Traditional (Big5) /* IsFarEastCP(CodePage) */ #define IsCJKCodePage(CodePage) \ ((CodePage) == CP_SHIFTJIS || (CodePage) == CP_HANGUL || \ /* (CodePage) == CP_JOHAB || */ \ (CodePage) == CP_BIG5 || (CodePage) == CP_GB2312) static inline INT GetWidthOfCharCJK( IN UINT nCodePage, IN WCHAR ch) { INT ret = WideCharToMultiByte(nCodePage, 0, &ch, 1, NULL, 0, NULL, NULL); if (ret == 0) ret = 1; else if (ret > 2) ret = 2; return ret; } /** * @brief Retrieves a new text line, or continue fetching the current one. * * @remark Manages setting Pager's CurrentLine, ichCurr, iEndLine, and the * line cache (CachedLine, cchCachedLine). Other functions must not * modify these values. **/ static BOOL GetNextLine( IN OUT PCON_PAGER Pager, IN PCTCH TextBuff, IN SIZE_T cch) { SIZE_T ich = Pager->ich; SIZE_T ichStart; SIZE_T cchLine; BOOL bCacheLine; Pager->ichCurr = 0; Pager->iEndLine = 0; /* * If we already had an existing line, then we can safely start a new one * and getting rid of any current cached line. Otherwise, we don't have * a current line and we may be caching a new one, in which case, continue * caching it until it becomes complete. */ // INVESTIGATE: Do that only if (ichStart >= iEndLine) ?? if (Pager->CurrentLine) { // ASSERT(Pager->CurrentLine == Pager->CachedLine); if (Pager->CachedLine) { HeapFree(GetProcessHeap(), 0, (PVOID)Pager->CachedLine); Pager->CachedLine = NULL; Pager->cchCachedLine = 0; } Pager->CurrentLine = NULL; } /* Nothing else to read if we are past the end of the buffer */ if (ich >= cch) { /* If we have a pending cached line, terminate it now */ if (Pager->CachedLine) goto TerminateLine; /* Otherwise, bail out */ return FALSE; } /* Start a new line, or continue an existing one */ ichStart = ich; /* Find where this line ends, looking for a NEWLINE character. * (NOTE: We cannot use strchr because the buffer is not NULL-terminated) */ for (; ich < cch; ++ich) { if (TextBuff[ich] == TEXT('\n')) { ++ich; break; } } Pager->ich = ich; cchLine = (ich - ichStart); // // FIXME: Impose a maximum string limit when the line is cached, in order // not to potentially grow memory indefinitely. When the limit is reached, // terminate the line. // /* * If we have stopped because we have exhausted the text buffer * and we have not found an end-of-line character, this may mean * that the text line spans across different text buffers. If we * have been told so, cache this line: we will complete it during * the next call(s) and only then, display it. * Otherwise, consider the line to be terminated now. */ bCacheLine = ((Pager->dwFlags & CON_PAGER_CACHE_INCOMPLETE_LINE) && (ich >= cch) && (TextBuff[ich - 1] != TEXT('\n'))); /* Allocate, or re-allocate, the cached line buffer */ if (bCacheLine && !Pager->CachedLine) { /* We start caching, allocate the cached line buffer */ Pager->CachedLine = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cchLine * sizeof(TCHAR)); Pager->cchCachedLine = 0; if (!Pager->CachedLine) { SetLastError(ERROR_NOT_ENOUGH_MEMORY); return FALSE; } } else if (Pager->CachedLine) { /* We continue caching, re-allocate the cached line buffer */ PVOID ptr = HeapReAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (PVOID)Pager->CachedLine, (Pager->cchCachedLine + cchLine) * sizeof(TCHAR)); if (!ptr) { HeapFree(GetProcessHeap(), 0, (PVOID)Pager->CachedLine); Pager->CachedLine = NULL; Pager->cchCachedLine = 0; SetLastError(ERROR_NOT_ENOUGH_MEMORY); return FALSE; } Pager->CachedLine = ptr; } if (Pager->CachedLine) { /* Copy/append the text to the cached line buffer */ RtlCopyMemory((PVOID)&Pager->CachedLine[Pager->cchCachedLine], &TextBuff[ichStart], cchLine * sizeof(TCHAR)); Pager->cchCachedLine += cchLine; } if (bCacheLine) { /* The line is currently incomplete, don't proceed further for now */ return FALSE; } TerminateLine: /* The line should be complete now. If we have an existing cached line, * it has been completed by appending the remaining text to it. */ /* We are starting a new line */ Pager->ichCurr = 0; if (Pager->CachedLine) { Pager->iEndLine = Pager->cchCachedLine; Pager->CurrentLine = Pager->CachedLine; } else { Pager->iEndLine = cchLine; Pager->CurrentLine = &TextBuff[ichStart]; } /* Increase only when we have got a NEWLINE */ if ((Pager->iEndLine > 0) && (Pager->CurrentLine[Pager->iEndLine - 1] == TEXT('\n'))) Pager->lineno++; return TRUE; } /** * @brief Does the main paging work: fetching text lines and displaying them. **/ static BOOL ConPagerWorker( IN PCON_PAGER Pager, IN PCTCH TextBuff, IN DWORD cch) { const DWORD PageColumns = Pager->PageColumns; const DWORD ScrollRows = Pager->ScrollRows; BOOL bFinitePaging = ((PageColumns > 0) && (Pager->PageRows > 0)); LONG nTabWidth = Pager->nTabWidth; PCTCH Line; SIZE_T ich; SIZE_T ichStart; SIZE_T iEndLine; DWORD iColumn = Pager->iColumn; UINT nCodePage = GetConsoleOutputCP(); BOOL IsCJK = IsCJKCodePage(nCodePage); UINT nWidthOfChar = 1; BOOL IsDoubleWidthCharTrailing = FALSE; /* Normalize the tab width: if negative or too large, * cap it to the number of columns. */ if (PageColumns > 0) // if (bFinitePaging) { if (nTabWidth < 0) nTabWidth = PageColumns - 1; else nTabWidth = min(nTabWidth, PageColumns - 1); } else { /* If no column width is known, default to 8 spaces if the * original value is negative; otherwise keep the current one. */ if (nTabWidth < 0) nTabWidth = 8; } /* Continue displaying the previous line, if any, or start a new one */ Line = Pager->CurrentLine; ichStart = Pager->ichCurr; iEndLine = Pager->iEndLine; ProcessLine: /* Stop now if we have displayed more page lines than requested */ if (bFinitePaging && (Pager->iLine >= ScrollRows)) goto End; if (!Line || (ichStart >= iEndLine)) { /* Start a new line */ if (!GetNextLine(Pager, TextBuff, cch)) goto End; Line = Pager->CurrentLine; ichStart = Pager->ichCurr; iEndLine = Pager->iEndLine; } else { /* Continue displaying the current line */ } // ASSERT(Line && ((ichStart < iEndLine) || (ichStart == iEndLine && iEndLine == 0))); /* Determine whether this line segment (from the current position till the end) should be displayed */ Pager->iColumn = iColumn; if (Pager->PagerLine && Pager->PagerLine(Pager, &Line[ichStart], iEndLine - ichStart)) { iColumn = Pager->iColumn; /* Done with this line; start a new one */ Pager->nSpacePending = 0; // And reset any pending space. ichStart = iEndLine; goto ProcessLine; } // else: Continue displaying the line. /* Print out any pending TAB expansion */ if (Pager->nSpacePending > 0) { ExpandTab: while (Pager->nSpacePending > 0) { /* Print filling spaces */ CON_STREAM_WRITE(Pager->Screen->Stream, TEXT(" "), 1); --(Pager->nSpacePending); ++iColumn; /* Check whether we are going across the column */ if ((PageColumns > 0) && (iColumn % PageColumns == 0)) { // Pager->nSpacePending = 0; // <-- This is the mode of most text editors... /* Reposition the cursor to the next line, first column */ if (!bFinitePaging || (PageColumns < Pager->Screen->csbi.dwSize.X)) CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\n"), 1); Pager->iLine++; /* Restart at the character */ // ASSERT(ichStart == ich); goto ProcessLine; } } } /* Find, within this line segment (starting from its * beginning), until where we can print to the page. */ for (ich = ichStart; ich < iEndLine; ++ich) { /* NEWLINE character */ if (Line[ich] == TEXT('\n')) { /* We should stop now */ // ASSERT(ich == iEndLine - 1); break; } /* TAB character */ if (Line[ich] == TEXT('\t') && (Pager->dwFlags & CON_PAGER_EXPAND_TABS)) { /* We should stop now */ break; } /* FORM-FEED character */ if (Line[ich] == TEXT('\f') && (Pager->dwFlags & CON_PAGER_EXPAND_FF)) { /* We should stop now */ break; } /* Other character - Handle double-width for CJK */ if (IsCJK) nWidthOfChar = GetWidthOfCharCJK(nCodePage, Line[ich]); /* Care about CJK character presentation only when outputting * to a device where the number of columns is known. */ if ((PageColumns > 0) && IsCJK) { IsDoubleWidthCharTrailing = (nWidthOfChar == 2) && ((iColumn + 1) % PageColumns == 0); if (IsDoubleWidthCharTrailing) { /* Reserve this character for the next line */ ++iColumn; // Count a blank instead. /* We should stop now */ break; } } iColumn += nWidthOfChar; /* Check whether we are going across the column */ if ((PageColumns > 0) && (iColumn % PageColumns == 0)) { ++ich; break; } } /* Output the pending line segment */ if (ich - ichStart > 0) CON_STREAM_WRITE(Pager->Screen->Stream, &Line[ichStart], ich - ichStart); /* Have we finished the line segment? */ if (ich >= iEndLine) { /* Restart at the character */ ichStart = ich; goto ProcessLine; } /* Handle special characters */ /* NEWLINE character */ if (Line[ich] == TEXT('\n')) { // ASSERT(ich == iEndLine - 1); /* Reposition the cursor to the next line, first column */ CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\n"), 1); Pager->iLine++; iColumn = 0; /* Done with this line; start a new one */ Pager->nSpacePending = 0; // And reset any pending space. ichStart = iEndLine; goto ProcessLine; } /* TAB character */ if (Line[ich] == TEXT('\t') && (Pager->dwFlags & CON_PAGER_EXPAND_TABS)) { /* Perform TAB expansion, unless the tab width is zero */ if (nTabWidth == 0) { ichStart = ++ich; goto ProcessLine; } ichStart = ++ich; /* Reset the number of spaces needed to develop this TAB character */ Pager->nSpacePending = nTabWidth - (iColumn % nTabWidth); goto ExpandTab; } /* FORM-FEED character */ if (Line[ich] == TEXT('\f') && (Pager->dwFlags & CON_PAGER_EXPAND_FF)) { if (bFinitePaging) { /* Clear until the end of the page */ while (Pager->iLine < ScrollRows) { /* Call the user paging function in order to know * whether we need to output the blank lines. */ Pager->iColumn = iColumn; if (Pager->PagerLine && Pager->PagerLine(Pager, TEXT("\n"), 1)) { /* Only one blank line displayed, that counts in the line count */ Pager->iLine++; break; } else { CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\n"), 1); Pager->iLine++; } } } else { /* Just output a FORM-FEED and a NEWLINE */ CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\f\n"), 2); Pager->iLine++; } iColumn = 0; Pager->nSpacePending = 0; // And reset any pending space. /* Skip and restart past the character */ ichStart = ++ich; goto ProcessLine; } /* If we output a double-width character that goes across the column, * fill with blank and display the character on the next line. */ if (IsDoubleWidthCharTrailing) { IsDoubleWidthCharTrailing = FALSE; // Reset the flag. CON_STREAM_WRITE(Pager->Screen->Stream, TEXT(" "), 1); /* Fall back below */ } /* Are we wrapping the line? */ if ((PageColumns > 0) && (iColumn % PageColumns == 0)) { /* Reposition the cursor to the next line, first column */ if (!bFinitePaging || (PageColumns < Pager->Screen->csbi.dwSize.X)) CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\n"), 1); Pager->iLine++; } /* Restart at the character */ ichStart = ich; goto ProcessLine; End: /* * We are exiting, either because we displayed all the required lines * (iLine >= ScrollRows), or, because we don't have more data to display. */ Pager->ichCurr = ichStart; Pager->iColumn = iColumn; // INVESTIGATE: Can we get rid of CurrentLine here? // if (ichStart >= iEndLine) ... /* Return TRUE if we displayed all the required lines; FALSE otherwise */ if (bFinitePaging && (Pager->iLine >= ScrollRows)) { Pager->iLine = 0; /* Reset the count of lines being printed */ return TRUE; } else { return FALSE; } } /** * @name ConWritePaging * Pages the contents of a user-specified character buffer on the screen. * * @param[in] Pager * Pager object that describes where the paged output is issued. * * @param[in] PagePrompt * A user-specific callback, called when a page has been displayed. * * @param[in] StartPaging * Set to TRUE for initializing the paging operation; FALSE during paging. * * @param[in] szStr * Pointer to the character buffer whose contents are to be paged. * * @param[in] len * Length of the character buffer pointed by @p szStr, specified * in number of characters. * * @return * TRUE when all the contents of the character buffer has been displayed; * FALSE if the paging operation has been stopped (controlled via @p PagePrompt). **/ BOOL ConWritePaging( IN PCON_PAGER Pager, IN PAGE_PROMPT PagePrompt, IN BOOL StartPaging, IN PCTCH szStr, IN DWORD len) { CONSOLE_SCREEN_BUFFER_INFO csbi; BOOL bIsConsole; /* Parameters validation */ if (!Pager) return FALSE; /* Get the size of the visual screen that can be printed to */ bIsConsole = ConGetScreenInfo(Pager->Screen, &csbi); if (bIsConsole) { /* Calculate the console screen extent */ Pager->PageColumns = csbi.dwSize.X; Pager->PageRows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; } else { /* We assume it's a file handle */ Pager->PageColumns = 0; Pager->PageRows = 0; } if (StartPaging) { if (bIsConsole && (Pager->PageRows >= 2)) { /* Reset to display one page by default */ Pager->ScrollRows = Pager->PageRows - 1; } else { /* File output, or single line: all lines are displayed at once; reset to a default value */ Pager->ScrollRows = 0; } /* Reset the internal data buffer */ Pager->CachedLine = NULL; Pager->cchCachedLine = 0; /* Reset the paging state */ Pager->CurrentLine = NULL; Pager->ichCurr = 0; Pager->iEndLine = 0; Pager->nSpacePending = 0; Pager->iColumn = 0; Pager->iLine = 0; Pager->lineno = 0; } /* Reset the reading index in the user-provided source buffer */ Pager->ich = 0; /* Run the pager even when the user-provided source buffer is * empty, in case we need to flush any remaining cached line. */ if (!Pager->CachedLine) { /* No cached line, bail out now */ if (len == 0 || szStr == NULL) return TRUE; } while (ConPagerWorker(Pager, szStr, len)) { /* Prompt the user only when we display to a console and the screen * is not too small: at least one line for the actual paged text and * one line for the prompt. */ if (bIsConsole && (Pager->PageRows >= 2)) { /* Reset to display one page by default */ Pager->ScrollRows = Pager->PageRows - 1; /* Prompt the user; give him some values for statistics */ // FIXME: Doesn't reflect what's currently being displayed. if (!PagePrompt(Pager, Pager->ich, len)) return FALSE; } /* If we display to a console, recalculate its screen extent * in case the user has redimensioned it during the prompt. */ if (bIsConsole && ConGetScreenInfo(Pager->Screen, &csbi)) { Pager->PageColumns = csbi.dwSize.X; Pager->PageRows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; } } return TRUE; } BOOL ConPutsPaging( IN PCON_PAGER Pager, IN PAGE_PROMPT PagePrompt, IN BOOL StartPaging, IN PCTSTR szStr) { DWORD len; /* Return if no string has been given */ if (szStr == NULL) return TRUE; len = wcslen(szStr); return ConWritePaging(Pager, PagePrompt, StartPaging, szStr, len); } BOOL ConResPagingEx( IN PCON_PAGER Pager, IN PAGE_PROMPT PagePrompt, IN BOOL StartPaging, IN HINSTANCE hInstance OPTIONAL, IN UINT uID) { INT Len; PCWSTR szStr = NULL; Len = K32LoadStringW(hInstance, uID, (PWSTR)&szStr, 0); if (szStr && Len) return ConWritePaging(Pager, PagePrompt, StartPaging, szStr, Len); else return TRUE; } BOOL ConResPaging( IN PCON_PAGER Pager, IN PAGE_PROMPT PagePrompt, IN BOOL StartPaging, IN UINT uID) { return ConResPagingEx(Pager, PagePrompt, StartPaging, NULL /*GetModuleHandleW(NULL)*/, uID); } /* EOF */