Skip to content

Commit

Permalink
multiplexer (#22)
Browse files Browse the repository at this point in the history
* multiplexer

* try fixing workflow
  • Loading branch information
atinylittleshell committed Nov 7, 2023
1 parent e1c9545 commit c003f0c
Show file tree
Hide file tree
Showing 32 changed files with 1,027 additions and 207 deletions.
1 change: 1 addition & 0 deletions .do_tasks
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
make terminal tree management logic unit testable
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: install dependencies
run: npm ci
- name: run build
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ A fast, elegant and intelligent cross-platform terminal emulator and multiplexer
- Not written in Rust, yet still blazing fast.
- GPU-accelerated rendering using [Xterm.js](https://xtermjs.org/).
- Consistent experience across platforms using [Electron](https://www.electronjs.org/).
- (WIP) Multi-chord key bindings.
- (WIP) Built-in tmux-like multiplexer.
- Multi-chord key bindings.
- Built-in tmux-like multiplexer.

## Installation

Expand Down
6 changes: 5 additions & 1 deletion apps/app/babel.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
};
1 change: 1 addition & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"scripts": {
"lint": "eslint . --max-warnings 0",
"lint:fix": "eslint . --max-warnings 0 --fix",
"build": "npm run build:app && shx cp -r ../terminal/dist/. dist/terminal/",
"build:app": "shx rm -rf dist && tsc -p . && webpack --config webpack.config.js && webpack --config webpack.preload.config.js",
"test": "jest --colors"
Expand Down
16 changes: 10 additions & 6 deletions apps/app/src/nativeBridge/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@ type NativeBridgeModuleMetadata = {
events: Record<string, ModuleEvent>;
};

export const MODULE_METADATA: Map<Function, NativeBridgeModuleMetadata> = new Map<
Function,
NativeBridgeModuleMetadata
>();
export const MODULE_METADATA: Map<Function, NativeBridgeModuleMetadata> =
new Map<Function, NativeBridgeModuleMetadata>();

function ensureModuleMetadata(ctor: Function): NativeBridgeModuleMetadata {
if (!MODULE_METADATA.has(ctor)) {
Expand Down Expand Up @@ -53,11 +51,17 @@ export function getModuleKey(moduleName: string): string {
return `TerminalOne:${moduleName}`;
}

export function getModuleFunctionKey(moduleName: string, functionName: string): string {
export function getModuleFunctionKey(
moduleName: string,
functionName: string,
): string {
return `${getModuleKey(moduleName)}:${functionName}`;
}

export function getModuleEventKey(moduleName: string, eventName: string): string {
export function getModuleEventKey(
moduleName: string,
eventName: string,
): string {
return `${getModuleKey(moduleName)}:${eventName}`;
}

Expand Down
24 changes: 20 additions & 4 deletions apps/app/src/nativeBridge/modules/applicationModule.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { LogLevel } from '@terminalone/types';
import { app, BrowserWindow, session } from 'electron';

import { moduleEvent, moduleFunction, NativeBridgeModule, nativeBridgeModule } from '../module';
import {
moduleEvent,
moduleFunction,
NativeBridgeModule,
nativeBridgeModule,
} from '../module';
import { Logger } from './common/logger';

@nativeBridgeModule('app')
Expand All @@ -12,7 +17,10 @@ export class ApplicationModule extends NativeBridgeModule {
}

@moduleFunction()
public async setOpenAtLogin(_mainWindow: BrowserWindow, openAtLogin: boolean): Promise<void> {
public async setOpenAtLogin(
_mainWindow: BrowserWindow,
openAtLogin: boolean,
): Promise<void> {
if (!app.isPackaged) {
// do not make the dev app launch on startup
return;
Expand All @@ -38,7 +46,11 @@ export class ApplicationModule extends NativeBridgeModule {
}

@moduleFunction()
public async log(_mainWindow: BrowserWindow, level: LogLevel, message: string): Promise<void> {
public async log(
_mainWindow: BrowserWindow,
level: LogLevel,
message: string,
): Promise<void> {
Logger.getInstance().log(level, message);
}

Expand All @@ -48,7 +60,11 @@ export class ApplicationModule extends NativeBridgeModule {
}

@moduleEvent('on')
public onLog(_mainWindow: BrowserWindow, _level: LogLevel, _message: string): void {
public onLog(
_mainWindow: BrowserWindow,
_level: LogLevel,
_message: string,
): void {
return;
}

Expand Down
22 changes: 18 additions & 4 deletions apps/app/src/nativeBridge/modules/configModule.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { DEFAULT_CONFIG, resolveConfig, ResolvedConfig } from '@terminalone/types';
import {
DEFAULT_CONFIG,
resolveConfig,
ResolvedConfig,
} from '@terminalone/types';
import { BrowserWindow } from 'electron';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs-extra';
import os from 'os';
import path from 'path';
import vm from 'vm';

import { moduleFunction, NativeBridgeModule, nativeBridgeModule } from '../module';
import {
moduleFunction,
NativeBridgeModule,
nativeBridgeModule,
} from '../module';
import { getAppDirs } from './common';
import { Logger } from './common/logger';

Expand Down Expand Up @@ -34,7 +42,10 @@ export class ConfigModule extends NativeBridgeModule {
const configContent = readFileSync(configPath, 'utf8');

try {
const script = new vm.Script(configContent, { filename: 'config.js', displayErrors: true });
const script = new vm.Script(configContent, {
filename: 'config.js',
displayErrors: true,
});
const mod: Record<string, unknown> = {};
script.runInNewContext({
module: mod,
Expand All @@ -49,7 +60,10 @@ export class ConfigModule extends NativeBridgeModule {
throw new Error('Invalid config: `module.exports` not set');
}

Logger.getInstance().log('info', `Loaded config from ${configPath}}: ${JSON.stringify(mod.exports)}`);
Logger.getInstance().log(
'info',
`Loaded config from ${configPath}}: ${JSON.stringify(mod.exports)}`,
);

const resolved = resolveConfig(mod.exports);
this.config = resolved;
Expand Down
9 changes: 7 additions & 2 deletions apps/app/src/nativeBridge/modules/externalLinksModule.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { BrowserWindow, shell } from 'electron';
import { existsSync } from 'fs-extra';

import { moduleFunction, NativeBridgeModule, nativeBridgeModule } from '../module';
import {
moduleFunction,
NativeBridgeModule,
nativeBridgeModule,
} from '../module';

@nativeBridgeModule('links')
export class ExternalLinksModule extends NativeBridgeModule {
@moduleFunction()
public async openExternalURL(_mainWindow: BrowserWindow, url: string) {
// Security ref: https://benjamin-altpeter.de/shell-openexternal-dangers/
if (typeof url !== 'string') throw new Error('openExternalURL limited to strings');
if (typeof url !== 'string')
throw new Error('openExternalURL limited to strings');
if (!url.startsWith('http://') && !url.startsWith('https://'))
throw new Error('openExternalURL limited to http and https protocol');
return shell.openExternal(url);
Expand Down
40 changes: 33 additions & 7 deletions apps/app/src/nativeBridge/modules/mainWindowModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { BrowserWindow } from 'electron';
import { existsSync, mkdirSync, readFileSync, writeFile } from 'fs-extra';
import path from 'path';

import { moduleEvent, moduleFunction, NativeBridgeModule, nativeBridgeModule } from '../module';
import {
moduleEvent,
moduleFunction,
NativeBridgeModule,
nativeBridgeModule,
} from '../module';
import { getAppDirs } from './common';

@nativeBridgeModule('win')
Expand All @@ -23,7 +28,10 @@ export class MainWindowModule extends NativeBridgeModule {
}

@moduleFunction()
public async maximize(mainWindow: BrowserWindow, maximize?: boolean): Promise<void> {
public async maximize(
mainWindow: BrowserWindow,
maximize?: boolean,
): Promise<void> {
if (maximize === undefined) {
maximize = true;
}
Expand All @@ -41,12 +49,20 @@ export class MainWindowModule extends NativeBridgeModule {
}

@moduleFunction()
public async setWindowSize(mainWindow: BrowserWindow, width: number, height: number) {
public async setWindowSize(
mainWindow: BrowserWindow,
width: number,
height: number,
) {
mainWindow.setSize(width, height);
}

@moduleFunction()
public async setWindowPosition(mainWindow: BrowserWindow, x: number, y: number) {
public async setWindowPosition(
mainWindow: BrowserWindow,
x: number,
y: number,
) {
mainWindow.setPosition(x, y);
}

Expand Down Expand Up @@ -77,7 +93,9 @@ export class MainWindowModule extends NativeBridgeModule {
const configDir = getAppDirs().userData;
const stateFilePath = path.join(configDir, 'window.json');
if (existsSync(stateFilePath)) {
const stateFromFile = JSON.parse(readFileSync(stateFilePath, { encoding: 'utf8' }));
const stateFromFile = JSON.parse(
readFileSync(stateFilePath, { encoding: 'utf8' }),
);
latestState = {
x: stateFromFile.x ?? x,
y: stateFromFile.y ?? y,
Expand Down Expand Up @@ -123,12 +141,20 @@ export class MainWindowModule extends NativeBridgeModule {
}

@moduleEvent('on')
public onWindowResized(_mainWindow: BrowserWindow, _w: number, _h: number): void {
public onWindowResized(
_mainWindow: BrowserWindow,
_w: number,
_h: number,
): void {
return;
}

@moduleEvent('on')
public onWindowMoved(_mainWindow: BrowserWindow, _x: number, _y: number): void {
public onWindowMoved(
_mainWindow: BrowserWindow,
_x: number,
_y: number,
): void {
return;
}
}
57 changes: 48 additions & 9 deletions apps/app/src/nativeBridge/modules/terminalModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,26 @@ import { IPty } from 'node-pty';
import os from 'os';
import { osLocaleSync } from 'os-locale';

import { moduleEvent, moduleFunction, NativeBridgeModule, nativeBridgeModule } from '../module';
import {
moduleEvent,
moduleFunction,
NativeBridgeModule,
nativeBridgeModule,
} from '../module';
import { Logger } from './common/logger';

class PTYInstance {
private ptyProcess: IPty;

public readonly id: string;

constructor(id: string, cols: number, rows: number, shellCommand: string, startupDirectory: string) {
constructor(
id: string,
cols: number,
rows: number,
shellCommand: string,
startupDirectory: string,
) {
this.id = id;

const shell =
Expand All @@ -35,7 +46,10 @@ class PTYInstance {
},
});

Logger.getInstance().log('info', `Created new terminal with id: ${id}, size: ${cols}x${rows}, shell: ${shell}`);
Logger.getInstance().log(
'info',
`Created new terminal with id: ${id}, size: ${cols}x${rows}, shell: ${shell}`,
);
}

public resize(cols: number, rows: number): void {
Expand Down Expand Up @@ -63,15 +77,25 @@ export class TerminalModule extends NativeBridgeModule {
private ptyInstances: { [id: string]: PTYInstance } = {};

@moduleFunction()
public async newTerminal(
public async createTerminalIfNotExist(
_mainWindow: BrowserWindow,
id: string,
cols: number,
rows: number,
shellCommand: string,
startupDirectory: string,
): Promise<void> {
const ptyInstance = new PTYInstance(id, cols, rows, shellCommand, startupDirectory);
if (this.ptyInstances[id]) {
return;
}

const ptyInstance = new PTYInstance(
id,
cols,
rows,
shellCommand,
startupDirectory,
);
ptyInstance.onData((data: string) => {
this.onData(_mainWindow, ptyInstance.id, data);
});
Expand All @@ -80,26 +104,41 @@ export class TerminalModule extends NativeBridgeModule {
}

@moduleFunction()
public async resizeTerminal(_mainWindow: BrowserWindow, id: string, cols: number, rows: number): Promise<void> {
public async resizeTerminal(
_mainWindow: BrowserWindow,
id: string,
cols: number,
rows: number,
): Promise<void> {
const ptyInstance = this.ptyInstances[id];
if (ptyInstance) {
ptyInstance.resize(cols, rows);
}
}

@moduleFunction()
public async killTerminal(_mainWindow: BrowserWindow, id: string): Promise<void> {
public async killTerminal(
_mainWindow: BrowserWindow,
id: string,
): Promise<void> {
const ptyInstance = this.ptyInstances[id];
if (ptyInstance) {
ptyInstance.kill();
delete this.ptyInstances[id];
} else {
Logger.getInstance().log('warn', `Could not find terminal with id: ${id}`);
Logger.getInstance().log(
'warn',
`Could not find terminal with id: ${id}`,
);
}
}

@moduleFunction()
public async writeTerminal(_mainWindow: BrowserWindow, id: string, data: string): Promise<void> {
public async writeTerminal(
_mainWindow: BrowserWindow,
id: string,
data: string,
): Promise<void> {
const ptyInstance = this.ptyInstances[id];
if (ptyInstance) {
ptyInstance.write(data);
Expand Down
Loading

0 comments on commit c003f0c

Please sign in to comment.