feat: telnet character mode

This commit is contained in:
hstyi
2025-07-09 19:57:56 +08:00
committed by GitHub
parent 2341b09f81
commit 9ff6d0afa1
6 changed files with 42 additions and 37 deletions

View File

@@ -2,18 +2,15 @@ package app.termora.plugin.internal.telnet
import app.termora.* import app.termora.*
import app.termora.account.AccountOwner import app.termora.account.AccountOwner
import app.termora.keymgr.KeyManager
import app.termora.plugin.internal.BasicProxyOption import app.termora.plugin.internal.BasicProxyOption
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatComboBox import com.formdev.flatlaf.extras.components.FlatComboBox
import com.formdev.flatlaf.ui.FlatTextBorder import com.formdev.flatlaf.ui.FlatTextBorder
import com.jgoodies.forms.builder.FormBuilder import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.lang3.StringUtils
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Component import java.awt.Component
import java.awt.KeyboardFocusManager import java.awt.KeyboardFocusManager
import java.awt.Window
import java.awt.event.ComponentAdapter import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent import java.awt.event.ComponentEvent
import java.nio.charset.Charset import java.nio.charset.Charset
@@ -25,7 +22,6 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
// telnet 不支持代理密码 // telnet 不支持代理密码
private val proxyOption = BasicProxyOption(authenticationTypes = listOf()) private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
private val terminalOption = TerminalOption() private val terminalOption = TerminalOption()
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
init { init {
addOption(generalOption) addOption(generalOption)
@@ -69,7 +65,10 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
env = terminalOption.environmentTextArea.text, env = terminalOption.environmentTextArea.text,
startupCommand = terminalOption.startupCommandTextField.text, startupCommand = terminalOption.startupCommandTextField.text,
serialComm = serialComm, serialComm = serialComm,
extras = mutableMapOf("backspace" to (terminalOption.backspaceComboBox.selectedItem as Backspace).name) extras = mutableMapOf(
"backspace" to (terminalOption.backspaceComboBox.selectedItem as Backspace).name,
"character-at-a-time" to (terminalOption.characterAtATimeTextField.selectedItem?.toString() ?: "false")
)
) )
return Host( return Host(
@@ -108,6 +107,8 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
terminalOption.startupCommandTextField.text = host.options.startupCommand terminalOption.startupCommandTextField.text = host.options.startupCommand
terminalOption.backspaceComboBox.selectedItem = terminalOption.backspaceComboBox.selectedItem =
Backspace.valueOf(host.options.extras["backspace"] ?: Backspace.Delete.name) Backspace.valueOf(host.options.extras["backspace"] ?: Backspace.Delete.name)
terminalOption.characterAtATimeTextField.selectedItem =
host.options.extras["character-at-a-time"]?.toBooleanStrictOrNull() ?: false
} }
@@ -186,7 +187,6 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
val usernameTextField = OutlineTextField(128) val usernameTextField = OutlineTextField(128)
val hostTextField = OutlineTextField(255) val hostTextField = OutlineTextField(255)
val passwordTextField = OutlinePasswordField(255) val passwordTextField = OutlinePasswordField(255)
val publicKeyComboBox = OutlineComboBox<String>()
val remarkTextArea = FixedLengthTextArea(512) val remarkTextArea = FixedLengthTextArea(512)
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>() val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
@@ -198,30 +198,6 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
private fun initView() { private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER) add(getCenterComponent(), BorderLayout.CENTER)
publicKeyComboBox.isEditable = false
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
var text = StringUtils.EMPTY
if (value is String) {
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
}
return super.getListCellRendererComponent(
list,
text,
index,
isSelected,
cellHasFocus
)
}
}
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() { authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent( override fun getListCellRendererComponent(
list: JList<*>?, list: JList<*>?,
@@ -340,6 +316,7 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
val charsetComboBox = JComboBox<String>() val charsetComboBox = JComboBox<String>()
val backspaceComboBox = JComboBox<Backspace>() val backspaceComboBox = JComboBox<Backspace>()
val startupCommandTextField = OutlineTextField() val startupCommandTextField = OutlineTextField()
val characterAtATimeTextField = YesOrNoComboBox()
val environmentTextArea = FixedLengthTextArea(2048) val environmentTextArea = FixedLengthTextArea(2048)
@@ -355,6 +332,7 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
backspaceComboBox.addItem(Backspace.Backspace) backspaceComboBox.addItem(Backspace.Backspace)
backspaceComboBox.addItem(Backspace.VT220) backspaceComboBox.addItem(Backspace.VT220)
characterAtATimeTextField.selectedItem = false
environmentTextArea.setFocusTraversalKeys( environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
@@ -409,6 +387,8 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
.add(charsetComboBox).xy(3, rows).apply { rows += step } .add(charsetComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.backspace")}:").xy(1, rows) .add("${I18n.getString("termora.new-host.terminal.backspace")}:").xy(1, rows)
.add(backspaceComboBox).xy(3, rows).apply { rows += step } .add(backspaceComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.character-mode")}:").xy(1, rows)
.add(characterAtATimeTextField).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows) .add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
.add(startupCommandTextField).xy(3, rows).apply { rows += step } .add(startupCommandTextField).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows) .add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)

View File

@@ -1,6 +1,7 @@
package app.termora.plugin.internal.telnet package app.termora.plugin.internal.telnet
import app.termora.terminal.StreamPtyConnector import app.termora.terminal.StreamPtyConnector
import org.apache.commons.io.IOUtils
import org.apache.commons.net.telnet.TelnetClient import org.apache.commons.net.telnet.TelnetClient
import org.apache.commons.net.telnet.TelnetOption import org.apache.commons.net.telnet.TelnetOption
import org.apache.commons.net.telnet.WindowSizeOptionHandler import org.apache.commons.net.telnet.WindowSizeOptionHandler
@@ -9,18 +10,27 @@ import java.nio.charset.Charset
class TelnetStreamPtyConnector( class TelnetStreamPtyConnector(
private val telnet: TelnetClient, private val telnet: TelnetClient,
private val charset: Charset private val charset: Charset,
) : private val characterMode: Boolean,
StreamPtyConnector(telnet.inputStream, telnet.outputStream) { ) : StreamPtyConnector(telnet.inputStream, telnet.outputStream) {
private val reader = InputStreamReader(telnet.inputStream, getCharset())
private val reader = InputStreamReader(telnet.inputStream, charset)
override fun read(buffer: CharArray): Int { override fun read(buffer: CharArray): Int {
return reader.read(buffer) return reader.read(buffer)
} }
override fun write(buffer: ByteArray, offset: Int, len: Int) { override fun write(buffer: ByteArray, offset: Int, len: Int) {
output.write(buffer, offset, len) if (characterMode) {
output.flush() for (i in offset until len + offset) {
output.write(byteArrayOf(buffer[i]))
output.flush()
}
} else {
output.write(buffer, offset, len)
output.flush()
}
} }
override fun resize(rows: Int, cols: Int) { override fun resize(rows: Int, cols: Int) {
@@ -33,10 +43,13 @@ class TelnetStreamPtyConnector(
} }
override fun close() { override fun close() {
IOUtils.closeQuietly(input)
IOUtils.closeQuietly(output)
telnet.disconnect() telnet.disconnect()
} }
override fun getCharset(): Charset { override fun getCharset(): Charset {
return charset return charset
} }
} }

View File

@@ -11,6 +11,7 @@ import java.net.InetSocketAddress
import java.net.Proxy import java.net.Proxy
import java.nio.charset.Charset import java.nio.charset.Charset
class TelnetTerminalTab( class TelnetTerminalTab(
windowScope: WindowScope, host: Host, windowScope: WindowScope, host: Host,
) : PtyHostTerminalTab(windowScope, host) { ) : PtyHostTerminalTab(windowScope, host) {
@@ -32,12 +33,18 @@ class TelnetTerminalTab(
) )
} }
val characterMode = host.options.extras["character-at-a-time"]?.toBooleanStrictOrNull() ?: false
val termtype = host.options.envs()["TERM"] ?: "xterm-256color" val termtype = host.options.envs()["TERM"] ?: "xterm-256color"
val ttopt = TerminalTypeOptionHandler(termtype, false, false, true, false) val ttopt = TerminalTypeOptionHandler(termtype, false, false, true, false)
val echoopt = EchoOptionHandler(false, true, false, true) val echoopt = EchoOptionHandler(false, true, false, true)
val gaopt = SuppressGAOptionHandler(true, true, true, true) val gaopt = SuppressGAOptionHandler(true, true, true, true)
val wsopt = WindowSizeOptionHandler(winSize.cols, winSize.rows, true, false, true, false) val wsopt = WindowSizeOptionHandler(winSize.cols, winSize.rows, true, false, true, false)
val bopt = SimpleOptionHandler(TelnetOption.BINARY, true, false, true, false)
val fcopt = SimpleOptionHandler(TelnetOption.REMOTE_FLOW_CONTROL, true, true, false, false)
telnet.addOptionHandler(bopt)
telnet.addOptionHandler(fcopt)
telnet.addOptionHandler(ttopt) telnet.addOptionHandler(ttopt)
telnet.addOptionHandler(echoopt) telnet.addOptionHandler(echoopt)
telnet.addOptionHandler(gaopt) telnet.addOptionHandler(gaopt)
@@ -45,6 +52,7 @@ class TelnetTerminalTab(
telnet.connect(host.host, host.port) telnet.connect(host.host, host.port)
telnet.keepAlive = true telnet.keepAlive = true
telnet.tcpNoDelay = characterMode
val encoder = terminal.getKeyEncoder() val encoder = terminal.getKeyEncoder()
if (encoder is KeyEncoderImpl) { if (encoder is KeyEncoderImpl) {
@@ -56,7 +64,8 @@ class TelnetTerminalTab(
} }
} }
return ptyConnectorFactory.decorate(TelnetStreamPtyConnector(telnet, telnet.charset))
return ptyConnectorFactory.decorate(TelnetStreamPtyConnector(telnet, telnet.charset, characterMode))
} }

View File

@@ -182,6 +182,7 @@ termora.new-host.proxy=Proxy
termora.new-host.terminal=${termora.settings.terminal} termora.new-host.terminal=${termora.settings.terminal}
termora.new-host.terminal.encoding=Encoding termora.new-host.terminal.encoding=Encoding
termora.new-host.terminal.backspace=Backspace termora.new-host.terminal.backspace=Backspace
termora.new-host.terminal.character-mode=Character-at-a-time
termora.new-host.terminal.heartbeat-interval=Heartbeat Interval termora.new-host.terminal.heartbeat-interval=Heartbeat Interval
termora.new-host.terminal.startup-commands=Startup Command termora.new-host.terminal.startup-commands=Startup Command
termora.new-host.terminal.env=Environment termora.new-host.terminal.env=Environment

View File

@@ -174,6 +174,7 @@ termora.new-host.proxy=代理
termora.new-host.terminal=${termora.settings.terminal} termora.new-host.terminal=${termora.settings.terminal}
termora.new-host.terminal.encoding=编码 termora.new-host.terminal.encoding=编码
termora.new-host.terminal.backspace=退格键 termora.new-host.terminal.backspace=退格键
termora.new-host.terminal.character-mode=单字符模式
termora.new-host.terminal.heartbeat-interval=心跳间隔 termora.new-host.terminal.heartbeat-interval=心跳间隔
termora.new-host.terminal.startup-commands=启动命令 termora.new-host.terminal.startup-commands=启动命令
termora.new-host.terminal.env=环境 termora.new-host.terminal.env=环境

View File

@@ -172,6 +172,7 @@ termora.new-host.proxy=代理
termora.new-host.terminal=${termora.settings.terminal} termora.new-host.terminal=${termora.settings.terminal}
termora.new-host.terminal.encoding=編碼 termora.new-host.terminal.encoding=編碼
termora.new-host.terminal.backspace=退格鍵 termora.new-host.terminal.backspace=退格鍵
termora.new-host.terminal.character-mode=單字元模式
termora.new-host.terminal.startup-commands=啟動命令 termora.new-host.terminal.startup-commands=啟動命令
termora.new-host.terminal.heartbeat-interval=心跳間隔 termora.new-host.terminal.heartbeat-interval=心跳間隔
termora.new-host.terminal.env=環境 termora.new-host.terminal.env=環境