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