pull/5370/head
filip mertens 1 year ago
parent 2b6a77cff1
commit 45c29243e6
  1. 16
      apps/remixdesktop/src/lib/index.ts
  2. 297
      apps/remixdesktop/src/plugins/xtermPlugin.ts
  3. 2
      apps/remixdesktop/yarn.lock

@ -0,0 +1,16 @@
export const ansiRegex = ({onlyFirst = false} = {}) => {
const pattern = [
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))'
].join('|');
return new RegExp(pattern, onlyFirst ? undefined : 'g');
}
export const stripAnsi = (string: string) => {
const regex = ansiRegex();
if (typeof string !== 'string') {
throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``);
}
return string.replace(regex, '');
}

@ -1,166 +1,197 @@
import { PluginClient } from "@remixproject/plugin";
import { Profile } from "@remixproject/plugin-utils";
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import os from 'os';
import * as pty from "node-pty"
import process from 'node:process';
import { userInfo } from 'node:os';
import { findExecutable } from "../utils/findExecutable";
import {PluginClient} from '@remixproject/plugin'
import {Profile} from '@remixproject/plugin-utils'
import {
ElectronBasePlugin,
ElectronBasePluginClient,
} from '@remixproject/plugin-electron'
import os from 'os'
import * as pty from 'node-pty'
import process from 'node:process'
import {userInfo} from 'node:os'
import {findExecutable} from '../utils/findExecutable'
import {spawnSync} from 'child_process'
import { stripAnsi } from '../lib'
export const detectDefaultShell = () => {
const { env } = process;
if (process.platform === 'win32') {
return env.SHELL || 'powershell.exe';
}
const {env} = process
try {
const { shell } = userInfo();
if (shell) {
return shell;
}
} catch { }
if (process.platform === 'win32') {
return env.SHELL || 'powershell.exe'
}
if (process.platform === 'darwin') {
return env.SHELL || '/bin/zsh';
try {
const {shell} = userInfo()
if (shell) {
return shell
}
} catch {}
return env.SHELL || '/bin/sh';
};
if (process.platform === 'darwin') {
return env.SHELL || '/bin/zsh'
}
return env.SHELL || '/bin/sh'
}
// Stores default shell when imported.
const defaultShell = detectDefaultShell();
const defaultShell = detectDefaultShell()
const getShellEnvArgs = [
'-ilc',
'echo -n "_SHELL_ENV_DELIMITER_"; env; echo -n "_SHELL_ENV_DELIMITER_"; exit',
]
const getShellEnvEnv = {
// Disables Oh My Zsh auto-update thing that can block the process.
DISABLE_AUTO_UPDATE: 'true',
}
export default defaultShell;
const parseEnv = (env: any) => {
env = env.split('_SHELL_ENV_DELIMITER_')[1]
const returnValue = {}
for (const line of stripAnsi(env)
.split('\n')
.filter((l) => Boolean(l))) {
const [key, ...values] = line.split('=')
console.log(key, values)
Object.assign(returnValue, {
[key]: values.join('='),
})
}
return returnValue
}
export default defaultShell
const profile: Profile = {
name: 'xterm',
displayName: 'xterm',
description: 'xterm plugin',
name: 'xterm',
displayName: 'xterm',
description: 'xterm plugin',
}
export class XtermPlugin extends ElectronBasePlugin {
clients: XtermPluginClient[] = []
constructor() {
super(profile, clientProfile, XtermPluginClient)
this.methods = [...super.methods, 'closeTerminals']
clients: XtermPluginClient[] = []
constructor() {
super(profile, clientProfile, XtermPluginClient)
this.methods = [...super.methods, 'closeTerminals']
}
new(webContentsId: any): void {
const client = this.clients.find((c) => c.webContentsId === webContentsId)
if (client) {
client.new()
}
}
new(webContentsId: any): void {
const client = this.clients.find(c => c.webContentsId === webContentsId)
if (client) {
client.new()
}
async closeTerminals(): Promise<void> {
for (const client of this.clients) {
await client.closeAll()
}
async closeTerminals(): Promise<void> {
for (const client of this.clients) {
await client.closeAll()
}
}
}
}
const clientProfile: Profile = {
name: 'xterm',
displayName: 'xterm',
description: 'xterm plugin',
methods: ['createTerminal', 'close', 'keystroke', 'getShells', 'resize']
name: 'xterm',
displayName: 'xterm',
description: 'xterm plugin',
methods: ['createTerminal', 'close', 'keystroke', 'getShells', 'resize'],
}
class XtermPluginClient extends ElectronBasePluginClient {
terminals: pty.IPty[] = []
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile)
this.onload(() => {
this.emit('loaded')
})
}
async keystroke(key: string, pid: number): Promise<void> {
this.terminals[pid].write(key)
}
async getShells(): Promise<string[]> {
if(os.platform() === 'win32') {
const bash = await findExecutable('bash.exe')
if(bash) {
const shells = ['powershell.exe', 'cmd.exe', ...bash]
// filter out duplicates
return shells.filter((v, i, a) => a.indexOf(v) === i)
}
return ['powershell.exe', 'cmd.exe']
}
return [defaultShell]
}
async createTerminal(path?: string, shell?: string): Promise<number> {
// filter undefined out of the env
const env = Object.keys(process.env)
.filter(key => process.env[key] !== undefined)
.reduce((env, key) => {
env[key] = process.env[key] || '';
return env;
}, {} as Record<string, string>);
const ptyProcess = pty.spawn(shell || defaultShell, [], {
name: 'xterm-color',
cols: 80,
rows: 20,
cwd: path || process.cwd(),
env: env
});
ptyProcess.onData((data: string) => {
this.sendData(data, ptyProcess.pid);
})
this.terminals[ptyProcess.pid] = ptyProcess
return ptyProcess.pid
terminals: pty.IPty[] = []
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile)
this.onload(() => {
this.emit('loaded')
})
}
async keystroke(key: string, pid: number): Promise<void> {
this.terminals[pid].write(key)
}
async getShells(): Promise<string[]> {
if (os.platform() === 'win32') {
const bash = await findExecutable('bash.exe')
if (bash) {
const shells = ['powershell.exe', 'cmd.exe', ...bash]
// filter out duplicates
return shells.filter((v, i, a) => a.indexOf(v) === i)
}
return ['powershell.exe', 'cmd.exe']
}
async close(pid: number): Promise<void> {
this.terminals[pid].kill()
delete this.terminals[pid]
this.emit('close', pid)
return [defaultShell]
}
async createTerminal(path?: string, shell?: string): Promise<number> {
let parsedEnv: any = null
if (!(process.platform === 'win32')) {
const {stdout} = spawnSync(defaultShell, getShellEnvArgs, {
encoding: 'utf8',
})
parsedEnv = parseEnv(stdout)
}
async resize(pid: number, {cols, rows}: {cols: number; rows: number}) {
if (this.terminals[pid]) {
try {
this.terminals[pid].resize(cols, rows);
} catch (_err) {
const err = _err as {stack: any};
console.error(err.stack);
}
} else {
console.warn('Warning: Attempted to resize a session with no pty');
}
// filter undefined out of the env
const env = Object.keys(parsedEnv || process.env)
.filter((key) => process.env[key] !== undefined)
.reduce((env, key) => {
env[key] = process.env[key] || ''
return env
}, {} as Record<string, string>)
const ptyProcess = pty.spawn(shell || defaultShell, [], {
name: 'xterm-color',
cols: 80,
rows: 20,
cwd: path || process.cwd(),
env: env,
})
ptyProcess.onData((data: string) => {
this.sendData(data, ptyProcess.pid)
})
this.terminals[ptyProcess.pid] = ptyProcess
setTimeout(() => {
this.sendData(JSON.stringify(env), ptyProcess.pid)
}, 2000)
return ptyProcess.pid
}
async close(pid: number): Promise<void> {
this.terminals[pid].kill()
delete this.terminals[pid]
this.emit('close', pid)
}
async resize(pid: number, {cols, rows}: {cols: number; rows: number}) {
if (this.terminals[pid]) {
try {
this.terminals[pid].resize(cols, rows)
} catch (_err) {
const err = _err as {stack: any}
console.error(err.stack)
}
async closeAll(): Promise<void> {
for (const pid in this.terminals) {
this.terminals[pid].kill()
delete this.terminals[pid]
this.emit('close', pid)
}
} else {
console.warn('Warning: Attempted to resize a session with no pty')
}
}
async sendData(data: string, pid: number) {
this.emit('data', data, pid)
async closeAll(): Promise<void> {
for (const pid in this.terminals) {
this.terminals[pid].kill()
delete this.terminals[pid]
this.emit('close', pid)
}
}
async new(): Promise<void> {
}
async sendData(data: string, pid: number) {
this.emit('data', data, pid)
}
}
async new(): Promise<void> {}
}

@ -4486,7 +4486,7 @@ string_decoder@^1.1.1:
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
dependencies:
ansi-regex "^6.0.1"

Loading…
Cancel
Save