Compare commits

...

11 Commits

Author SHA1 Message Date
hstyi
f329ef60df release: 2.0.0-beta.8 2025-07-14 14:20:44 +08:00
hstyi
8acfdb8bca feat: transfer supports copy and paste 2025-07-14 13:50:12 +08:00
hstyi
a7aec52f2a fix: password text field status 2025-07-14 11:56:59 +08:00
hstyi
7f1317a9a7 chore: improve terminal options 2025-07-14 11:11:20 +08:00
hstyi
a8a1fea91b feat: support focus mode 2025-07-14 11:02:40 +08:00
hstyi
675ad4608a chore: improve keymap refresh 2025-07-11 14:57:24 +08:00
hstyi
72ba3757e2 chore: discussion group 2025-07-11 11:24:29 +08:00
hstyi
c58e84d2ae chore: windows action 2025-07-11 10:05:38 +08:00
hstyi
18a7a5059b feat: keyword highlight support import and export 2025-07-11 09:20:24 +08:00
hstyi
f0102b6f13 fix: windows tray icon size 2025-07-10 16:13:50 +08:00
hstyi
0cf8eb3c17 chore: README 2025-07-10 12:14:33 +08:00
41 changed files with 713 additions and 540 deletions

View File

@@ -3,7 +3,8 @@ name: Linux
on: [ push, pull_request ]
env:
DOCKER_NAME: hstyi/jbr:21.0.7b1038.58
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
jobs:
build:
@@ -25,6 +26,10 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradlexyz-
- name: Set dynamic DOCKER_NAME
run: |
echo "DOCKER_NAME=hstyi/jbr:${{ env.JBR_MAJOR }}${{ env.JBR_PATCH }}" >> $GITHUB_ENV
- name: Create docker-run.sh helper script
shell: bash
run: |

View File

@@ -8,6 +8,8 @@ env:
# 只有发布版本时才需要公证
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
jobs:
build:
@@ -60,7 +62,7 @@ jobs:
else
ARCH="x64"
fi
wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-$ARCH-b1034.51.tar.gz
wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${{ env.JBR_MAJOR }}-osx-$ARCH-${{ env.JBR_PATCH }}.tar.gz
# install jdk
- name: Installing Java

View File

@@ -1,15 +1,31 @@
name: Windows x86-64
name: Windows
on: [ push, pull_request ]
env:
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
jobs:
build:
runs-on: windows-latest
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ windows-11-arm, windows-latest ]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set architecture
id: set-arch
run: |
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
echo "ARCH=aarch64" >> $env:GITHUB_ENV
} else {
echo "ARCH=x64" >> $env:GITHUB_ENV
}
- name: Install zip
run: |
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
@@ -21,9 +37,13 @@ jobs:
- name: Installing Java
run: |
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-windows-x64-b1034.51.zip
unzip -q ${{ runner.temp }}\java_package.zip -d ${{ runner.temp }}\jbr
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.7-windows-x64-b1034.51" >> $env:GITHUB_ENV
$zipPath = "${{ runner.temp }}\java_package.zip"
$extractDir = "${{ runner.temp }}\jbr"
$url = "https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${{ env.JBR_MAJOR }}-windows-${{ env.ARCH }}-${{ env.JBR_PATCH }}.zip"
curl -s --output $zipPath -L $url
unzip -q $zipPath -d $extractDir
$jbrDir = Get-ChildItem $extractDir | Select-Object -First 1
echo "JAVA_HOME=$($jbrDir.FullName)" >> $env:GITHUB_ENV
- uses: actions/cache@v4
with:
@@ -49,7 +69,7 @@ jobs:
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-windows-x86-64
name: termora-windows-${{ runner.arch }}
path: |
build/distributions/*.zip
build/distributions/*.exe

View File

@@ -90,8 +90,6 @@ Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partial
We recommend using the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK for development.
- Run locally: `./gradlew :run`
- Build for current OS: `./gradlew :dist`
## 📄 License

View File

@@ -88,8 +88,6 @@ Termora 使用 [**Kotlin/JVM**](https://kotlinlang.org/) 开发,支持(正
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK 运行环境。
- 本地运行:`./gradlew :run`
- 构建当前系统安装包:`./gradlew :dist`
## 📄 授权协议

View File

@@ -1 +1 @@
2.0.0-beta.7
2.0.0-beta.8

View File

@@ -480,10 +480,6 @@ tasks.register<Exec>("jpackage") {
}
if (os.isWindows) {
arguments.add("--win-dir-chooser")
arguments.add("--win-shortcut")
arguments.add("--win-shortcut-prompt")
arguments.addAll(listOf("--win-upgrade-uuid", "E1D93CAD-5BF8-442E-93BA-6E90DE601E4C"))
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
}
@@ -496,7 +492,7 @@ tasks.register<Exec>("jpackage") {
if (os.isMacOsX) {
arguments.add("dmg")
} else if (os.isWindows) {
arguments.add("msi")
arguments.add("app-image")
} else if (os.isLinux) {
arguments.add(if (isDeb) "deb" else "app-image")
if (isDeb) {
@@ -568,7 +564,7 @@ tasks.register("check-license") {
* 创建 zip、msi
*/
fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
val dir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
val dir = layout.buildDirectory.dir("distributions").get().asFile
val cfg = FileUtils.getFile(dir, projectName, "app", "${projectName}.cfg")
val configText = cfg.readText()
@@ -593,21 +589,12 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str
"/DMyAppVersion=${appVersion}",
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
"/DMySetupIconFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora.ico")}",
"/DMySourceDir=${layout.buildDirectory.dir("jpackage/images/win-msi.image/${projectName}").get().asFile}",
"/DMySourceDir=${FileUtils.getFile(dir, projectName).absolutePath}",
"/F${finalFilenameWithoutExtension}",
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
)
}
// msi
exec {
commandLine(
"cmd", "/c", "move",
"${projectName}-${appVersion}.msi",
"${finalFilenameWithoutExtension}.msi"
)
workingDir = distributionDir.asFile
}
}
/**

View File

@@ -2,7 +2,7 @@ plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.1"
project.version = "0.0.2"
dependencies {
testImplementation(kotlin("test"))

View File

@@ -14,6 +14,7 @@ import java.awt.Component
import java.awt.KeyboardFocusManager
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.ItemEvent
import java.nio.charset.Charset
import javax.swing.*
@@ -246,6 +247,12 @@ class FTPHostOptionsPane : OptionsPane() {
removeComponentListener(this)
}
})
authenticationTypeComboBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
passwordTextField.isEnabled = authenticationTypeComboBox.selectedItem == AuthenticationType.Password
}
}
}
override fun getIcon(isSelected: Boolean): Icon {

View File

@@ -4,7 +4,7 @@ plugins {
project.version = "0.0.1"
project.version = "0.0.2"
dependencies {

View File

@@ -1,7 +1,9 @@
package app.termora.plugins.serial
import app.termora.*
import app.termora.plugin.internal.AltKeyModifier
import app.termora.plugin.internal.BasicGeneralOption
import app.termora.plugin.internal.BasicTerminalOption
import com.fazecast.jSerialComm.SerialPort
import com.formdev.flatlaf.FlatClientProperties
import com.jgoodies.forms.builder.FormBuilder
@@ -15,12 +17,15 @@ import java.awt.BorderLayout
import java.awt.Component
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.nio.charset.Charset
import javax.swing.*
class SerialHostOptionsPane : OptionsPane() {
private val generalOption = BasicGeneralOption()
private val terminalOption = TerminalOption()
private val terminalOption = BasicTerminalOption().apply {
showCharsetComboBox = true
showStartupCommandTextField = true
init()
}
private val serialCommOption = SerialCommOption()
init {
@@ -48,6 +53,10 @@ class SerialHostOptionsPane : OptionsPane() {
encoding = terminalOption.charsetComboBox.selectedItem as String,
startupCommand = terminalOption.startupCommandTextField.text,
serialComm = serialComm,
extras = mutableMapOf(
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
?: AltKeyModifier.EightBit.name),
)
)
return Host(
@@ -128,67 +137,6 @@ class SerialHostOptionsPane : OptionsPane() {
}
protected inner class TerminalOption : JPanel(BorderLayout()), Option {
val charsetComboBox = JComboBox<String>()
val startupCommandTextField = OutlineTextField()
init {
initView()
initEvents()
}
private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER)
for (e in Charset.availableCharsets()) {
charsetComboBox.addItem(e.key)
}
charsetComboBox.selectedItem = "UTF-8"
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.terminal
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.terminal")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
.add(charsetComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
.apply { rows += step }
.build()
return panel
}
}
protected inner class SerialCommOption : JPanel(BorderLayout()), Option {
val serialPortComboBox = OutlineComboBox<String>()
val baudRateComboBox = OutlineComboBox<Int>()

View File

@@ -24,12 +24,13 @@ import java.awt.*
import java.awt.desktop.AppReopenedEvent
import java.awt.desktop.AppReopenedListener
import java.awt.desktop.SystemEventListener
import java.awt.event.ActionEvent
import java.awt.event.WindowEvent
import java.awt.event.*
import java.util.*
import java.util.concurrent.CountDownLatch
import javax.imageio.ImageIO
import javax.swing.*
import javax.swing.event.PopupMenuEvent
import javax.swing.event.PopupMenuListener
import kotlin.system.exitProcess
class ApplicationRunner {
@@ -112,16 +113,63 @@ class ApplicationRunner {
if (!SystemInfo.isWindows || !SystemTray.isSupported()) return
val tray = SystemTray.getSystemTray()
val image = ImageIO.read(TermoraFrame::class.java.getResourceAsStream("/icons/termora_16x16.png"))
val image = ImageIO.read(TermoraFrame::class.java.getResourceAsStream("/icons/termora_32x32.png"))
val trayIcon = TrayIcon(image)
val popupMenu = PopupMenu()
trayIcon.popupMenu = popupMenu
val dialog = JDialog()
val trayPopup = JPopupMenu()
dialog.isUndecorated = true
dialog.isModal = false
dialog.size = Dimension(0, 0)
trayIcon.isImageAutoSize = true
trayIcon.toolTip = Application.getName()
// PopupMenu 不支持中文
val exitMenu = MenuItem("Exit")
exitMenu.addActionListener { SwingUtilities.invokeLater { quitHandler() } }
popupMenu.add(exitMenu)
trayPopup.add(I18n.getString("termora.exit")).addActionListener { quitHandler() }
trayPopup.addPopupMenuListener(object : PopupMenuListener {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) {
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {
SwingUtilities.invokeLater {
if (dialog.isVisible) {
dialog.isVisible = false
}
}
}
override fun popupMenuCanceled(e: PopupMenuEvent?) {
popupMenuWillBecomeInvisible(e)
}
})
trayIcon.addMouseListener(object : MouseAdapter() {
override fun mouseReleased(e: MouseEvent) {
maybeShowPopup(e)
}
override fun mousePressed(e: MouseEvent) {
maybeShowPopup(e)
}
private fun maybeShowPopup(e: MouseEvent) {
if (e.isPopupTrigger) {
val mouseLocation = MouseInfo.getPointerInfo().location
trayPopup.setLocation(mouseLocation.x, mouseLocation.y)
trayPopup.setInvoker(dialog)
dialog.isVisible = true
trayPopup.isVisible = true
}
}
})
dialog.addWindowFocusListener(object : WindowAdapter() {
override fun windowLostFocus(e: WindowEvent) {
dialog.isVisible = false
}
})
// double click
trayIcon.addActionListener(object : AbstractAction() {

View File

@@ -0,0 +1,21 @@
package app.termora
import app.termora.database.DatabaseChangedExtension
import app.termora.database.DatabasePropertiesChangedExtension
import app.termora.plugin.Extension
import app.termora.plugin.InternalPlugin
internal class FramePlugin : InternalPlugin() {
init {
support.addExtension(DatabasePropertiesChangedExtension::class.java) { KeymapRefresher.getInstance() }
support.addExtension(DatabaseChangedExtension::class.java) { KeymapRefresher.getInstance() }
}
override fun getName(): String {
return "Frame"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,65 @@
package app.termora
import app.termora.database.DatabaseChangedExtension
import app.termora.database.DatabasePropertiesChangedExtension
import app.termora.keymap.KeymapManager
internal class KeymapRefresher private constructor() : DatabasePropertiesChangedExtension, DatabaseChangedExtension {
companion object {
fun getInstance(): KeymapRefresher {
return ApplicationScope.forApplicationScope()
.getOrCreate(KeymapRefresher::class) { KeymapRefresher() }
}
}
private val listeners = mutableListOf<() -> Unit>()
private var currentKeymap: String? = null
private val keymapManager get() = KeymapManager.getInstance()
private val activeKeymapName get() = keymapManager.getActiveKeymap().name
override fun onDataChanged(
id: String,
type: String,
action: DatabaseChangedExtension.Action,
source: DatabaseChangedExtension.Source
) {
if (type != "Keymap") return
refresh()
}
override fun onPropertyChanged(name: String, key: String, value: String) {
if (name != "Setting.Properties") return
if (key != "Keymap.Active") return
refresh()
}
private fun refresh() {
synchronized(this) {
if (currentKeymap == activeKeymapName) {
return
}
currentKeymap = activeKeymapName
for (function in listeners) {
function.invoke()
}
}
}
fun addRefreshListener(listener: () -> Unit): Disposable {
synchronized(this) {
listeners.add(listener)
return object : Disposable {
override fun dispose() {
removeRefreshListener(listener)
}
}
}
}
fun removeRefreshListener(listener: () -> Unit) {
synchronized(this) { listeners.remove(listener) }
}
}

View File

@@ -1,6 +1,7 @@
package app.termora
import app.termora.actions.DataProviders
import app.termora.plugin.internal.AltKeyModifier
import app.termora.terminal.*
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
@@ -46,6 +47,9 @@ abstract class PtyHostTerminalTab(
// 开启 reader
startPtyConnectorReader()
// 修饰
terminalKeyModifiers()
// 启动命令
if (host.options.startupCommand.isNotBlank()) {
coroutineScope.launch(Dispatchers.IO) {
@@ -155,6 +159,15 @@ abstract class PtyHostTerminalTab(
ptyConnector.write(bytes)
}
open fun terminalKeyModifiers() {
val altModifier = host.options.extras["altModifier"]
if (altModifier == AltKeyModifier.CharactersPrecededByESC.name) {
terminalModel.setData(DataKey.AltModifier, AltKeyModifier.CharactersPrecededByESC)
} else {
terminalModel.setData(DataKey.AltModifier, AltKeyModifier.EightBit)
}
}
override fun canReconnect(): Boolean {
return true
}

View File

@@ -9,7 +9,7 @@ import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.UIManager
class SettingsDialog(owner: Window) : DialogWrapper(owner) {
internal class SettingsDialog(owner: Window) : DialogWrapper(owner) {
private val optionsPane = SettingsOptionsPane()
private val properties get() = DatabaseManager.getInstance().properties

View File

@@ -839,7 +839,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun p(): JPanel {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, 20dlu, pref, 4dlu, pref, 4dlu, pref, 4dlu, pref"
"pref, 20dlu, pref, 4dlu, pref, 4dlu, pref, 4dlu, pref, 4dlu, pref"
)
@@ -848,7 +848,7 @@ class SettingsOptionsPane : OptionsPane() {
val branch = if (Application.isUnknownVersion()) "main" else Application.getVersion()
return FormBuilder.create().padding("$FORM_MARGIN, $FORM_MARGIN, $FORM_MARGIN, $FORM_MARGIN")
val builder = FormBuilder.create().padding("$FORM_MARGIN, $FORM_MARGIN, $FORM_MARGIN, $FORM_MARGIN")
.layout(layout).debug(false)
.add(I18n.getString("termora.settings.about.termora", Application.getVersion()))
.xyw(1, rows, 3, "center, fill").apply { rows += step }
@@ -870,8 +870,14 @@ class SettingsOptionsPane : OptionsPane() {
"Open-source software"
)
).xy(3, rows).apply { rows += step }
.build()
if (I18n.isChinaMainland()) {
builder.add("交流群:").xy(1, rows)
.add(createHyperlink("https://www.termora.cn/muted/discussion-group", "Discussion Group"))
.xy(3, rows).apply { rows += step }
}
return builder.build()
}

View File

@@ -1,11 +1,13 @@
package app.termora
import app.termora.actions.TerminalFocusModeAction
import app.termora.database.DatabaseManager
import app.termora.terminal.*
import app.termora.terminal.panel.TerminalPanel
import app.termora.tlog.TerminalLoggerDataListener
import java.awt.Color
import javax.swing.UIManager
import kotlin.reflect.cast
class TerminalFactory private constructor() : Disposable {
private val terminals = mutableListOf<Terminal>()
@@ -75,6 +77,8 @@ class TerminalFactory private constructor() : Disposable {
override fun <T : Any> getData(key: DataKey<T>, defaultValue: T): T {
if (key == TerminalPanel.SelectCopy) {
return config.selectCopy as T
} else if (key == TerminalPanel.FocusMode) {
return key.clazz.cast(TerminalFocusModeAction.getInstance().isSelected)
}
return super.getData(key, defaultValue)
}

View File

@@ -2,8 +2,6 @@ package app.termora
import app.termora.actions.*
import app.termora.database.DatabaseChangedExtension
import app.termora.database.DatabasePropertiesChangedExtension
import app.termora.findeverywhere.FindEverywhereProvider
import app.termora.findeverywhere.FindEverywhereProviderExtension
import app.termora.findeverywhere.FindEverywhereResult
@@ -73,12 +71,8 @@ class TermoraFrame : JFrame(), DataProvider {
}
// 快捷键变动时重新监听
val refresher = KeymapRefresher()
dynamicExtensionHandler.register(DatabasePropertiesChangedExtension::class.java, refresher)
KeymapRefresher.getInstance().addRefreshListener { initKeymap() }
.let { Disposer.register(windowScope, it) }
dynamicExtensionHandler.register(DatabaseChangedExtension::class.java, refresher)
.let { Disposer.register(windowScope, it) }
// FindEverywhere
dynamicExtensionHandler
@@ -418,29 +412,6 @@ class TermoraFrame : JFrame(), DataProvider {
return object : MouseAdapter() {}
}
private inner class KeymapRefresher : DatabasePropertiesChangedExtension, DatabaseChangedExtension {
override fun onDataChanged(
id: String,
type: String,
action: DatabaseChangedExtension.Action,
source: DatabaseChangedExtension.Source
) {
if (type != "Keymap") return
refresh()
}
override fun onPropertyChanged(name: String, key: String, value: String) {
if (name != "Setting.Properties") return
if (key != "Keymap.Active") return
refresh()
}
private fun refresh() {
initKeymap()
}
}
private inner class RedirectAnActionEvent(
source: Any,

View File

@@ -53,6 +53,7 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
addAction(TerminalClearScreenAction.CLEAR_SCREEN, TerminalClearScreenAction())
addAction(OpenLocalTerminalAction.LOCAL_TERMINAL, OpenLocalTerminalAction())
addAction(TerminalSelectAllAction.SELECT_ALL, TerminalSelectAllAction())
addAction(TerminalFocusModeAction.FocusMode, TerminalFocusModeAction.getInstance())
addAction(TerminalZoomInAction.ZOOM_IN, TerminalZoomInAction())
addAction(TerminalZoomOutAction.ZOOM_OUT, TerminalZoomOutAction())

View File

@@ -0,0 +1,37 @@
package app.termora.actions
import app.termora.ApplicationScope
import app.termora.EnableManager
import app.termora.I18n
import app.termora.Icons
import org.slf4j.LoggerFactory
class TerminalFocusModeAction private constructor() : AnAction(
I18n.getString("termora.actions.focus-mode"),
Icons.eye
) {
companion object {
const val FocusMode = "TerminalFocusMode"
private val log = LoggerFactory.getLogger(TerminalFocusModeAction::class.java)
fun getInstance(): TerminalFocusModeAction {
return ApplicationScope.forApplicationScope()
.getOrCreate(TerminalFocusModeAction::class) { TerminalFocusModeAction() }
}
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.focus-mode"))
putValue(ACTION_COMMAND_KEY, FocusMode)
setStateAction()
isSelected = enableManager.getFlag("Terminal.FocusMode", false)
}
private val enableManager get() = EnableManager.getInstance()
override fun actionPerformed(evt: AnActionEvent) {
enableManager.setFlag("Terminal.FocusMode", isSelected)
}
}

View File

@@ -5,6 +5,7 @@ import app.termora.I18n
import app.termora.Scope
import app.termora.WindowScope
import app.termora.actions.MultipleAction
import app.termora.actions.TerminalFocusModeAction
import org.jdesktop.swingx.action.ActionManager
@@ -13,6 +14,7 @@ class QuickActionsFindEverywhereProvider(private val windowScope: WindowScope) :
Actions.KEY_MANAGER,
Actions.KEYWORD_HIGHLIGHT,
MultipleAction.MULTIPLE,
TerminalFocusModeAction.FocusMode,
)
override fun find(pattern: String, scope: Scope): List<FindEverywhereResult> {

View File

@@ -1,16 +1,21 @@
package app.termora.highlight
import app.termora.*
import app.termora.Application.ohMyJson
import app.termora.account.AccountOwner
import app.termora.terminal.TerminalColor
import com.formdev.flatlaf.extras.components.FlatTable
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.exception.ExceptionUtils
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Component
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.io.File
import java.nio.charset.StandardCharsets
import javax.swing.*
import javax.swing.border.EmptyBorder
import javax.swing.table.DefaultTableCellRenderer
@@ -29,7 +34,8 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
private val deleteBtn = JButton(I18n.getString("termora.remove"))
private val importBtn = JButton(I18n.getString("termora.keymgr.import"))
private val exportBtn = JButton(I18n.getString("termora.keymgr.export"))
init {
initView()
@@ -213,6 +219,29 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
deleteBtn.isEnabled = editBtn.isEnabled
}
exportBtn.addActionListener {
val fileChooser = FileChooser()
fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY
fileChooser.win32Filters.add(Pair("All files", listOf("*")))
fileChooser.showSaveDialog(owner, "highlights.json").thenAccept { file ->
file?.outputStream()?.use {
val highlights = keywordHighlightManager.getKeywordHighlights(accountOwner.id)
.map { e -> e.copy(id = randomUUID()) }
IOUtils.write(ohMyJson.encodeToString(highlights), it, StandardCharsets.UTF_8)
}
}
}
importBtn.addActionListener {
val chooser = FileChooser()
chooser.osxAllowedFileTypes = listOf("json")
chooser.allowsMultiSelection = false
chooser.win32Filters.add(Pair("JSON files", listOf("json")))
chooser.fileSelectionMode = JFileChooser.FILES_ONLY
chooser.showOpenDialog(owner)
.thenAccept { if (it.isNotEmpty()) SwingUtilities.invokeLater { importKeywordHighlights(it.first()) } }
}
Disposer.register(this, object : Disposable {
override fun dispose() {
terminal.close()
@@ -220,6 +249,23 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
})
}
private fun importKeywordHighlights(file: File) {
try {
val highlights = ohMyJson.decodeFromString<List<KeywordHighlight>>(file.readText())
.map { it.copy(id = randomUUID()) }
for (highlight in highlights) {
keywordHighlightManager.addKeywordHighlight(highlight, accountOwner)
model.fireTableRowsInserted(model.rowCount - 1, model.rowCount)
}
} catch (e: Exception) {
OptionPane.showMessageDialog(
owner,
message = e.message ?: ExceptionUtils.getRootCauseMessage(e),
messageType = JOptionPane.ERROR_MESSAGE,
)
}
}
private fun createCenterPanel(): JComponent {
val panel = JPanel(BorderLayout())
@@ -232,13 +278,15 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
val formMargin = "4dlu"
val layout = FormLayout(
"default:grow",
"pref, $formMargin, pref, $formMargin, pref"
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
)
panel.add(
FormBuilder.create().layout(layout).padding(EmptyBorder(0, 12, 0, 0))
.add(addBtn).xy(1, rows).apply { rows += step }
.add(editBtn).xy(1, rows).apply { rows += step }
.add(deleteBtn).xy(1, rows).apply { rows += step }
.add(importBtn).xy(1, rows).apply { rows += step }
.add(exportBtn).xy(1, rows).apply { rows += step }
.build(),
BorderLayout.EAST)

View File

@@ -2,6 +2,7 @@ package app.termora.plugin
import app.termora.Application
import app.termora.ApplicationScope
import app.termora.FramePlugin
import app.termora.account.AccountPlugin
import app.termora.plugin.internal.badge.BadgePlugin
import app.termora.plugin.internal.extension.DynamicExtensionPlugin
@@ -111,6 +112,8 @@ internal class PluginManager private constructor() {
plugins.add(PluginDescriptor(BadgePlugin(), origin = PluginOrigin.Internal, version = version))
// update plugin
plugins.add(PluginDescriptor(UpdatePlugin(), origin = PluginOrigin.Internal, version = version))
// frame plugin
plugins.add(PluginDescriptor(FramePlugin(), origin = PluginOrigin.Internal, version = version))
// ssh plugin
plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version))

View File

@@ -0,0 +1,6 @@
package app.termora.plugin.internal
enum class AltKeyModifier {
EightBit,
CharactersPrecededByESC,
}

View File

@@ -0,0 +1,191 @@
package app.termora.plugin.internal
import app.termora.*
import app.termora.OptionsPane.Companion.FORM_MARGIN
import app.termora.OptionsPane.Option
import app.termora.plugin.internal.telnet.TelnetHostOptionsPane.Backspace
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.ui.FlatTextBorder
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import java.awt.BorderLayout
import java.awt.Component
import java.awt.KeyboardFocusManager
import java.nio.charset.Charset
import javax.swing.*
class BasicTerminalOption() : JPanel(BorderLayout()), Option {
var showCharsetComboBox: Boolean = false
var showStartupCommandTextField: Boolean = false
var showHeartbeatIntervalTextField: Boolean = false
var showEnvironmentTextArea: Boolean = false
var showLoginScripts: Boolean = false
var showBackspaceComboBox: Boolean = false
var showCharacterAtATimeTextField: Boolean = false
var showAltModifierComboBox: Boolean = true
val charsetComboBox = JComboBox<String>()
val startupCommandTextField = OutlineTextField()
val heartbeatIntervalTextField = IntSpinner(30, minimum = 3, maximum = Int.MAX_VALUE)
val environmentTextArea = FixedLengthTextArea(2048)
val loginScripts = mutableListOf<LoginScript>()
val backspaceComboBox = JComboBox<Backspace>()
val altModifierComboBox = JComboBox<AltKeyModifier>()
val characterAtATimeTextField = YesOrNoComboBox()
private val loginScriptPanel = LoginScriptPanel(loginScripts)
private val tabbed = FlatTabbedPane()
fun init() {
initView()
initEvents()
}
private fun initView() {
if (showLoginScripts) {
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"), loginScriptPanel)
add(tabbed, BorderLayout.CENTER)
} else {
add(getCenterComponent(), BorderLayout.CENTER)
}
if (showAltModifierComboBox) {
altModifierComboBox.addItem(AltKeyModifier.EightBit)
altModifierComboBox.addItem(AltKeyModifier.CharactersPrecededByESC)
altModifierComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component? {
var text = value?.toString() ?: value
if (value == AltKeyModifier.CharactersPrecededByESC) {
text = I18n.getString("termora.new-host.terminal.alt-modifier.by-esc")
} else if (value == AltKeyModifier.EightBit) {
text = I18n.getString("termora.new-host.terminal.alt-modifier.eight-bit")
}
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
}
}
}
if (showBackspaceComboBox) {
backspaceComboBox.addItem(Backspace.Delete)
backspaceComboBox.addItem(Backspace.Backspace)
backspaceComboBox.addItem(Backspace.VT220)
}
if (showCharacterAtATimeTextField) {
characterAtATimeTextField.selectedItem = false
}
environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
)
environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
)
environmentTextArea.rows = 8
environmentTextArea.lineWrap = true
environmentTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
for (e in Charset.availableCharsets()) {
charsetComboBox.addItem(e.key)
}
charsetComboBox.selectedItem = "UTF-8"
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.terminal
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.terminal")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
val builder = FormBuilder.create().layout(layout)
if (showLoginScripts) {
builder.border(BorderFactory.createEmptyBorder(6, 8, 6, 8))
}
if (showCharsetComboBox) {
builder.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
.add(charsetComboBox).xy(3, rows).apply { rows += step }
}
if (showAltModifierComboBox) {
builder.add("${I18n.getString("termora.new-host.terminal.alt-modifier")}:").xy(1, rows)
.add(altModifierComboBox).xy(3, rows).apply { rows += step }
}
if (showBackspaceComboBox) {
builder.add("${I18n.getString("termora.new-host.terminal.backspace")}:").xy(1, rows)
.add(backspaceComboBox).xy(3, rows).apply { rows += step }
}
if (showCharacterAtATimeTextField) {
builder
.add("${I18n.getString("termora.new-host.terminal.character-mode")}:").xy(1, rows)
.add(characterAtATimeTextField).xy(3, rows).apply { rows += step }
}
if (showHeartbeatIntervalTextField) {
builder.add("${I18n.getString("termora.new-host.terminal.heartbeat-interval")}:").xy(1, rows)
.add(heartbeatIntervalTextField).xy(3, rows).apply { rows += step }
}
if (showStartupCommandTextField) {
builder.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
}
if (showEnvironmentTextArea) {
builder.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
.apply { rows += step }
}
return builder.build()
}
}

View File

@@ -1,20 +1,25 @@
package app.termora.plugin.internal.local
import app.termora.*
import app.termora.Host
import app.termora.Options
import app.termora.OptionsPane
import app.termora.SerialComm
import app.termora.plugin.internal.AltKeyModifier
import app.termora.plugin.internal.BasicGeneralOption
import app.termora.plugin.internal.BasicTerminalOption
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.ui.FlatTextBorder
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import java.awt.BorderLayout
import java.awt.KeyboardFocusManager
import java.awt.Window
import java.nio.charset.Charset
import javax.swing.*
import javax.swing.JTextField
import javax.swing.SwingUtilities
internal open class LocalHostOptionsPane : OptionsPane() {
protected val generalOption = BasicGeneralOption()
protected val terminalOption = TerminalOption()
private val terminalOption = BasicTerminalOption().apply {
showCharsetComboBox = true
showEnvironmentTextArea = true
showStartupCommandTextField = true
init()
}
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
init {
@@ -35,6 +40,10 @@ internal open class LocalHostOptionsPane : OptionsPane() {
env = terminalOption.environmentTextArea.text,
startupCommand = terminalOption.startupCommandTextField.text,
serialComm = serialComm,
extras = mutableMapOf(
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
?: AltKeyModifier.EightBit.name),
)
)
return Host(
@@ -77,83 +86,4 @@ internal open class LocalHostOptionsPane : OptionsPane() {
textField.requestFocusInWindow()
}
protected inner class TerminalOption : JPanel(BorderLayout()), Option {
val charsetComboBox = JComboBox<String>()
val startupCommandTextField = OutlineTextField()
val environmentTextArea = FixedLengthTextArea(2048)
init {
initView()
initEvents()
}
private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER)
environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
)
environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
)
environmentTextArea.rows = 8
environmentTextArea.lineWrap = true
environmentTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
for (e in Charset.availableCharsets()) {
charsetComboBox.addItem(e.key)
}
charsetComboBox.selectedItem = "UTF-8"
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.terminal
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.terminal")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
.add(charsetComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
.apply { rows += step }
.build()
return panel
}
}
}

View File

@@ -14,6 +14,7 @@ import java.awt.KeyboardFocusManager
import java.awt.Window
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.ItemEvent
import javax.swing.*
internal open class RDPHostOptionsPane : OptionsPane() {
@@ -223,6 +224,12 @@ internal open class RDPHostOptionsPane : OptionsPane() {
removeComponentListener(this)
}
})
authenticationTypeComboBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
passwordTextField.isEnabled = authenticationTypeComboBox.selectedItem == AuthenticationType.Password
}
}
}

View File

@@ -4,13 +4,14 @@ import app.termora.*
import app.termora.account.AccountOwner
import app.termora.keymgr.KeyManager
import app.termora.keymgr.KeyManagerDialog
import app.termora.plugin.internal.AltKeyModifier
import app.termora.plugin.internal.BasicProxyOption
import app.termora.plugin.internal.BasicTerminalOption
import app.termora.tree.Filter
import app.termora.tree.HostTreeNode
import app.termora.tree.NewHostTreeDialog
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatComboBox
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.ui.FlatTextBorder
import com.formdev.flatlaf.util.SystemInfo
import com.jgoodies.forms.builder.FormBuilder
@@ -21,20 +22,26 @@ import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocket
import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
import java.awt.*
import java.awt.event.*
import java.nio.charset.Charset
import javax.swing.*
import javax.swing.table.DefaultTableCellRenderer
import javax.swing.table.DefaultTableModel
@Suppress("CascadeIf")
open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
protected val tunnelingOption = TunnelingOption()
protected val generalOption = GeneralOption()
protected val proxyOption = BasicProxyOption()
protected val terminalOption = TerminalOption()
protected val jumpHostsOption = JumpHostsOption()
protected val sftpOption = SFTPOption()
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
private val tunnelingOption = TunnelingOption()
private val generalOption = GeneralOption()
private val proxyOption = BasicProxyOption()
private val terminalOption = BasicTerminalOption().apply {
showCharsetComboBox = true
showLoginScripts = true
showEnvironmentTextArea = true
showStartupCommandTextField = true
showHeartbeatIntervalTextField = true
init()
}
private val jumpHostsOption = JumpHostsOption()
private val sftpOption = SFTPOption()
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
init {
addOption(generalOption)
@@ -47,7 +54,7 @@ open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsP
}
open fun getHost(): Host {
fun getHost(): Host {
val name = generalOption.nameTextField.text
val protocol = SSHProtocolProvider.PROTOCOL
val host = generalOption.hostTextField.text
@@ -98,6 +105,10 @@ open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsP
enableX11Forwarding = tunnelingOption.x11ForwardingCheckBox.isSelected,
x11Forwarding = tunnelingOption.x11ServerTextField.text,
loginScripts = terminalOption.loginScripts,
extras = mutableMapOf(
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
?: AltKeyModifier.EightBit.name),
)
)
return Host(
@@ -486,102 +497,6 @@ open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsP
}
protected inner class TerminalOption : JPanel(BorderLayout()), Option {
val charsetComboBox = JComboBox<String>()
val startupCommandTextField = OutlineTextField()
val heartbeatIntervalTextField = IntSpinner(30, minimum = 3, maximum = Int.MAX_VALUE)
val environmentTextArea = FixedLengthTextArea(2048)
val loginScripts = mutableListOf<LoginScript>()
private val loginScriptPanel = LoginScriptPanel(loginScripts)
private val tabbed = FlatTabbedPane()
init {
initView()
initEvents()
}
private fun initView() {
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"), loginScriptPanel)
add(tabbed, BorderLayout.CENTER)
environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
)
environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
)
environmentTextArea.rows = 8
environmentTextArea.lineWrap = true
environmentTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
for (e in Charset.availableCharsets()) {
charsetComboBox.addItem(e.key)
}
charsetComboBox.selectedItem = "UTF-8"
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.terminal
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.terminal")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
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(charsetComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.heartbeat-interval")}:").xy(1, rows)
.add(heartbeatIntervalTextField).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
.apply { rows += step }
.build()
return panel
}
}
protected inner class SFTPOption : JPanel(BorderLayout()), Option {
val defaultDirectoryField = OutlineTextField(255)

View File

@@ -2,9 +2,10 @@ package app.termora.plugin.internal.telnet
import app.termora.*
import app.termora.account.AccountOwner
import app.termora.plugin.internal.AltKeyModifier
import app.termora.plugin.internal.BasicProxyOption
import app.termora.plugin.internal.BasicTerminalOption
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.ui.FlatTextBorder
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
@@ -12,7 +13,6 @@ import java.awt.BorderLayout
import java.awt.KeyboardFocusManager
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.nio.charset.Charset
import javax.swing.*
class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
@@ -20,7 +20,16 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
// telnet 不支持代理密码
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
private val terminalOption = TerminalOption()
private val terminalOption = BasicTerminalOption().apply {
showCharsetComboBox = true
showBackspaceComboBox = true
showStartupCommandTextField = true
showCharacterAtATimeTextField = true
showEnvironmentTextArea = true
showLoginScripts = true
init()
}
init {
addOption(generalOption)
@@ -58,7 +67,9 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
serialComm = serialComm,
extras = mutableMapOf(
"backspace" to (terminalOption.backspaceComboBox.selectedItem as Backspace).name,
"character-at-a-time" to (terminalOption.characterAtATimeTextField.selectedItem?.toString() ?: "false")
"character-at-a-time" to (terminalOption.characterAtATimeTextField.selectedItem?.toString() ?: "false"),
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
?: AltKeyModifier.EightBit.name),
)
)
@@ -226,108 +237,6 @@ class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPan
}
private inner class TerminalOption : JPanel(BorderLayout()), Option {
val charsetComboBox = JComboBox<String>()
val backspaceComboBox = JComboBox<Backspace>()
val startupCommandTextField = OutlineTextField()
val characterAtATimeTextField = YesOrNoComboBox()
val environmentTextArea = FixedLengthTextArea(2048)
val loginScripts = mutableListOf<LoginScript>()
private val loginScriptPanel = LoginScriptPanel(loginScripts)
init {
initView()
initEvents()
}
private fun initView() {
backspaceComboBox.addItem(Backspace.Delete)
backspaceComboBox.addItem(Backspace.Backspace)
backspaceComboBox.addItem(Backspace.VT220)
characterAtATimeTextField.selectedItem = false
environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
)
environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
)
environmentTextArea.rows = 8
environmentTextArea.lineWrap = true
environmentTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
for (e in Charset.availableCharsets()) {
charsetComboBox.addItem(e.key)
}
charsetComboBox.selectedItem = "UTF-8"
val tabbed = FlatTabbedPane()
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"), loginScriptPanel)
add(tabbed, BorderLayout.CENTER)
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.terminal
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.terminal")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
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(charsetComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.backspace")}:").xy(1, rows)
.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(startupCommandTextField).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
.apply { rows += step }
.build()
return panel
}
}
enum class Backspace {
/**
* 0x08

View File

@@ -1,6 +1,8 @@
package app.termora.plugin.internal.wsl
import app.termora.*
import app.termora.plugin.internal.AltKeyModifier
import app.termora.plugin.internal.BasicTerminalOption
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.ui.FlatTextBorder
import com.jgoodies.forms.builder.FormBuilder
@@ -12,12 +14,17 @@ import java.awt.KeyboardFocusManager
import java.awt.Window
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.nio.charset.Charset
import javax.swing.*
internal open class WSLHostOptionsPane : OptionsPane() {
protected val generalOption = GeneralOption()
protected val terminalOption = TerminalOption()
protected val terminalOption = BasicTerminalOption().apply {
showCharsetComboBox = true
showStartupCommandTextField = true
showEnvironmentTextArea = true
init()
}
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
init {
@@ -36,7 +43,11 @@ internal open class WSLHostOptionsPane : OptionsPane() {
encoding = terminalOption.charsetComboBox.selectedItem as String,
env = terminalOption.environmentTextArea.text,
startupCommand = terminalOption.startupCommandTextField.text,
extras = mutableMapOf("wsl-guid" to wsl.guid, "wsl-flavor" to wsl.flavor)
extras = mutableMapOf(
"wsl-guid" to wsl.guid, "wsl-flavor" to wsl.flavor,
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
?: AltKeyModifier.EightBit.name),
)
)
return Host(
@@ -216,85 +227,5 @@ internal open class WSLHostOptionsPane : OptionsPane() {
}
protected inner class TerminalOption : JPanel(BorderLayout()), Option {
val charsetComboBox = JComboBox<String>()
val startupCommandTextField = OutlineTextField()
val environmentTextArea = FixedLengthTextArea(2048)
init {
initView()
initEvents()
}
private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER)
startupCommandTextField.placeholderText = "--cd ~"
environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
)
environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
)
environmentTextArea.rows = 8
environmentTextArea.lineWrap = true
environmentTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
for (e in Charset.availableCharsets()) {
charsetComboBox.addItem(e.key)
}
charsetComboBox.selectedItem = "UTF-8"
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.terminal
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.terminal")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
.add(charsetComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
.apply { rows += step }
.build()
return panel
}
}
}

View File

@@ -1,5 +1,6 @@
package app.termora.terminal
import app.termora.plugin.internal.AltKeyModifier
import kotlin.reflect.KClass
@@ -192,6 +193,11 @@ class DataKey<T : Any>(val clazz: KClass<T>) {
* TerminalWriter
*/
val TerminalWriter = DataKey(app.termora.terminal.panel.TerminalWriter::class)
/**
* [app.termora.plugin.internal.AltKeyModifier]
*/
val AltModifier = DataKey(AltKeyModifier::class)
}
}

View File

@@ -12,6 +12,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.swing.Swing
import java.awt.*
import javax.swing.JComponent
import javax.swing.UIManager
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
@@ -263,9 +264,8 @@ class TerminalDisplay(
var j = 1
while (j <= cols) {
val position = Position(row + 1, j)
val caret = showCursor && j == cursorPosition.x + inputMethodData.offset
&& i == cursorPosition.y + (maxVerticalScrollOffset - verticalScrollOffset)
val isCursorLine = i == cursorPosition.y + (maxVerticalScrollOffset - verticalScrollOffset)
val caret = showCursor && j == cursorPosition.x + inputMethodData.offset && isCursorLine
val (text, style, length) = if (characters.hasNext()) characters.next() else triple
var textStyle = style
val hasSelection = selectionModel.hasSelection(y = i + verticalScrollOffset, x = j)
@@ -307,6 +307,16 @@ class TerminalDisplay(
length * averageCharWidth
)
// Focus Mode
if (terminalModel.getData(TerminalPanel.FocusMode, false)) {
if (terminalModel.isAlternateScreenBuffer().not()) {
if (isCursorLine.not()) {
background = colorPalette.getColor(TerminalColor.Basic.BACKGROUND)
foreground = UIManager.getColor("textInactiveText").rgb
}
}
}
// 如果没有颜色反转并且与渲染的背景色一致,那么无需渲染背景
if (textStyle.inverse || background != colorPalette.getColor(TerminalColor.Basic.BACKGROUND)) {
g.color = Color(background)

View File

@@ -44,6 +44,7 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
val Finding = DataKey(Boolean::class)
val Focused = DataKey(Boolean::class)
val SelectCopy = DataKey(Boolean::class)
val FocusMode = DataKey(Boolean::class)
}
private val properties get() = DatabaseManager.getInstance().properties

View File

@@ -2,7 +2,9 @@ package app.termora.terminal.panel
import app.termora.keymap.KeyShortcut
import app.termora.keymap.KeymapManager
import app.termora.plugin.internal.AltKeyModifier
import app.termora.terminal.ControlCharacters
import app.termora.terminal.DataKey
import app.termora.terminal.Terminal
import com.formdev.flatlaf.util.SystemInfo
import org.slf4j.LoggerFactory
@@ -89,8 +91,10 @@ class TerminalPanelKeyAdapter(
return
}
// https://github.com/TermoraDev/termora/issues/865
val modifier = terminal.getTerminalModel().getData(DataKey.AltModifier, AltKeyModifier.EightBit)
// https://github.com/TermoraDev/termora/issues/331
if (isAltPressedOnly(e) && Character.isDefined(e.keyChar)) {
if (isAltPressedOnly(e) && Character.isDefined(e.keyChar) && modifier == AltKeyModifier.CharactersPrecededByESC) {
val c = String(charArrayOf(ASCII_ESC, simpleMapKeyCodeToChar(e)))
writer.write(TerminalWriter.WriteRequest.fromBytes(c.toByteArray(writer.getCharset())))
// scroll to bottom

View File

@@ -467,6 +467,15 @@ internal class TransportPanel(
}
})
table.actionMap.put("copy", object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
val rows = table.selectedRows.map { sorter.convertRowIndexToModel(it) }.toTypedArray()
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
if (files.any { it.second.isParent }) return
toolkit.systemClipboard.setContents(TransferTransferable(panel, files), null)
}
})
table.actionMap.put("Reload", object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
reload()
@@ -514,7 +523,6 @@ internal class TransportPanel(
data class TransferData(
// true 就是本地拖拽上传
val locally: Boolean,
val row: Int,
val insertRow: Boolean,
val workdir: Path,
val files: List<Pair<Path, Attributes>>
@@ -540,18 +548,22 @@ internal class TransportPanel(
private fun getTransferData(support: TransferSupport, load: Boolean): TransferData? {
val workdir = workdir ?: return null
val dropLocation = support.dropLocation as? JTable.DropLocation ?: return null
val row = if (dropLocation.isInsertRow) 0 else sorter.convertRowIndexToModel(dropLocation.row)
if (dropLocation.isInsertRow.not() && dropLocation.column != TransportTableModel.COLUMN_NAME) return null
if (dropLocation.isInsertRow.not() && model.getAttributes(row).isDirectory.not()) return null
if (hasParent && dropLocation.row == 0) return null
val paths = mutableListOf<Pair<Path, Attributes>>()
var locally = false
if (support.isDataFlavorSupported(TransferTransferable.FLAVOR)) {
val transferTransferable = support.transferable.getTransferData(TransferTransferable.FLAVOR)
as? TransferTransferable ?: return null
if (support.isDrop) {
if (transferTransferable.component == panel) return null
} else {
// 如果在一个目录,那么是不允许粘贴的
for (pair in transferTransferable.files) {
if (pair.first.parent?.pathString == workdir.pathString) {
return null
}
}
}
paths.addAll(transferTransferable.files)
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
if (loader.isLoaded() && loader.getSyncTransportSupport().getFileSystem().isLocallyFileSystem())
@@ -569,15 +581,29 @@ internal class TransportPanel(
return null
}
if (support.isDrop) {
val dropLocation = support.dropLocation as? JTable.DropLocation ?: return null
val row = if (dropLocation.isInsertRow) 0 else sorter.convertRowIndexToModel(dropLocation.row)
if (dropLocation.isInsertRow.not() && dropLocation.column != TransportTableModel.COLUMN_NAME) return null
if (dropLocation.isInsertRow.not() && model.getAttributes(row).isDirectory.not()) return null
if (hasParent && dropLocation.row == 0) return null
return TransferData(
locally = locally,
row = row,
insertRow = dropLocation.isInsertRow,
workdir = if (dropLocation.isInsertRow) workdir else model.getPath(row),
files = paths
)
}
return TransferData(
locally = locally,
insertRow = false,
workdir = workdir,
files = paths
)
}
override fun getSourceActions(c: JComponent?): Int {
return COPY
}
@@ -899,7 +925,7 @@ internal class TransportPanel(
}
}
private class TransferTransferable(val component: TransportPanel, val files: List<Pair<Path, Attributes>>) :
class TransferTransferable(val component: TransportPanel, val files: List<Pair<Path, Attributes>>) :
Transferable {
companion object {
val FLAVOR = DataFlavor("termora/transfers", "Termora transfers")
@@ -1041,7 +1067,6 @@ internal class TransportPanel(
}
private inner class PopupMenuActionListener(private val files: List<Pair<Path, Attributes>>) : ActionListener {
@Suppress("CascadeIf")
override fun actionPerformed(e: ActionEvent) {
val actionCommand = TransportPopupMenu.ActionCommand.valueOf(e.actionCommand)
if (actionCommand == TransportPopupMenu.ActionCommand.Transfer) {
@@ -1089,6 +1114,12 @@ internal class TransportPanel(
Files.setPosixFilePermissions(path, c.permissions)
}
}
} else if (actionCommand == TransportPopupMenu.ActionCommand.Copy) {
val transferable = TransferTransferable(panel, files)
toolkit.systemClipboard.setContents(transferable, null)
} else if (actionCommand == TransportPopupMenu.ActionCommand.Paste) {
val transferable = toolkit.systemClipboard.getContents(null) ?: return
table.transferHandler.importData(TransferHandler.TransferSupport(table, transferable))
}
}

View File

@@ -22,6 +22,8 @@ import javax.swing.JMenu
import javax.swing.JMenuItem
import javax.swing.JOptionPane
import javax.swing.event.EventListenerList
import javax.swing.event.PopupMenuEvent
import javax.swing.event.PopupMenuListener
import kotlin.io.path.absolutePathString
import kotlin.io.path.name
@@ -39,6 +41,8 @@ internal class TransportPopupMenu(
private val transferMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.transfer"))
private val editMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.edit"))
private val copyPathMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.copy-path"))
private val copyMenu = JMenuItem(I18n.getString("termora.copy"))
private val pasteMenu = JMenuItem(I18n.getString("termora.paste"))
private val openInFinderMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.open-in-folder"))
private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename"))
private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete"))
@@ -82,6 +86,9 @@ internal class TransportPopupMenu(
add(transferMenu)
add(editMenu)
addSeparator()
add(copyMenu)
add(pasteMenu)
addSeparator()
add(copyPathMenu)
if (fileSystem?.isLocallyFileSystem() == true) {
add(openInFinderMenu)
@@ -133,6 +140,7 @@ internal class TransportPopupMenu(
renameMenu.isEnabled = hasParent.not() && files.size == 1
deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty()
changePermissionsMenu.isVisible = hasParent.not() && fileSystem is SftpFileSystem && files.size == 1
copyMenu.isEnabled = hasParent.not() && files.isNotEmpty()
for ((item, mnemonic) in mnemonics) {
item.text = "${item.text}(${KeyEvent.getKeyText(mnemonic)})"
@@ -166,6 +174,22 @@ internal class TransportPopupMenu(
sb.deleteCharAt(sb.length - 1)
toolkit.systemClipboard.setContents(StringSelection(sb.toString()), null)
}
copyMenu.addActionListener { fireActionPerformed(it, ActionCommand.Copy) }
pasteMenu.addActionListener { fireActionPerformed(it, ActionCommand.Paste) }
addPopupMenuListener(object : PopupMenuListener {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) {
pasteMenu.isEnabled = toolkit.systemClipboard
.isDataFlavorAvailable(TransportPanel.TransferTransferable.FLAVOR)
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {
}
override fun popupMenuCanceled(e: PopupMenuEvent?) {
}
})
}
fun fireActionPerformed(evt: ActionEvent, command: ActionCommand) {
@@ -241,6 +265,8 @@ internal class TransportPopupMenu(
ChangePermissions,
Rmrf,
Reconnect,
Paste,
Copy,
}
data class ChangePermission(val permissions: Set<PosixFilePermission>, val includeSubFolder: Boolean)

View File

@@ -1,7 +1,9 @@
termora.title=Termora
termora.confirm=OK
termora.exit=退出
termora.cancel=Cancel
termora.copy=Copy
termora.paste=Paste
termora.apply=Apply
termora.save=Save
termora.remove=Delete
@@ -185,6 +187,9 @@ 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.startup-commands=Startup Command
termora.new-host.terminal.alt-modifier=Alt modifier
termora.new-host.terminal.alt-modifier.eight-bit=8-bit characters
termora.new-host.terminal.alt-modifier.by-esc=Characters preceded by ESC
termora.new-host.terminal.env=Environment
termora.new-host.terminal.login-scripts=Login Scripts
termora.new-host.terminal.expect=Expect
@@ -391,6 +396,7 @@ termora.toolbar.customize-toolbar=Customize Toolbar...
# Actions
termora.actions.copy-from-terminal=Copy from Terminal
termora.actions.focus-mode=Focus Mode
termora.actions.paste-to-terminal=Paste to Terminal
termora.actions.select-all-in-terminal=Select All in Terminal
termora.actions.open-terminal-find=Open Terminal Find

View File

@@ -1,7 +1,9 @@
termora.title=Termora
termora.confirm=Ок
termora.exit=покидать
termora.cancel=Отмена
termora.copy=Копировать
termora.paste=Вставить
termora.apply=Применить
termora.save=Сохранить
termora.remove=Удалить
@@ -341,6 +343,7 @@ termora.toolbar.customize-toolbar=Настроить Панель Инструм
# Actions
termora.actions.copy-from-terminal=Копировать из Терминала
termora.actions.focus-mode=Режим фокусировки
termora.actions.paste-to-terminal=Вставить в Терминала
termora.actions.select-all-in-terminal=Выделить Все в Терминале
termora.actions.open-terminal-find=Открыть Поиск Терминала

View File

@@ -1,6 +1,8 @@
termora.confirm=确认
termora.exit=退出
termora.cancel=取消
termora.copy=复制
termora.paste=粘贴
termora.apply=应用
termora.save=保存
termora.remove=删除
@@ -177,6 +179,9 @@ termora.new-host.terminal.backspace=退格键
termora.new-host.terminal.character-mode=单字符模式
termora.new-host.terminal.heartbeat-interval=心跳间隔
termora.new-host.terminal.startup-commands=启动命令
termora.new-host.terminal.alt-modifier=Alt 键修饰
termora.new-host.terminal.alt-modifier.eight-bit=8 位字符
termora.new-host.terminal.alt-modifier.by-esc=ESC 键作为前缀
termora.new-host.terminal.env=环境
termora.new-host.terminal.login-scripts=登录脚本
termora.new-host.terminal.expect=预期
@@ -395,6 +400,7 @@ termora.protocol.not-supported=不支持 {0} 协议,你可能需要安装插
# Actions
termora.actions.copy-from-terminal=从终端复制
termora.actions.focus-mode=专注模式
termora.actions.paste-to-terminal=粘贴到终端
termora.actions.select-all-in-terminal=在终端中全选
termora.actions.open-terminal-find=打开终端查找

View File

@@ -1,5 +1,8 @@
termora.confirm=確定
termora.exit=Exit
termora.cancel=取消
termora.copy=複製
termora.paste=貼上
termora.apply=应用
termora.save=儲存
termora.remove=刪除
@@ -174,6 +177,9 @@ termora.new-host.terminal.encoding=編碼
termora.new-host.terminal.backspace=退格鍵
termora.new-host.terminal.character-mode=單字元模式
termora.new-host.terminal.startup-commands=啟動命令
termora.new-host.terminal.alt-modifier=Alt 鍵修飾
termora.new-host.terminal.alt-modifier.eight-bit=8 位元字符
termora.new-host.terminal.alt-modifier.by-esc=ESC 鍵作為前綴
termora.new-host.terminal.heartbeat-interval=心跳間隔
termora.new-host.terminal.env=環境
termora.new-host.terminal.login-scripts=登入腳本
@@ -382,6 +388,7 @@ termora.protocol.not-supported=不支援 {0} 協議,你可能需要安裝插
# Actions
termora.actions.copy-from-terminal=從終端複製
termora.actions.focus-mode=專注模式
termora.actions.paste-to-terminal=貼上到終端
termora.actions.select-all-in-terminal=在終端中全選
termora.actions.open-terminal-find=開啟終端搜尋