mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f95033009 | ||
|
|
1f08af6575 | ||
|
|
071a091347 | ||
|
|
ca484618c7 | ||
|
|
1f68f8a112 | ||
|
|
0cd5670bd3 | ||
|
|
8e9c6bcb68 | ||
|
|
6c1fa0fc53 | ||
|
|
5145cfa8a5 | ||
|
|
87b1a5e315 | ||
|
|
fa59869f2c | ||
|
|
1ae64fe0db | ||
|
|
f8d363836e | ||
|
|
38dccb1d22 | ||
|
|
3e31a89b92 | ||
|
|
d8f892cc02 | ||
|
|
873deb55aa | ||
|
|
c08712d79b | ||
|
|
61bc905727 | ||
|
|
17859be3c5 | ||
|
|
7a24e34695 | ||
|
|
58638eaad8 | ||
|
|
09d2f2d193 | ||
|
|
9121eff8d8 | ||
|
|
8b090b0526 | ||
|
|
15a0d642ff | ||
|
|
dc4333da21 | ||
|
|
184f6d46dc | ||
|
|
68788905fe | ||
|
|
fc46216a3f | ||
|
|
563143645e | ||
|
|
891ccb901b | ||
|
|
928a866fe7 | ||
|
|
ea25b5b46f | ||
|
|
1de10e6129 | ||
|
|
aaf9c2e8d2 | ||
|
|
b8196b5730 | ||
|
|
0a83e8beb4 |
4
.github/workflows/linux-aarch64.yml
vendored
4
.github/workflows/linux-aarch64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-aarch64-b895.91.tar.gz
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-aarch64-b1034.51.tar.gz
|
||||
|
||||
# appimagetool
|
||||
- run: sudo apt install libfuse2
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
with:
|
||||
distribution: 'jdkfile'
|
||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||
java-version: '21.0.6'
|
||||
java-version: '21.0.7'
|
||||
architecture: aarch64
|
||||
|
||||
- uses: actions/cache@v4
|
||||
|
||||
4
.github/workflows/linux-x86-64.yml
vendored
4
.github/workflows/linux-x86-64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b895.91.tar.gz
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-x64-b1034.51.tar.gz
|
||||
|
||||
# appimagetool
|
||||
- run: sudo apt install libfuse2
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
with:
|
||||
distribution: 'jdkfile'
|
||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||
java-version: '21.0.6'
|
||||
java-version: '21.0.7'
|
||||
architecture: x64
|
||||
|
||||
- uses: actions/cache@v4
|
||||
|
||||
4
.github/workflows/osx-aarch64.yml
vendored
4
.github/workflows/osx-aarch64.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b895.91.tar.gz
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-aarch64-b1034.51.tar.gz
|
||||
|
||||
# install jdk
|
||||
- name: Installing Java
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
with:
|
||||
distribution: 'jdkfile'
|
||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||
java-version: '21.0.6'
|
||||
java-version: '21.0.7'
|
||||
architecture: aarch64
|
||||
|
||||
- uses: actions/cache@v4
|
||||
|
||||
4
.github/workflows/osx-x86-64.yml
vendored
4
.github/workflows/osx-x86-64.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b895.91.tar.gz
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-x64-b1034.51.tar.gz
|
||||
|
||||
# install jdk
|
||||
- name: Installing Java
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
with:
|
||||
distribution: 'jdkfile'
|
||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||
java-version: '21.0.6'
|
||||
java-version: '21.0.7'
|
||||
architecture: x64
|
||||
|
||||
|
||||
|
||||
4
.github/workflows/windows-x86-64.yml
vendored
4
.github/workflows/windows-x86-64.yml
vendored
@@ -21,9 +21,9 @@ jobs:
|
||||
|
||||
- name: Installing Java
|
||||
run: |
|
||||
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-windows-x64-b895.91.zip
|
||||
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.6-windows-x64-b895.91" >> $env:GITHUB_ENV
|
||||
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.7-windows-x64-b1034.51" >> $env:GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
|
||||
2
.github/workflows/winget.yml
vendored
2
.github/workflows/winget.yml
vendored
@@ -10,5 +10,5 @@ jobs:
|
||||
if: github.repository == 'TermoraDev/termora'
|
||||
with:
|
||||
identifier: TermoraDev.Termora
|
||||
installers-regex: 'x86-64\.exe$' # Only x86-64.exe files
|
||||
installers-regex: '\.exe$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
126
build.gradle.kts
126
build.gradle.kts
@@ -14,13 +14,14 @@ plugins {
|
||||
java
|
||||
idea
|
||||
application
|
||||
`maven-publish`
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlinx.serialization)
|
||||
}
|
||||
|
||||
|
||||
group = "app.termora"
|
||||
version = "1.0.14"
|
||||
version = "1.0.17"
|
||||
|
||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||
@@ -56,67 +57,67 @@ dependencies {
|
||||
|
||||
// implementation(platform(libs.koin.bom))
|
||||
// implementation(libs.koin.core)
|
||||
implementation(libs.slf4j.api)
|
||||
implementation(libs.pty4j)
|
||||
implementation(libs.slf4j.tinylog)
|
||||
implementation(libs.tinylog.impl)
|
||||
implementation(libs.commons.codec)
|
||||
implementation(libs.commons.io)
|
||||
implementation(libs.commons.lang3)
|
||||
implementation(libs.commons.csv)
|
||||
implementation(libs.commons.net)
|
||||
implementation(libs.commons.text)
|
||||
implementation(libs.commons.compress)
|
||||
implementation(libs.commons.vfs2) { exclude(group = "*", module = "*") }
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
api(libs.slf4j.api)
|
||||
api(libs.pty4j)
|
||||
api(libs.slf4j.tinylog)
|
||||
api(libs.tinylog.impl)
|
||||
api(libs.commons.codec)
|
||||
api(libs.commons.io)
|
||||
api(libs.commons.lang3)
|
||||
api(libs.commons.csv)
|
||||
api(libs.commons.net)
|
||||
api(libs.commons.text)
|
||||
api(libs.commons.compress)
|
||||
api(libs.commons.vfs2) { exclude(group = "*", module = "*") }
|
||||
api(libs.kotlinx.coroutines.swing)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
|
||||
implementation(libs.flatlaf) {
|
||||
api(libs.flatlaf) {
|
||||
artifact {
|
||||
if (useNoNativesFlatLaf) {
|
||||
classifier = "no-natives"
|
||||
}
|
||||
}
|
||||
}
|
||||
implementation(libs.flatlaf.extras) {
|
||||
api(libs.flatlaf.extras) {
|
||||
if (useNoNativesFlatLaf) {
|
||||
exclude(group = "com.formdev", module = "flatlaf")
|
||||
}
|
||||
}
|
||||
implementation(libs.flatlaf.swingx) {
|
||||
api(libs.flatlaf.swingx) {
|
||||
if (useNoNativesFlatLaf) {
|
||||
exclude(group = "com.formdev", module = "flatlaf")
|
||||
}
|
||||
}
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.swingx)
|
||||
implementation(libs.jgoodies.forms)
|
||||
implementation(libs.jna)
|
||||
implementation(libs.jna.platform)
|
||||
implementation(libs.versioncompare)
|
||||
implementation(libs.oshi.core)
|
||||
implementation(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
||||
implementation(libs.jfa) { exclude(group = "*", module = "*") }
|
||||
implementation(libs.jbr.api)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.okhttp.logging)
|
||||
implementation(libs.sshd.core)
|
||||
implementation(libs.commonmark)
|
||||
implementation(libs.jgit)
|
||||
implementation(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||
implementation(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||
implementation(libs.eddsa)
|
||||
implementation(libs.jnafilechooser)
|
||||
implementation(libs.xodus.vfs)
|
||||
implementation(libs.xodus.openAPI)
|
||||
implementation(libs.xodus.environment)
|
||||
implementation(libs.bip39)
|
||||
implementation(libs.colorpicker)
|
||||
implementation(libs.mixpanel)
|
||||
implementation(libs.jSerialComm)
|
||||
implementation(libs.ini4j)
|
||||
implementation(libs.restart4j)
|
||||
api(libs.kotlinx.serialization.json)
|
||||
api(libs.swingx)
|
||||
api(libs.jgoodies.forms)
|
||||
api(libs.jna)
|
||||
api(libs.jna.platform)
|
||||
api(libs.versioncompare)
|
||||
api(libs.oshi.core)
|
||||
api(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
||||
api(libs.jfa) { exclude(group = "*", module = "*") }
|
||||
api(libs.jbr.api)
|
||||
api(libs.okhttp)
|
||||
api(libs.okhttp.logging)
|
||||
api(libs.sshd.core)
|
||||
api(libs.commonmark)
|
||||
api(libs.jgit)
|
||||
api(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||
api(libs.eddsa)
|
||||
api(libs.jnafilechooser)
|
||||
api(libs.xodus.vfs)
|
||||
api(libs.xodus.openAPI)
|
||||
api(libs.xodus.environment)
|
||||
api(libs.bip39)
|
||||
api(libs.colorpicker)
|
||||
api(libs.mixpanel)
|
||||
api(libs.jSerialComm)
|
||||
api(libs.ini4j)
|
||||
api(libs.restart4j)
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -133,6 +134,7 @@ application {
|
||||
args.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||
args.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||
args.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||
args.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||
args.add("-Dsun.java2d.metal=true")
|
||||
args.add("-Dapple.awt.application.appearance=system")
|
||||
}
|
||||
@@ -147,6 +149,37 @@ application {
|
||||
mainClass = "app.termora.MainKt"
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("mavenJava") {
|
||||
from(components["java"])
|
||||
pom {
|
||||
name = project.name
|
||||
description = "Termora is a terminal emulator and SSH client for Windows, macOS and Linux"
|
||||
url = "https://github.com/TermoraDev/termora"
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name = "AGPL-3.0"
|
||||
url = "https://opensource.org/license/agpl-v3"
|
||||
}
|
||||
}
|
||||
|
||||
developers {
|
||||
developer {
|
||||
name = "hstyi"
|
||||
url = "https://github.com/hstyi"
|
||||
}
|
||||
}
|
||||
|
||||
scm {
|
||||
url = "https://github.com/TermoraDev/termora"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -356,6 +389,7 @@ tasks.register<Exec>("jpackage") {
|
||||
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||
options.add("-Dapple.awt.application.appearance=system")
|
||||
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||
options.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
if (os.isLinux) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
kotlin = "2.1.20"
|
||||
kotlin = "2.1.21"
|
||||
slf4j = "2.0.17"
|
||||
pty4j = "0.13.4"
|
||||
pty4j = "0.13.6"
|
||||
tinylog = "2.7.0"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
flatlaf = "3.6"
|
||||
@@ -35,7 +35,7 @@ bip39 = "1.0.9"
|
||||
colorpicker = "2.0.1"
|
||||
rhino = "1.8.0"
|
||||
delight-rhino-sandbox = "0.0.17"
|
||||
testcontainers = "1.21.0"
|
||||
testcontainers = "1.21.1"
|
||||
mixpanel = "1.5.3"
|
||||
jSerialComm = "2.11.0"
|
||||
ini4j = "0.5.5-2"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0"
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||
}
|
||||
rootProject.name = "termora"
|
||||
|
||||
|
||||
@@ -355,7 +355,27 @@ class ApplicationRunner {
|
||||
.event(getAnalyticsUserID(), "launch", properties)
|
||||
val delivery = ClientDelivery()
|
||||
delivery.addMessage(message)
|
||||
MixpanelAPI().deliver(delivery, true)
|
||||
val endpoints = listOf(
|
||||
"https://api-eu.mixpanel.com",
|
||||
"https://api-in.mixpanel.com",
|
||||
"https://api.mixpanel.com",
|
||||
"http://api.mixpanel.com",
|
||||
)
|
||||
for (endpoint in endpoints) {
|
||||
try {
|
||||
MixpanelAPI(
|
||||
"$endpoint/track",
|
||||
"$endpoint/engage",
|
||||
"$endpoint/groups"
|
||||
).deliver(delivery, true)
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
|
||||
import app.termora.actions.MultipleAction
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import java.awt.Component
|
||||
@@ -20,6 +19,7 @@ import kotlin.math.min
|
||||
|
||||
class CustomizeToolBarDialog(
|
||||
owner: Window,
|
||||
private val windowScope: WindowScope,
|
||||
private val toolbar: TermoraToolBar
|
||||
) : DialogWrapper(owner) {
|
||||
|
||||
@@ -147,9 +147,7 @@ class CustomizeToolBarDialog(
|
||||
leftList.model.removeAllElements()
|
||||
rightList.model.removeAllElements()
|
||||
for (action in toolbar.getAllActions()) {
|
||||
actionManager.getAction(action.id)?.let {
|
||||
rightList.model.addElement(ActionHolder(action.id, it))
|
||||
}
|
||||
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,14 +257,11 @@ class CustomizeToolBarDialog(
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
removeWindowListener(this)
|
||||
|
||||
|
||||
for (action in toolbar.getActions()) {
|
||||
if (action.visible) {
|
||||
actionManager.getAction(action.id)
|
||||
?.let { rightList.model.addElement(ActionHolder(action.id, it)) }
|
||||
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
||||
} else {
|
||||
actionManager.getAction(action.id)
|
||||
?.let { leftList.model.addElement(ActionHolder(action.id, it)) }
|
||||
getActionHolder(action.id)?.let { leftList.model.addElement(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,6 +269,17 @@ class CustomizeToolBarDialog(
|
||||
})
|
||||
}
|
||||
|
||||
private fun getActionHolder(actionId: String): ActionHolder? {
|
||||
var action = actionManager.getAction(actionId)
|
||||
if (action == null) {
|
||||
if (actionId == MultipleAction.MULTIPLE) {
|
||||
action = MultipleAction.getInstance(windowScope)
|
||||
}
|
||||
}
|
||||
if (action == null) return null
|
||||
return ActionHolder(actionId, action)
|
||||
}
|
||||
|
||||
private fun resetMoveButtons() {
|
||||
val indices = rightList.selectedIndices
|
||||
if (indices.isEmpty()) {
|
||||
|
||||
@@ -523,6 +523,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
*/
|
||||
var beep by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* 超链接
|
||||
*/
|
||||
var hyperlink by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* 光标闪烁
|
||||
*/
|
||||
@@ -643,6 +648,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
*/
|
||||
var backgroundRunning by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 标签关闭前确认
|
||||
*/
|
||||
var confirmTabClose by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 背景图片的地址
|
||||
*/
|
||||
|
||||
@@ -25,6 +25,7 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
||||
isModal = true
|
||||
title = I18n.getString("termora.new-host.title")
|
||||
setLocationRelativeTo(null)
|
||||
pane.setSelectedIndex(0)
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.commons.lang3.RegExUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.PageantConnector
|
||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector
|
||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
|
||||
import java.awt.*
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.event.*
|
||||
import java.nio.charset.Charset
|
||||
import javax.swing.*
|
||||
@@ -221,7 +223,24 @@ open class HostOptionsPane : OptionsPane() {
|
||||
val nameTextField = OutlineTextField(128)
|
||||
val protocolTypeComboBox = FlatComboBox<Protocol>()
|
||||
val usernameTextField = OutlineTextField(128)
|
||||
val hostTextField = OutlineTextField(255)
|
||||
val hostTextField = object : OutlineTextField(255) {
|
||||
override fun paste() {
|
||||
if (!toolkit.systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
return
|
||||
}
|
||||
|
||||
var text = toolkit.systemClipboard.getData(DataFlavor.stringFlavor)?.toString() ?: return
|
||||
if (text.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
// 移除所有不可见字符
|
||||
text = RegExUtils.replaceAll(text, "[\\p{C}\\s]", StringUtils.EMPTY)
|
||||
|
||||
// text
|
||||
replaceSelection(text)
|
||||
}
|
||||
}
|
||||
private val passwordPanel = JPanel(BorderLayout())
|
||||
private val chooseKeyBtn = JButton(Icons.greyKey)
|
||||
val passwordTextField = OutlinePasswordField(255)
|
||||
|
||||
@@ -135,10 +135,12 @@ class NewHostTree : SimpleTree() {
|
||||
// double click
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (getPathForLocation(e.x, e.y) == null) return
|
||||
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
|
||||
if (lastNode.host.protocol != Protocol.Folder) {
|
||||
val path = tree.getClosestPathForLocation(e.x, e.y) ?: return
|
||||
val bounds = tree.getRowBounds(tree.getRowForPath(path)) ?: return
|
||||
if ((e.y >= bounds.y && e.y < (bounds.y + bounds.height)).not()) return
|
||||
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, lastNode.host, e))
|
||||
}
|
||||
}
|
||||
@@ -854,7 +856,8 @@ class NewHostTree : SimpleTree() {
|
||||
val port = map["Port"]?.toIntOrNull() ?: 22
|
||||
val username = map["Username"] ?: StringUtils.EMPTY
|
||||
val protocol = map["Protocol"] ?: "SSH"
|
||||
if (!StringUtils.equalsIgnoreCase(protocol, "SSH")) continue
|
||||
// 仅支持 SSH、RDP 协议
|
||||
if (StringUtils.equalsAnyIgnoreCase(protocol, "SSH", "RDP").not()) continue
|
||||
if (StringUtils.isAllBlank(hostname, label)) continue
|
||||
|
||||
var p: HostTreeNode? = null
|
||||
@@ -889,7 +892,7 @@ class NewHostTree : SimpleTree() {
|
||||
host = hostname,
|
||||
port = port,
|
||||
username = username,
|
||||
protocol = Protocol.SSH,
|
||||
protocol = runCatching { Protocol.valueOf(protocol) }.getOrNull() ?: Protocol.SSH,
|
||||
parentId = p?.host?.id ?: StringUtils.EMPTY,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ class NewHostTreeDialog(
|
||||
|
||||
|
||||
init()
|
||||
setLocationRelativeTo(null)
|
||||
setLocationRelativeTo(owner)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ open class OptionsPane : JPanel(BorderLayout()) {
|
||||
}
|
||||
private val cardLayout = CardLayout()
|
||||
private val contentPanel = JPanel(cardLayout)
|
||||
private val loadedComponents = mutableMapOf<String, JComponent>()
|
||||
|
||||
init {
|
||||
initView()
|
||||
@@ -103,16 +104,15 @@ open class OptionsPane : JPanel(BorderLayout()) {
|
||||
throw UnsupportedOperationException("Title already exists")
|
||||
}
|
||||
}
|
||||
contentPanel.add(option.getJComponent(), option.getTitle())
|
||||
tabListModel.addElement(option)
|
||||
|
||||
if (tabList.selectedIndex < 0) {
|
||||
tabList.selectedIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
fun removeOption(option: Option) {
|
||||
contentPanel.remove(option.getJComponent())
|
||||
val title = option.getTitle()
|
||||
loadedComponents[title]?.let {
|
||||
contentPanel.remove(it)
|
||||
loadedComponents.remove(title)
|
||||
}
|
||||
tabListModel.removeElement(option)
|
||||
}
|
||||
|
||||
@@ -123,7 +123,17 @@ open class OptionsPane : JPanel(BorderLayout()) {
|
||||
private fun initEvents() {
|
||||
tabList.addListSelectionListener {
|
||||
if (tabList.selectedIndex >= 0) {
|
||||
cardLayout.show(contentPanel, tabListModel.get(tabList.selectedIndex).getTitle())
|
||||
val option = tabListModel.get(tabList.selectedIndex)
|
||||
val title = option.getTitle()
|
||||
|
||||
if (!loadedComponents.containsKey(title)) {
|
||||
val component = option.getJComponent()
|
||||
loadedComponents[title] = component
|
||||
contentPanel.add(component, title)
|
||||
SwingUtilities.updateComponentTreeUI(component)
|
||||
}
|
||||
|
||||
cardLayout.show(contentPanel, title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,10 +223,9 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
}
|
||||
}
|
||||
|
||||
override fun willBeClose(): Boolean {
|
||||
override fun beforeClose() {
|
||||
// 保存窗口状态
|
||||
terminalPanel.storeVisualWindows(host.id)
|
||||
return super.willBeClose()
|
||||
}
|
||||
|
||||
private inner class MySessionListener : SessionListener, Disposable {
|
||||
|
||||
@@ -3,8 +3,6 @@ package app.termora
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
@@ -20,8 +18,10 @@ class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
||||
title = I18n.getString("termora.setting")
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
init()
|
||||
val index = properties.getString("Settings-SelectedOption")?.toIntOrNull() ?: 0
|
||||
optionsPane.setSelectedIndex(index)
|
||||
|
||||
init()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
@@ -31,14 +31,6 @@ class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
||||
properties.putString("Settings-SelectedOption", optionsPane.getSelectedIndex().toString())
|
||||
}
|
||||
})
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowActivated(e: WindowEvent) {
|
||||
removeWindowListener(this)
|
||||
val index = properties.getString("Settings-SelectedOption")?.toIntOrNull() ?: return
|
||||
optionsPane.setSelectedIndex(index)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
|
||||
@@ -33,6 +33,10 @@ import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import com.jthemedetecor.OsThemeDetector
|
||||
import com.sun.jna.LastErrorException
|
||||
import com.sun.jna.Native
|
||||
import com.sun.jna.platform.win32.Shell32
|
||||
import com.sun.jna.platform.win32.ShlObj
|
||||
import com.sun.jna.platform.win32.WinDef
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
@@ -132,6 +136,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
val themeComboBox = FlatComboBox<String>()
|
||||
val languageComboBox = FlatComboBox<String>()
|
||||
val backgroundComBoBox = YesOrNoComboBox()
|
||||
val confirmTabCloseComBoBox = YesOrNoComboBox()
|
||||
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
||||
val preferredThemeBtn = JButton(Icons.settings)
|
||||
val opacitySpinner = NumberSpinner(100, 0, 100)
|
||||
@@ -180,6 +185,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
followSystemCheckBox.isSelected = appearance.followSystem
|
||||
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
|
||||
backgroundComBoBox.selectedItem = appearance.backgroundRunning
|
||||
confirmTabCloseComBoBox.selectedItem = appearance.confirmTabClose
|
||||
|
||||
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
||||
themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
|
||||
@@ -230,6 +236,13 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
confirmTabCloseComBoBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
appearance.confirmTabClose = confirmTabCloseComBoBox.selectedItem as Boolean
|
||||
}
|
||||
}
|
||||
|
||||
followSystemCheckBox.addActionListener {
|
||||
appearance.followSystem = followSystemCheckBox.isSelected
|
||||
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
||||
@@ -368,7 +381,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun getFormPanel(): JPanel {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
val box = FlatToolBar()
|
||||
box.add(followSystemCheckBox)
|
||||
@@ -401,7 +414,13 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.add(opacitySpinner).xy(3, rows).apply { rows += step }
|
||||
|
||||
builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows)
|
||||
.add(backgroundComBoBox).xy(3, rows)
|
||||
.add(backgroundComBoBox).xy(3, rows).apply { rows += step }
|
||||
|
||||
val confirmTabCloseBox = Box.createHorizontalBox()
|
||||
confirmTabCloseBox.add(JLabel("${I18n.getString("termora.settings.appearance.confirm-tab-close")}:"))
|
||||
confirmTabCloseBox.add(Box.createHorizontalStrut(8))
|
||||
confirmTabCloseBox.add(confirmTabCloseComBoBox)
|
||||
builder.add(confirmTabCloseBox).xyw(1, rows, 3).apply { rows += step }
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
@@ -422,6 +441,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private val selectCopyComboBox = YesOrNoComboBox()
|
||||
private val autoCloseTabComboBox = YesOrNoComboBox()
|
||||
private val floatingToolbarComboBox = YesOrNoComboBox()
|
||||
private val hyperlinkComboBox = YesOrNoComboBox()
|
||||
|
||||
init {
|
||||
initView()
|
||||
@@ -499,6 +519,13 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
hyperlinkComboBox.addItemListener { e ->
|
||||
if (e.stateChange == ItemEvent.SELECTED) {
|
||||
terminalSetting.hyperlink = hyperlinkComboBox.selectedItem as Boolean
|
||||
TerminalPanelFactory.getInstance().repaintAll()
|
||||
}
|
||||
}
|
||||
|
||||
cursorBlinkComboBox.addItemListener { e ->
|
||||
if (e.stateChange == ItemEvent.SELECTED) {
|
||||
terminalSetting.cursorBlink = cursorBlinkComboBox.selectedItem as Boolean
|
||||
@@ -577,20 +604,33 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
shellComboBox.selectedItem = terminalSetting.localShell
|
||||
|
||||
val fonts = linkedSetOf("JetBrains Mono", "Source Code Pro", "Monospaced")
|
||||
FontUtils.getAllFonts().forEach {
|
||||
if (!fonts.contains(it.family)) {
|
||||
fonts.addLast(it.family)
|
||||
}
|
||||
}
|
||||
fontComboBox.addItem(terminalSetting.font)
|
||||
var fontsLoaded = false
|
||||
|
||||
for (font in fonts) {
|
||||
fontComboBox.addItem(font)
|
||||
}
|
||||
fontComboBox.addPopupMenuListener(object : PopupMenuListener {
|
||||
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||
if (!fontsLoaded) {
|
||||
val selectedItem = fontComboBox.selectedItem
|
||||
fontComboBox.removeAllItems();
|
||||
fontComboBox.addItem("JetBrains Mono")
|
||||
fontComboBox.addItem("Source Code Pro")
|
||||
fontComboBox.addItem("Monospaced")
|
||||
FontUtils.getAvailableFontFamilyNames().forEach {
|
||||
fontComboBox.addItem(it)
|
||||
}
|
||||
fontComboBox.selectedItem = selectedItem
|
||||
fontsLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {}
|
||||
override fun popupMenuCanceled(e: PopupMenuEvent) {}
|
||||
})
|
||||
|
||||
fontComboBox.selectedItem = terminalSetting.font
|
||||
debugComboBox.selectedItem = terminalSetting.debug
|
||||
beepComboBox.selectedItem = terminalSetting.beep
|
||||
hyperlinkComboBox.selectedItem = terminalSetting.hyperlink
|
||||
cursorBlinkComboBox.selectedItem = terminalSetting.cursorBlink
|
||||
cursorStyleComboBox.selectedItem = terminalSetting.cursor
|
||||
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
|
||||
@@ -613,7 +653,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
val beepBtn = JButton(Icons.run)
|
||||
@@ -636,6 +676,8 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.add("${I18n.getString("termora.settings.terminal.beep")}:").xy(1, rows)
|
||||
.add(beepComboBox).xy(3, rows)
|
||||
.add(beepBtn).xy(5, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.hyperlink")}:").xy(1, rows)
|
||||
.add(hyperlinkComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
||||
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
||||
@@ -1476,6 +1518,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private val sftpCommandField = OutlineTextField(255)
|
||||
private val defaultDirectoryField = OutlineTextField(255)
|
||||
private val browseDirectoryBtn = JButton(Icons.folder)
|
||||
private val browseEditCommandBtn = JButton(Icons.folder)
|
||||
private val pinTabComboBox = YesOrNoComboBox()
|
||||
private val preserveModificationTimeComboBox = YesOrNoComboBox()
|
||||
private val sftp get() = database.sftp
|
||||
@@ -1547,6 +1590,41 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
browseEditCommandBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val chooser = FileChooser()
|
||||
chooser.allowsMultiSelection = false
|
||||
chooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||
|
||||
if (SystemInfo.isMacOS) {
|
||||
chooser.defaultDirectory = "/Applications"
|
||||
} else {
|
||||
if (SystemInfo.isWindows) {
|
||||
val pszPath = CharArray(WinDef.MAX_PATH)
|
||||
Shell32.INSTANCE.SHGetFolderPath(
|
||||
null,
|
||||
ShlObj.CSIDL_DESKTOPDIRECTORY, null, ShlObj.SHGFP_TYPE_CURRENT,
|
||||
pszPath
|
||||
)
|
||||
chooser.defaultDirectory = Native.toString(pszPath)
|
||||
} else {
|
||||
chooser.defaultDirectory = SystemUtils.USER_HOME
|
||||
}
|
||||
}
|
||||
|
||||
chooser.showOpenDialog(owner).thenAccept { files ->
|
||||
if (files.isNotEmpty()) {
|
||||
val file = files.first()
|
||||
if (SystemInfo.isMacOS) {
|
||||
editCommandField.text = "open -a ${file.absolutePath} {0}"
|
||||
} else {
|
||||
editCommandField.text = "${file.absolutePath} {0}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1563,6 +1641,8 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
sftpCommandField.placeholderText = "sftp"
|
||||
}
|
||||
|
||||
editCommandField.trailingComponent = browseEditCommandBtn
|
||||
|
||||
defaultDirectoryField.placeholderText = SystemUtils.USER_HOME
|
||||
defaultDirectoryField.trailingComponent = browseDirectoryBtn
|
||||
|
||||
|
||||
@@ -265,9 +265,14 @@ object SshClients {
|
||||
} catch (e: Exception) {
|
||||
if (e !is SshException || e.disconnectCode != SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e
|
||||
val owner = client.properties["owner"] as Window? ?: throw e
|
||||
val authentication = ask(host, owner) ?: throw e
|
||||
if (authentication.type == AuthenticationType.No) throw e
|
||||
return doOpenSession(host.copy(authentication = authentication), client)
|
||||
val askUserInfo = ask(host, entry, owner) ?: throw e
|
||||
if (askUserInfo.authentication.type == AuthenticationType.No) throw e
|
||||
return doOpenSession(
|
||||
host.copy(
|
||||
authentication = askUserInfo.authentication,
|
||||
username = askUserInfo.username
|
||||
), client
|
||||
)
|
||||
}
|
||||
|
||||
session.setAttribute(HOST_KEY, host)
|
||||
@@ -414,20 +419,32 @@ object SshClients {
|
||||
return sshClient
|
||||
}
|
||||
|
||||
private fun ask(host: Host, owner: Window): Authentication? {
|
||||
val ref = AtomicReference<Authentication>(null)
|
||||
|
||||
private data class AskUserInfo(val username: String, val authentication: Authentication)
|
||||
|
||||
private fun ask(host: Host, entry: HostConfigEntry, owner: Window): AskUserInfo? {
|
||||
val ref = AtomicReference<AskUserInfo>(null)
|
||||
|
||||
SwingUtilities.invokeAndWait {
|
||||
val dialog = RequestAuthenticationDialog(owner, host)
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
val authentication = dialog.getAuthentication().apply { ref.set(this) }
|
||||
val authentication = dialog.getAuthentication()
|
||||
ref.set(AskUserInfo(dialog.getUsername(), authentication))
|
||||
|
||||
// save
|
||||
if (dialog.isRemembered()) {
|
||||
hostManager.addHost(
|
||||
host.copy(
|
||||
authentication = authentication,
|
||||
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||
// fix https://github.com/TermoraDev/termora/issues/609
|
||||
val hostId = entry.getProperty("Host", host.id)
|
||||
val h = hostManager.getHost(hostId)
|
||||
if (h != null) {
|
||||
hostManager.addHost(
|
||||
h.copy(
|
||||
authentication = authentication,
|
||||
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return ref.get()
|
||||
|
||||
@@ -4,6 +4,7 @@ import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.actions.MultipleAction
|
||||
import app.termora.highlight.KeywordHighlightPaintListener
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.terminal.Terminal
|
||||
import app.termora.terminal.panel.TerminalHyperlinkPaintListener
|
||||
@@ -40,6 +41,10 @@ class TerminalPanelFactory : Disposable {
|
||||
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
||||
val writer = MyTerminalWriter(ptyConnector)
|
||||
val terminalPanel = TerminalPanel(terminal, writer)
|
||||
|
||||
// processDeviceStatusReport
|
||||
terminal.getTerminalModel().setData(DataKey.TerminalWriter, writer)
|
||||
|
||||
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
||||
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
||||
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Database.Appearance
|
||||
import app.termora.actions.DataProvider
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.swing.Icon
|
||||
@@ -44,10 +45,15 @@ interface TerminalTab : Disposable, DataProvider {
|
||||
fun canClose(): Boolean = true
|
||||
|
||||
/**
|
||||
* 返回 true 表示可以关闭
|
||||
* 返回 true 表示可以关闭,只有当 [Appearance.confirmTabClose] 为 false 时才会调用
|
||||
*/
|
||||
fun willBeClose(): Boolean = true
|
||||
|
||||
/**
|
||||
* 即将关闭,已经无法挽回
|
||||
*/
|
||||
fun beforeClose() {}
|
||||
|
||||
/**
|
||||
* 是否可以克隆
|
||||
*/
|
||||
|
||||
@@ -32,6 +32,7 @@ class TerminalTabbed(
|
||||
private val actionManager = ActionManager.getInstance()
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private val titleProperty = UUID.randomUUID().toSimpleString()
|
||||
private val appearance get() = Database.getDatabase().appearance
|
||||
private val iconListener = PropertyChangeListener { e ->
|
||||
val source = e.source
|
||||
if (e.propertyName == "icon" && source is TerminalTab) {
|
||||
@@ -153,8 +154,29 @@ class TerminalTabbed(
|
||||
if (tabbedPane.isTabClosable(index)) {
|
||||
val tab = tabs[index]
|
||||
|
||||
// 询问是否可以关闭
|
||||
if (disposable) {
|
||||
if (!tab.willBeClose()) {
|
||||
// 如果开启了关闭确认,那么直接询问用户
|
||||
if (appearance.confirmTabClose) {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
windowScope.window,
|
||||
I18n.getString("termora.tabbed.tab.close-prompt"),
|
||||
messageType = JOptionPane.QUESTION_MESSAGE,
|
||||
optionType = JOptionPane.OK_CANCEL_OPTION
|
||||
) != JOptionPane.OK_OPTION
|
||||
) {
|
||||
return
|
||||
}
|
||||
} else if (!tab.willBeClose()) { // 如果没有开启则询问用户
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 通知即将关闭
|
||||
if (disposable) {
|
||||
try {
|
||||
tab.beforeClose()
|
||||
} catch (_: Exception) {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -400,12 +422,11 @@ class TerminalTabbed(
|
||||
private fun showContextMenu(event: MouseEvent) {
|
||||
val popupMenu = FlatPopupMenu()
|
||||
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
|
||||
val dialog = CustomizeToolBarDialog(
|
||||
SwingUtilities.getWindowAncestor(this@TerminalTabbed),
|
||||
termoraToolBar
|
||||
)
|
||||
val owner = SwingUtilities.getWindowAncestor(this@TerminalTabbed)
|
||||
val dialog = CustomizeToolBarDialog(owner, windowScope, termoraToolBar)
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
if (dialog.open()) {
|
||||
termoraToolBar.rebuild()
|
||||
TermoraToolBar.rebuild()
|
||||
}
|
||||
}
|
||||
popupMenu.show(event.component, event.x, event.y)
|
||||
|
||||
@@ -38,7 +38,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
private val id = UUID.randomUUID().toString()
|
||||
private val windowScope = ApplicationScope.forWindowScope(this)
|
||||
private val tabbedPane = MyTabbedPane()
|
||||
private val toolbar = TermoraToolBar(windowScope, this, tabbedPane)
|
||||
private val toolbar = TermoraToolBar(windowScope, this)
|
||||
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private val welcomePanel = WelcomePanel(windowScope)
|
||||
@@ -260,6 +260,10 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
|
||||
|
||||
private class GlassPane : JComponent() {
|
||||
init {
|
||||
isFocusable = false
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
|
||||
val g2d = g as Graphics2D
|
||||
@@ -271,5 +275,9 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER)
|
||||
}
|
||||
|
||||
override fun contains(x: Int, y: Int): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ class TermoraFrameManager : Disposable {
|
||||
|
||||
fun createWindow(): TermoraFrame {
|
||||
val frame = TermoraFrame().apply { registerCloseCallback(this) }
|
||||
frame.title = if (SystemInfo.isLinux) null else Application.getName()
|
||||
frame.title = Application.getName()
|
||||
frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||
|
||||
val rectangle = getFrameRectangle() ?: FrameRectangle(-1, -1, 1280, 800, 0)
|
||||
|
||||
@@ -5,7 +5,6 @@ import app.termora.actions.*
|
||||
import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@@ -26,10 +25,21 @@ data class ToolBarAction(
|
||||
class TermoraToolBar(
|
||||
private val windowScope: WindowScope,
|
||||
private val frame: TermoraFrame,
|
||||
private val tabbedPane: FlatTabbedPane
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun rebuild() {
|
||||
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
||||
val toolbars = SwingUtils.getDescendantsOfClass(MyToolBar::class.java, frame)
|
||||
for (toolbar in toolbars) {
|
||||
toolbar.rebuild()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val properties by lazy { Database.getDatabase().properties }
|
||||
private val toolbar by lazy { MyToolBar().apply { rebuild(this) } }
|
||||
private val toolbar by lazy { MyToolBar().apply { rebuild() } }
|
||||
|
||||
|
||||
fun getJToolBar(): JToolBar {
|
||||
@@ -87,63 +97,6 @@ class TermoraToolBar(
|
||||
return storageActions
|
||||
}
|
||||
|
||||
fun rebuild() {
|
||||
rebuild(this.toolbar)
|
||||
}
|
||||
|
||||
private fun rebuild(toolbar: JToolBar) {
|
||||
val actionManager = ActionManager.getInstance()
|
||||
val actionContainerFactory = ActionContainerFactory(actionManager)
|
||||
|
||||
toolbar.removeAll()
|
||||
|
||||
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
|
||||
}
|
||||
|
||||
override fun isEnabled(): Boolean {
|
||||
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
|
||||
}
|
||||
}))
|
||||
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
|
||||
if (SystemInfo.isLinux || SystemInfo.isWindows) {
|
||||
toolbar.add(Box.createHorizontalStrut(16))
|
||||
}
|
||||
|
||||
|
||||
// update btn
|
||||
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
||||
updateBtn.isVisible = updateBtn.isEnabled
|
||||
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
|
||||
toolbar.add(updateBtn)
|
||||
|
||||
|
||||
// 获取显示的Action,如果不是 false 那么就是显示出来
|
||||
for (action in getActions()) {
|
||||
if (action.visible) {
|
||||
val ac = actionManager.getAction(action.id)
|
||||
if (ac == null) {
|
||||
if (action.id == MultipleAction.MULTIPLE) {
|
||||
toolbar.add(actionContainerFactory.createButton(MultipleAction.getInstance(windowScope)))
|
||||
}
|
||||
} else {
|
||||
toolbar.add(actionContainerFactory.createButton(ac))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (toolbar is MyToolBar) {
|
||||
toolbar.adjust()
|
||||
}
|
||||
|
||||
toolbar.revalidate()
|
||||
toolbar.repaint()
|
||||
}
|
||||
|
||||
private inner class MyToolBar : JToolBar() {
|
||||
init {
|
||||
// 监听窗口大小变动,然后修改边距避开控制按钮
|
||||
@@ -179,5 +132,60 @@ class TermoraToolBar(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun rebuild() {
|
||||
val toolbar: JToolBar = this
|
||||
val actionManager = ActionManager.getInstance()
|
||||
val actionContainerFactory = ActionContainerFactory(actionManager)
|
||||
|
||||
toolbar.removeAll()
|
||||
|
||||
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
|
||||
}
|
||||
|
||||
override fun isEnabled(): Boolean {
|
||||
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
|
||||
}
|
||||
}))
|
||||
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
|
||||
if (SystemInfo.isLinux || SystemInfo.isWindows) {
|
||||
toolbar.add(Box.createHorizontalStrut(16))
|
||||
}
|
||||
|
||||
|
||||
// update btn
|
||||
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
||||
updateBtn.isVisible = updateBtn.isEnabled
|
||||
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
|
||||
toolbar.add(updateBtn)
|
||||
|
||||
|
||||
// 获取显示的Action,如果不是 false 那么就是显示出来
|
||||
for (action in getActions()) {
|
||||
if (action.visible) {
|
||||
val ac = actionManager.getAction(action.id)
|
||||
if (ac == null) {
|
||||
if (action.id == MultipleAction.MULTIPLE) {
|
||||
toolbar.add(actionContainerFactory.createButton(MultipleAction.getInstance(windowScope)))
|
||||
}
|
||||
} else {
|
||||
toolbar.add(actionContainerFactory.createButton(ac))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (toolbar is MyToolBar) {
|
||||
toolbar.adjust()
|
||||
}
|
||||
|
||||
toolbar.revalidate()
|
||||
toolbar.repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -645,9 +645,13 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
||||
return
|
||||
}
|
||||
|
||||
ohKeyPair = ohKeyPair.copy(
|
||||
name = nameTextField.text,
|
||||
remark = remarkTextField.text,
|
||||
)
|
||||
|
||||
if (ohKeyPair.remark.isEmpty()) {
|
||||
ohKeyPair = ohKeyPair.copy(
|
||||
name = nameTextField.text,
|
||||
remark = "Import on " + DateFormatUtils.format(Date(), I18n.getString("termora.date-format"))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class MacroDialog(owner: Window) : DialogWrapper(owner) {
|
||||
initEvents()
|
||||
|
||||
init()
|
||||
setLocationRelativeTo(null)
|
||||
setLocationRelativeTo(owner)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import app.termora.*
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.SettingsAction
|
||||
import app.termora.sftp.FileSystemViewTable.AskTransfer.Action
|
||||
import app.termora.vfs2.VFSWalker
|
||||
import app.termora.vfs2.sftp.MySftpFileObject
|
||||
import app.termora.vfs2.sftp.MySftpFileSystem
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
@@ -37,7 +38,6 @@ import java.nio.file.FileVisitor
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.nio.file.attribute.FileTime
|
||||
import java.text.MessageFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -45,6 +45,21 @@ import java.util.regex.Pattern
|
||||
import javax.swing.*
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import kotlin.collections.ArrayDeque
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.all
|
||||
import kotlin.collections.contains
|
||||
import kotlin.collections.filter
|
||||
import kotlin.collections.filterIsInstance
|
||||
import kotlin.collections.find
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.collections.isEmpty
|
||||
import kotlin.collections.isNotEmpty
|
||||
import kotlin.collections.last
|
||||
import kotlin.collections.listOf
|
||||
import kotlin.collections.map
|
||||
import kotlin.collections.mapOf
|
||||
import kotlin.collections.mutableListOf
|
||||
import kotlin.collections.sortedArray
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
@@ -218,7 +233,7 @@ class FileSystemViewTable(
|
||||
val localTarget = sftpPanel.getLocalTarget()
|
||||
val table = localTarget.getData(SFTPDataProviders.FileSystemViewTable) ?: return false
|
||||
// 委托最左侧的本地文件系统传输
|
||||
table.transfer(paths, true, targetWorkdir)
|
||||
table.transfer(paths, true, targetWorkdir, fileSystemViewPanel)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -360,34 +375,7 @@ class FileSystemViewTable(
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val last = files.last()
|
||||
if (last !is MySftpFileObject) return
|
||||
|
||||
val dialog = PosixFilePermissionDialog(
|
||||
SwingUtilities.getWindowAncestor(table),
|
||||
model.getFilePermissions(last)
|
||||
)
|
||||
val permissions = dialog.open() ?: return
|
||||
|
||||
if (fileSystemViewPanel.requestLoading()) {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
val c = runCatching { last.setPosixFilePermissions(permissions) }.onFailure {
|
||||
withContext(Dispatchers.Swing) {
|
||||
OptionPane.showMessageDialog(
|
||||
owner,
|
||||
ExceptionUtils.getMessage(it),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// stop loading
|
||||
fileSystemViewPanel.stopLoading()
|
||||
|
||||
// reload
|
||||
if (c.isSuccess) {
|
||||
fileSystemViewPanel.reload(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
changePermission(last)
|
||||
}
|
||||
})
|
||||
refresh.addActionListener { fileSystemViewPanel.reload() }
|
||||
@@ -409,6 +397,80 @@ class FileSystemViewTable(
|
||||
popupMenu.show(table, e.x, e.y)
|
||||
}
|
||||
|
||||
private fun changePermission(file: MySftpFileObject) {
|
||||
|
||||
val dialog = PosixFilePermissionDialog(
|
||||
SwingUtilities.getWindowAncestor(table),
|
||||
model.getFilePermissions(file)
|
||||
)
|
||||
val permissions = dialog.open() ?: return
|
||||
val isIncludeSubdirectories = dialog.isIncludeSubdirectories()
|
||||
|
||||
if (fileSystemViewPanel.requestLoading()) {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
val c = runCatching {
|
||||
file.setPosixFilePermissions(permissions)
|
||||
if (isIncludeSubdirectories && file.isFolder) {
|
||||
file.refresh()
|
||||
VFSWalker.walk(file, object : FileVisitor<FileObject> {
|
||||
override fun preVisitDirectory(
|
||||
dir: FileObject,
|
||||
attrs: BasicFileAttributes
|
||||
): FileVisitResult {
|
||||
dir.refresh()
|
||||
if (dir is MySftpFileObject) {
|
||||
dir.setPosixFilePermissions(permissions)
|
||||
}
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
override fun visitFile(
|
||||
file: FileObject,
|
||||
attrs: BasicFileAttributes
|
||||
): FileVisitResult {
|
||||
if (file is MySftpFileObject) {
|
||||
file.setPosixFilePermissions(permissions)
|
||||
}
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
override fun visitFileFailed(
|
||||
file: FileObject,
|
||||
exc: IOException
|
||||
): FileVisitResult {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
|
||||
override fun postVisitDirectory(
|
||||
dir: FileObject,
|
||||
exc: IOException?
|
||||
): FileVisitResult {
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}.onFailure {
|
||||
withContext(Dispatchers.Swing) {
|
||||
OptionPane.showMessageDialog(
|
||||
owner,
|
||||
ExceptionUtils.getMessage(it),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// stop loading
|
||||
fileSystemViewPanel.stopLoading()
|
||||
|
||||
// reload
|
||||
if (c.isSuccess) {
|
||||
fileSystemViewPanel.reload(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renameSelection() {
|
||||
val index = selectedRow
|
||||
if (index < 0) return
|
||||
@@ -619,12 +681,13 @@ class FileSystemViewTable(
|
||||
private fun transfer(
|
||||
files: List<FileObject>,
|
||||
fromLocalSystem: Boolean = false,
|
||||
targetWorkdir: FileObject? = null
|
||||
targetWorkdir: FileObject? = null,
|
||||
target: FileSystemViewPanel? = null,
|
||||
) {
|
||||
|
||||
assertEventDispatchThread()
|
||||
|
||||
val target = sftpPanel.getTarget(table) ?: return
|
||||
val target = (target ?: sftpPanel.getTarget(table)) ?: return
|
||||
val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return
|
||||
var isApplyAll = false
|
||||
var lastAction = Action.Overwrite
|
||||
@@ -650,7 +713,7 @@ class FileSystemViewTable(
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
doTransfer(file, lastAction, fromLocalSystem, targetWorkdir)
|
||||
doTransfer(file, lastAction, fromLocalSystem, targetWorkdir, target)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
@@ -801,10 +864,11 @@ class FileSystemViewTable(
|
||||
file: FileObject,
|
||||
action: Action,
|
||||
fromLocalSystem: Boolean,
|
||||
targetWorkdir: FileObject?
|
||||
targetWorkdir: FileObject?,
|
||||
target: FileSystemViewPanel? = null
|
||||
) {
|
||||
val sftpPanel = this.sftpPanel
|
||||
val target = sftpPanel.getTarget(table) ?: return
|
||||
val target = (target ?: sftpPanel.getTarget(table)) ?: return
|
||||
|
||||
/**
|
||||
* 定义一个添加器,它可以自动的判断导入/拖拽行为
|
||||
@@ -886,36 +950,7 @@ class FileSystemViewTable(
|
||||
dir: FileObject,
|
||||
visitor: FileVisitor<FileObject>,
|
||||
): FileVisitResult {
|
||||
|
||||
// clear cache
|
||||
if (visitor.preVisitDirectory(dir, EmptyBasicFileAttributes.INSTANCE) == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
|
||||
for (e in dir.children) {
|
||||
if (e.name.baseName == ".." || e.name.baseName == ".") continue
|
||||
if (e.isFolder) {
|
||||
if (walk(dir.resolveFile(e.name.baseName), visitor) == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
} else {
|
||||
val result = visitor.visitFile(
|
||||
dir.resolveFile(e.name.baseName),
|
||||
EmptyBasicFileAttributes.INSTANCE
|
||||
)
|
||||
if (result == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
} else if (result == FileVisitResult.SKIP_SUBTREE) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (visitor.postVisitDirectory(dir, null) == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
|
||||
return FileVisitResult.CONTINUE
|
||||
return VFSWalker.walk(dir, visitor)
|
||||
}
|
||||
|
||||
private fun addTransport(
|
||||
@@ -974,47 +1009,5 @@ class FileSystemViewTable(
|
||||
|
||||
}
|
||||
|
||||
private class EmptyBasicFileAttributes : BasicFileAttributes {
|
||||
companion object {
|
||||
val INSTANCE = EmptyBasicFileAttributes()
|
||||
}
|
||||
|
||||
override fun lastModifiedTime(): FileTime {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun lastAccessTime(): FileTime {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun creationTime(): FileTime {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isRegularFile(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isDirectory(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isSymbolicLink(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isOther(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun size(): Long {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun fileKey(): Any {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -157,7 +157,11 @@ class FileSystemViewTableModel : DefaultTableModel() {
|
||||
fun getPathNames(): Set<String> {
|
||||
val names = linkedSetOf<String>()
|
||||
for (i in 0 until rowCount) {
|
||||
names.add(getFileObject(i).name.baseName)
|
||||
if (hasParent && i == 0) {
|
||||
names.add("..")
|
||||
} else {
|
||||
names.add(getFileObject(i).name.baseName)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class PosixFilePermissionDialog(
|
||||
private val otherRead = JCheckBox(I18n.getString("termora.transport.permissions.read"))
|
||||
private val otherWrite = JCheckBox(I18n.getString("termora.transport.permissions.write"))
|
||||
private val otherExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute"))
|
||||
private val includeSubFolder = JCheckBox(I18n.getString("termora.transport.permissions.include-subfolder"))
|
||||
|
||||
private var isCancelled = false
|
||||
|
||||
@@ -60,13 +61,14 @@ class PosixFilePermissionDialog(
|
||||
otherRead.isFocusable = false
|
||||
otherWrite.isFocusable = false
|
||||
otherExecute.isFocusable = false
|
||||
includeSubFolder.isFocusable = false
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val formMargin = "7dlu"
|
||||
val layout = FormLayout(
|
||||
"default:grow, $formMargin, default:grow, $formMargin, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref"
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
||||
@@ -95,6 +97,8 @@ class PosixFilePermissionDialog(
|
||||
otherBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.others"))
|
||||
builder.add(otherBox).xy(5, 3)
|
||||
|
||||
builder.add(includeSubFolder).xyw(1, 5, 5)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@@ -103,6 +107,10 @@ class PosixFilePermissionDialog(
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
fun isIncludeSubdirectories(): Boolean {
|
||||
return includeSubFolder.isSelected
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回空表示取消了
|
||||
*/
|
||||
|
||||
@@ -56,6 +56,16 @@ class SFTPAction : AnAction("SFTP", Icons.folder) {
|
||||
}
|
||||
|
||||
val host = hostManager.getHost(hostId) ?: return
|
||||
for (i in 0 until tabbed.tabCount) {
|
||||
val c = tabbed.getComponentAt(i)
|
||||
if (c is SFTPFileSystemViewPanel) {
|
||||
if (c.state == SFTPFileSystemViewPanel.State.Initialized) {
|
||||
c.selectHost(host)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tabbed.addSFTPFileSystemViewPanelTab(host)
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.swing.*
|
||||
import javax.swing.event.TreeExpansionEvent
|
||||
import javax.swing.event.TreeExpansionListener
|
||||
|
||||
class SFTPFileSystemViewPanel(
|
||||
var host: Host? = null,
|
||||
@@ -35,17 +37,18 @@ class SFTPFileSystemViewPanel(
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(SFTPFileSystemViewPanel::class.java)
|
||||
}
|
||||
|
||||
private enum class State {
|
||||
Initialized,
|
||||
Connecting,
|
||||
Connected,
|
||||
ConnectFailed,
|
||||
}
|
||||
enum class State {
|
||||
Initialized,
|
||||
Connecting,
|
||||
Connected,
|
||||
ConnectFailed,
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var state = State.Initialized
|
||||
var state = State.Initialized
|
||||
private set
|
||||
private val cardLayout = CardLayout()
|
||||
private val cardPanel = JPanel(cardLayout)
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
@@ -283,12 +286,20 @@ class SFTPFileSystemViewPanel(
|
||||
val node = tree.getLastSelectedPathNode() ?: return
|
||||
if (node.isFolder) return
|
||||
val host = node.data as Host
|
||||
that.setTabTitle(host.name)
|
||||
that.host = host
|
||||
that.connect()
|
||||
selectHost(host)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
tree.addTreeExpansionListener(object : TreeExpansionListener {
|
||||
override fun treeExpanded(event: TreeExpansionEvent) {
|
||||
properties.putString("SFTPTabbed.Tree.state", TreeUtils.saveExpansionState(tree))
|
||||
}
|
||||
|
||||
override fun treeCollapsed(event: TreeExpansionEvent) {
|
||||
properties.putString("SFTPTabbed.Tree.state", TreeUtils.saveExpansionState(tree))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
@@ -305,6 +316,12 @@ class SFTPFileSystemViewPanel(
|
||||
}
|
||||
}
|
||||
|
||||
fun selectHost(host: Host) {
|
||||
that.setTabTitle(host.name)
|
||||
that.host = host
|
||||
that.connect()
|
||||
}
|
||||
|
||||
private fun setTabTitle(title: String) {
|
||||
val tabbed = SwingUtilities.getAncestorOfClass(JTabbedPane::class.java, that)
|
||||
if (tabbed is JTabbedPane) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import java.awt.Point
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -13,7 +12,6 @@ import javax.swing.JButton
|
||||
import javax.swing.JToolBar
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.UIManager
|
||||
import kotlin.math.max
|
||||
|
||||
@Suppress("DuplicatedCode")
|
||||
class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPane(), Disposable {
|
||||
@@ -43,23 +41,20 @@ class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPan
|
||||
private fun initEvents() {
|
||||
addBtn.addActionListener(object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val dialog = NewHostTreeDialog(SwingUtilities.getWindowAncestor(tabbed))
|
||||
dialog.location = Point(
|
||||
max(0, addBtn.locationOnScreen.x - dialog.width / 2 + addBtn.width / 2),
|
||||
addBtn.locationOnScreen.y + max(tabHeight, addBtn.height)
|
||||
)
|
||||
dialog.setFilter { it.host.protocol == Protocol.SSH }
|
||||
dialog.setTreeName("SFTPTabbed.Tree")
|
||||
dialog.allowMulti = true
|
||||
dialog.isVisible = true
|
||||
|
||||
val hosts = dialog.hosts
|
||||
if (hosts.isEmpty()) return
|
||||
|
||||
for (host in hosts) {
|
||||
addSFTPFileSystemViewPanelTab(host)
|
||||
for (i in 0 until tabCount) {
|
||||
val c = getComponentAt(i)
|
||||
if (c !is SFTPFileSystemViewPanel) continue
|
||||
if (c.state != SFTPFileSystemViewPanel.State.Initialized) continue
|
||||
selectedIndex = i
|
||||
return
|
||||
}
|
||||
|
||||
// 添加一个新的
|
||||
addTab(
|
||||
I18n.getString("termora.transport.sftp.select-host"),
|
||||
SFTPFileSystemViewPanel(transportManager = transportManager)
|
||||
)
|
||||
selectedIndex = tabCount - 1
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -18,6 +18,16 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
|
||||
}
|
||||
|
||||
const val SNIPPET = "SnippetAction"
|
||||
|
||||
// \r \n \t \a \e \b
|
||||
private val SpecialChars = mutableMapOf(
|
||||
'r' to '\r',
|
||||
'n' to '\n',
|
||||
't' to '\t',
|
||||
'a' to ControlCharacters.BEL,
|
||||
'e' to ControlCharacters.ESC,
|
||||
'b' to ControlCharacters.BS
|
||||
)
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
@@ -27,31 +37,39 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
|
||||
|
||||
fun runSnippet(snippet: Snippet, writer: TerminalWriter) {
|
||||
if (snippet.type != SnippetType.Snippet) return
|
||||
val map = mapOf(
|
||||
"\n" to ControlCharacters.LF,
|
||||
"\r" to ControlCharacters.CR,
|
||||
"\t" to ControlCharacters.TAB,
|
||||
"\b" to ControlCharacters.BS,
|
||||
"\\a" to ControlCharacters.BEL,
|
||||
"\\e" to ControlCharacters.ESC,
|
||||
)
|
||||
val chars = snippet.snippet.toCharArray()
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(unescape(snippet.snippet).toByteArray(writer.getCharset())))
|
||||
}
|
||||
|
||||
private fun unescape(text: String): String {
|
||||
val chars = text.toCharArray()
|
||||
val sb = StringBuilder()
|
||||
for (i in chars.indices) {
|
||||
val c = chars[i]
|
||||
if (i == 0) continue
|
||||
if (c != '\n') continue
|
||||
if (chars[i - 1] != '\\') continue
|
||||
// 每一行的最后一个 \ 比较特殊,先转成 null 然后再去 unescapeJava
|
||||
chars[i - 1] = Char.Null
|
||||
|
||||
// 不是特殊字符不处理
|
||||
if (SpecialChars.containsKey(c).not()) {
|
||||
sb.append(c)
|
||||
continue
|
||||
}
|
||||
|
||||
// 特殊字符前面不是 `\` 不处理
|
||||
if (chars.getOrNull(i - 1) != '\\') {
|
||||
sb.append(c)
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果构成的字符串是:\\r 就会生成 \r 字符串,并非转译成:CR
|
||||
if (chars.getOrNull(i - 2) == '\\') {
|
||||
sb.deleteCharAt(sb.length - 1)
|
||||
sb.append(c)
|
||||
continue
|
||||
}
|
||||
|
||||
// 命中条件之后,那么 sb 最后一个字符肯定是 \
|
||||
sb.deleteCharAt(sb.length - 1)
|
||||
sb.append(SpecialChars.getValue(c))
|
||||
}
|
||||
|
||||
var text = chars.joinToString(StringUtils.EMPTY)
|
||||
text = StringEscapeUtils.unescapeJava(text)
|
||||
for (e in map.entries) {
|
||||
text = text.replace(e.key, e.value.toString())
|
||||
}
|
||||
text = snippet.snippet.replace(Char.Null, '\\')
|
||||
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(text.toByteArray(writer.getCharset())))
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
@@ -41,8 +41,10 @@ class SnippetPanel : JPanel(BorderLayout()), Disposable {
|
||||
private fun initViews() {
|
||||
val splitPane = JSplitPane()
|
||||
splitPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||
val scrollPane = JScrollPane(snippetTree)
|
||||
scrollPane.border = BorderFactory.createEmptyBorder()
|
||||
|
||||
leftPanel.add(snippetTree, BorderLayout.CENTER)
|
||||
leftPanel.add(scrollPane, BorderLayout.CENTER)
|
||||
leftPanel.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
|
||||
BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
|
||||
@@ -360,8 +360,9 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Send Device Attributes (Primary DA).
|
||||
// Send Device Attributes (Primary DA).
|
||||
'c' -> {
|
||||
sendDeviceAttributes()
|
||||
}
|
||||
|
||||
// CSI Ps M Delete Ps Line(s) (default = 1) (DL).
|
||||
@@ -505,6 +506,22 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
||||
|
||||
}
|
||||
|
||||
private fun sendDeviceAttributes() {
|
||||
|
||||
assertEventDispatchThread()
|
||||
|
||||
if (!terminalModel.hasData(DataKey.TerminalWriter)) {
|
||||
return
|
||||
}
|
||||
|
||||
val writer = terminalModel.getData(DataKey.TerminalWriter)
|
||||
|
||||
// VT102_RESPONSE
|
||||
val bytes = "${ControlCharacters.ESC}[?6c".toByteArray(writer.getCharset())
|
||||
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-?-Pm-h.1D0E
|
||||
*/
|
||||
|
||||
@@ -21,6 +21,16 @@ interface SelectionModel {
|
||||
*/
|
||||
fun setSelection(startPosition: Position, endPosition: Position)
|
||||
|
||||
/**
|
||||
* 设置块选中模式
|
||||
*/
|
||||
fun setBlockSelection(block: Boolean)
|
||||
|
||||
/**
|
||||
* 是否是块选中模式
|
||||
*/
|
||||
fun isBlockSelection(): Boolean
|
||||
|
||||
/**
|
||||
* 获取开始选中的位置
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@ import kotlin.math.min
|
||||
open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
||||
private var startPosition = Position.unknown
|
||||
private var endPosition = Position.unknown
|
||||
private var block = false
|
||||
private val document = terminal.getDocument()
|
||||
|
||||
internal companion object {
|
||||
@@ -67,29 +68,60 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
val iterator = getChars(getSelectionStartPosition(), getSelectionEndPosition())
|
||||
while (iterator.hasNext()) {
|
||||
val line = iterator.next()
|
||||
val chars = line.chars()
|
||||
if (chars.isEmpty() || chars.first().first.isNull) {
|
||||
continue
|
||||
}
|
||||
val start = getSelectionStartPosition()
|
||||
val end = getSelectionEndPosition()
|
||||
|
||||
for (e in chars) {
|
||||
if (e.first.isSoftHyphen) {
|
||||
continue
|
||||
} else if (e.first.isNull) {
|
||||
break
|
||||
if (isBlockSelection()) {
|
||||
val left = min(start.x, end.x)
|
||||
val right = max(start.x, end.x)
|
||||
val top = min(start.y, end.y)
|
||||
val bottom = max(start.y, end.y)
|
||||
|
||||
for (lineNum in top..bottom) {
|
||||
val line = document.getLine(lineNum)
|
||||
val chars = line.chars()
|
||||
|
||||
// 块选中要处理超出边界
|
||||
val from = (left - 1).coerceAtLeast(0)
|
||||
val to = right.coerceAtMost(chars.size)
|
||||
|
||||
if (from < to) {
|
||||
val selected = chars.subList(from, to)
|
||||
.filter { !it.first.isNull && !it.first.isSoftHyphen }
|
||||
.joinToString("") { it.first.toString() }
|
||||
sb.append(selected)
|
||||
}
|
||||
|
||||
if (lineNum != bottom) {
|
||||
sb.appendLine()
|
||||
}
|
||||
sb.append(e.first)
|
||||
}
|
||||
|
||||
if (line.wrapped) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
val iterator = getChars(start, end)
|
||||
while (iterator.hasNext()) {
|
||||
val line = iterator.next()
|
||||
val chars = line.chars()
|
||||
if (chars.isEmpty() || chars.first().first.isNull) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (iterator.hasNext()) {
|
||||
sb.appendLine()
|
||||
for (e in chars) {
|
||||
if (e.first.isSoftHyphen) {
|
||||
continue
|
||||
} else if (e.first.isNull) {
|
||||
break
|
||||
}
|
||||
sb.append(e.first)
|
||||
}
|
||||
|
||||
if (line.wrapped) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (iterator.hasNext()) {
|
||||
sb.appendLine()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +203,12 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
||||
fireSelectionChanged()
|
||||
}
|
||||
|
||||
override fun setBlockSelection(block: Boolean) {
|
||||
this.block = block
|
||||
}
|
||||
|
||||
override fun isBlockSelection() = block
|
||||
|
||||
override fun getSelectionStartPosition(): Position {
|
||||
return startPosition
|
||||
}
|
||||
@@ -202,13 +240,20 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
||||
}
|
||||
|
||||
override fun hasSelection(x: Int, y: Int): Boolean {
|
||||
return hasSelection() && isPointInsideArea(
|
||||
startPosition,
|
||||
endPosition,
|
||||
x,
|
||||
y,
|
||||
terminal.getTerminalModel().getCols()
|
||||
)
|
||||
|
||||
if (hasSelection().not()) return false
|
||||
|
||||
// 如果是块选中
|
||||
if (isBlockSelection()) {
|
||||
val left = min(startPosition.x, endPosition.x)
|
||||
val right = max(startPosition.x, endPosition.x)
|
||||
val top = min(startPosition.y, endPosition.y)
|
||||
val bottom = max(startPosition.y, endPosition.y)
|
||||
|
||||
return x in left..right && y in top..bottom
|
||||
}
|
||||
|
||||
return isPointInsideArea(startPosition, endPosition, x, y, terminal.getTerminalModel().getCols())
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.termora.terminal.panel
|
||||
|
||||
import app.termora.Application
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.Database
|
||||
import app.termora.terminal.*
|
||||
import java.awt.Graphics
|
||||
import java.net.URI
|
||||
@@ -16,6 +17,7 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
||||
}
|
||||
|
||||
private val regex = Regex("https?://\\S*[^.\\s'\",()<>\\[\\]]")
|
||||
private val isEnableHyperlink get() = Database.getDatabase().terminal.hyperlink
|
||||
|
||||
override fun before(
|
||||
offset: Int,
|
||||
@@ -25,6 +27,9 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
||||
terminalDisplay: TerminalDisplay,
|
||||
terminal: Terminal
|
||||
) {
|
||||
|
||||
if (isEnableHyperlink.not()) return
|
||||
|
||||
val document = terminal.getDocument()
|
||||
var startOffset = offset
|
||||
var endOffset = startOffset + count
|
||||
@@ -91,4 +96,18 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
||||
}
|
||||
}
|
||||
|
||||
override fun after(
|
||||
offset: Int,
|
||||
count: Int,
|
||||
g: Graphics,
|
||||
terminalPanel: TerminalPanel,
|
||||
terminalDisplay: TerminalDisplay,
|
||||
terminal: Terminal
|
||||
) {
|
||||
if (isEnableHyperlink.not()) {
|
||||
// 删除之前的
|
||||
terminal.getMarkupModel().removeAllHighlighters(Highlighter.HYPERLINK)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -134,6 +134,8 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
|
||||
// 如果不判断的话可能会导致移动了一点点就就进入选择状态了
|
||||
val diff = terminalPanel.getAverageCharWidth() / 5.0
|
||||
if (abs(mousePressedPoint.y - e.y) >= diff || abs(mousePressedPoint.x - e.x) >= diff) {
|
||||
// 设置选中模式
|
||||
terminal.getSelectionModel().setBlockSelection(isOnlyAltDown(e))
|
||||
beginSelect(
|
||||
Position(x = mousePressedPoint.x, y = mousePressedPoint.y),
|
||||
)
|
||||
@@ -141,6 +143,13 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
|
||||
}
|
||||
}
|
||||
|
||||
private fun isOnlyAltDown(e: MouseEvent): Boolean {
|
||||
return e.isAltDown &&
|
||||
e.isMetaDown.not() &&
|
||||
e.isControlDown.not() &&
|
||||
e.isShiftDown.not() &&
|
||||
e.isAltGraphDown.not()
|
||||
}
|
||||
|
||||
private fun beginSelect(position: Position) {
|
||||
|
||||
|
||||
88
src/main/kotlin/app/termora/vfs2/VFSWalker.kt
Normal file
88
src/main/kotlin/app/termora/vfs2/VFSWalker.kt
Normal file
@@ -0,0 +1,88 @@
|
||||
package app.termora.vfs2
|
||||
|
||||
import org.apache.commons.vfs2.FileObject
|
||||
import java.nio.file.FileVisitResult
|
||||
import java.nio.file.FileVisitor
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.nio.file.attribute.FileTime
|
||||
|
||||
object VFSWalker {
|
||||
fun walk(
|
||||
dir: FileObject,
|
||||
visitor: FileVisitor<FileObject>,
|
||||
): FileVisitResult {
|
||||
|
||||
// clear cache
|
||||
if (visitor.preVisitDirectory(dir, EmptyBasicFileAttributes.INSTANCE) == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
|
||||
for (e in dir.children) {
|
||||
if (e.name.baseName == ".." || e.name.baseName == ".") continue
|
||||
if (e.isFolder) {
|
||||
if (walk(dir.resolveFile(e.name.baseName), visitor) == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
} else {
|
||||
val result = visitor.visitFile(
|
||||
dir.resolveFile(e.name.baseName),
|
||||
EmptyBasicFileAttributes.INSTANCE
|
||||
)
|
||||
if (result == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
} else if (result == FileVisitResult.SKIP_SUBTREE) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (visitor.postVisitDirectory(dir, null) == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
private class EmptyBasicFileAttributes : BasicFileAttributes {
|
||||
companion object {
|
||||
val INSTANCE = EmptyBasicFileAttributes()
|
||||
}
|
||||
|
||||
override fun lastModifiedTime(): FileTime {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun lastAccessTime(): FileTime {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun creationTime(): FileTime {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isRegularFile(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isDirectory(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isSymbolicLink(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isOther(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun size(): Long {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun fileKey(): Any {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ termora.settings.appearance.follow-system=Sync with OS
|
||||
termora.settings.appearance.opacity=Opacity
|
||||
termora.settings.appearance.background-image=BG Image
|
||||
termora.settings.appearance.background-running=Backgrounding
|
||||
termora.settings.appearance.confirm-tab-close=Confirm tab close
|
||||
|
||||
termora.setting.security=Security
|
||||
termora.setting.security.enter-password=Enter password
|
||||
@@ -73,6 +74,7 @@ termora.settings.terminal.size=Size
|
||||
termora.settings.terminal.max-rows=Max rows
|
||||
termora.settings.terminal.debug=Debug mode
|
||||
termora.settings.terminal.beep=Beep
|
||||
termora.settings.terminal.hyperlink=Hyperlink
|
||||
termora.settings.terminal.select-copy=Select copy
|
||||
termora.settings.terminal.cursor-style=Cursor type
|
||||
termora.settings.terminal.cursor-blink=Cursor blink
|
||||
@@ -232,6 +234,7 @@ termora.tabbed.contextmenu.close-other-tabs=Close Other Tabs
|
||||
termora.tabbed.contextmenu.close-all-tabs=Close All Tabs
|
||||
termora.tabbed.contextmenu.reconnect=Reconnect
|
||||
termora.tabbed.local-tab.close-prompt=Do you want to terminal a running process in this terminal?
|
||||
termora.tabbed.tab.close-prompt=Are you sure you want to close this tab?
|
||||
|
||||
# Terminal logger
|
||||
termora.terminal-logger=Terminal Logger
|
||||
@@ -309,6 +312,7 @@ termora.transport.permissions.execute=Execute
|
||||
termora.transport.permissions.owner=Owner
|
||||
termora.transport.permissions.group=Group
|
||||
termora.transport.permissions.others=Others
|
||||
termora.transport.permissions.include-subfolder=Include subdirectories
|
||||
|
||||
termora.transport.sftp.retry=Retry
|
||||
termora.transport.sftp.select-another-host=Select another host
|
||||
|
||||
@@ -54,6 +54,7 @@ termora.settings.appearance.follow-system=跟随系统
|
||||
termora.settings.appearance.opacity=透明度
|
||||
termora.settings.appearance.background-image=背景图
|
||||
termora.settings.appearance.background-running=后台运行
|
||||
termora.settings.appearance.confirm-tab-close=标签关闭前确认
|
||||
|
||||
termora.setting.security=安全
|
||||
termora.setting.security.enter-password=请输入密码
|
||||
@@ -77,6 +78,7 @@ termora.settings.terminal.size=大小
|
||||
termora.settings.terminal.max-rows=最大行数
|
||||
termora.settings.terminal.debug=调试模式
|
||||
termora.settings.terminal.beep=蜂鸣声
|
||||
termora.settings.terminal.hyperlink=超链接
|
||||
termora.settings.terminal.select-copy=选中复制
|
||||
termora.settings.terminal.cursor-style=光标样式
|
||||
termora.settings.terminal.cursor-blink=光标闪烁
|
||||
@@ -221,6 +223,7 @@ termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页
|
||||
termora.tabbed.contextmenu.close-all-tabs=关闭所有标签页
|
||||
termora.tabbed.contextmenu.reconnect=重新连接
|
||||
termora.tabbed.local-tab.close-prompt=你想要终止这个终端中正在运行的进程吗?
|
||||
termora.tabbed.tab.close-prompt=你确定要关闭这个标签页吗?
|
||||
|
||||
|
||||
# Terminal logger
|
||||
@@ -322,6 +325,7 @@ termora.transport.permissions.execute=执行
|
||||
termora.transport.permissions.owner=所有者
|
||||
termora.transport.permissions.group=组
|
||||
termora.transport.permissions.others=其他
|
||||
termora.transport.permissions.include-subfolder=包含子目录
|
||||
|
||||
# transport job
|
||||
termora.transport.jobs.table.name=名称
|
||||
|
||||
@@ -55,6 +55,7 @@ termora.settings.appearance.follow-system=跟隨系統
|
||||
termora.settings.appearance.opacity=透明度
|
||||
termora.settings.appearance.background-image=背景圖
|
||||
termora.settings.appearance.background-running=後台運行
|
||||
termora.settings.appearance.confirm-tab-close=關閉分頁確認
|
||||
|
||||
termora.setting.security=安全
|
||||
termora.setting.security.enter-password=請輸入密碼
|
||||
@@ -88,7 +89,8 @@ termora.settings.terminal.font=字體
|
||||
termora.settings.terminal.size=大小
|
||||
termora.settings.terminal.max-rows=最大行數
|
||||
termora.settings.terminal.debug=偵錯模式
|
||||
termora.settings.terminal.beep=蜂鳴聲
|
||||
termora.settings.terminal.beep=超連結
|
||||
termora.settings.terminal.hyperlink=Hyperlink
|
||||
termora.settings.terminal.select-copy=選取複製
|
||||
termora.settings.terminal.cursor-style=遊標風格
|
||||
termora.settings.terminal.cursor-blink=遊標閃爍
|
||||
@@ -217,7 +219,7 @@ termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁
|
||||
termora.tabbed.contextmenu.close-all-tabs=關閉所有標籤
|
||||
termora.tabbed.contextmenu.reconnect=重新連接
|
||||
termora.tabbed.local-tab.close-prompt=你想要終止這個終端機中正在運作的進程嗎?
|
||||
|
||||
termora.tabbed.tab.close-prompt=你確定要關閉這個分頁嗎?
|
||||
|
||||
|
||||
# Terminal logger
|
||||
@@ -305,6 +307,17 @@ termora.transport.sftp.already-exists.destination=目標文件
|
||||
termora.transport.sftp.already-exists.source=原始檔
|
||||
termora.transport.sftp.already-exists.actions=操作
|
||||
|
||||
# permissions
|
||||
termora.transport.permissions=更改權限
|
||||
termora.transport.permissions.file-folder-permissions=檔案/資料夾權限
|
||||
termora.transport.permissions.read=讀取
|
||||
termora.transport.permissions.write=寫入
|
||||
termora.transport.permissions.execute=執行
|
||||
termora.transport.permissions.owner=所有者
|
||||
termora.transport.permissions.group=群組
|
||||
termora.transport.permissions.others=其他
|
||||
termora.transport.permissions.include-subfolder=包含子目錄
|
||||
|
||||
# transport job
|
||||
termora.transport.jobs.table.name=名稱
|
||||
termora.transport.jobs.table.status=狀態
|
||||
|
||||
@@ -55,6 +55,10 @@ Source: "{#MySourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdir
|
||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[InstallDelete]
|
||||
Type: files; Name: "{app}\app\*.jar"
|
||||
Type: filesandordirs; Name: "{app}\runtime\*"
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall; Check: ShouldPromptStart
|
||||
Filename: "{app}\{#MyAppExeName}"; Flags: nowait runhidden; Check: ShouldAutoStart
|
||||
|
||||
29
src/test/resources/issue-564/Dockerfile
Normal file
29
src/test/resources/issue-564/Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
FROM debian:bookworm-slim
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV TZ=Asia/Shanghai
|
||||
# 安装基础包 + sshd + nvim 依赖
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
openssh-server curl ca-certificates tzdata git unzip \
|
||||
libfuse2 locales && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
# 安装 nvim 最新版(AppImage 提取)
|
||||
RUN curl -LO https://github.com/neovim/neovim/releases/download/v0.11.1/nvim-linux-arm64.appimage && \
|
||||
mv nvim-linux-arm64.appimage nvim.appimage && chmod u+x nvim.appimage && ./nvim.appimage --appimage-extract && \
|
||||
mv squashfs-root/usr/bin/nvim /usr/local/bin/nvim && \
|
||||
rm -rf squashfs-root nvim.appimage
|
||||
# 配置 SSH
|
||||
RUN mkdir /var/run/sshd && \
|
||||
echo 'root:root' | chpasswd && \
|
||||
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config && \
|
||||
echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
|
||||
# 设置语言环境(可选)
|
||||
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \
|
||||
apt-get update && apt-get install -y locales && \
|
||||
locale-gen en_US.UTF-8 && \
|
||||
update-locale LANG=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
LANGUAGE=en_US:en \
|
||||
LC_ALL=en_US.UTF-8
|
||||
# 启动 SSHD
|
||||
EXPOSE 22
|
||||
CMD ["/usr/sbin/sshd", "-D"]
|
||||
Reference in New Issue
Block a user