Skip to content

Commit

Permalink
✨ feat: refactor TerminalLogs component to use EventBus for event han…
Browse files Browse the repository at this point in the history
…dling and add showLogs prop
  • Loading branch information
futjesus committed Feb 10, 2025
1 parent f9dec04 commit 958a278
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 119 deletions.
9 changes: 1 addition & 8 deletions lib/components/TerminalLogs/TerminalLogs.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react';

import { TerminalLogs as TerminalLogsComponent } from './TerminalLogs';
import { TerminalEvent, TerminalEventCallback } from './event-bus';

type Story = StoryObj<typeof TerminalLogsComponent>;

Expand All @@ -12,15 +11,9 @@ const meta = {

export const TerminalLogs = {
render: () => {
const listeners = {
[TerminalEvent.TERMINAL_LOADED]: (event) => {
console.log(event);
},
} satisfies Record<TerminalEvent, TerminalEventCallback<TerminalEvent>>;

return (
<div className="w-[600px] h-[600px]">
<TerminalLogsComponent listeners={listeners} />
<TerminalLogsComponent showLogs />
</div>
);
},
Expand Down
23 changes: 10 additions & 13 deletions lib/components/TerminalLogs/TerminalLogs.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
import { FC, useEffect, useRef } from 'react';
import { ComponentRef, FC, useEffect, useRef } from 'react';
import { Root } from '@radix-ui/react-tabs';

import { useTheme } from '@/contexts';
import { cn } from '@/utils';

import { eventAdapter, Terminal } from './adapters';
import { Terminal } from './adapters';
import { Body, Header } from './components';
import { TerminalLogsProps } from './TerminalLogs.types';
import { wrapperTerminalLogsVariants } from './TerminalLogs.variants';

export const TerminalLogs: FC<TerminalLogsProps> = ({
theme,
className,
listeners,
showLogs,
}) => {
const emitter = useRef(eventAdapter(listeners));
const terminalRef = useRef<Terminal>(Terminal.create({}, emitter.current));
const terminalWrapperRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<Terminal>(Terminal.create({ showLogs }));
const terminalWrapperRef = useRef<ComponentRef<'div'>>(null);
const { theme: contexTheme } = useTheme();
const inheritTheme = theme ?? contexTheme;

useEffect(() => {
const terminalInstance = terminalRef.current;
const currentEmitter = emitter.current;

if (terminalWrapperRef.current) {
terminalInstance.open(terminalWrapperRef.current);
}

return () => {
terminalInstance.dispose();
currentEmitter?.removeAllListeners();
};
}, []);

Expand All @@ -40,11 +37,11 @@ export const TerminalLogs: FC<TerminalLogsProps> = ({
wrapperTerminalLogsVariants({ className, theme: inheritTheme }),
)}
>
<Root defaultValue="tab-1" orientation="horizontal">
<Header />
<Body>
<div ref={terminalWrapperRef} className="relative" />
</Body>
<Root defaultValue="tab-1" orientation="horizontal" asChild={true}>
<>
<Header />
<Body ref={terminalWrapperRef} />
</>
</Root>
</div>
);
Expand Down
3 changes: 1 addition & 2 deletions lib/components/TerminalLogs/TerminalLogs.types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { VariantProps } from 'class-variance-authority';

import { wrapperTerminalLogsVariants } from './TerminalLogs.variants';
import { TerminalEvent, TerminalEventCallback } from './event-bus';

export type TerminalLogsProps = VariantProps<
typeof wrapperTerminalLogsVariants
> & {
className?: string;
listeners?: Record<TerminalEvent, TerminalEventCallback<TerminalEvent>>;
showLogs?: boolean;
};
21 changes: 0 additions & 21 deletions lib/components/TerminalLogs/adapters/events/events.ts

This file was deleted.

9 changes: 0 additions & 9 deletions lib/components/TerminalLogs/adapters/events/events.types.ts

This file was deleted.

2 changes: 0 additions & 2 deletions lib/components/TerminalLogs/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
export * from './terminal/terminal';
export * from './events/events';
export * from './events/events.types';
39 changes: 31 additions & 8 deletions lib/components/TerminalLogs/adapters/terminal/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import { FitAddon } from '@xterm/addon-fit';
import { SearchAddon } from '@xterm/addon-search';

import { TerminalPrimitiveConfig } from './terminal.types';
import { EventEmitterAdapter, TerminalEvent } from '../events/events.types';
import { EventBus, TerminalEvent } from '../../event-bus';

const SEARCH_OPTIONS = { caseSensitive: false };

export class Terminal {
private emitter: EventBus;
private readonly terminalInstance: TerminalPrimitive;
private searchAddon!: SearchAddon;

private constructor(terminal: TerminalPrimitive) {
private constructor(terminal: TerminalPrimitive, showLogs: boolean) {
this.terminalInstance = terminal;
this.emitter = new EventBus({ showLogs });
}

private static createNewIntance({
Expand All @@ -35,15 +37,15 @@ export class Terminal {
}

static create(
config?: TerminalPrimitiveConfig,
emitter?: EventEmitterAdapter,
config?: TerminalPrimitiveConfig & { showLogs?: boolean },
): Terminal {
const terminalPrimitive = Terminal.createNewIntance(config);
const terminal = new Terminal(terminalPrimitive);
const { showLogs = false, ...restConfig } = config ?? {};
const terminalPrimitive = Terminal.createNewIntance(restConfig);
const terminal = new Terminal(terminalPrimitive, showLogs);

terminal.addAddons();

emitter?.emit(TerminalEvent.TERMINAL_LOADED);
terminal.bindEvents();
terminal.emitter.emit(TerminalEvent.TERMINAL_LOADED);

return terminal;
}
Expand All @@ -57,11 +59,32 @@ export class Terminal {
this.searchAddon = searchAddon;
}

private bindEvents() {
this.emitter.on(TerminalEvent.WRITE_LOG, (event) => {
this.terminalInstance.writeln(event.message);
});

let i = 0;

const interval = setInterval(() => {
i++;

this.emitter.emit(TerminalEvent.WRITE_LOG, {
message: 'Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ',
});

if (i === 20) {
clearInterval(interval);
}
}, 1000);
}

open(container: HTMLElement): void {
this.terminalInstance.open(container);
}

dispose(): void {
this.emitter.removeAllListeners();
this.terminalInstance.dispose();
}

Expand Down
16 changes: 7 additions & 9 deletions lib/components/TerminalLogs/components/Body/Body.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { Content } from '@radix-ui/react-tabs';
import { FC, PropsWithChildren } from 'react';
import { ComponentRef, forwardRef } from 'react';

export const Body: FC<PropsWithChildren> = ({ children }) => {
export const Body = forwardRef<ComponentRef<'div'>>((_, ref) => {
return (
<div className="mt-5 rounded-md overflow-hidden w-full relative">
<Content className="flex-1 bg-slate-200" value="tab-1">
{children}
</Content>
<Content className="flex-1 bg-slate-200" value="tab-2">
{children}
<div className="mt-5 rounded overflow-hidden w-full relative">
<Content className="flex-1 bg-[#0F172A] text-white" value="tab-1">
<p>Hello World</p>
</Content>
<Content className="flex-1 bg-[#0F172A] p-3" value="tab-2" ref={ref} />
</div>
);
};
});
28 changes: 15 additions & 13 deletions lib/components/TerminalLogs/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,22 @@ export const Header: FC<HeaderProps> = ({ theme }) => {

return (
<div className={cn(wrapperVariants({ theme: inheritTheme }))}>
<List className="flex gap-3 items-start">
<Trigger
value="tab-1"
className={cn(tabTriggerVariants({ theme: inheritTheme }))}
>
Consice
</Trigger>
<List className="flex gap-3 items-start" asChild={true}>
<>
<Trigger
value="tab-1"
className={cn(tabTriggerVariants({ theme: inheritTheme }))}
>
Consice
</Trigger>

<Trigger
value="tab-2"
className={cn(tabTriggerVariants({ theme: inheritTheme }))}
>
Verbose
</Trigger>
<Trigger
value="tab-2"
className={cn(tabTriggerVariants({ theme: inheritTheme }))}
>
Verbose
</Trigger>
</>
</List>

<div className={cn(wrapperInputVariants({ theme: inheritTheme }))}>
Expand Down
92 changes: 59 additions & 33 deletions lib/components/TerminalLogs/event-bus/event-bus.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,61 @@
import { EventEmitter } from 'events';

import { IEventEmitter } from './event-bus.types';

export const eventEmitter = (): IEventEmitter => {
const emitter = new EventEmitter();

return {
on(event, callback) {
emitter.on(event, callback);

return () => {
return emitter.off(event, callback);
};
},

off(event, callback) {
emitter.off(event, callback);
},

emit(event, data?) {
const eventData = {
type: event,
timestamp: Date.now(),
...data,
};

emitter.emit(event, eventData);
},

removeAllListeners() {
emitter.removeAllListeners();
},
};
};
import {
EventEmitterExtraDataArgs,
IEventEmitter,
TerminalEvent,
TerminalEventCallback,
} from './event-bus.types';

export class EventBus implements IEventEmitter {
private readonly emitter: EventEmitter;
private readonly showLogs: boolean;

constructor({ showLogs }: { showLogs: boolean }) {
this.emitter = new EventEmitter();
this.showLogs = showLogs;
}

on<T extends TerminalEvent>(
event: T,
callback: TerminalEventCallback<T>,
): VoidFunction {
const handleCallback: TerminalEventCallback<T> = (data) => {
if (this.showLogs) {
console.log('Event:', event, data);
}
callback(data);
};

this.emitter.on(event, handleCallback);

return () => {
return this.emitter.off(event, callback);
};
}

off<T extends TerminalEvent>(
event: T,
callback: TerminalEventCallback<T>,
): void {
this.emitter.off(event, callback);
}

emit<T extends TerminalEvent>(
event: T,
...data: EventEmitterExtraDataArgs<T>
) {
const [extraData] = data;
const eventData = {
type: event,
timestamp: Date.now(),
...extraData,
};

this.emitter.emit(event, eventData);
}

removeAllListeners(): void {
this.emitter.removeAllListeners();
}
}
4 changes: 3 additions & 1 deletion lib/components/TerminalLogs/event-bus/event-bus.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export enum TerminalEvent {
TERMINAL_LOADED = 'TERMINAL_LOADED',
WRITE_LOG = 'WRITE_LOG',
}

type BaseEvent<T extends TerminalEvent = TerminalEvent> = {
Expand All @@ -9,6 +10,7 @@ type BaseEvent<T extends TerminalEvent = TerminalEvent> = {

export type TerminalEventMap = {
[TerminalEvent.TERMINAL_LOADED]: BaseEvent;
[TerminalEvent.WRITE_LOG]: BaseEvent & { message: string };
};

export type TerminalEventCallback<T extends TerminalEvent> = (
Expand All @@ -20,7 +22,7 @@ type TerminalEventExtraData<T extends TerminalEvent> = Omit<
keyof BaseEvent
>;

type EventEmitterExtraDataArgs<T extends TerminalEvent> =
export type EventEmitterExtraDataArgs<T extends TerminalEvent> =
BaseEvent extends TerminalEventMap[T]
? [undefined?]
: [TerminalEventExtraData<T>];
Expand Down

0 comments on commit 958a278

Please sign in to comment.