Skip to content

Commit 393f378

Browse files
committed
fix(preview): draw the slanted tab separators kitty actually uses
The slanted powerline separator was the complement of kitty's glyph. Its filled half takes the tab's own background and the rest shows the next tab's, so drawing the opposite triangle put the tab's colour on the far side of the slant: the tab ended, the bar showed through, and then a wedge of the tab's colour appeared again against the following tab. kitty/tab_bar.py picks U+E0BC there, not U+E0B8. The slant tab bar style had the same pair the wrong way round along the bottom edge, where kitty uses E0B8 then E0BA. The soft separator between two tabs of one colour was inset like the chevron and the arc beside it, which imitate powerline glyphs. That one imitates U+2571, a box-drawing diagonal that runs corner to corner, so it was rendering as a stroke that fell short at both ends. The spec alongside pins each shape to the corner it fills, and drops a spare copy of the slant that nothing drew.
1 parent 9a4122f commit 393f378

3 files changed

Lines changed: 90 additions & 9 deletions

File tree

src/components/live-preview/preview-metrics.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,18 @@ export const POWERLINE_GLYPHS = {
126126
} as const;
127127

128128
/**
129-
* Thin separator stroke definitions. Used when two adjacent tabs share a
130-
* background color, so the filled glyph would render invisibly. These
131-
* stand in for the Powerline soft-separator codepoints (U+E0B1, U+E0B3,
132-
* U+E0B5, U+E0B9, U+E0BD).
129+
* Thin separator stroke definitions, used when two adjacent tabs share a
130+
* background colour and the filled glyph would render invisibly.
131+
*
132+
* The chevron and the arc imitate U+E0B1 and U+E0B5, which sit inset within
133+
* their cell in every Powerline font. The slant does not: Kitty reaches for
134+
* U+2571 there, a box-drawing character that runs corner to corner, so an inset
135+
* copy of it reads as a stroke that fell short at both ends.
133136
*/
134137
export const POWERLINE_SOFT = {
135138
angled: "M0.15 0 L0.85 0.5 L0.15 1",
136-
slanted: "M0.85 0 L0.15 1",
139+
slanted: "M1 0 L0 1",
137140
round: "M0.3 0 Q0.85 0 0.85 0.5 Q0.85 1 0.3 1",
138-
slantTop: "M0.85 0 L0.15 1",
139141
} as const;
140142

141143
/** macOS-style traffic light cluster. */
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, expect, it } from "vitest";
2+
import { POWERLINE_GLYPHS, POWERLINE_SOFT } from "./preview-metrics";
3+
4+
/**
5+
* The preview draws Powerline separators as SVG rather than as glyphs, because
6+
* it cannot count on a patched font being installed. That means the shapes are
7+
* ours to get right, and getting one wrong is not obvious: the slanted
8+
* separator was drawing the complement of the triangle Kitty uses, which put
9+
* the tab's own colour on the far side of the slant and stranded a wedge of it
10+
* against the following tab.
11+
*
12+
* Kitty picks its separators in kitty/tab_bar.py:
13+
*
14+
* powerline_symbols = {'slanted': ('', '╱'), 'round': ('', '')}
15+
* ... default ('', '')
16+
*
17+
* and for the slant tab bar style:
18+
*
19+
* left_sep, right_sep = ('', '') if tab_bar_edge in ('top', 'left')
20+
* else ('', '')
21+
*
22+
* These pin the geometry to those codepoints. A triangle is named by the corner
23+
* region it fills, in a box whose origin is the top left.
24+
*/
25+
describe("powerline separator geometry", () => {
26+
/** The three corners of a filled triangle path, in order. */
27+
function corners(path: string): string {
28+
const numbers = path.match(/-?\d*\.?\d+/g) ?? [];
29+
const points: string[] = [];
30+
for (let i = 0; i + 1 < numbers.length; i += 2) {
31+
points.push(`${numbers[i]},${numbers[i + 1]}`);
32+
}
33+
return points.join(" ");
34+
}
35+
36+
it.each([
37+
["e0b0", "right-pointing triangle", "0,0 1,0.5 0,1"],
38+
["e0b2", "left-pointing triangle", "1,0 0,0.5 1,1"],
39+
["e0b8", "lower left", "1,0 0,1 1,1"],
40+
["e0ba", "lower right", "0,0 0,1 1,1"],
41+
["e0bc", "upper left", "0,0 1,0 0,1"],
42+
["e0be", "upper right", "0,0 1,0 1,1"],
43+
])("draws %s as the %s shape", (name, _description, expected) => {
44+
expect(corners(POWERLINE_GLYPHS[name as keyof typeof POWERLINE_GLYPHS])).toBe(
45+
expected,
46+
);
47+
});
48+
49+
it("gives the slanted soft separator the full height of its cell", () => {
50+
// U+2571 is a box-drawing diagonal, which runs corner to corner. The
51+
// chevron and arc it sits beside imitate Powerline glyphs, which do not.
52+
expect(POWERLINE_SOFT.slanted).toBe("M1 0 L0 1");
53+
expect(POWERLINE_SOFT.angled).toContain("0.15");
54+
expect(POWERLINE_SOFT.round).toContain("0.3");
55+
});
56+
57+
it("has no separator shape nothing draws", () => {
58+
// A spare copy of the slant sat here unused for long enough to be
59+
// mistaken for the one in service.
60+
expect(Object.keys(POWERLINE_SOFT).sort()).toEqual([
61+
"angled",
62+
"round",
63+
"slanted",
64+
]);
65+
});
66+
});

src/components/live-preview/terminal-tab-bar.component.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,17 @@ export class TerminalTabBarComponent {
182182
() => this.store.configState().tab_bar.tab_powerline_style,
183183
);
184184

185+
/**
186+
* The slant style carves the bar's own background out of each end of a tab,
187+
* so a tab reads as a trapezium. Kitty flips which corner is cut when the bar
188+
* sits along the bottom, otherwise the tabs would lean into the terminal
189+
* rather than away from it: E0BC/E0BE along the top, E0B8/E0BA below.
190+
*/
185191
readonly slantLeftPath = computed(() =>
186-
this.edge() === "top" ? POWERLINE_GLYPHS.e0bc : POWERLINE_GLYPHS.e0ba,
192+
this.edge() === "top" ? POWERLINE_GLYPHS.e0bc : POWERLINE_GLYPHS.e0b8,
187193
);
188194
readonly slantRightPath = computed(() =>
189-
this.edge() === "top" ? POWERLINE_GLYPHS.e0be : POWERLINE_GLYPHS.e0b8,
195+
this.edge() === "top" ? POWERLINE_GLYPHS.e0be : POWERLINE_GLYPHS.e0ba,
190196
);
191197

192198
private readonly tabBar = computed(() => this.store.configState().tab_bar);
@@ -263,12 +269,19 @@ export class TerminalTabBarComponent {
263269
return this.edge() === "top" ? inner : outer;
264270
});
265271

272+
/**
273+
* The glyph Kitty draws between two powerline tabs, from the table in
274+
* tab_bar.py. Its filled part takes the tab's own background and the rest
275+
* shows the next tab's, so picking the complementary triangle puts the tab's
276+
* colour on the far side of the slant and leaves a wedge of it stranded
277+
* against the following tab.
278+
*/
266279
powerlinePath(): string {
267280
switch (this.powerlineShape()) {
268281
case "round":
269282
return POWERLINE_GLYPHS.e0b4;
270283
case "slanted":
271-
return POWERLINE_GLYPHS.e0b8;
284+
return POWERLINE_GLYPHS.e0bc;
272285
default:
273286
return POWERLINE_GLYPHS.e0b0;
274287
}

0 commit comments

Comments
 (0)