From 77695cb40525285d75cca3b961bb229669e6f217 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Mon, 2 Feb 2026 12:33:33 -0800
Subject: [PATCH] Make arrows configurable via API
---
css/xterm.css | 14 +++++++++-
demo/client/client.ts | 1 +
src/browser/Viewport.ts | 1 +
src/browser/scrollable/horizontalScrollbar.ts | 2 +-
src/browser/scrollable/scrollableElement.ts | 1 -
.../scrollable/scrollableElementOptions.ts | 6 -----
src/browser/scrollable/scrollbarArrow.ts | 10 +++----
src/browser/scrollable/verticalScrollbar.ts | 26 +++++++++++++++++--
src/common/services/OptionsService.ts | 1 +
src/common/services/Services.ts | 5 ++++
typings/xterm.d.ts | 16 ++++++++++++
11 files changed, 65 insertions(+), 18 deletions(-)
diff --git a/css/xterm.css b/css/xterm.css
index e9a1e3ad..b775f651 100644
--- a/css/xterm.css
+++ b/css/xterm.css
@@ -225,7 +225,19 @@
/* Arrows */
.xterm .xterm-scrollable-element > .scrollbar > .scra {
cursor: pointer;
- font-size: 11px !important;
+ background-color: var(--vscode-scrollbarSliderBackground, rgba(100, 100, 100, 0.4));
+ mask-image: url("data:image/svg+xml;utf8,");
+ -webkit-mask-image: url("data:image/svg+xml;utf8,");
+ mask-repeat: no-repeat;
+ -webkit-mask-repeat: no-repeat;
+ mask-position: center center;
+ -webkit-mask-position: center center;
+ mask-size: 100% 100%;
+ -webkit-mask-size: 100% 100%;
+}
+
+.xterm .xterm-scrollable-element > .scrollbar > .scra.xterm-arrow-down {
+ transform: rotate(180deg);
}
.xterm .xterm-scrollable-element > .visible {
diff --git a/demo/client/client.ts b/demo/client/client.ts
index 45bdd1d4..2665a398 100644
--- a/demo/client/client.ts
+++ b/demo/client/client.ts
@@ -275,6 +275,7 @@ function createTerminal(): Terminal {
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
term = new Terminal({
+ scrollbar: { showArrows: true },
allowProposedApi: true,
windowsPty: isWindows ? {
// In a real scenario, these values should be verified on the backend
diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts
index 8110ff05..56b6fc30 100644
--- a/src/browser/Viewport.ts
+++ b/src/browser/Viewport.ts
@@ -56,6 +56,7 @@ export class Viewport extends Disposable {
horizontal: ScrollbarVisibility.HIDDEN,
useShadows: false,
mouseWheelSmoothScroll: true,
+ verticalHasArrows: this._optionsService.rawOptions.scrollbar?.showArrows ?? false,
...this._getChangeOptions()
}, scrollable));
this._register(this._optionsService.onMultipleOptionChange([
diff --git a/src/browser/scrollable/horizontalScrollbar.ts b/src/browser/scrollable/horizontalScrollbar.ts
index 70058ca7..470410c4 100644
--- a/src/browser/scrollable/horizontalScrollbar.ts
+++ b/src/browser/scrollable/horizontalScrollbar.ts
@@ -17,7 +17,7 @@ export class HorizontalScrollbar extends AbstractScrollbar {
lazyRender: options.lazyRender,
host: host,
scrollbarState: new ScrollbarState(
- (options.horizontalHasArrows ? options.arrowSize : 0),
+ (options.horizontalHasArrows ? options.horizontalScrollbarSize : 0),
(options.horizontal === ScrollbarVisibility.HIDDEN ? 0 : options.horizontalScrollbarSize),
(options.vertical === ScrollbarVisibility.HIDDEN ? 0 : options.verticalScrollbarSize),
scrollDimensions.width,
diff --git a/src/browser/scrollable/scrollableElement.ts b/src/browser/scrollable/scrollableElement.ts
index 7734d530..82ba75f1 100644
--- a/src/browser/scrollable/scrollableElement.ts
+++ b/src/browser/scrollable/scrollableElement.ts
@@ -572,7 +572,6 @@ function resolveOptions(opts: IScrollableElementCreationOptions): IScrollableEle
fastScrollSensitivity: (typeof opts.fastScrollSensitivity !== 'undefined' ? opts.fastScrollSensitivity : 5),
scrollPredominantAxis: (typeof opts.scrollPredominantAxis !== 'undefined' ? opts.scrollPredominantAxis : true),
mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true),
- arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11),
listenOnDomNode: (typeof opts.listenOnDomNode !== 'undefined' ? opts.listenOnDomNode : null),
diff --git a/src/browser/scrollable/scrollableElementOptions.ts b/src/browser/scrollable/scrollableElementOptions.ts
index 6787189e..8b241a60 100644
--- a/src/browser/scrollable/scrollableElementOptions.ts
+++ b/src/browser/scrollable/scrollableElementOptions.ts
@@ -67,11 +67,6 @@ export interface IScrollableElementCreationOptions {
* Defaults to true.
*/
scrollPredominantAxis?: boolean;
- /**
- * Height for vertical arrows (top/bottom) and width for horizontal arrows (left/right).
- * Defaults to 11.
- */
- arrowSize?: number;
/**
* The dom node events should be bound to.
* If no listenOnDomNode is provided, the constructor dom node is used.
@@ -151,7 +146,6 @@ export interface IScrollableElementResolvedOptions {
fastScrollSensitivity: number;
scrollPredominantAxis: boolean;
mouseWheelSmoothScroll: boolean;
- arrowSize: number;
listenOnDomNode: HTMLElement | null;
horizontal: ScrollbarVisibility;
horizontalScrollbarSize: number;
diff --git a/src/browser/scrollable/scrollbarArrow.ts b/src/browser/scrollable/scrollbarArrow.ts
index 5dc19da4..3b8f9adc 100644
--- a/src/browser/scrollable/scrollbarArrow.ts
+++ b/src/browser/scrollable/scrollbarArrow.ts
@@ -8,11 +8,6 @@ import { Widget } from './widget';
import { TimeoutTimer } from 'common/Async';
import * as dom from '../Dom';
-/**
- * The arrow image size.
- */
-const ARROW_IMG_SIZE = 11;
-
export interface IScrollbarArrowOptions {
handleActivate: () => void;
className: string;
@@ -63,8 +58,9 @@ export class ScrollbarArrow extends Widget {
// this.domNode.classList.add(...ThemeIcon.asClassNameArray(opts.icon));
this.domNode.style.position = 'absolute';
- this.domNode.style.width = ARROW_IMG_SIZE + 'px';
- this.domNode.style.height = ARROW_IMG_SIZE + 'px';
+ const arrowSize = Math.min(opts.bgWidth, opts.bgHeight);
+ this.domNode.style.width = arrowSize + 'px';
+ this.domNode.style.height = arrowSize + 'px';
if (typeof opts.top !== 'undefined') {
this.domNode.style.top = opts.top + 'px';
}
diff --git a/src/browser/scrollable/verticalScrollbar.ts b/src/browser/scrollable/verticalScrollbar.ts
index e55373e2..5cecc87f 100644
--- a/src/browser/scrollable/verticalScrollbar.ts
+++ b/src/browser/scrollable/verticalScrollbar.ts
@@ -17,7 +17,7 @@ export class VerticalScrollbar extends AbstractScrollbar {
lazyRender: options.lazyRender,
host: host,
scrollbarState: new ScrollbarState(
- (options.verticalHasArrows ? options.arrowSize : 0),
+ (options.verticalHasArrows ? options.verticalScrollbarSize : 0),
(options.vertical === ScrollbarVisibility.HIDDEN ? 0 : options.verticalScrollbarSize),
0,
scrollDimensions.height,
@@ -31,7 +31,24 @@ export class VerticalScrollbar extends AbstractScrollbar {
});
if (options.verticalHasArrows) {
- throw new Error('horizontalHasArrows is not supported in xterm.js');
+ const arrowSize = options.verticalScrollbarSize;
+ const arrowDelta = 0;
+ this._createArrow({
+ className: 'scra xterm-arrow-up',
+ top: arrowDelta,
+ left: arrowDelta,
+ bgWidth: options.verticalScrollbarSize,
+ bgHeight: arrowSize,
+ handleActivate: () => this._arrowScroll(-arrowSize)
+ });
+ this._createArrow({
+ className: 'scra xterm-arrow-down',
+ bottom: arrowDelta,
+ left: arrowDelta,
+ bgWidth: options.verticalScrollbarSize,
+ bgHeight: arrowSize,
+ handleActivate: () => this._arrowScroll(arrowSize)
+ });
}
this._createSlider(0, Math.floor((options.verticalScrollbarSize - options.verticalSliderSize) / 2), options.verticalSliderSize, undefined);
@@ -76,6 +93,11 @@ export class VerticalScrollbar extends AbstractScrollbar {
target.scrollTop = scrollPosition;
}
+ private _arrowScroll(delta: number): void {
+ const currentPosition = this._scrollable.getCurrentScrollPosition();
+ this._scrollable.setScrollPositionNow({ scrollTop: currentPosition.scrollTop + delta });
+ }
+
public updateOptions(options: IScrollableElementResolvedOptions): void {
this.updateScrollbarSize(options.vertical === ScrollbarVisibility.HIDDEN ? 0 : options.verticalScrollbarSize);
this._scrollbarState.setOppositeScrollbarSize(0);
diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts
index 2eab930f..3cb1b55b 100644
--- a/src/common/services/OptionsService.ts
+++ b/src/common/services/OptionsService.ts
@@ -31,6 +31,7 @@ export const DEFAULT_OPTIONS: Readonly> = {
logLevel: 'info',
logger: null,
scrollback: 1000,
+ scrollbar: {},
scrollOnEraseInDisplay: false,
scrollOnUserInput: true,
scrollSensitivity: 1,
diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts
index 341c0bc2..4a88de3d 100644
--- a/src/common/services/Services.ts
+++ b/src/common/services/Services.ts
@@ -266,6 +266,7 @@ export interface ITerminalOptions {
wordSeparator?: string;
overviewRuler?: IOverviewRulerOptions;
quirks?: ITerminalQuirks;
+ scrollbar?: IScrollbarOptions;
scrollOnEraseInDisplay?: boolean;
vtExtensions?: IVtExtensions;
@@ -309,6 +310,10 @@ export interface ITerminalQuirks {
allowSetCursorBlink?: boolean;
}
+export interface IScrollbarOptions {
+ showArrows?: boolean;
+}
+
export interface IVtExtensions {
kittyKeyboard?: boolean;
kittySgrBoldFaintControl?: boolean;
diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts
index 19ab3da0..97901351 100644
--- a/typings/xterm.d.ts
+++ b/typings/xterm.d.ts
@@ -274,6 +274,11 @@ declare module '@xterm/xterm' {
*/
scrollSensitivity?: number;
+ /**
+ * Options for configuring the scrollbar.
+ */
+ scrollbar?: IScrollbarOptions;
+
/**
* The duration to smoothly scroll between the origin and the target in
* milliseconds. Set to 0 to disable smooth scrolling and scroll instantly.
@@ -722,6 +727,17 @@ declare module '@xterm/xterm' {
showBottomBorder?: boolean;
}
+ /**
+ * Options for configuring the scrollbar.
+ */
+ export interface IScrollbarOptions {
+ /**
+ * Whether to show arrows at the top and bottom of the scrollbar. Defaults
+ * to false.
+ */
+ showArrows?: boolean;
+ }
+
/**
* Enable various window manipulation and report features
* (`CSI Ps ; Ps ; Ps t`).