Skip to content

Commit

Permalink
Bugfix (Core, Input, Confirm, Expand, Rawlist, Password): Fix cursor …
Browse files Browse the repository at this point in the history
…moves being ignored (Fix #1262)
  • Loading branch information
SBoudrias committed Jul 28, 2023
1 parent 08b0ab4 commit c1a5582
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 6 deletions.
17 changes: 15 additions & 2 deletions packages/core/src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ export function createPrompt<Value, Config extends AsyncPromptConfig>(

let cancel: () => void = () => {};
const answer = new CancelablePromise<Value>((resolve, reject) => {
const checkCursorPos = () => {
screen.checkCursorPos();
};

const onExit = () => {
try {
let len = hooksCleanup.length;
Expand All @@ -260,8 +264,9 @@ export function createPrompt<Value, Config extends AsyncPromptConfig>(
}
screen.done();

resetHookState();
process.removeListener('SIGINT', onForceExit);
sessionRl?.input.removeListener('keypress', checkCursorPos);
resetHookState();
};

cancel = () => {
Expand Down Expand Up @@ -313,7 +318,15 @@ export function createPrompt<Value, Config extends AsyncPromptConfig>(
};

// TODO: we should display a loader while we get the default options.

Check warning on line 320 in packages/core/src/index.mts

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected 'todo' comment: 'TODO: we should display a loader while...'

Check warning on line 320 in packages/core/src/index.mts

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO: we should display a loader while...'

Check warning on line 320 in packages/core/src/index.mts

View workflow job for this annotation

GitHub Actions / build (16)

Unexpected 'todo' comment: 'TODO: we should display a loader while...'
getPromptConfig(config).then(workLoop, reject);
getPromptConfig(config).then((resolvedConfig) => {
workLoop(resolvedConfig);

// Re-renders only happen when the state change; but the readline cursor could change position
// and that also requires a re-render (and a manual one because we mute the streams).
// We set the listener after the initial workLoop to avoid a double render if render triggered
// by a state change sets the cursor to the right position.
sessionRl?.input.on('keypress', checkCursorPos);
}, reject);
});

answer.catch(() => {
Expand Down
22 changes: 18 additions & 4 deletions packages/core/src/lib/screen-manager.mts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CursorPos } from 'node:readline';
import cliWidth from 'cli-width';
import stripAnsi from 'strip-ansi';
import ansiEscapes from 'ansi-escapes';
Expand All @@ -11,9 +12,11 @@ export default class ScreenManager {
// These variables are keeping information to allow correct prompt re-rendering
private height: number = 0;
private extraLinesUnderPrompt: number = 0;
private cursorPos: CursorPos;

constructor(private readonly rl: InquirerReadline) {
this.rl = rl;
this.cursorPos = rl.getCursorPos();
}

render(content: string, bottomContent: string = '') {
Expand All @@ -38,9 +41,9 @@ export default class ScreenManager {
this.rl.setPrompt(prompt);

// SetPrompt will change cursor position, now we can get correct value
const cursorPos = this.rl.getCursorPos();
const width = cliWidth({ defaultWidth: 80, output: this.rl.output });
this.cursorPos = this.rl.getCursorPos();

const width = cliWidth({ defaultWidth: 80, output: this.rl.output });
content = breakLines(content, width);
bottomContent = breakLines(bottomContent, width);

Expand All @@ -59,15 +62,16 @@ export default class ScreenManager {

// We need to consider parts of the prompt under the cursor as part of the bottom
// content in order to correctly cleanup and re-render.
const promptLineUpDiff = Math.floor(rawPromptLine.length / width) - cursorPos.rows;
const promptLineUpDiff =
Math.floor(rawPromptLine.length / width) - this.cursorPos.rows;
const bottomContentHeight =
promptLineUpDiff + (bottomContent ? height(bottomContent) : 0);

// Return cursor to the input position (on top of the bottomContent)
if (bottomContentHeight > 0) output += ansiEscapes.cursorUp(bottomContentHeight);

// Return cursor to the initial left offset.
output += ansiEscapes.cursorTo(cursorPos.cols);
output += ansiEscapes.cursorTo(this.cursorPos.cols);

/**
* Set up state for next re-rendering
Expand All @@ -79,6 +83,16 @@ export default class ScreenManager {
this.rl.output.mute();
}

checkCursorPos() {
const cursorPos = this.rl.getCursorPos();
if (cursorPos.cols !== this.cursorPos.cols) {
this.rl.output.unmute();
this.rl.output.write(ansiEscapes.cursorTo(cursorPos.cols));
this.rl.output.mute();
this.cursorPos = cursorPos;
}
}

clean() {
this.rl.output.unmute();
this.rl.output.write(
Expand Down

0 comments on commit c1a5582

Please sign in to comment.