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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ Open the gear icon in the toolbar:
|---|---|
| Theme | `Auto (match app)` follows CloudCLI's light/dark mode |
| Font size | 8–32 px, applied to every tab |
| Font family | Preset monospace families (JetBrains Mono, Fira Code, …); only fonts installed on the viewing machine take effect, with CJK and emoji fallbacks kept |
| Letter spacing | −2 to 4 px |
| Line height | 1.00–2.00 in 0.05 steps |
| Cursor | Block, bar or underline |
| Shell (new tabs) | Any shell discovered on the host; existing tabs are unaffected |
| Copy on select | Off by default |
Expand Down
47 changes: 47 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { PluginAPI, PluginContext } from './types.js';
import { TerminalSession } from './session.js';
import {
loadPrefs, savePrefs, loadStoredTabs, saveStoredTabs,
FONT_PRESETS, clampLetterSpacing, clampLineHeight,
type Prefs, type StoredTab,
} from './prefs.js';
import { injectStyles } from './ui/styles.js';
Expand Down Expand Up @@ -219,6 +220,31 @@ export async function mount(container: HTMLElement, api: PluginAPI): Promise<voi
fontRow.append(fontMinus, fontValue, fontPlus);
popover.appendChild(fontRow);

const fontOptions = FONT_PRESETS.map((preset) => ({ value: preset.family, label: preset.label }));
// A family set outside the picker (hand-edited storage) stays selectable
// instead of the picker silently claiming "Default".
if (!fontOptions.some((option) => option.value === prefs.fontFamily)) {
fontOptions.push({ value: prefs.fontFamily, label: 'Custom' });
}
const fontFamily = selectField('Font Family', fontOptions, prefs.fontFamily);
popover.appendChild(fontFamily.wrapper);

popover.appendChild(el('label', undefined, 'Letter Spacing'));
const spacingRow = el('div', 'wt-fs-row');
const spacingMinus = iconButton(IC.minus, 'Decrease letter spacing');
const spacingValue = el('span', undefined, `${prefs.letterSpacing}px`);
const spacingPlus = iconButton(IC.plus, 'Increase letter spacing');
spacingRow.append(spacingMinus, spacingValue, spacingPlus);
popover.appendChild(spacingRow);

popover.appendChild(el('label', undefined, 'Line Height'));
const lineHeightRow = el('div', 'wt-fs-row');
const lineHeightMinus = iconButton(IC.minus, 'Decrease line height');
const lineHeightValue = el('span', undefined, prefs.lineHeight.toFixed(2));
const lineHeightPlus = iconButton(IC.plus, 'Increase line height');
lineHeightRow.append(lineHeightMinus, lineHeightValue, lineHeightPlus);
popover.appendChild(lineHeightRow);

const cursor = selectField('Cursor', [
{ value: 'block', label: 'Block' },
{ value: 'bar', label: 'Bar' },
Expand Down Expand Up @@ -577,6 +603,27 @@ export async function mount(container: HTMLElement, api: PluginAPI): Promise<voi
fontMinus.addEventListener('click', () => changeFontSize(-1));
fontPlus.addEventListener('click', () => changeFontSize(1));

fontFamily.select.addEventListener('change', () => {
prefs.fontFamily = fontFamily.select.value;
applyPrefsToAll();
});

const changeLetterSpacing = (delta: number): void => {
prefs.letterSpacing = clampLetterSpacing(prefs.letterSpacing + delta);
spacingValue.textContent = `${prefs.letterSpacing}px`;
applyPrefsToAll();
};
spacingMinus.addEventListener('click', () => changeLetterSpacing(-1));
spacingPlus.addEventListener('click', () => changeLetterSpacing(1));

const changeLineHeight = (delta: number): void => {
prefs.lineHeight = clampLineHeight(prefs.lineHeight + delta);
lineHeightValue.textContent = prefs.lineHeight.toFixed(2);
applyPrefsToAll();
};
lineHeightMinus.addEventListener('click', () => changeLineHeight(-0.05));
lineHeightPlus.addEventListener('click', () => changeLineHeight(0.05));

// Search
function setSearch(open: boolean): void {
searchBar.classList.toggle('wt-open', open);
Expand Down
39 changes: 39 additions & 0 deletions src/prefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,34 @@ export const DEFAULT_FONT_FAMILY =
+ '"Noto Sans Mono CJK JP", "Noto Sans CJK JP", "Microsoft YaHei", "MS Gothic", Meiryo, '
+ '"PingFang SC", "Hiragino Sans GB", "Noto Color Emoji", Menlo, Monaco, "Courier New", monospace';

/** Appended to every preset so picking a family never loses CJK or emoji glyphs. */
const FONT_FALLBACK = '"Noto Sans Mono CJK JP", "Noto Sans CJK JP", "Microsoft YaHei", "Noto Color Emoji", monospace';

/** Families offered in the settings popover; only installed fonts actually take effect. */
export const FONT_PRESETS: ReadonlyArray<{ label: string; family: string }> = [
{ label: 'Default', family: DEFAULT_FONT_FAMILY },
{ label: 'JetBrains Mono', family: `"JetBrains Mono", ${FONT_FALLBACK}` },
{ label: 'Fira Code', family: `"Fira Code", ${FONT_FALLBACK}` },
{ label: 'SF Mono / Menlo', family: `"SF Mono", Menlo, Monaco, ${FONT_FALLBACK}` },
{ label: 'Cascadia / Consolas', family: `"Cascadia Mono", Consolas, ${FONT_FALLBACK}` },
{ label: 'Ubuntu Mono', family: `"Ubuntu Mono", ${FONT_FALLBACK}` },
{ label: 'DejaVu Sans Mono', family: `"DejaVu Sans Mono", ${FONT_FALLBACK}` },
];

/** xterm only renders whole-pixel letter spacing. */
export const LETTER_SPACING_RANGE = { min: -2, max: 4 } as const;
/** xterm throws on a line height below 1. */
export const LINE_HEIGHT_RANGE = { min: 1, max: 2 } as const;

export function clampLetterSpacing(value: number): number {
return Math.max(LETTER_SPACING_RANGE.min, Math.min(LETTER_SPACING_RANGE.max, Math.round(value)));
}

/** Rounds to 0.05 steps so repeated nudges do not accumulate float noise. */
export function clampLineHeight(value: number): number {
return Math.max(LINE_HEIGHT_RANGE.min, Math.min(LINE_HEIGHT_RANGE.max, Math.round(value * 20) / 20));
}

/** Lines of history kept per terminal. */
export const SCROLLBACK = 10000;

Expand All @@ -20,6 +48,9 @@ export interface Prefs {
theme: string;
fontSize: number;
fontFamily: string;
/** Extra pixels between characters; may be negative. */
letterSpacing: number;
lineHeight: number;
cursorStyle: CursorStyle;
copyOnSelect: boolean;
webgl: boolean;
Expand All @@ -33,6 +64,8 @@ export const DEFAULT_PREFS: Prefs = {
theme: AUTO_THEME,
fontSize: 14,
fontFamily: DEFAULT_FONT_FAMILY,
letterSpacing: 0,
lineHeight: 1,
cursorStyle: 'block',
copyOnSelect: false,
webgl: true,
Expand Down Expand Up @@ -66,6 +99,12 @@ export function loadPrefs(): Prefs {
fontFamily: typeof stored.fontFamily === 'string' && stored.fontFamily
? stored.fontFamily
: DEFAULT_PREFS.fontFamily,
letterSpacing: Number.isFinite(stored.letterSpacing)
? clampLetterSpacing(stored.letterSpacing as number)
: DEFAULT_PREFS.letterSpacing,
lineHeight: Number.isFinite(stored.lineHeight)
? clampLineHeight(stored.lineHeight as number)
: DEFAULT_PREFS.lineHeight,
cursorStyle,
copyOnSelect: stored.copyOnSelect === true,
webgl: typeof stored.webgl === 'boolean' ? stored.webgl : readLegacyWebglFlag(),
Expand Down
4 changes: 4 additions & 0 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ export class TerminalSession {
cursorStyle: options.prefs.cursorStyle,
fontSize: options.prefs.fontSize,
fontFamily: options.prefs.fontFamily,
letterSpacing: options.prefs.letterSpacing,
lineHeight: options.prefs.lineHeight,
allowProposedApi: true,
scrollback: SCROLLBACK,
tabStopWidth: 8,
Expand Down Expand Up @@ -611,6 +613,8 @@ export class TerminalSession {
applyPrefs(prefs: Prefs, hostTheme: 'dark' | 'light'): void {
this.terminal.options.fontSize = prefs.fontSize;
this.terminal.options.fontFamily = prefs.fontFamily;
this.terminal.options.letterSpacing = prefs.letterSpacing;
this.terminal.options.lineHeight = prefs.lineHeight;
this.terminal.options.cursorStyle = prefs.cursorStyle;
this.terminal.options.screenReaderMode = prefs.screenReaderMode;
this.terminal.options.theme = THEMES[resolveThemeName(prefs.theme, hostTheme)];
Expand Down