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
74 changes: 74 additions & 0 deletions packages/core/src/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// ─────────────────────────────────────────────────────
// @termuijs/core — History Manager
// ─────────────────────────────────────────────────────

export interface HistoryOptions {
maxSize?: number;
}

export class History<T> {
private undoStack: T[] = [];
private redoStack: T[] = [];
private readonly maxSize: number;

constructor(options: HistoryOptions = {}) {
this.maxSize = options.maxSize ?? 100;
}

push(state: T): void {
this.undoStack.push(state);

if (this.undoStack.length > this.maxSize) {
this.undoStack.shift();
}

this.redoStack = [];
}

undo(): T | undefined {
if (this.undoStack.length <= 1) {
return this.undoStack[0];
}

const current = this.undoStack.pop();

if (current !== undefined) {
this.redoStack.push(current);
}

return this.undoStack[this.undoStack.length - 1];
}

redo(): T | undefined {
const state = this.redoStack.pop();

if (state !== undefined) {
this.undoStack.push(state);
}

return state;
}

current(): T | undefined {
return this.undoStack[this.undoStack.length - 1];
}

canUndo(): boolean {
return this.undoStack.length > 1;
}

canRedo(): boolean {
return this.redoStack.length > 0;
}

clear(): void {
this.undoStack = [];
this.redoStack = [];
}
}

export function createHistory<T>(
options?: HistoryOptions,
): History<T> {
return new History<T>(options);
}
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export { throttle } from './utils/throttle.js';
export type { ThrottleOptions } from './utils/throttle.js';
export { CommandHistory } from "./history/CommandHistory.js";
export type { CommandHistoryOptions } from "./history/CommandHistory.js";
export { History, createHistory } from "./history.js";
export type { HistoryOptions } from "./history.js";
export * from './errors.js';

// ── Accessibility ─────────────────────────────────────
Expand Down
32 changes: 30 additions & 2 deletions packages/widgets/src/display/Breadcrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ import { Widget } from '../base/Widget.js';
export interface BreadcrumbsOptions {
/** Separator drawn between segments. Default: caps.unicode ? '❯' : '>' */
separator?: string;
/** Color of the last (current) segment. Default: cyan */

/** Color of the active breadcrumb */
activeColor?: Color;

/** Called when a breadcrumb is selected */
onSelect?: (index: number, label: string) => void;
}

/**
Expand All @@ -22,12 +26,15 @@ export class Breadcrumbs extends Widget {
private _segments: string[];
private _separator?: string;
private _activeColor?: Color;
private _selectedIndex = 0;
private _onSelect?: (index: number, label: string) => void;

constructor(segments: string[], style: Partial<Style> = {}, opts: BreadcrumbsOptions = {}) {
super(style);
this._segments = segments;
this._separator = opts.separator;
this._activeColor = opts.activeColor;
this._onSelect = opts.onSelect;
}

/** Update the trail of segments. */
Expand All @@ -42,6 +49,24 @@ export class Breadcrumbs extends Widget {
this.markDirty();
}

handleKey(event: { key: string }): void {
if (event.key === 'left') {
this._selectedIndex = Math.max(0, this._selectedIndex - 1);
this.markDirty();
} else if (event.key === 'right') {
this._selectedIndex = Math.min(
this._segments.length - 1,
this._selectedIndex + 1
);
this.markDirty();
} else if (event.key === 'enter') {
this._onSelect?.(
this._selectedIndex,
this._segments[this._selectedIndex]
);
}
}

protected _renderSelf(screen: Screen): void {
const { x, y, width, height } = this._getContentRect();
if (width <= 0 || height <= 0 || this._segments.length === 0) return;
Expand Down Expand Up @@ -85,7 +110,10 @@ export class Breadcrumbs extends Widget {
if (i > 0 || (showEllipsis && hasTruncated)) {
renderList.push({ type: 'separator', text: sep });
}
const isLast = i === visibleSegments.length - 1;
const originalIndex =
this._segments.length - visibleSegments.length + i;

const isLast = originalIndex === this._selectedIndex;
renderList.push({
type: isLast ? 'active' : 'normal',
text: visibleSegments[i]
Expand Down
38 changes: 38 additions & 0 deletions packages/widgets/src/display/StatusBar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, expect, it } from 'vitest';
import { StatusBar } from './StatusBar.js';

describe('StatusBar', () => {
it('creates a StatusBar instance', () => {
const statusBar = new StatusBar({}, {
left: 'Ready',
center: 'Dashboard',
right: 'Ctrl+C Exit',
});

expect(statusBar).toBeDefined();
});

it('updates left section', () => {
const statusBar = new StatusBar();

statusBar.setLeft('Connected');

expect(statusBar).toBeDefined();
});

it('updates center section', () => {
const statusBar = new StatusBar();

statusBar.setCenter('Workspace');

expect(statusBar).toBeDefined();
});

it('updates right section', () => {
const statusBar = new StatusBar();

statusBar.setRight('F1 Help');

expect(statusBar).toBeDefined();
});
});
91 changes: 91 additions & 0 deletions packages/widgets/src/display/StatusBar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// ─────────────────────────────────────────────────────
// @termuijs/widgets — StatusBar widget
// ─────────────────────────────────────────────────────

import {
type Screen,
type Style,
styleToCellAttrs,
truncate,
stringWidth,
} from '@termuijs/core';

import { Widget } from '../base/Widget.js';

export interface StatusBarOptions {
left?: string;
center?: string;
right?: string;
}

export class StatusBar extends Widget {
private _left: string;
private _center: string;
private _right: string;

constructor(
style: Partial<Style> = {},
options: StatusBarOptions = {},
) {
super(style);

this._left = options.left ?? '';
this._center = options.center ?? '';
this._right = options.right ?? '';
}

setLeft(text: string): void {
this._left = text;
this.markDirty();
}

setCenter(text: string): void {
this._center = text;
this.markDirty();
}

setRight(text: string): void {
this._right = text;
this.markDirty();
}

protected _renderSelf(screen: Screen): void {
const { x, y, width, height } = this._getContentRect();

if (width <= 0 || height <= 0) return;

const attrs = styleToCellAttrs(this._style);

// Left
screen.writeString(
x,
y,
truncate(this._left, width),
attrs,
);

// Center
const centerX = x + Math.max(
0,
Math.floor((width - stringWidth(this._center)) / 2),
);

screen.writeString(
centerX,
y,
truncate(this._center, width),
attrs,
);

// Right
const rightWidth = stringWidth(this._right);
const rightX = Math.max(x, x + width - rightWidth);

screen.writeString(
rightX,
y,
truncate(this._right, width),
attrs,
);
}
}
11 changes: 10 additions & 1 deletion packages/widgets/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,14 @@ export type { DraggableOptions, DroppableOptions } from './layout/DragAndDrop.js
export { Fill } from './layout/Fill.js';
export type { FillOptions } from './layout/Fill.js';
export { SplitPane } from './layout/SplitPane.js';
export type { SplitPaneOptions, SplitDirection } from './layout/SplitPane.js';
export type { SplitPaneOptions } from './layout/SplitPane.js';
export { Workspace } from './layout/Workspace.js';

export type {
WorkspaceLayout,
WorkspaceOptions,
WorkspaceStorage,
} from './layout/Workspace.js';

// ── Feedback Widgets ──────────────────────────────────
export { ProgressBar } from './feedback/ProgressBar.js';
Expand Down Expand Up @@ -182,6 +189,8 @@ export type { BulletChartOptions, BulletRange } from './data/BulletChart.js';
// ── New Display Widgets ───────────────────────────────
export { Breadcrumbs } from './display/Breadcrumbs.js';
export type { BreadcrumbsOptions } from './display/Breadcrumbs.js';
export { StatusBar } from './display/StatusBar.js';
export type { StatusBarOptions } from './display/StatusBar.js';
export { Avatar } from './display/Avatar.js';
export type { AvatarOptions } from './display/Avatar.js';

Expand Down
Loading
Loading