Skip to content
Merged
Show file tree
Hide file tree
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 Apr 14, 2024
28a9304
refactor: remove setting and default
kgryte Apr 20, 2024
11a3b70
refactor: support programmatic disabling and enabling
kgryte Apr 20, 2024
1fa14b6
fix: add missing types
kgryte Apr 21, 2024
8c588ba
fix: avoid clashing with existing TAB completion behavior
kgryte Apr 21, 2024
fd3e109
refactor: rename parameter and internal variable
kgryte Apr 21, 2024
45e0660
refactor: add methods for programmatically toggling behavior
kgryte Apr 21, 2024
18bedb7
fix: ensure booleans are always returned
kgryte Apr 21, 2024
c672f48
refactor: implement paging as a transform stream
kgryte Apr 21, 2024
262131d
Merge branch 'develop' of https://github.com/stdlib-js/stdlib into pr…
kgryte Apr 21, 2024
5e02a44
fix: update API calls
kgryte Apr 21, 2024
08f25c7
refactor: close output stream on REPL close and address failing tests
kgryte Apr 21, 2024
a7446a3
refactor: move logic to private methods
kgryte Apr 21, 2024
b059a94
refactor: reintroduce setting
kgryte Apr 21, 2024
919db02
docs: document setting
kgryte Apr 21, 2024
44a8e87
refactor: add `pager` command and fix settings handling
kgryte Apr 21, 2024
c18cd25
fix: update paging check and allow paging bypass
kgryte Apr 21, 2024
4e3f5d9
fix: update conditionals
kgryte Apr 21, 2024
04f6291
refactor: apply De Morgan's law
kgryte Apr 21, 2024
556690e
refactor: batch write the pager content
kgryte Apr 21, 2024
cffb6ce
refactor: replace loops with calls to `substring`
kgryte Apr 21, 2024
530f0f8
refactor: guard against index increment errors
kgryte Apr 21, 2024
da40034
refactor: minimize the number of ops between clearing screen and disp…
kgryte Apr 21, 2024
3915bd8
fix: disable auto-paging during tests
kgryte Apr 21, 2024
ee971ca
refactor: add support for detecting terminal resize events
kgryte Apr 21, 2024
5be95e3
style: use consistent spacing
kgryte Apr 21, 2024
b19355b
fix: directly write msg to output to avoid triggering pager
Snehil-Shah Apr 22, 2024
e4914e6
fix: avoid suppressing SIGINT interrupts
Snehil-Shah Apr 22, 2024
2bea814
fix: avoid paging if viewport height too low
Snehil-Shah Apr 22, 2024
6a422fa
fix: exit pager if viewport is resized to smaller than minimum
Snehil-Shah Apr 22, 2024
4b4430f
docs: add TODO concerning implementation robustness
kgryte Apr 22, 2024
ee0be20
fix: pager not displaying the last line
Snehil-Shah Apr 22, 2024
3ab38a3
test: add tests for pager
Snehil-Shah Apr 22, 2024
7ee0b92
style: change variable case
Snehil-Shah Apr 23, 2024
1acac0c
docs: add comment
kgryte Apr 24, 2024
ebfbe26
docs: update comments
kgryte Apr 24, 2024
978190c
Merge branch 'develop' of https://github.com/stdlib-js/stdlib into pr…
kgryte Apr 24, 2024
eb9cdff
test: refactor test fixture option handling and update test fixture l…
kgryte Apr 24, 2024
849bb48
style: re-add line to be consistent with other functions
kgryte Apr 24, 2024
683d390
docs: indicate that options are optional
kgryte Apr 25, 2024
95c544f
refactor: avoid repetition
kgryte Apr 25, 2024
78a7e34
style: group operands to visually reinforce operator precedence
kgryte Apr 25, 2024
b69c99a
refactor: simplify height determination
kgryte Apr 25, 2024
6b14914
test: update descriptions
kgryte Apr 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/node_modules/@stdlib/repl/lib/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ function defaults() {
'autoDeletePairs': true,

// Flag indicating whether to enable the display of completion previews for auto-completion (note: default depends on whether TTY):
'completionPreviews': void 0
'completionPreviews': void 0,

// Flag indicating whether to enable the scrolling of long outputs:
'pageLongOutputs': true
}
};
}
Expand Down
4 changes: 4 additions & 0 deletions lib/node_modules/@stdlib/repl/lib/display_prompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ function displayPrompt( repl, preserveCursor ) {
var re;
var ws;

// If REPL is in paging mode on the previous output, don't display the next prompt to block the REPL flow...
if ( repl._isPaging ) {
return;
}
len = repl._cmd.length;
if ( len === 0 ) {
if ( repl._padding && repl._count >= 0 ) {
Expand Down
35 changes: 34 additions & 1 deletion lib/node_modules/@stdlib/repl/lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ var processLine = require( './process_line.js' );
var completerFactory = require( './completer.js' );
var PreviewCompleter = require( './completer_preview.js' );
var AutoCloser = require( './auto_close_pairs.js' );
var Pager = require( './pager.js' );
var ALIAS_OVERRIDES = require( './alias_overrides.js' );
var SETTINGS = require( './settings.js' );
var SETTINGS_VALIDATORS = require( './settings_validators.js' );
Expand Down Expand Up @@ -220,6 +221,8 @@ function REPL( options ) {
setNonEnumerable( this._multiline, 'active', false );
setNonEnumerable( this._multiline, 'mode', 'incomplete_expression' );

setNonEnumerable( this, '_isPaging', false );

// Initialize an internal flag indicating whether the REPL has been closed:
setNonEnumerable( this, '_closed', false );

Expand Down Expand Up @@ -257,12 +260,20 @@ function REPL( options ) {
// Initialize a preview completer:
setNonEnumerableReadOnly( this, '_previewCompleter', new PreviewCompleter( this._rli, this._completer, this._ostream ) );

setNonEnumerableReadOnly( this, '_pager', new Pager( this, this._ostream, this._ostream.write ) );

// Cache a reference to the private readline interface `ttyWrite` to allow calling the method when wanting default behavior:
setNonEnumerableReadOnly( this, '_ttyWrite', this._rli._ttyWrite );

// Overwrite the private `ttyWrite` method to allow processing input before a "keypress" event is triggered:
this._rli._ttyWrite = beforeKeypress; // WARNING: overwriting a private property

// Cache a reference to the output stream `write` to allow calling method when wanting default behavior:
setNonEnumerableReadOnly( this, '_ostreamWrite', this._ostream.write );

// Overwrite the `write` method to allow processing input before a "write" event is triggered:
this._ostream.write = beforeWrite;

// Add event listeners:
this._rli.on( 'close', onClose );
this._rli.on( 'line', onLine );
Expand All @@ -278,7 +289,7 @@ function REPL( options ) {
this._istream.on( 'keypress', onKeypress );

// Write a welcome message:
this._ostream.write( opts.welcome );
this._ostreamWrite.call( this._ostream, opts.welcome );

// TODO: check whether to synchronously initialize a REPL history file

Expand All @@ -302,6 +313,11 @@ function REPL( options ) {
*/
function beforeKeypress( data, key ) {
var settings = self._settings;

if ( settings.pageLongOutputs && self._isPaging ) {
self._pager.beforeKeypress( data, key );
return;
}
if ( settings.autoDeletePairs ) {
self._autoCloser.beforeKeypress( data, key );
}
Expand Down Expand Up @@ -341,6 +357,23 @@ function REPL( options ) {
}
}

/**
* Callback invoked prior to emitting a "write" event.
*
* @private
* @param {string} data - input data
* @param {(string)} encoding - encoding string
* @param {(Function)} cb - callback
*/
function beforeWrite( data, encoding, cb ) {
if ( self._settings.pageLongOutputs ) {
// Detect a multi-page output:
self._pager.beforeWrite( data, encoding, cb );
return;
}
self._ostreamWrite.call( self._ostream, data, encoding, cb );
}

/**
* Callback invoked upon a readline interface "line" event.
*
Expand Down
217 changes: 217 additions & 0 deletions lib/node_modules/@stdlib/repl/lib/pager.js
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' );
}

// 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;
4 changes: 4 additions & 0 deletions lib/node_modules/@stdlib/repl/lib/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ var SETTINGS = {
'completionPreviews': {
'desc': 'Enable the display of completion previews for auto-completion.',
'type': 'boolean'
},
'pageLongOutputs': {
'desc': 'Allow scrolling of long outputs.',
'type': 'boolean'
}
};

Expand Down