open and close animations

This commit is contained in:
Isaac Johnson 2026-06-01 19:36:39 -05:00
parent 41f8f18fcb
commit 34e3efb8df
3 changed files with 235 additions and 13 deletions

View File

@ -10,6 +10,90 @@ readline.emitKeypressEvents(process.stdin);
// Spinner chars for loading animations
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let spinnerIndex = 0;
const GRAVESTONE_FRAMES = [
[
chalk.gray(' '),
chalk.gray(' '),
chalk.gray(' '),
chalk.gray(' '),
chalk.gray(' ___________ '),
],
[
chalk.gray(' '),
chalk.gray(' '),
chalk.gray(' '),
chalk.gray(' ___ '),
chalk.gray(' ___/___\\___ '),
],
[
chalk.gray(' '),
chalk.gray(' '),
chalk.gray(' ___ '),
chalk.gray(' / \\ '),
chalk.gray(' __|_____|__ '),
],
[
chalk.gray(' '),
chalk.gray(' ___ '),
chalk.gray(' / \\ '),
chalk.gray(' | RIP | '),
chalk.gray(' __|_____|__ '),
],
[
chalk.gray(' ___ '),
chalk.gray(' / \\ '),
chalk.gray(' | RIP | '),
chalk.gray(' | | '),
chalk.gray(' __|_____|__ '),
]
];
const ZOMBIE_FRAMES = [
[
chalk.gray(' ___ '),
chalk.gray(' / \\ '),
chalk.gray(' | RIP | '),
chalk.gray(' | | '),
chalk.gray(' __|_____|__ '),
],
[
chalk.gray(' ___ '),
chalk.gray(' / \\ '),
chalk.gray(' | RIP | '),
chalk.gray(' | | '),
chalk.gray(' __|_ ') + chalk.green('.') + chalk.gray(' _|__ '),
],
[
chalk.gray(' ___ '),
chalk.gray(' / \\ '),
chalk.gray(' | RIP | '),
chalk.gray(' | ') + chalk.green('^') + chalk.gray(' | '),
chalk.gray(' __|_') + chalk.green('/ \\') + chalk.gray('_|__ '),
],
[
chalk.gray(' ___ '),
chalk.gray(' / \\ '),
chalk.gray(' | ') + chalk.green('o_o') + chalk.gray(' | '),
chalk.gray(' | ') + chalk.green('/|\\') + chalk.gray(' | '),
chalk.gray(' __|_ ') + chalk.green('|') + chalk.gray(' _|__ '),
],
[
chalk.gray(' ___ '),
chalk.gray(' /') + chalk.green('o_o') + chalk.gray('\\ '),
chalk.gray(' | ') + chalk.green('/|\\') + chalk.gray(' | '),
chalk.gray(' | ') + chalk.green('|') + chalk.gray(' | '),
chalk.gray(' __|_') + chalk.green('/ \\') + chalk.gray('_|__ '),
],
[
chalk.gray(' \\') + chalk.green('o_o') + chalk.gray('/ '),
chalk.gray(' / ') + chalk.green('|') + chalk.gray(' \\ '),
chalk.gray(' | ') + chalk.green('/ \\') + chalk.gray(' | '),
chalk.gray(' | | '),
chalk.gray(' __|_____|__ '),
]
];
let spinnerInterval: NodeJS.Timeout | null = null;
/**
@ -105,6 +189,8 @@ export class TuiEngine {
private onQuit: () => void = () => {};
private activeSearchInput: boolean = false;
private searchInputBuffer: string = '';
private animationFrame: number = 0;
private animationInterval: NodeJS.Timeout | null = null;
constructor(initialState: AppState) {
this.state = initialState;
@ -145,6 +231,10 @@ export class TuiEngine {
*/
public stop() {
this.stopSpinner();
if (this.animationInterval) {
clearInterval(this.animationInterval);
this.animationInterval = null;
}
// Exit alternate screen buffer and show standard cursor
process.stdout.write('\x1B[?1049l\x1B[?25h');
if (process.stdin.isTTY) {
@ -794,9 +884,29 @@ export class TuiEngine {
this.renderAddTimeScreen(cols, rows);
} else if (this.state.screen === 'confirm-state-change') {
this.renderConfirmStateChangeScreen(cols, rows);
} else if (this.state.screen === 'animating-close') {
this.renderAnimationScreen(cols, rows, GRAVESTONE_FRAMES);
} else if (this.state.screen === 'animating-reopen') {
this.renderAnimationScreen(cols, rows, ZOMBIE_FRAMES);
}
}
private renderAnimationScreen(cols: number, rows: number, frames: string[][]) {
const frame = frames[Math.min(this.animationFrame, frames.length - 1)];
const paddingRows = Math.max(0, Math.floor((rows - frame.length) / 2));
for (let i = 0; i < paddingRows; i++) console.log(' '.repeat(cols));
for (const line of frame) {
const plainLen = stripAnsi(line).length;
const padLeft = Math.max(0, Math.floor((cols - plainLen) / 2));
console.log(' '.repeat(padLeft) + line);
}
const remainingRows = Math.max(0, rows - paddingRows - frame.length - 1);
for (let i = 0; i < remainingRows; i++) console.log(' '.repeat(cols));
}
/**
* Draw Setup Form
*/
@ -1944,21 +2054,44 @@ export class TuiEngine {
if (!issue) return;
this.state.error = null;
this.state.loading = true;
const newState = issue.state === 'open' ? 'closed' : 'open';
this.state.screen = newState === 'closed' ? 'animating-close' : 'animating-reopen';
this.animationFrame = 0;
this.render();
if (this.animationInterval) {
clearInterval(this.animationInterval);
}
this.animationInterval = setInterval(() => {
this.animationFrame++;
const frameCount = newState === 'closed' ? GRAVESTONE_FRAMES.length : ZOMBIE_FRAMES.length;
this.render();
if (this.animationFrame >= frameCount - 1) {
if (this.animationInterval) {
clearInterval(this.animationInterval);
this.animationInterval = null;
}
setTimeout(async () => {
this.state.loading = true;
this.render();
try {
const newState = issue.state === 'open' ? 'closed' : 'open';
await changeIssueState(this.state.config, issue.number, newState);
this.state.screen = 'details';
this.state.loading = false;
// Reload to show the updated state
this.reloadSingleIssue();
} catch (err: any) {
this.state.error = err.message;
this.state.screen = 'details';
this.state.loading = false;
this.render();
}
}, 300);
}
}, 300);
return;
}
}

View File

@ -69,7 +69,7 @@ export interface AddTimeForm {
timeInput: string;
}
export type ScreenType = 'setup' | 'repo-picker' | 'list' | 'details' | 'create-issue' | 'add-comment' | 'edit-issue' | 'add-time' | 'confirm-state-change';
export type ScreenType = 'setup' | 'repo-picker' | 'list' | 'details' | 'create-issue' | 'add-comment' | 'edit-issue' | 'add-time' | 'confirm-state-change' | 'animating-close' | 'animating-reopen';
export interface RepoItem {

89
test-frames.js Normal file
View File

@ -0,0 +1,89 @@
const GRAVESTONE_FRAMES = [
[
' ',
' ',
' ',
' ',
' ___________ ',
],
[
' ',
' ',
' ',
' ___ ',
' ___/___\\___ ',
],
[
' ',
' ',
' ___ ',
' / \\ ',
' __|_____|__ ',
],
[
' ',
' ___ ',
' / \\ ',
' | RIP | ',
' __|_____|__ ',
],
[
' ___ ',
' / \\ ',
' | RIP | ',
' | | ',
' __|_____|__ ',
]
];
const ZOMBIE_FRAMES = [
[
' ___ ',
' / \\ ',
' | RIP | ',
' | | ',
' __|_____|__ ',
],
[
' ___ ',
' / \\ ',
' | RIP | ',
' | | ',
' __|_ . _|__ ',
],
[
' ___ ',
' / \\ ',
' | RIP | ',
' | ^ | ',
' __|_/ \\_|__ ',
],
[
' ___ ',
' / \\ ',
' | o_o | ',
' | /|\\ | ',
' __|_ | _|__ ',
],
[
' ___ ',
' /o_o\\ ',
' | /|\\ | ',
' | | | ',
' __|_/ \\_|__ ',
],
[
' \\o_o/ ',
' / | \\ ',
' | / \\ | ',
' | | ',
' __|_____|__ ',
]
];
const checkLens = frames => frames.forEach((f, i) => f.forEach((l, j) => {
if (l.length !== 17) console.log(`Frame ${i} line ${j} len ${l.length}: ${l}`);
}));
checkLens(GRAVESTONE_FRAMES);
checkLens(ZOMBIE_FRAMES);
console.log("Checked lengths!");