diff --git a/.yarn/yarn.lock b/.yarn/yarn.lock index 404b909..3b850b7 100644 --- a/.yarn/yarn.lock +++ b/.yarn/yarn.lock @@ -5703,7 +5703,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2": +"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -9560,6 +9560,7 @@ __metadata: "@vscode/vsce": ^2.18.0 common: "workspace:*" event-stream: ^4.0.1 + iconv-lite: ^0.6.3 jsonc-parser: ^3.2.0 prettier: ^2.6.2 semver: ^7.3.5 diff --git a/common/src/fileSystemConfig.ts b/common/src/fileSystemConfig.ts index d5db4b7..cf7b719 100644 --- a/common/src/fileSystemConfig.ts +++ b/common/src/fileSystemConfig.ts @@ -117,6 +117,8 @@ export interface FileSystemConfig extends ConnectConfig { instantConnection?: boolean; /** List of special flags to enable/disable certain fixes/features. Flags are usually used for issues or beta testing. Flags can disappear/change anytime! */ flags?: string[]; + /** Specifies the character encoding used for the SSH terminal. If undefined or an unsupported by iconv-lite, UTF-8 will be used */ + encoding?: string; /** Internal property saying where this config comes from. Undefined if this config is merged or something */ _location?: ConfigLocation; /** Internal property keeping track of where this config comes from (including merges) */ diff --git a/package.json b/package.json index 1629798..b93dc33 100644 --- a/package.json +++ b/package.json @@ -429,6 +429,7 @@ "dependencies": { "common": "workspace:*", "event-stream": "^4.0.1", + "iconv-lite": "^0.6.3", "jsonc-parser": "^3.2.0", "semver": "^7.3.5", "socks": "^2.2.0", diff --git a/src/pseudoTerminal.ts b/src/pseudoTerminal.ts index 5debc33..1c76112 100644 --- a/src/pseudoTerminal.ts +++ b/src/pseudoTerminal.ts @@ -2,6 +2,7 @@ import type { EnvironmentVariable, FileSystemConfig } from 'common/fileSystemCon import * as path from 'path'; import type { ClientChannel, PseudoTtyOptions } from 'ssh2'; import * as vscode from 'vscode'; +import * as iconv from 'iconv-lite'; import { getFlagBoolean } from './flags'; import type { Connection } from './connection'; import { Logging, LOGGING_NO_STACKTRACE } from './logging'; @@ -142,6 +143,24 @@ export async function createTerminal(options: TerminalOptions): Promise(); const onDidOpen = new vscode.EventEmitter(); let terminal: vscode.Terminal | undefined; + + // Encodes user input (originally UTF-8 in JS string) into the remote encoding, if configured. + // Returns a Buffer if encoding is valid, otherwise returns the original string. + let encodeTerminalInput: (data: string) => Buffer | string; + + // Decodes data received from the remote side (as Buffer) into a string using the configured encoding. + // If encoding is not set or invalid, defaults to data.toString() (UTF-8). + let decodeTerminalOutput: (data: Buffer) => string; + + const encoding = actualConfig.encoding; + if (encoding && iconv.encodingExists(encoding)) { + encodeTerminalInput = (data: string) => iconv.encode(data, encoding); + decodeTerminalOutput = (data: Buffer) => iconv.decode(data, encoding); + } else { + encodeTerminalInput = (data: string) => data; + decodeTerminalOutput = (data: Buffer) => data.toString(); + } + // Won't actually open the remote terminal until pseudo.open(dims) is called const pseudo: SSHPseudoTerminal = { status: 'opening', @@ -241,8 +260,8 @@ export async function createTerminal(options: TerminalOptions): Promise onDidWrite.fire(chunk.toString())); - channel.stderr!.on('data', chunk => onDidWrite.fire(chunk.toString())); + channel.on('data', chunk => onDidWrite.fire(decodeTerminalOutput(chunk))); + channel.stderr!.on('data', chunk => onDidWrite.fire(decodeTerminalOutput(chunk))); // TODO: ^ Keep track of stdout's color, switch to red, output, then switch back? } catch (e) { Logging.error`Error starting SSH terminal:\n${e}`; @@ -264,7 +283,7 @@ export async function createTerminal(options: TerminalOptions): Promise } +export function encoding(config: FileSystemConfig, onChange: FSCChanged<'encoding'>): React.ReactElement { + const callback = (newValue?: string) => onChange('encoding', newValue); + const description = (<>Text encoding used for terminal input/output. For a list of supported encodings, see iconv-lite wiki); + const values = ['utf8', 'iso-8859-1', 'Shift_JIS', 'EUC-JP', 'EUC-KR']; + return +} + export type FieldFactory = (config: FileSystemConfig, onChange: FSCChanged, onChangeMultiple: FSCChangedMultiple) => React.ReactElement | null; export const FIELDS: FieldFactory[] = [ name, label, group, merge, extend, putty, host, port, root, agent, username, password, privateKeyPath, passphrase, - newFileMode, agentForward, sftpCommand, sftpSudo, terminalCommand, taskCommand, + newFileMode, agentForward, sftpCommand, sftpSudo, terminalCommand, taskCommand, encoding, PROXY_FIELD]; diff --git a/webview/src/FieldTypes/base.tsx b/webview/src/FieldTypes/base.tsx index 8c3d315..89bd7f5 100644 --- a/webview/src/FieldTypes/base.tsx +++ b/webview/src/FieldTypes/base.tsx @@ -4,7 +4,7 @@ import './index.css'; export interface Props { label?: string; - description?: string; + description?: React.ReactNode; value: T; optional?: boolean; group?: FieldGroup;