mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: support login scripts
This commit is contained in:
@@ -88,7 +88,27 @@ data class SerialComm(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HostTag(val text: String)
|
data class LoginScript(
|
||||||
|
/**
|
||||||
|
* 等待字符串
|
||||||
|
*/
|
||||||
|
val expect: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待之后发送
|
||||||
|
*/
|
||||||
|
val send: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [expect] 是否是正则
|
||||||
|
*/
|
||||||
|
val regex: Boolean = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [expect] 是否大小写匹配,如果为 true 表示不忽略大小写,也就是:'A != a';如果为 false 那么 'A == a'
|
||||||
|
*/
|
||||||
|
val matchCase: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -97,6 +117,10 @@ data class Options(
|
|||||||
* 跳板机
|
* 跳板机
|
||||||
*/
|
*/
|
||||||
val jumpHosts: List<String> = mutableListOf(),
|
val jumpHosts: List<String> = mutableListOf(),
|
||||||
|
/**
|
||||||
|
* 登录脚本
|
||||||
|
*/
|
||||||
|
val loginScripts: List<LoginScript> = emptyList(),
|
||||||
/**
|
/**
|
||||||
* 编码
|
* 编码
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ abstract class PtyHostTerminalTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 开启 PTY
|
// 开启 PTY
|
||||||
val ptyConnector = openPtyConnector()
|
val ptyConnector = loginScriptsPtyConnector(host, openPtyConnector())
|
||||||
ptyConnectorDelegate.ptyConnector = ptyConnector
|
ptyConnectorDelegate.ptyConnector = ptyConnector
|
||||||
|
|
||||||
// 开启 reader
|
// 开启 reader
|
||||||
@@ -81,6 +81,73 @@ abstract class PtyHostTerminalTab(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录脚本
|
||||||
|
*/
|
||||||
|
open fun loginScriptsPtyConnector(host: Host, ptyConnector: PtyConnector): PtyConnector {
|
||||||
|
val loginScripts = host.options.loginScripts.toMutableList()
|
||||||
|
if (loginScripts.isEmpty()) {
|
||||||
|
return ptyConnector
|
||||||
|
}
|
||||||
|
|
||||||
|
return object : PtyConnectorDelegate(ptyConnector) {
|
||||||
|
override fun read(buffer: CharArray): Int {
|
||||||
|
val len = super.read(buffer)
|
||||||
|
|
||||||
|
// 获取一个匹配的登录脚本
|
||||||
|
val scripts = runCatching { popLoginScript(buffer, len) }.getOrNull() ?: return len
|
||||||
|
if (scripts.isEmpty()) return len
|
||||||
|
|
||||||
|
for (script in scripts) {
|
||||||
|
// send
|
||||||
|
write(script.send.toByteArray(getCharset()))
|
||||||
|
|
||||||
|
// send \r or \n
|
||||||
|
val enter = terminal.getKeyEncoder().encode(TerminalKeyEvent(KeyEvent.VK_ENTER))
|
||||||
|
.toByteArray(getCharset())
|
||||||
|
write(enter)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return len
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun popLoginScript(buffer: CharArray, len: Int): List<LoginScript> {
|
||||||
|
if (loginScripts.isEmpty()) return emptyList()
|
||||||
|
if (len < 1) return emptyList()
|
||||||
|
|
||||||
|
val scripts = mutableListOf<LoginScript>()
|
||||||
|
val text = String(buffer, 0, len)
|
||||||
|
val iterator = loginScripts.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val script = iterator.next()
|
||||||
|
if (script.expect.isEmpty()) {
|
||||||
|
scripts.add(script)
|
||||||
|
iterator.remove()
|
||||||
|
continue
|
||||||
|
} else if (script.regex) {
|
||||||
|
val regex = if (script.matchCase) script.expect.toRegex()
|
||||||
|
else script.expect.toRegex(RegexOption.IGNORE_CASE)
|
||||||
|
if (regex.matches(text)) {
|
||||||
|
scripts.add(script)
|
||||||
|
iterator.remove()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (text.contains(script.expect, script.matchCase.not())) {
|
||||||
|
scripts.add(script)
|
||||||
|
iterator.remove()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return scripts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
|
open fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
|
||||||
ptyConnector.write(bytes)
|
ptyConnector.write(bytes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ class NewKeywordHighlightDialog(
|
|||||||
Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND)),
|
Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND)),
|
||||||
I18n.getString("termora.highlight.background-color")
|
I18n.getString("termora.highlight.background-color")
|
||||||
)
|
)
|
||||||
val matchCaseBtn = JToggleButton(Icons.matchCase)
|
val matchCaseBtn = JToggleButton(Icons.matchCase).apply { toolTipText = I18n.getString("termora.match-case") }
|
||||||
val regexBtn = JToggleButton(Icons.regex)
|
val regexBtn = JToggleButton(Icons.regex).apply { toolTipText = I18n.getString("termora.regex") }
|
||||||
|
|
||||||
|
|
||||||
private val textColorRevert = JButton(Icons.revert)
|
private val textColorRevert = JButton(Icons.revert)
|
||||||
|
|||||||
@@ -188,6 +188,11 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
|||||||
// Nothing
|
// Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun loginScriptsPtyConnector(host: Host, ptyConnector: PtyConnector): PtyConnector {
|
||||||
|
// Nothing
|
||||||
|
return ptyConnector
|
||||||
|
}
|
||||||
|
|
||||||
private inner class PasswordReporterDataListener(private val host: Host) : DataListener {
|
private inner class PasswordReporterDataListener(private val host: Host) : DataListener {
|
||||||
override fun onChanged(key: DataKey<*>, data: Any) {
|
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||||
if (key == VisualTerminal.Companion.Written && data is String) {
|
if (key == VisualTerminal.Companion.Written && data is String) {
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import app.termora.tree.HostTreeNode
|
|||||||
import app.termora.tree.NewHostTreeDialog
|
import app.termora.tree.NewHostTreeDialog
|
||||||
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.extras.components.FlatTabbedPane
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatTable
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
@@ -23,6 +26,7 @@ import java.nio.charset.Charset
|
|||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.table.DefaultTableCellRenderer
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
import javax.swing.table.DefaultTableModel
|
import javax.swing.table.DefaultTableModel
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
@Suppress("CascadeIf")
|
@Suppress("CascadeIf")
|
||||||
open class SSHHostOptionsPane : OptionsPane() {
|
open class SSHHostOptionsPane : OptionsPane() {
|
||||||
@@ -95,6 +99,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||||
enableX11Forwarding = tunnelingOption.x11ForwardingCheckBox.isSelected,
|
enableX11Forwarding = tunnelingOption.x11ForwardingCheckBox.isSelected,
|
||||||
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
||||||
|
loginScripts = terminalOption.loginScripts,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Host(
|
return Host(
|
||||||
@@ -138,6 +143,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
terminalOption.environmentTextArea.text = host.options.env
|
terminalOption.environmentTextArea.text = host.options.env
|
||||||
terminalOption.startupCommandTextField.text = host.options.startupCommand
|
terminalOption.startupCommandTextField.text = host.options.startupCommand
|
||||||
terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval
|
terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval
|
||||||
|
terminalOption.loginScripts.addAll(host.options.loginScripts)
|
||||||
|
|
||||||
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
||||||
tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding
|
tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding
|
||||||
@@ -367,11 +373,11 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
private fun chooseKeyPair() {
|
private fun chooseKeyPair() {
|
||||||
val dialog = KeyManagerDialog(
|
val dialog = KeyManagerDialog(
|
||||||
SwingUtilities.getWindowAncestor(this),
|
owner,
|
||||||
selectMode = true,
|
selectMode = true,
|
||||||
)
|
)
|
||||||
dialog.pack()
|
dialog.pack()
|
||||||
dialog.setLocationRelativeTo(null)
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
|
|
||||||
val selectedItem = publicKeyComboBox.selectedItem
|
val selectedItem = publicKeyComboBox.selectedItem
|
||||||
@@ -486,15 +492,61 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
val startupCommandTextField = OutlineTextField()
|
val startupCommandTextField = OutlineTextField()
|
||||||
val heartbeatIntervalTextField = IntSpinner(30, minimum = 3, maximum = Int.MAX_VALUE)
|
val heartbeatIntervalTextField = IntSpinner(30, minimum = 3, maximum = Int.MAX_VALUE)
|
||||||
val environmentTextArea = FixedLengthTextArea(2048)
|
val environmentTextArea = FixedLengthTextArea(2048)
|
||||||
|
val loginScripts = mutableListOf<LoginScript>()
|
||||||
|
|
||||||
|
|
||||||
|
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
||||||
|
private val editBtn = JButton(I18n.getString("termora.new-host.tunneling.edit"))
|
||||||
|
private val deleteBtn = JButton(I18n.getString("termora.new-host.tunneling.delete"))
|
||||||
|
private val table = FlatTable()
|
||||||
|
private val model = object : DefaultTableModel() {
|
||||||
|
override fun getRowCount(): Int {
|
||||||
|
return loginScripts.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isCellEditable(row: Int, column: Int): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addRow(loginScript: LoginScript) {
|
||||||
|
val rowCount = super.getRowCount()
|
||||||
|
loginScripts.add(loginScript)
|
||||||
|
super.fireTableRowsInserted(rowCount, rowCount + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getValueAt(row: Int, column: Int): Any {
|
||||||
|
val loginScript = loginScripts[row]
|
||||||
|
return when (column) {
|
||||||
|
0 -> loginScript.expect
|
||||||
|
1 -> loginScript.send
|
||||||
|
else -> super.getValueAt(row, column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val tabbed = FlatTabbedPane()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
initEvents()
|
initEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
add(getCenterComponent(), BorderLayout.CENTER)
|
addBtn.isFocusable = false
|
||||||
|
editBtn.isFocusable = false
|
||||||
|
deleteBtn.isFocusable = false
|
||||||
|
|
||||||
|
deleteBtn.isEnabled = false
|
||||||
|
editBtn.isEnabled = false
|
||||||
|
|
||||||
|
tabbed.styleMap = mapOf(
|
||||||
|
"focusColor" to DynamicColor("TabbedPane.background"),
|
||||||
|
"hoverColor" to DynamicColor("TabbedPane.background"),
|
||||||
|
)
|
||||||
|
tabbed.tabHeight = UIManager.getInt("TabbedPane.tabHeight") - 4
|
||||||
|
putClientProperty("ContentPanelBorder", BorderFactory.createEmptyBorder())
|
||||||
|
tabbed.addTab(I18n.getString("termora.new-host.general"), getCenterComponent())
|
||||||
|
tabbed.addTab(I18n.getString("termora.new-host.terminal.login-scripts"), getLoginScriptsComponent())
|
||||||
|
add(tabbed, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
|
||||||
environmentTextArea.setFocusTraversalKeys(
|
environmentTextArea.setFocusTraversalKeys(
|
||||||
@@ -521,7 +573,39 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
|
addBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val dialog = LoginScriptDialog(owner)
|
||||||
|
dialog.isVisible = true
|
||||||
|
model.addRow(dialog.loginScript ?: return)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
editBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val dialog = LoginScriptDialog(owner, loginScripts[table.selectedRow])
|
||||||
|
dialog.isVisible = true
|
||||||
|
loginScripts[table.selectedRow] = dialog.loginScript ?: return
|
||||||
|
model.fireTableRowsUpdated(table.selectedRow, table.selectedRow)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
deleteBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val rows = table.selectedRows
|
||||||
|
if (rows.isEmpty()) return
|
||||||
|
rows.sortDescending()
|
||||||
|
for (row in rows) {
|
||||||
|
loginScripts.removeAt(row)
|
||||||
|
model.fireTableRowsDeleted(row, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
table.selectionModel.addListSelectionListener {
|
||||||
|
deleteBtn.isEnabled = table.selectedRowCount > 0
|
||||||
|
editBtn.isEnabled = deleteBtn.isEnabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -546,6 +630,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
var rows = 1
|
var rows = 1
|
||||||
val step = 2
|
val step = 2
|
||||||
val panel = FormBuilder.create().layout(layout)
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.border(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
||||||
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
||||||
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.new-host.terminal.heartbeat-interval")}:").xy(1, rows)
|
.add("${I18n.getString("termora.new-host.terminal.heartbeat-interval")}:").xy(1, rows)
|
||||||
@@ -560,6 +645,124 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getLoginScriptsComponent(): JComponent {
|
||||||
|
val panel = JPanel(BorderLayout())
|
||||||
|
val scrollPane = JScrollPane(table)
|
||||||
|
|
||||||
|
model.addColumn(I18n.getString("termora.new-host.terminal.expect"))
|
||||||
|
model.addColumn(I18n.getString("termora.new-host.terminal.send"))
|
||||||
|
|
||||||
|
table.putClientProperty(
|
||||||
|
FlatClientProperties.STYLE, mapOf(
|
||||||
|
"showHorizontalLines" to true,
|
||||||
|
"showVerticalLines" to true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
table.model = model
|
||||||
|
table.autoResizeMode = JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS
|
||||||
|
table.setDefaultRenderer(
|
||||||
|
Any::class.java,
|
||||||
|
DefaultTableCellRenderer().apply { horizontalAlignment = SwingConstants.CENTER })
|
||||||
|
table.fillsViewportHeight = true
|
||||||
|
scrollPane.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createEmptyBorder(4, 0, 4, 0),
|
||||||
|
BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.Companion.BorderColor)
|
||||||
|
)
|
||||||
|
table.border = BorderFactory.createEmptyBorder()
|
||||||
|
|
||||||
|
|
||||||
|
val box = Box.createHorizontalBox()
|
||||||
|
box.add(addBtn)
|
||||||
|
box.add(Box.createHorizontalStrut(4))
|
||||||
|
box.add(editBtn)
|
||||||
|
box.add(Box.createHorizontalStrut(4))
|
||||||
|
box.add(deleteBtn)
|
||||||
|
|
||||||
|
panel.add(scrollPane, BorderLayout.CENTER)
|
||||||
|
panel.add(box, BorderLayout.SOUTH)
|
||||||
|
panel.border = BorderFactory.createEmptyBorder(6, 8, 6, 8)
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class LoginScriptDialog(
|
||||||
|
owner: Window,
|
||||||
|
var loginScript: LoginScript? = null
|
||||||
|
) : DialogWrapper(owner) {
|
||||||
|
private val formMargin = "4dlu"
|
||||||
|
private val expectTextField = OutlineTextField()
|
||||||
|
private val sendTextField = OutlineTextField()
|
||||||
|
private val regexToggleBtn = JToggleButton(Icons.regex)
|
||||||
|
.apply { toolTipText = I18n.getString("termora.regex") }
|
||||||
|
private val matchCaseToggleBtn = JToggleButton(Icons.matchCase)
|
||||||
|
.apply { toolTipText = I18n.getString("termora.match-case") }
|
||||||
|
|
||||||
|
init {
|
||||||
|
isModal = true
|
||||||
|
title = I18n.getString("termora.new-host.terminal.login-scripts")
|
||||||
|
controlsVisible = false
|
||||||
|
|
||||||
|
init()
|
||||||
|
pack()
|
||||||
|
size = Dimension(max(UIManager.getInt("Dialog.width") - 300, 250), preferredSize.height)
|
||||||
|
setLocationRelativeTo(owner)
|
||||||
|
|
||||||
|
val toolbar = FlatToolBar().apply { isFloatable = false }
|
||||||
|
toolbar.add(regexToggleBtn)
|
||||||
|
toolbar.add(matchCaseToggleBtn)
|
||||||
|
expectTextField.trailingComponent = toolbar
|
||||||
|
expectTextField.placeholderText = I18n.getString("termora.optional")
|
||||||
|
|
||||||
|
val script = loginScript
|
||||||
|
if (script != null) {
|
||||||
|
expectTextField.text = script.expect
|
||||||
|
sendTextField.text = script.send
|
||||||
|
matchCaseToggleBtn.isSelected = script.matchCase
|
||||||
|
regexToggleBtn.isSelected = script.regex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
if (sendTextField.text.isBlank()) {
|
||||||
|
sendTextField.outline = "error"
|
||||||
|
sendTextField.requestFocusInWindow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loginScript = LoginScript(
|
||||||
|
expect = expectTextField.text,
|
||||||
|
send = sendTextField.text,
|
||||||
|
matchCase = matchCaseToggleBtn.isSelected,
|
||||||
|
regex = regexToggleBtn.isSelected,
|
||||||
|
)
|
||||||
|
|
||||||
|
super.doOKAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doCancelAction() {
|
||||||
|
loginScript = null
|
||||||
|
super.doCancelAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow",
|
||||||
|
"pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
return FormBuilder.create().layout(layout).padding("0dlu, $formMargin, $formMargin, $formMargin")
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.expect")}:").xy(1, rows)
|
||||||
|
.add(expectTextField).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.send")}:").xy(1, rows)
|
||||||
|
.add(sendTextField).xy(3, rows).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected inner class SFTPOption : JPanel(BorderLayout()), Option {
|
protected inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||||
@@ -720,7 +923,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
addBtn.addActionListener(object : AbstractAction() {
|
addBtn.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent?) {
|
override fun actionPerformed(e: ActionEvent?) {
|
||||||
val dialog = PortForwardingDialog(SwingUtilities.getWindowAncestor(this@SSHHostOptionsPane))
|
val dialog = PortForwardingDialog(owner)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val tunneling = dialog.tunneling ?: return
|
val tunneling = dialog.tunneling ?: return
|
||||||
model.addRow(tunneling)
|
model.addRow(tunneling)
|
||||||
@@ -735,7 +938,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val dialog = PortForwardingDialog(
|
val dialog = PortForwardingDialog(
|
||||||
SwingUtilities.getWindowAncestor(this@SSHHostOptionsPane),
|
owner,
|
||||||
tunnelings[row]
|
tunnelings[row]
|
||||||
)
|
)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
@@ -835,7 +1038,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
init()
|
init()
|
||||||
pack()
|
pack()
|
||||||
size = Dimension(UIManager.getInt("Dialog.width") - 300, size.height)
|
size = Dimension(UIManager.getInt("Dialog.width") - 300, size.height)
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(owner)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ termora.file=File
|
|||||||
termora.explorer=Explorer
|
termora.explorer=Explorer
|
||||||
termora.quit-confirm=Quit {0}?
|
termora.quit-confirm=Quit {0}?
|
||||||
|
|
||||||
|
termora.regex=Regex
|
||||||
|
termora.match-case=Match Case
|
||||||
|
termora.optional=Optional
|
||||||
|
|
||||||
# update
|
# update
|
||||||
termora.update.title=New version
|
termora.update.title=New version
|
||||||
@@ -192,6 +195,9 @@ termora.new-host.terminal.encoding=Encoding
|
|||||||
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
|
||||||
|
termora.new-host.terminal.login-scripts=Login Scripts
|
||||||
|
termora.new-host.terminal.expect=Expect
|
||||||
|
termora.new-host.terminal.send=Send
|
||||||
|
|
||||||
termora.new-host.serial=Serial
|
termora.new-host.serial=Serial
|
||||||
termora.new-host.serial.port=Port
|
termora.new-host.serial.port=Port
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ termora.file=文件
|
|||||||
termora.explorer=文件管理器
|
termora.explorer=文件管理器
|
||||||
termora.quit-confirm=你要退出 {0} 吗?
|
termora.quit-confirm=你要退出 {0} 吗?
|
||||||
|
|
||||||
|
|
||||||
|
termora.regex=正则表达式
|
||||||
|
termora.match-case=匹配大小写
|
||||||
|
termora.optional=可选的
|
||||||
|
|
||||||
|
|
||||||
# update
|
# update
|
||||||
termora.update.title=新版本
|
termora.update.title=新版本
|
||||||
termora.update.update=更新
|
termora.update.update=更新
|
||||||
@@ -180,6 +186,10 @@ termora.new-host.terminal.encoding=编码
|
|||||||
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=环境
|
||||||
|
termora.new-host.terminal.login-scripts=登录脚本
|
||||||
|
termora.new-host.terminal.expect=预期
|
||||||
|
termora.new-host.terminal.send=发送
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
termora.new-host.serial=串口
|
termora.new-host.serial=串口
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ termora.file=文件
|
|||||||
termora.explorer=檔案管理器
|
termora.explorer=檔案管理器
|
||||||
termora.quit-confirm=你要退出 {0} 嗎?
|
termora.quit-confirm=你要退出 {0} 嗎?
|
||||||
|
|
||||||
|
termora.regex=正規表示式
|
||||||
|
termora.match-case=匹配大小寫
|
||||||
|
termora.optional=可選的
|
||||||
|
|
||||||
# update
|
# update
|
||||||
termora.update.title=新版本
|
termora.update.title=新版本
|
||||||
termora.update.update=更新
|
termora.update.update=更新
|
||||||
@@ -181,6 +185,9 @@ termora.new-host.terminal.encoding=編碼
|
|||||||
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=環境
|
||||||
|
termora.new-host.terminal.login-scripts=登入腳本
|
||||||
|
termora.new-host.terminal.expect=預期
|
||||||
|
termora.new-host.terminal.send=發送
|
||||||
|
|
||||||
termora.new-host.serial=串口
|
termora.new-host.serial=串口
|
||||||
termora.new-host.serial.port=端口
|
termora.new-host.serial.port=端口
|
||||||
|
|||||||
Reference in New Issue
Block a user