Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
85 changes: 85 additions & 0 deletions packages/core/src/terminal/Screen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,91 @@ describe('Screen', () => {
});
});

describe('getStyleLine', () => {
it('returns empty string for out-of-bounds row', () => {
const screen = new Screen(5, 3);
expect(screen.getStyleLine(-1)).toBe('');
expect(screen.getStyleLine(3)).toBe('');
});

it('different ansi256 codes produce different hashes', () => {
const screen = new Screen(5, 1);
screen.setCell(0, 0, { char: 'A', fg: { type: 'ansi256', code: 1 } });
const hash1 = screen.getStyleLine(0);

screen.setCell(0, 0, { char: 'A', fg: { type: 'ansi256', code: 9 } });
const hash2 = screen.getStyleLine(0);

expect(hash1).not.toBe(hash2);
});

it('different named colors produce different hashes', () => {
const screen = new Screen(5, 1);
screen.setCell(0, 0, { char: 'A', fg: { type: 'named', name: 'red' } });
const hash1 = screen.getStyleLine(0);

screen.setCell(0, 0, { char: 'A', fg: { type: 'named', name: 'blue' } });
const hash2 = screen.getStyleLine(0);

expect(hash1).not.toBe(hash2);
});

it('different rgb values produce different hashes', () => {
const screen = new Screen(5, 1);
screen.setCell(0, 0, { char: 'A', fg: { type: 'rgb', r: 255, g: 0, b: 0 } });
const hash1 = screen.getStyleLine(0);

screen.setCell(0, 0, { char: 'A', fg: { type: 'rgb', r: 0, g: 255, b: 0 } });
const hash2 = screen.getStyleLine(0);

expect(hash1).not.toBe(hash2);
});

it('different hex values produce different hashes', () => {
const screen = new Screen(5, 1);
screen.setCell(0, 0, { char: 'A', fg: { type: 'hex', hex: '#ff0000' } });
const hash1 = screen.getStyleLine(0);

screen.setCell(0, 0, { char: 'A', fg: { type: 'hex', hex: '#00ff00' } });
const hash2 = screen.getStyleLine(0);

expect(hash1).not.toBe(hash2);
});

it('same color values produce the same hash', () => {
const screen = new Screen(5, 1);
screen.setCell(0, 0, { char: 'A', fg: { type: 'ansi256', code: 42 } });
const hash1 = screen.getStyleLine(0);

screen.setCell(0, 0, { char: 'A', fg: { type: 'ansi256', code: 42 } });
const hash2 = screen.getStyleLine(0);

expect(hash1).toBe(hash2);
});

it('style flags still affect the hash', () => {
const screen = new Screen(5, 1);
screen.setCell(0, 0, { char: 'A', bold: false });
const hash1 = screen.getStyleLine(0);

screen.setCell(0, 0, { char: 'A', bold: true });
const hash2 = screen.getStyleLine(0);

expect(hash1).not.toBe(hash2);
});

it('background color changes are detected', () => {
const screen = new Screen(5, 1);
screen.setCell(0, 0, { char: 'A', bg: { type: 'ansi256', code: 1 } });
const hash1 = screen.getStyleLine(0);

screen.setCell(0, 0, { char: 'A', bg: { type: 'ansi256', code: 9 } });
const hash2 = screen.getStyleLine(0);

expect(hash1).not.toBe(hash2);
});
});

describe('cellsEqual', () => {
it('returns true for identical cells', () => {
const a = emptyCell();
Expand Down
32 changes: 29 additions & 3 deletions packages/core/src/terminal/Screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ import { stringWidth, segmenter } from '../utils/unicode.js';
import { stripAnsiControl } from '../utils/ansi.js';
import { caps } from './env-caps.js';

/**
* Produce a numeric fingerprint from a Color value.
* The hash incorporates both the color type and its specific value
* (code, name, r/g/b, or hex) so that two colors of the same type
* but different values produce distinct hashes.
*/
function hashColor(c: Color): number {
switch (c.type) {
case 'none':
return 0;
case 'named':
return c.name.charCodeAt(0) * 31 + c.name.charCodeAt(c.name.length - 1);
Comment thread
atul-upadhyay-7 marked this conversation as resolved.
case 'ansi256':
return c.code * 7 + 1;
case 'rgb':
return (c.r << 16) | (c.g << 8) | c.b;
case 'hex': {
let h = 0;
for (let i = 0; i < c.hex.length; i++) {
h = ((h << 5) - h + c.hex.charCodeAt(i)) | 0;
}
return h;
}
}
}

const EMPTY_COLOR: Color = Object.freeze({ type: 'none' } as const);

/**
Expand Down Expand Up @@ -194,16 +220,16 @@ export class Screen {
let hash = 0;
for (const cell of this.back[row]) {
if (cell.width === 0) continue;
const fg = cell.fg.type;
const bg = cell.bg.type;
const fgVal = hashColor(cell.fg);
const bgVal = hashColor(cell.bg);
const bits =
(cell.bold ? 1 : 0) |
(cell.italic ? 2 : 0) |
(cell.underline ? 4 : 0) |
(cell.dim ? 8 : 0) |
(cell.strikethrough ? 16 : 0) |
(cell.inverse ? 32 : 0);
const seed = fg.charCodeAt(0) * 65536 + bg.charCodeAt(0) * 4096 + bits;
const seed = fgVal * 65536 + bgVal * 4096 + bits;
hash = ((hash << 7) - hash + seed) | 0;
if (cell.link) {
for (let i = 0; i < cell.link.length; i++)
Expand Down
Loading