parent
2b6a77cff1
commit
45c29243e6
@ -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> {} |
||||
} |
||||
|
Loading…
Reference in new issue