-
-
Notifications
You must be signed in to change notification settings - Fork 867
feat: add pager to allow scrolling of long outputs in the REPL #2162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
e4c1a43
feat: add pager to allow scrolling of long outputs
Snehil-Shah 28a9304
refactor: remove setting and default
kgryte 11a3b70
refactor: support programmatic disabling and enabling
kgryte 1fa14b6
fix: add missing types
kgryte 8c588ba
fix: avoid clashing with existing TAB completion behavior
kgryte fd3e109
refactor: rename parameter and internal variable
kgryte 45e0660
refactor: add methods for programmatically toggling behavior
kgryte 18bedb7
fix: ensure booleans are always returned
kgryte c672f48
refactor: implement paging as a transform stream
kgryte 262131d
Merge branch 'develop' of https://github.com/stdlib-js/stdlib into pr…
kgryte 5e02a44
fix: update API calls
kgryte 08f25c7
refactor: close output stream on REPL close and address failing tests
kgryte a7446a3
refactor: move logic to private methods
kgryte b059a94
refactor: reintroduce setting
kgryte 919db02
docs: document setting
kgryte 44a8e87
refactor: add `pager` command and fix settings handling
kgryte c18cd25
fix: update paging check and allow paging bypass
kgryte 4e3f5d9
fix: update conditionals
kgryte 04f6291
refactor: apply De Morgan's law
kgryte 556690e
refactor: batch write the pager content
kgryte cffb6ce
refactor: replace loops with calls to `substring`
kgryte 530f0f8
refactor: guard against index increment errors
kgryte da40034
refactor: minimize the number of ops between clearing screen and disp…
kgryte 3915bd8
fix: disable auto-paging during tests
kgryte ee971ca
refactor: add support for detecting terminal resize events
kgryte 5be95e3
style: use consistent spacing
kgryte b19355b
fix: directly write msg to output to avoid triggering pager
Snehil-Shah e4914e6
fix: avoid suppressing SIGINT interrupts
Snehil-Shah 2bea814
fix: avoid paging if viewport height too low
Snehil-Shah 6a422fa
fix: exit pager if viewport is resized to smaller than minimum
Snehil-Shah 4b4430f
docs: add TODO concerning implementation robustness
kgryte ee0be20
fix: pager not displaying the last line
Snehil-Shah 3ab38a3
test: add tests for pager
Snehil-Shah 7ee0b92
style: change variable case
Snehil-Shah 1acac0c
docs: add comment
kgryte ebfbe26
docs: update comments
kgryte 978190c
Merge branch 'develop' of https://github.com/stdlib-js/stdlib into pr…
kgryte eb9cdff
test: refactor test fixture option handling and update test fixture l…
kgryte 849bb48
style: re-add line to be consistent with other functions
kgryte 683d390
docs: indicate that options are optional
kgryte 95c544f
refactor: avoid repetition
kgryte 78a7e34
style: group operands to visually reinforce operator precedence
kgryte b69c99a
refactor: simplify height determination
kgryte 6b14914
test: update descriptions
kgryte File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
/** | ||
* @license Apache-2.0 | ||
* | ||
* Copyright (c) 2024 The Stdlib Authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
/* eslint-disable no-restricted-syntax, no-underscore-dangle, no-invalid-this */ | ||
|
||
'use strict'; | ||
|
||
// MODULES // | ||
|
||
var readline = require( 'readline' ); | ||
var logger = require( 'debug' ); | ||
var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' ); | ||
var max = require( '@stdlib/math/base/special/max' ); | ||
var repeat = require( '@stdlib/string/repeat' ); | ||
var displayPrompt = require( './display_prompt.js' ); | ||
|
||
|
||
// VARIABLES // | ||
|
||
var debug = logger( 'repl:pager' ); | ||
|
||
|
||
// MAIN // | ||
|
||
/** | ||
* Constructor for creating a pager. | ||
* | ||
* @private | ||
* @constructor | ||
* @param {REPL} repl - REPL instance | ||
* @param {WritableStream} ostream - writable stream | ||
* @param {Function} ostreamWrite - default ostream `write` | ||
* @returns {Pager} pager instance | ||
*/ | ||
function Pager( repl, ostream, ostreamWrite ) { | ||
if ( !(this instanceof Pager) ) { | ||
return new Pager( repl, ostream, ostreamWrite ); | ||
} | ||
debug( 'Creating a pager...' ); | ||
|
||
// Cache a reference to the provided REPL instance: | ||
this.repl = repl; | ||
|
||
// Cache a reference to the output writable stream: | ||
this._ostream = ostream; | ||
|
||
// Cache a reference to the ostream write callback: | ||
this._write = ostreamWrite; | ||
|
||
// Initialize a buffer to store the output viewport height: | ||
this._viewportHeight = -1; | ||
|
||
// Initialize a buffer to store the index of the current scroll position: | ||
this._startIndex = -1; | ||
|
||
// Initialize a buffer to store entire output data: | ||
this._data = ''; | ||
|
||
// Initialize a buffer containing all lines of the output: | ||
this._lines = []; | ||
|
||
return this; | ||
} | ||
|
||
/** | ||
* Displays the scroll view of the initial page. | ||
* | ||
* @name _displayPage | ||
* @memberof Pager.prototype | ||
* @returns {void} | ||
*/ | ||
setNonEnumerableReadOnly( Pager.prototype, '_displayPage', function displayPage() { | ||
var i; | ||
|
||
// Display the initial page till the `viewportHeight`: | ||
this._write.call( this._ostream, '\n' ); | ||
for ( i = 0; i < this._viewportHeight; i++ ) { | ||
this._write.call( this._ostream, this._lines[ i ] + '\n' ); | ||
} | ||
// Draw a seperator to denote that the output is scrollable below: | ||
this._write.call( this._ostream, repeat( '_', this._ostream.columns ) + '\n' ); | ||
|
||
// Display scroll instructions: | ||
this._write.call( this._ostream, '\u001b[1mUse UP/DOWN arrow keys to scroll. Press CTRL+X to exit...\u001b[22m' ); | ||
readline.moveCursor( this._ostream, 0, -1 * ( this._viewportHeight + 3 ) ); | ||
}); | ||
|
||
/** | ||
* Scrolls the current page based on the updated index. | ||
* | ||
* @name _scroll | ||
* @memberof Pager.prototype | ||
* @returns {void} | ||
*/ | ||
setNonEnumerableReadOnly( Pager.prototype, '_scroll', function scroll() { | ||
var i; | ||
|
||
// Clear previous output: | ||
readline.clearScreenDown( this._ostream ); | ||
|
||
// If scroll not at the start of the page, draw a seperator to denote that the output is scrollable above: | ||
if ( this._startIndex === 0 ) { | ||
this._write.call( this._ostream, '\n\n' ); | ||
} else { | ||
this._write.call( this._ostream, '\n' + repeat( '_', this._ostream.columns ) + '\n' ); | ||
kgryte marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
// Fill the viewport from the current index: | ||
for ( i = this._startIndex; i < this._viewportHeight + this._startIndex; i++ ) { // eslint-disable-line max-len | ||
this._write.call( this._ostream, this._lines[ i ] + '\n' ); | ||
} | ||
|
||
// If scroll not at the end of the page, draw a seperator to denote that the output is scrollable below: | ||
if ( this._viewportHeight + this._startIndex === this._lines.length ) { | ||
this._write.call( this._ostream, '\n' ); | ||
} else { | ||
this._write.call( this._ostream, repeat( '_', this._ostream.columns ) + '\n' ); | ||
} | ||
|
||
// Display scroll instructions: | ||
this._write.call( this._ostream, '\u001b[1mUse UP/DOWN arrow keys to scroll. Press CTRL+X to exit...\u001b[22m' ); | ||
readline.moveCursor( this._ostream, 0, -1 * ( this._viewportHeight + 3 ) ); | ||
}); | ||
|
||
/** | ||
* Callback which should be invoked **before** a "keypress" event is processed by a readline interface. | ||
* | ||
* @name beforeKeypress | ||
* @memberof Pager.prototype | ||
* @param {string} data - input data | ||
* @param {(Object|void)} key - key object | ||
* @returns {void} | ||
*/ | ||
setNonEnumerableReadOnly( Pager.prototype, 'beforeKeypress', function beforeKeypress( data, key ) { | ||
if ( key.name === 'up' ) { | ||
if ( this._startIndex === 0 ) { | ||
// Don't scroll if at the top of the page... | ||
return; | ||
} | ||
// Scroll up: | ||
debug( 'Recieved an UP keypress event, moving up the page...' ); | ||
this._startIndex -= 1; | ||
this._scroll(); | ||
} else if ( key.name === 'down' ) { | ||
if ( this._viewportHeight + this._startIndex === this._lines.length ) { | ||
// Don't scroll if at the bottom of the page... | ||
return; | ||
} | ||
// Scroll down: | ||
debug( 'Recieved a DOWN keypress event, moving down the page...' ); | ||
this._startIndex += 1; | ||
this._scroll(); | ||
} else if ( key.ctrl && ( key.name === 'x' || key.name === 'c' ) ) { | ||
// Stop pager: | ||
debug( 'Recieved an interrupt, exiting scroll view...' ); | ||
this._startIndex = -1; | ||
this.repl._isPaging = false; | ||
this._write.call( this._ostream, this._data ); // display the original data normally... | ||
displayPrompt( this.repl, false ); // display the next prompt | ||
this._write.call( this._ostream, '\x1B[?25h' ); // re-display the cursor | ||
} | ||
}); | ||
|
||
/** | ||
* Callback which should be invoked **before** a "write" event is processed by a readline interface. | ||
* | ||
* @name beforeWrite | ||
* @memberof Pager.prototype | ||
* @param {string} data - input data | ||
* @param {(string)} encoding - encoding string | ||
* @param {(Function)} cb - callback | ||
* @returns {void} | ||
*/ | ||
setNonEnumerableReadOnly( Pager.prototype, 'beforeWrite', function beforeWrite( data, encoding, cb ) { | ||
var thresholdHeight; | ||
|
||
if ( this.repl._isPaging || typeof data !== 'string' ) { | ||
this._write.call( this._ostream, data, encoding, cb ); | ||
return; | ||
} | ||
thresholdHeight = max( this._ostream.rows - 1, 4 ); // 4 rows being the minimum possible output height to support paging | ||
this._viewportHeight = thresholdHeight - 3; // 3 rows consist of the seperators and the scroll instruction | ||
this._data = data; | ||
this._lines = data.split( '\n' ); | ||
|
||
// If output can fit in the terminal without scroll, write normally and exit... | ||
if ( this._lines.length < thresholdHeight ) { | ||
this._write.call( this._ostream, data ); | ||
return; | ||
} | ||
// Start paging if output doesn't fit without scroll: | ||
debug( 'Recieved output eligible for scrolling. Initializing scroll view...' ); | ||
this.repl._isPaging = true; | ||
this._startIndex = 0; | ||
this._write.call( this._ostream, '\x1B[?25l' ); // hide the cursor | ||
this._displayPage(); | ||
}); | ||
|
||
|
||
// EXPORTS // | ||
|
||
module.exports = Pager; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.