Compare commits

..

32 Commits

Author SHA1 Message Date
hstyi
1c2315b5e9 fix: Linux unable to open local terminal 2025-08-12 10:22:08 +08:00
hstyi
d48e412580 release: 2.0.0-beta.12 2025-08-12 09:44:27 +08:00
dependabot[bot]
3b3fb41384 chore(deps): bump com.qcloud:cos_api from 5.6.249 to 5.6.251
Bumps [com.qcloud:cos_api](https://github.com/tencentyun/cos-java-sdk-v5) from 5.6.249 to 5.6.251.
- [Release notes](https://github.com/tencentyun/cos-java-sdk-v5/releases)
- [Changelog](https://github.com/tencentyun/cos-java-sdk-v5/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tencentyun/cos-java-sdk-v5/compare/v5.6.249...v5.6.251)

---
updated-dependencies:
- dependency-name: com.qcloud:cos_api
  dependency-version: 5.6.251
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 08:17:38 +08:00
hstyi
190ac697fb chore: turn off the ApplePressAndHoldEnabled 2025-08-09 16:24:43 +08:00
hstyi
8cdbf24cdc fix: file name containing : cannot be transferred 2025-08-09 15:56:32 +08:00
hstyi
6e182b6813 chore: remember the colspan state of the fence layout 2025-08-09 15:33:22 +08:00
hstyi
3fa4064655 feat: hyperlinks require holding down the function key to open 2025-08-09 12:45:06 +08:00
hstyi
a77a03d8b3 fix: transfer causing repositioning after refresh 2025-08-09 12:29:19 +08:00
hstyi
5f8b9d36e2 chore: Agent Forwarding 2025-08-09 11:45:37 +08:00
hstyi
1ed5e164de feat: linux window opacity 2025-08-08 18:12:00 +08:00
hstyi
c67d5b0276 feat: ssh ForwardAgent 2025-08-08 18:11:51 +08:00
hstyi
9646a98f6d feat: background image supports fill mode 2025-08-08 15:06:13 +08:00
hstyi
aee34415a7 release: 2.0.0-beta.11 2025-08-08 13:41:31 +08:00
hstyi
e4e70cc72c chore: osx dist 2025-08-08 13:37:27 +08:00
hstyi
49779fe8f2 feat: tab order settings 2025-08-08 10:02:16 +08:00
hstyi
969ddc3662 feat: add tab index 2025-08-07 23:47:52 +08:00
hstyi
de9b418c75 feat: editor support maximization 2025-08-07 16:44:31 +08:00
hstyi
f8588745cd fix: compression not working & popup not closing 2025-08-07 16:44:19 +08:00
hstyi
7c0cbab187 chore: improve subversion 2025-08-07 13:17:31 +08:00
hstyi
176fa64de0 chore: improve osx pack 2025-08-07 11:29:13 +08:00
hstyi
495ab69195 chore: improve ssh loading 2025-08-07 11:28:59 +08:00
hstyi
93c28242fb chore: App Store 2025-08-06 19:56:06 +08:00
hstyi
57662f717b chore: README 2025-08-06 18:56:38 +08:00
dependabot[bot]
3669bd1f88 chore(deps): bump com.github.hstyi:geolite2
Bumps com.github.hstyi:geolite2 from v1.0-202507280101 to v1.0-202508040102.

---
updated-dependencies:
- dependency-name: com.github.hstyi:geolite2
  dependency-version: v1.0-202508040102
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-05 21:43:25 +08:00
dependabot[bot]
00e695b7d5 chore(deps): bump org.commonmark:commonmark from 0.25.0 to 0.25.1
Bumps [org.commonmark:commonmark](https://github.com/commonmark/commonmark-java) from 0.25.0 to 0.25.1.
- [Release notes](https://github.com/commonmark/commonmark-java/releases)
- [Changelog](https://github.com/commonmark/commonmark-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.25.0...commonmark-parent-0.25.1)

---
updated-dependencies:
- dependency-name: org.commonmark:commonmark
  dependency-version: 0.25.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-04 12:13:31 +08:00
dependabot[bot]
02c92e6019 chore(deps): bump commons-net:commons-net from 3.11.1 to 3.12.0
Bumps [commons-net:commons-net](https://github.com/apache/commons-net) from 3.11.1 to 3.12.0.
- [Changelog](https://github.com/apache/commons-net/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-net/compare/rel/commons-net-3.11.1...rel/commons-net-3.12.0)

---
updated-dependencies:
- dependency-name: commons-net:commons-net
  dependency-version: 3.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-04 12:13:09 +08:00
hstyi
8ba74f0846 fix: terminal exception caused by reconnection 2025-08-01 18:10:34 +08:00
hstyi
79ed6d3858 feat: support right click copy and paste 2025-08-01 17:39:19 +08:00
hstyi
8a66606275 chore: match case tooltip 2025-08-01 11:44:24 +08:00
dependabot[bot]
3ebdf73fbf chore(deps): bump exposed from 1.0.0-beta-4 to 1.0.0-beta-5
Bumps `exposed` from 1.0.0-beta-4 to 1.0.0-beta-5.

Updates `org.jetbrains.exposed:exposed-core` from 1.0.0-beta-4 to 1.0.0-beta-5
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/commits)

Updates `org.jetbrains.exposed:exposed-crypt` from 1.0.0-beta-4 to 1.0.0-beta-5
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/commits)

Updates `org.jetbrains.exposed:exposed-jdbc` from 1.0.0-beta-4 to 1.0.0-beta-5
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/commits)

Updates `org.jetbrains.exposed:exposed-migration` from 1.0.0-beta-4 to 1.0.0-beta-5
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/commits)

---
updated-dependencies:
- dependency-name: org.jetbrains.exposed:exposed-core
  dependency-version: 1.0.0-beta-5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-crypt
  dependency-version: 1.0.0-beta-5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-jdbc
  dependency-version: 1.0.0-beta-5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-migration
  dependency-version: 1.0.0-beta-5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-01 11:09:44 +08:00
hstyi
d249e5da5a fix: XTerm Set Scrolling Region 2025-07-31 16:57:33 +08:00
hstyi
7243e933e6 fix: aura selection text color 2025-07-31 16:22:32 +08:00
71 changed files with 773 additions and 256 deletions

View File

@@ -81,6 +81,10 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
- name: Install create-dmg
shell: bash
run: brew install create-dmg
- name: Compile
shell: bash
run: ./gradlew :check-license && ./gradlew classes -x test
@@ -93,13 +97,6 @@ jobs:
shell: bash
run: ./gradlew :jpackage && ./gradlew :dist
- name: Upload zip artifact
uses: actions/upload-artifact@v4
with:
name: termora-osx-zip-${{ runner.arch }}
path: |
build/distributions/*.zip
- name: Upload dmg artifact
uses: actions/upload-artifact@v4
with:

View File

@@ -82,6 +82,7 @@ Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partial
- 🧾 [Latest Release](https://github.com/TermoraDev/termora/releases/latest)
- 🍺 **Homebrew**: `brew install --cask termora`
- 🔨 **WinGet**: `winget install termora`
- <img src="https://apps.microsoft.com/assets/icons/logo-16x16.png" alt="microsoft logo"/> <b>Microsoft Store</b>: <a href="https://apps.microsoft.com/store/detail/9NRZBHG43SB9?cid=DevShareMCLPCS">Visit Termora in the Microsoft Store</a>

View File

@@ -80,6 +80,7 @@ Termora 使用 [**Kotlin/JVM**](https://kotlinlang.org/) 开发,支持(正
- 🧾 [Latest release](https://github.com/TermoraDev/termora/releases/latest)
- 🍺 **Homebrew**`brew install --cask termora`
- 🪟 **WinGet**`winget install termora`
- <img src="https://apps.microsoft.com/assets/icons/logo-16x16.png" alt="microsoft logo"/> <b>Microsoft Store</b>: <a href="https://apps.microsoft.com/store/detail/9NRZBHG43SB9?cid=DevShareMCLPCS">Termora</a>

View File

@@ -1 +1 @@
2.0.0-beta.10
2.0.0-beta.12

View File

@@ -32,6 +32,7 @@ val appVersion = project.version.toString().split("-")[0]
val makeAppx = if (os.isWindows) StringUtils.defaultString(System.getenv("MAKEAPPX_PATH")) else StringUtils.EMPTY
val isDeb = os.isLinux && System.getenv("TERMORA_TYPE") == "deb"
val isAppx = os.isWindows && makeAppx.isNotBlank() && System.getenv("TERMORA_TYPE") == "appx"
val isBeta = project.version.toString().contains("beta", ignoreCase = true)
// macOS 签名信息
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
@@ -173,10 +174,12 @@ publishing {
}
tasks.processResources {
val betaVersion = project.version.toString().substringAfterLast('.')
filesMatching("**/AppxManifest.xml") {
filter<ReplaceTokens>(
"tokens" to mapOf(
"version" to appVersion,
"betaVersion" to if (isBeta) betaVersion else "0",
"architecture" to if (arch.isArm64) "arm64" else "x64",
"projectDir" to project.projectDir.absolutePath,
)
@@ -230,9 +233,10 @@ tasks.register<Copy>("copy-dependencies") {
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
val osName = if (os.isWindows) "win32" else if (os.isMacOsX) "darwin" else "linux"
val targetDir = FileUtils.getFile(dylib, pty4j.name, osName)
FileUtils.forceMkdir(targetDir)
val myArchName = if (arch.isArm) "aarch64" else "x86-64"
val targetDir = if (os.isMacOsX) FileUtils.getFile(dylib, pty4j.name, osName)
else FileUtils.getFile(dylib, pty4j.name, osName, myArchName)
FileUtils.forceMkdir(targetDir)
if (os.isWindows) {
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*win/${myArchName}/*", "-d", targetDir.absolutePath) }
@@ -442,7 +446,7 @@ tasks.register<Exec>("jpackage") {
throw UnsupportedOperationException()
}
if (os.isMacOsX && macOSSign) {
if (macOSSign) {
arguments.add("--mac-sign")
arguments.add("--mac-signing-key-user-name")
arguments.add(macOSSignUsername)

View File

@@ -9,7 +9,7 @@ kotlinx-serialization-json = "1.9.0"
commons-codec = "1.19.0"
commons-lang3 = "3.18.0"
commons-csv = "1.14.1"
commons-net = "3.11.1"
commons-net = "3.12.0"
commons-text = "1.14.0"
commons-compress = "1.28.0"
commons-vfs2 = "2.10.0"
@@ -28,7 +28,7 @@ okhttp = "5.1.0"
sshj = "0.39.0"
sshd-core = "2.15.0"
jgit = "7.2.0.202503040940-r"
commonmark = "0.25.0"
commonmark = "0.25.1"
jnafilechooser = "1.1.2"
xodus = "2.0.1"
bip39 = "1.0.9"
@@ -41,7 +41,7 @@ jSerialComm = "2.11.2"
ini4j = "0.5.5-2"
restart4j = "0.0.1"
eddsa = "0.3.0"
exposed = "1.0.0-beta-4"
exposed = "1.0.0-beta-5"
h2 = "2.3.232"
sqlite = "3.50.3.0"
jug = "5.1.0"

View File

@@ -3,7 +3,7 @@ plugins {
}
project.version = "0.0.5"
project.version = "0.0.6"

View File

@@ -18,4 +18,8 @@ object Appearance {
set(value) {
enableManager.setFlag("Plugins.bg.interval", value)
}
var fillMode: String
get() = enableManager.getFlag("Plugins.bg.fillMode", FillMode.STRETCH.name)
set(value) = enableManager.setFlag("Plugins.bg.fillMode", value)
}

View File

@@ -2,6 +2,8 @@ package app.termora.plugins.bg
import app.termora.GlassPaneExtension
import app.termora.WindowScope
import app.termora.restore
import app.termora.save
import com.formdev.flatlaf.FlatLaf
import java.awt.AlphaComposite
import java.awt.Graphics2D
@@ -12,15 +14,52 @@ class BGGlassPaneExtension private constructor() : GlassPaneExtension {
val instance = BGGlassPaneExtension()
}
override fun paint(scope: WindowScope, c: JComponent, g2d: Graphics2D) {
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
g2d.save()
g2d.composite = AlphaComposite.getInstance(
AlphaComposite.SRC_OVER,
if (FlatLaf.isLafDark()) 0.2f else 0.1f
)
when (Appearance.fillMode) {
FillMode.STRETCH.name -> {
g2d.drawImage(img, 0, 0, c.width, c.height, null)
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER)
}
FillMode.CENTER.name -> {
val x = (c.width - img.width) / 2
val y = (c.height - img.height) / 2
g2d.drawImage(img, x, y, null)
}
FillMode.TILE.name -> {
val iw = img.width
val ih = img.height
var y = 0
while (y < c.height) {
var x = 0
while (x < c.width) {
g2d.drawImage(img, x, y, null)
x += iw
}
y += ih
}
}
FillMode.FIT.name -> {
val scale = maxOf(c.width.toDouble() / img.width, c.height.toDouble() / img.height)
val newW = (img.width * scale).toInt()
val newH = (img.height * scale).toInt()
val x = (c.width - newW) / 2
val y = (c.height - newH) / 2
g2d.drawImage(img, x, y, newW, newH, null)
}
}
g2d.restore()
}
}

View File

@@ -10,6 +10,8 @@ import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.exception.ExceptionUtils
import org.slf4j.LoggerFactory
import java.awt.BorderLayout
import java.awt.Component
import java.awt.event.ItemEvent
import java.io.File
import java.nio.file.StandardCopyOption
import javax.swing.*
@@ -23,6 +25,7 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
private val owner get() = SwingUtilities.getWindowAncestor(this)
val backgroundImageTextField = OutlineTextField()
val fillModeComboBox = OutlineComboBox<FillMode>()
val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400)
private val backgroundButton = JButton(Icons.folder)
@@ -36,6 +39,38 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
private fun initView() {
fillModeComboBox.addItem(FillMode.STRETCH)
fillModeComboBox.addItem(FillMode.FIT)
fillModeComboBox.addItem(FillMode.CENTER)
fillModeComboBox.addItem(FillMode.TILE)
fillModeComboBox.selectedItem = runCatching { FillMode.valueOf(Appearance.fillMode) }
.getOrNull() ?: FillMode.STRETCH
fillModeComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component? {
var text = value?.toString()
if (value == FillMode.STRETCH) {
text = BGI18n.getString("termora.plugins.bg.fill-mode.stretch")
} else if (value == FillMode.FIT) {
text = BGI18n.getString("termora.plugins.bg.fill-mode.fit")
} else if (value == FillMode.CENTER) {
text = BGI18n.getString("termora.plugins.bg.fill-mode.center")
} else if (value == FillMode.TILE) {
text = BGI18n.getString("termora.plugins.bg.fill-mode.tile")
}
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
}
}
backgroundImageTextField.isEditable = false
backgroundImageTextField.trailingComponent = backgroundButton
backgroundImageTextField.text = Appearance.backgroundImage
@@ -80,6 +115,15 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
Appearance.interval = value
}
}
fillModeComboBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
Appearance.fillMode = fillModeComboBox.selectedItem?.toString() ?: FillMode.STRETCH.name
for (frame in TermoraFrameManager.getInstance().getWindows()) {
SwingUtilities.invokeLater { SwingUtilities.updateComponentTreeUI(frame) }
}
}
}
}
private fun onSelectedBackgroundImage(file: File) {
@@ -124,7 +168,7 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
private fun getFormPanel(): JPanel {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default",
"pref, $FORM_MARGIN, pref"
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
@@ -138,6 +182,10 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
.add(bgClearBox).xy(5, rows)
.apply { rows += step }
builder.add("${BGI18n.getString("termora.plugins.bg.fill-mode")}:").xy(1, rows)
.add(fillModeComboBox).xy(3, rows)
.apply { rows += step }
builder.add("${BGI18n.getString("termora.plugins.bg.interval")}:").xy(1, rows)
.add(intervalSpinner).xy(3, rows)
.apply { rows += step }

View File

@@ -0,0 +1,8 @@
package app.termora.plugins.bg
enum class FillMode {
STRETCH, // 拉伸
FIT, // 等比例铺满
CENTER, // 居中
TILE, // 平铺
}

View File

@@ -1,2 +1,7 @@
termora.plugins.bg.interval=Interval
termora.plugins.bg.fill-mode=Fill Mode
termora.plugins.bg.fill-mode.stretch=Stretch
termora.plugins.bg.fill-mode.fit=Fit
termora.plugins.bg.fill-mode.center=Center
termora.plugins.bg.fill-mode.tile=Tile
termora.plugins.bg.background-image=Background Image

View File

@@ -1,2 +1,8 @@
termora.plugins.bg.background-image=背景图
termora.plugins.bg.interval=切换间隔
termora.plugins.bg.fill-mode=填充模式
termora.plugins.bg.fill-mode.stretch=拉伸
termora.plugins.bg.fill-mode.fit=适合
termora.plugins.bg.fill-mode.center=居中
termora.plugins.bg.fill-mode.tile=平铺

View File

@@ -1,2 +1,8 @@
termora.plugins.bg.background-image=背景圖
termora.plugins.bg.interval=切換間隔
termora.plugins.bg.fill-mode=填充模式
termora.plugins.bg.fill-mode.stretch=拉伸
termora.plugins.bg.fill-mode.fit=適合
termora.plugins.bg.fill-mode.center=居中
termora.plugins.bg.fill-mode.tile=平鋪

View File

@@ -8,7 +8,7 @@ project.version = "0.0.4"
dependencies {
testImplementation(kotlin("test"))
implementation("com.qcloud:cos_api:5.6.249")
implementation("com.qcloud:cos_api:5.6.251")
compileOnly(project(":"))
}

View File

@@ -4,7 +4,7 @@ plugins {
project.version = "0.0.6"
project.version = "0.0.7"
dependencies {

View File

@@ -1,94 +0,0 @@
package app.termora.plugins.editor
import app.termora.DialogWrapper
import app.termora.Disposable
import app.termora.Disposer
import app.termora.OptionPane
import java.awt.Dimension
import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.io.File
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JComponent
import javax.swing.JOptionPane
import javax.swing.UIManager
import kotlin.io.path.absolutePathString
import kotlin.io.path.name
class EditorDialog(file: Path, owner: Window, private val myDisposable: Disposable) : DialogWrapper(null) {
private val filename = file.name
private val filepath = File(file.absolutePathString())
private val editorPanel = EditorPanel(this, filepath)
private val disposed = AtomicBoolean()
init {
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
isModal = false
controlsVisible = true
isResizable = true
title = filename
iconImages = owner.iconImages
escapeDispose = false
defaultCloseOperation = DO_NOTHING_ON_CLOSE
initEvents()
setLocationRelativeTo(owner)
init()
}
private fun initEvents() {
addWindowListener(object : WindowAdapter() {
override fun windowClosing(e: WindowEvent?) {
if (disposed.compareAndSet(false, true)) {
doCancelAction()
}
}
})
Disposer.register(myDisposable, object : Disposable {
override fun dispose() {
if (disposed.compareAndSet(false, true)) {
doCancelAction()
}
}
})
Disposer.register(disposable, object : Disposable {
override fun dispose() {
if (disposed.compareAndSet(false, true)) {
Disposer.dispose(myDisposable)
}
}
})
}
override fun doCancelAction() {
if (editorPanel.changes()) {
if (OptionPane.showConfirmDialog(
this,
"文件尚未保存,你确定要退出吗?",
optionType = JOptionPane.OK_CANCEL_OPTION,
) != JOptionPane.OK_OPTION
) {
return
}
}
super.doCancelAction()
}
override fun createCenterPanel(): JComponent {
return editorPanel
}
override fun createSouthPanel(): JComponent? {
return null
}
}

View File

@@ -0,0 +1,91 @@
package app.termora.plugins.editor
import app.termora.Disposable
import app.termora.Disposer
import app.termora.EnableManager
import app.termora.OptionPane
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.io.File
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JFrame
import javax.swing.JOptionPane
import javax.swing.UIManager
import kotlin.io.path.absolutePathString
import kotlin.io.path.name
import kotlin.math.max
class EditorFrame(private val file: Path, private val owner: Window, private val disposable: Disposable) : JFrame() {
private val enableManager get() = EnableManager.getInstance()
private val disposed = AtomicBoolean()
private val filepath = File(file.absolutePathString())
private val frame get() = this
private val editorPanel = EditorPanel(this, filepath)
init {
initView()
initEvent()
}
private fun initEvent() {
Disposer.register(disposable, object : Disposable {
override fun dispose() {
if (disposed.compareAndSet(false, true)) frame.dispose()
}
})
addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
if (disposed.compareAndSet(false, true)) Disposer.dispose(disposable)
enableManager.setFlag("Plugins.editor.dialog.width", width)
enableManager.setFlag("Plugins.editor.dialog.height", height)
enableManager.setFlag("Plugins.editor.dialog.extendedState", extendedState)
}
override fun windowClosing(e: WindowEvent?) {
if (editorPanel.changes()) {
if (OptionPane.showConfirmDialog(
frame,
EditorI18n.getString("termora.plugins.editor.not-save"),
optionType = JOptionPane.OK_CANCEL_OPTION,
) == JOptionPane.OK_OPTION
) {
frame.dispose()
}
} else {
frame.dispose()
}
}
})
}
private fun initView() {
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
val state = enableManager.getFlag("Plugins.editor.dialog.extendedState", 0)
if ((state and MAXIMIZED_BOTH) == MAXIMIZED_BOTH) {
frame.setLocationRelativeTo(null)
frame.extendedState = state
} else {
val mySize = size
mySize.width = max(enableManager.getFlag("Plugins.editor.dialog.width", mySize.width), mySize.width)
mySize.height = max(enableManager.getFlag("Plugins.editor.dialog.height", mySize.height), mySize.height)
size = mySize
setLocationRelativeTo(owner)
}
title = file.name
iconImages = owner.iconImages
defaultCloseOperation = DO_NOTHING_ON_CLOSE
rootPane.contentPane.layout = BorderLayout()
rootPane.contentPane.add(editorPanel, BorderLayout.CENTER)
}
}

View File

@@ -0,0 +1,13 @@
package app.termora.plugins.editor
import app.termora.NamedI18n
import org.slf4j.Logger
import org.slf4j.LoggerFactory
object EditorI18n : NamedI18n("i18n/messages") {
private val log = LoggerFactory.getLogger(EditorI18n::class.java)
override fun getLogger(): Logger {
return log
}
}

View File

@@ -35,7 +35,7 @@ import javax.swing.event.DocumentEvent
import kotlin.math.max
import kotlin.math.min
class EditorPanel(private val window: JDialog, private val file: File) : JPanel(BorderLayout()) {
class EditorPanel(private val window: JFrame, private val file: File) : JPanel(BorderLayout()) {
companion object {
private val log = LoggerFactory.getLogger(EditorPanel::class.java)

View File

@@ -14,7 +14,7 @@ class MyTransportEditFileExtension private constructor() : TransportEditFileExte
override fun edit(owner: Window, path: Path): Disposable {
val disposable = Disposer.newDisposable()
SwingUtilities.invokeLater { EditorDialog(path, owner, disposable).isVisible = true }
SwingUtilities.invokeLater { EditorFrame(path, owner, disposable).isVisible = true }
return disposable
}
}

View File

@@ -0,0 +1,2 @@
termora.plugins.editor.not-save=The file has not been saved. Are you sure you want to exit?

View File

@@ -0,0 +1 @@
termora.plugins.editor.not-save=Файл не сохранён. Вы уверены, что хотите выйти?

View File

@@ -0,0 +1 @@
termora.plugins.editor.not-save=文件尚未保存,你确定要退出吗?

View File

@@ -0,0 +1 @@
termora.plugins.editor.not-save=檔案尚未儲存,你確定要退出嗎?

View File

@@ -9,7 +9,7 @@ dependencies {
compileOnly(project(":"))
implementation("com.maxmind.geoip2:geoip2:4.3.1")
// https://github.com/hstyi/geolite2
implementation("com.github.hstyi:geolite2:v1.0-202507280101")
implementation("com.github.hstyi:geolite2:v1.0-202508040102")
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -4,7 +4,7 @@ plugins {
project.version = "0.0.4"
project.version = "0.0.5"
dependencies {

View File

@@ -1,9 +1,6 @@
package app.termora.plugins.serial
import app.termora.Host
import app.termora.Icons
import app.termora.PtyHostTerminalTab
import app.termora.WindowScope
import app.termora.*
import app.termora.terminal.PtyConnector
import org.apache.commons.io.Charsets
import java.nio.charset.StandardCharsets
@@ -11,6 +8,8 @@ import javax.swing.Icon
class SerialTerminalTab(windowScope: WindowScope, host: Host) :
PtyHostTerminalTab(windowScope, host) {
override suspend fun openPtyConnector(): PtyConnector {
val serialPort = Serials.openPort(host)
return SerialPortPtyConnector(
@@ -19,6 +18,10 @@ class SerialTerminalTab(windowScope: WindowScope, host: Host) :
)
}
override fun createReconnectTerminalTab(): TerminalTab {
return SerialTerminalTab(windowScope, host)
}
override fun getIcon(): Icon {
return Icons.plugin
}

View File

@@ -12,6 +12,7 @@ enum class AppLayout {
* macOS
*/
App,
AppStore,
/**
* Linux

View File

@@ -0,0 +1,21 @@
package app.termora
import com.formdev.flatlaf.util.SystemInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
internal class ApplePressAndHoldEnabledApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
companion object {
val instance = ApplePressAndHoldEnabledApplicationRunnerExtension()
}
override fun ready() {
if (SystemInfo.isMacOS.not()) return
swingCoroutineScope.launch(Dispatchers.IO) {
Runtime.getRuntime()
.exec(arrayOf("defaults", "write", "app.termora", "ApplePressAndHoldEnabled", "-bool", "false"))
.waitFor()
}
}
}

View File

@@ -9,6 +9,7 @@ internal class FramePlugin : InternalPlugin() {
init {
support.addExtension(DatabasePropertiesChangedExtension::class.java) { KeymapRefresher.getInstance() }
support.addExtension(DatabaseChangedExtension::class.java) { KeymapRefresher.getInstance() }
support.addExtension(ApplicationRunnerExtension::class.java) { ApplePressAndHoldEnabledApplicationRunnerExtension.instance }
}
override fun getName(): String {

View File

@@ -3,14 +3,16 @@ package app.termora
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProvider
import app.termora.actions.DataProviders
import app.termora.terminal.*
import app.termora.terminal.ControlCharacters
import app.termora.terminal.DataKey
import app.termora.terminal.DataListener
import app.termora.terminal.Terminal
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.swing.Swing
import org.apache.commons.lang3.StringUtils
import java.beans.PropertyChangeEvent
import java.util.*
import javax.swing.Icon
@@ -27,13 +29,8 @@ abstract class HostTerminalTab(
protected val terminalTabbedManager
get() = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
.getData(DataProviders.TerminalTabbedManager)
protected val coroutineScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Swing) }
protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Swing)
protected val terminalModel get() = terminal.getTerminalModel()
protected var unread = false
set(value) {
field = value
firePropertyChange(PropertyChangeEvent(this, "icon", null, null))
}
/* visualTerminal */
@@ -45,15 +42,6 @@ abstract class HostTerminalTab(
terminal.getTerminalModel().setData(Host, host)
terminal.getTerminalModel().addDataListener(object : DataListener {
override fun onChanged(key: DataKey<*>, data: Any) {
if (key == VisualTerminal.Written) {
if (hasFocus || unread) {
return
}
// 如果当前选中的不是这个 Tab那么设置成未读
if (terminalTabbedManager?.getSelectedTerminalTab() != this@HostTerminalTab) {
unread = true
}
}
}
})
}
@@ -75,8 +63,6 @@ abstract class HostTerminalTab(
override fun onGrabFocus() {
super.onGrabFocus()
if (!unread) return
unread = false
}
@Suppress("UNCHECKED_CAST")

View File

@@ -533,7 +533,7 @@ class AuraLaf : FlatPropertiesLaf("Aura", Properties().apply {
TerminalColor.Bright.WHITE -> 0xffffff
TerminalColor.Basic.SELECTION_BACKGROUND,
TerminalColor.Cursor.BACKGROUND -> 0xedecee
TerminalColor.Cursor.BACKGROUND -> 0xacacac
else -> Int.MAX_VALUE
}

View File

@@ -2,19 +2,21 @@ package app.termora
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import app.termora.actions.SwitchTabAction
import app.termora.database.DatabaseManager
import app.termora.keymap.KeyShortcut
import app.termora.keymap.KeymapManager
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.ui.FlatTabbedPaneUI
import org.apache.commons.lang3.StringUtils
import java.awt.*
import java.awt.event.*
import java.awt.image.BufferedImage
import java.util.*
import javax.swing.ImageIcon
import javax.swing.JDialog
import javax.swing.JLabel
import javax.swing.SwingUtilities
import javax.swing.*
import kotlin.math.abs
class MyTabbedPane : FlatTabbedPane() {
internal class MyTabbedPane : FlatTabbedPane(), Disposable {
private val dragMouseAdaptor = DragMouseAdaptor()
private val terminalTabbedManager
@@ -23,6 +25,30 @@ class MyTabbedPane : FlatTabbedPane() {
private val owner
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
.getData(DataProviders.TermoraFrame) as TermoraFrame
private val keymap get() = KeymapManager.getInstance().getActiveKeymap()
private val tabOrder get() = DatabaseManager.getInstance().appearance.tabOrder
private val isScreen get() = TermoraLayout.Layout == TermoraLayout.Screen
private var isSwitchTabMode = false
set(value) {
if (tabOrder == TabOrder.Always.name) {
if (field.not()) {
field = true
repaint()
}
return
} else if (tabOrder == TabOrder.Hide.name) {
if (field) {
field = false
repaint()
}
return
} else if (tabOrder == TabOrder.AsNeed.name) {
if (field != value) {
field = value
repaint()
}
}
}
init {
isFocusable = false
@@ -38,6 +64,16 @@ class MyTabbedPane : FlatTabbedPane() {
private fun initEvents() {
addMouseListener(dragMouseAdaptor)
addMouseMotionListener(dragMouseAdaptor)
val awtEventListener = MyAWTEventListener()
toolkit.addAWTEventListener(awtEventListener, AWTEvent.KEY_EVENT_MASK or AWTEvent.WINDOW_EVENT_MASK)
Disposer.register(this, object : Disposable {
override fun dispose() {
toolkit.removeAWTEventListener(awtEventListener)
}
})
}
override fun processMouseEvent(e: MouseEvent) {
@@ -70,6 +106,32 @@ class MyTabbedPane : FlatTabbedPane() {
firePropertyChange("selectedIndex", oldIndex, index)
}
override fun updateUI() {
super.updateUI()
setUI(MyMyTabbedPaneUI())
}
private inner class MyAWTEventListener : AWTEventListener {
override fun eventDispatched(event: AWTEvent) {
if (event is KeyEvent) {
if (isSwitchTabMode) isSwitchTabMode = false
val shortcuts = keymap.getShortcut(SwitchTabAction.SWITCH_TAB)
if (shortcuts.isEmpty()) return
val shortcut = shortcuts.first() as KeyShortcut
val modifiers = KeyStroke.getKeyStroke(event.keyCode, event.modifiersEx).modifiers
if (shortcut.keyStroke.modifiers != modifiers) return
if (SwingUtilities.getWindowAncestor(event.component) != owner) return
if (isSwitchTabMode.not()) isSwitchTabMode = true
} else if (event is WindowEvent) {
if (event.id == WindowEvent.WINDOW_LOST_FOCUS || event.id == WindowEvent.WINDOW_DEACTIVATED) {
if (isSwitchTabMode) isSwitchTabMode = false
} else if (event.id == WindowEvent.WINDOW_GAINED_FOCUS || event.id == WindowEvent.WINDOW_ACTIVATED) {
// 触发一次刷新
isSwitchTabMode = isSwitchTabMode
}
}
}
}
private inner class DragMouseAdaptor : MouseAdapter(), KeyEventDispatcher {
private var mousePressedPoint = Point()
@@ -267,5 +329,81 @@ class MyTabbedPane : FlatTabbedPane() {
}
}
private inner class MyMyTabbedPaneUI : FlatTabbedPaneUI() {
override fun paintIcon(
g: Graphics,
tabPlacement: Int,
tabIndex: Int,
icon: Icon,
iconRect: Rectangle?,
isSelected: Boolean
) {
super.paintIcon(g, tabPlacement, tabIndex, MyIcon(icon, tabIndex, isSelected), iconRect, isSelected)
}
override fun createMoreTabsButton(): JButton {
return MyMoreTabsButton()
}
private inner class MyMoreTabsButton : FlatMoreTabsButton() {
override fun createTabMenuItem(tabIndex: Int): JMenuItem? {
val item = super.createTabMenuItem(tabIndex)
if (tabIndex == 0 && isScreen) {
item.text = Application.getName()
}
return item
}
}
}
override fun getIconAt(index: Int): Icon? {
if (isSwitchTabMode) {
return MyIcon(super.getIconAt(index), index, selectedIndex == index)
}
return super.getIconAt(index)
}
private inner class MyIcon(private val icon: Icon, private val tabIndex: Int, private val isSelected: Boolean) :
Icon {
override fun paintIcon(c: Component, g: Graphics, x: Int, y: Int) {
if (isScreen && tabIndex == 0) {
icon.paintIcon(c, g, x, y)
return
}
if (isSwitchTabMode.not()) {
icon.paintIcon(c, g, x, y)
return
}
if (g !is Graphics2D) return
g.save()
setupAntialiasing(g)
val fm = g.getFontMetrics(g.font)
val text = "${tabIndex + 1}"
val textWidth = fm.stringWidth(text)
val textHeight = fm.ascent
val centerX = x + (icon.iconWidth - textWidth) / 2
val centerY = y + (icon.iconHeight + textHeight) / 2 - 1
g.color = c.getForeground()
g.drawString(text, centerX, centerY)
g.restore()
}
override fun getIconWidth(): Int {
return icon.iconWidth
}
override fun getIconHeight(): Int {
return icon.iconHeight
}
}
}

View File

@@ -173,9 +173,20 @@ abstract class PtyHostTerminalTab(
}
override fun reconnect() {
stop()
start()
val manager = terminalTabbedManager ?: return
val index = manager.indexOfTerminalTab(this)
if (index < 0) return
val tab = createReconnectTerminalTab()
manager.addTerminalTab(index, tab, true)
manager.closeTerminalTab(this, true)
if (tab is HostTerminalTab) {
tab.start()
}
}
protected abstract fun createReconnectTerminalTab(): TerminalTab
override fun getJComponent(): JComponent {
return terminalPanel

View File

@@ -21,6 +21,7 @@ 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.WindowUtils
import com.sun.jna.platform.win32.Shell32
import com.sun.jna.platform.win32.ShlObj
import com.sun.jna.platform.win32.WinDef
@@ -114,6 +115,7 @@ class SettingsOptionsPane : OptionsPane() {
val languageComboBox = FlatComboBox<String>()
val backgroundComBoBox = YesOrNoComboBox()
val confirmTabCloseComBoBox = YesOrNoComboBox()
val tabOrderComboBox = FlatComboBox<TabOrder>()
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
val preferredThemeBtn = JButton(Icons.settings)
val opacitySpinner = NumberSpinner(100, 0, 100)
@@ -128,6 +130,12 @@ class SettingsOptionsPane : OptionsPane() {
private fun initView() {
tabOrderComboBox.addItem(TabOrder.Hide)
tabOrderComboBox.addItem(TabOrder.AsNeed)
tabOrderComboBox.addItem(TabOrder.Always)
tabOrderComboBox.selectedItem = runCatching { TabOrder.valueOf(appearance.tabOrder) }
.getOrNull() ?: TabOrder.Hide
layoutComboBox.addItem(TermoraLayout.Screen)
layoutComboBox.addItem(TermoraLayout.Fence)
layoutComboBox.renderer = object : DefaultListCellRenderer() {
@@ -162,7 +170,8 @@ class SettingsOptionsPane : OptionsPane() {
backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS
opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows
opacitySpinner.isEnabled = (SystemInfo.isMacOS || SystemInfo.isWindows)
|| (SystemInfo.isLinux && WindowUtils.isWindowAlphaSupported())
opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
override fun getNextValue(): Any {
return super.getNextValue() ?: maximum
@@ -226,6 +235,14 @@ class SettingsOptionsPane : OptionsPane() {
}
})
tabOrderComboBox.addItemListener(object : ItemListener {
override fun itemStateChanged(e: ItemEvent) {
if (e.stateChange == ItemEvent.SELECTED) {
appearance.tabOrder = tabOrderComboBox.selectedItem?.toString() ?: return
}
}
})
opacitySpinner.addChangeListener {
val opacity = opacitySpinner.value
if (opacity is Double) {
@@ -349,7 +366,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun getFormPanel(): JPanel {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
val box = FlatToolBar()
box.add(followSystemCheckBox)
@@ -380,6 +397,9 @@ class SettingsOptionsPane : OptionsPane() {
builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows)
.add(backgroundComBoBox).xy(3, rows).apply { rows += step }
builder.add("${I18n.getString("termora.settings.appearance.tab-order")}:").xy(1, rows)
.add(tabOrderComboBox).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))
@@ -404,6 +424,7 @@ class SettingsOptionsPane : OptionsPane() {
private val fontSizeTextField = IntSpinner(0, 9, 99)
private val terminalSetting get() = DatabaseManager.getInstance().terminal
private val selectCopyComboBox = YesOrNoComboBox()
private val rightClickComboBox = OutlineComboBox<String>()
private val autoCloseTabComboBox = YesOrNoComboBox()
private val floatingToolbarComboBox = YesOrNoComboBox()
private val hyperlinkComboBox = YesOrNoComboBox()
@@ -417,6 +438,12 @@ class SettingsOptionsPane : OptionsPane() {
}
}
rightClickComboBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
terminalSetting.rightClick = rightClickComboBox.selectedItem as String
}
}
fallbackFontComboBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
terminalSetting.fallbackFont = fallbackFontComboBox.selectedItem as String
@@ -518,6 +545,10 @@ class SettingsOptionsPane : OptionsPane() {
fontSizeTextField.value = terminalSetting.fontSize
maxRowsTextField.value = terminalSetting.maxRows
rightClickComboBox.addItem("Copy")
rightClickComboBox.addItem("CopyAndPaste")
rightClickComboBox.selectedItem = terminalSetting.rightClick
cursorStyleComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
@@ -532,6 +563,24 @@ class SettingsOptionsPane : OptionsPane() {
}
}
rightClickComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
var text = value?.toString()
if (value == "Copy") {
text = I18n.getString("termora.settings.terminal.right-click.copy")
} else if (value == "CopyAndPaste") {
text = I18n.getString("termora.settings.terminal.right-click.copy-and-paste")
}
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
}
}
cursorStyleComboBox.addItem(CursorStyle.Block)
cursorStyleComboBox.addItem(CursorStyle.Bar)
cursorStyleComboBox.addItem(CursorStyle.Underline)
@@ -595,7 +644,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, left:pref, $FORM_MARGIN, pref, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
val beepBtn = JButton(Icons.run)
@@ -624,6 +673,8 @@ class SettingsOptionsPane : OptionsPane() {
.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.right-click")}:").xy(1, rows)
.add(rightClickComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
.add(cursorStyleComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.terminal.cursor-blink")}:").xy(1, rows)

View File

@@ -0,0 +1,7 @@
package app.termora
internal enum class TabOrder {
Hide,
AsNeed,
Always,
}

View File

@@ -337,13 +337,7 @@ class TerminalTabbed(
val c = tab.getJComponent()
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
tabbedPane.insertTab(
title,
tab.getIcon(),
c,
StringUtils.EMPTY,
index
)
tabbedPane.insertTab(title, tab.getIcon(), c, StringUtils.EMPTY, index)
// 设置标题
c.putClientProperty(titleProperty, title)
@@ -367,6 +361,10 @@ class TerminalTabbed(
}
}
override fun indexOfTerminalTab(tab: TerminalTab):Int {
return tabbedPane.indexOfComponent(tab.getJComponent())
}
private inner class SwitchFindEverywhereResult(
private val title: String,
private val icon: Icon?,

View File

@@ -8,4 +8,5 @@ interface TerminalTabbedManager {
fun setSelectedTerminalTab(tab: TerminalTab)
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
fun refreshTerminalTabs()
fun indexOfTerminalTab(tab: TerminalTab): Int
}

View File

@@ -72,10 +72,12 @@ class TermoraFencePanel(
leftTreePanel.addComponentListener(object : ComponentAdapter() {
override fun componentHidden(e: ComponentEvent) {
toolbar.isVisible = true
enableManager.setFlag("Termora.Fence.colspan", true)
}
override fun componentShown(e: ComponentEvent) {
toolbar.isVisible = false
enableManager.setFlag("Termora.Fence.colspan", false)
}
})
@@ -86,6 +88,16 @@ class TermoraFencePanel(
toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK
), "toggle"
)
splitPane.addPropertyChangeListener("dividerLocation") {
if (leftTreePanel.isVisible)
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
}
if (enableManager.getFlag("Termora.Fence.colspan", false)) {
toggle()
}
}
private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable {
@@ -144,19 +156,19 @@ class TermoraFencePanel(
}
override fun actionPerformed(evt: AnActionEvent) {
toggle()
}
}
}
private fun toggle() {
if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation
leftTreePanel.isVisible = leftTreePanel.isVisible.not()
if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
}
}
mySplitPane.doLayout()
}
override fun dispose() {
if (leftTreePanel.isVisible)
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
}
fun getHostTree(): NewHostTree {
return leftTreePanel.hostTree
}

View File

@@ -164,6 +164,8 @@ class TermoraFrame : JFrame(), DataProvider {
}).let { Disposer.register(windowScope, it) }
Disposer.register(windowScope, tabbedPane)
}
private fun initView() {

View File

@@ -5,6 +5,7 @@ import app.termora.plugin.ExtensionManager
import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary
import com.formdev.flatlaf.util.SystemInfo
import com.sun.jna.Pointer
import com.sun.jna.platform.WindowUtils
import com.sun.jna.platform.win32.User32
import com.sun.jna.platform.win32.WinDef
import com.sun.jna.platform.win32.WinUser.*
@@ -206,7 +207,7 @@ class TermoraFrameManager : Disposable {
}
fun setOpacity(opacity: Double) {
if (opacity < 0 || opacity > 1 || SystemInfo.isLinux) return
if (opacity < 0 || opacity > 1) return
for (window in getWindows()) {
setOpacity(window, opacity)
}
@@ -227,6 +228,8 @@ class TermoraFrameManager : Disposable {
User32.INSTANCE.SetWindowLong(hwnd, GWL_EXSTYLE, exStyle or WS_EX_LAYERED)
}
User32.INSTANCE.SetLayeredWindowAttributes(hwnd, 0, alpha, LWA_ALPHA)
} else if (SystemInfo.isLinux && WindowUtils.isWindowAlphaSupported()) {
WindowUtils.setWindowAlpha(window, opacity.toFloat())
}
}

View File

@@ -224,7 +224,7 @@ class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProv
override fun getTitle(): String {
return I18n.getString("termora.title")
return StringUtils.EMPTY
}
override fun getIcon(): Icon {

View File

@@ -8,7 +8,7 @@ object DataProviders {
val Terminal = DataKey(app.termora.terminal.Terminal::class)
val TerminalWriter get() = DataKey.TerminalWriter
val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
internal val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
val TerminalTab = DataKey(app.termora.TerminalTab::class)
val TerminalTabbedManager = DataKey(app.termora.TerminalTabbedManager::class)

View File

@@ -666,6 +666,11 @@ class DatabaseManager private constructor() : Disposable {
*/
var selectCopy by BooleanPropertyDelegate(false)
/**
* 右键点击Copy、CopyAndPaste
*/
var rightClick by StringPropertyDelegate("Copy")
/**
* 光标样式
*/
@@ -716,6 +721,11 @@ class DatabaseManager private constructor() : Disposable {
*/
var layout by StringPropertyDelegate(TermoraLayout.Screen.name)
/**
* 标签序号
*/
var tabOrder by StringPropertyDelegate(TabOrder.Hide.name)
/**
* 跟随系统
*/

View File

@@ -82,7 +82,7 @@ class NewKeywordHighlightDialog(
FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON
)
matchCaseBtn.toolTipText = "Match case"
matchCaseBtn.toolTipText = I18n.getString("termora.match-case")
val box = FlatToolBar()

View File

@@ -31,7 +31,7 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
}
override fun getIcon(): Icon {
return if (unread) Icons.terminalUnread else Icons.terminal
return Icons.terminal
}
override fun willBeClose(): Boolean {
@@ -62,6 +62,9 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
) == JOptionPane.OK_OPTION
}
override fun createReconnectTerminalTab(): TerminalTab {
return LocalTerminalTab(windowScope, host)
}
private fun getPtyProcessConnector(): PtyProcessConnector? {
var p = getPtyConnector() as PtyConnector?

View File

@@ -185,6 +185,10 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
return Icons.fileFormat
}
override fun createReconnectTerminalTab(): TerminalTab {
return SFTPPtyTerminalTab(windowScope, host)
}
override fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
// Nothing
}

View File

@@ -114,7 +114,8 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
?: AltKeyModifier.EightBit.name),
"keywordHighlightSetId" to ((terminalOption.highlightSetComboBox.selectedItem as? KeywordHighlight)?.id
?: "-1"),
"timeout" to (terminalOption.timeoutTextField.value ?: 60).toString()
"timeout" to (terminalOption.timeoutTextField.value ?: 60).toString(),
"forwardAgent" to tunnelingOption.forwardAgentCheckBox.isSelected.toString(),
)
)
@@ -182,6 +183,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
tunnelingOption.tunnelings.addAll(host.tunnelings)
tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding
tunnelingOption.x11ServerTextField.text = StringUtils.defaultIfBlank(host.options.x11Forwarding, "localhost:0")
tunnelingOption.forwardAgentCheckBox.isSelected = host.options.extras["forwardAgent"]?.toBoolean() ?: false
if (host.options.jumpHosts.isNotEmpty()) {
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
@@ -570,9 +572,10 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
}
}
protected inner class TunnelingOption : JPanel(BorderLayout()), Option {
private inner class TunnelingOption : JPanel(BorderLayout()), Option {
val tunnelings = mutableListOf<Tunneling>()
val x11ForwardingCheckBox = JCheckBox("X DISPLAY:")
val forwardAgentCheckBox = JCheckBox("Enable ForwardAgent")
val x11ServerTextField = OutlineTextField(255)
private val model = object : DefaultTableModel() {
@@ -649,6 +652,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
box.add(deleteBtn)
x11ForwardingCheckBox.isFocusable = false
forwardAgentCheckBox.isFocusable = false
if (x11ServerTextField.text.isBlank()) {
x11ServerTextField.text = "localhost:0"
@@ -662,6 +666,13 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
x11Forwarding.add(x11ForwardingCheckBox)
x11Forwarding.add(x11ServerTextField)
val forwardAgent = Box.createHorizontalBox()
forwardAgent.border = BorderFactory.createCompoundBorder(
BorderFactory.createTitledBorder("Agent Forwarding"),
BorderFactory.createEmptyBorder(4, 4, 4, 4)
)
forwardAgent.add(forwardAgentCheckBox)
x11ServerTextField.isEnabled = x11ForwardingCheckBox.isSelected
val panel = JPanel(BorderLayout())
@@ -670,8 +681,13 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
panel.add(box, BorderLayout.SOUTH)
panel.border = BorderFactory.createEmptyBorder(0, 0, 8, 0)
val forwardingBox = Box.createHorizontalBox()
forwardingBox.add(x11Forwarding)
forwardingBox.add(Box.createHorizontalStrut(4))
forwardingBox.add(forwardAgent)
add(panel, BorderLayout.CENTER)
add(x11Forwarding, BorderLayout.SOUTH)
add(forwardingBox, BorderLayout.SOUTH)
}

View File

@@ -54,6 +54,10 @@ class SSHTerminalTab(
return mutex.isLocked.not()
}
override fun createReconnectTerminalTab(): TerminalTab {
return SSHTerminalTab(windowScope, host)
}
override suspend fun openPtyConnector(): PtyConnector {
if (mutex.tryLock()) {
try {
@@ -79,15 +83,14 @@ class SSHTerminalTab(
}
val loading = coroutineScope.launch(Dispatchers.Swing) {
val braille = "⡿⣟⣯⣷⣾⣽⣻⢿".reversed().toCharArray()
// val braille = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏".toCharArray()
var c = 0
while (isActive) {
if (++c > 6) c = 1
terminal.write("${ControlCharacters.ESC}[1;32m")
terminal.write(".".repeat(c))
terminal.write(" ".repeat(6 - c))
terminal.write("${ControlCharacters.ESC}[0m")
delay(350.milliseconds)
terminal.write("${ControlCharacters.BS}".repeat(6))
if (++c >= braille.size) c = 0
terminal.write("${braille[c]}")
delay(100.milliseconds)
terminal.write("${ControlCharacters.BS}")
}
}
@@ -211,17 +214,6 @@ class SSHTerminalTab(
return super.getData(dataKey)
}
override fun reconnect() {
stop()
// 重新连接时就等于重新打开了一个标签handler 重置
handler.client = null
handler.session = null
handler.client = null
start()
}
override fun stop() {
if (mutex.tryLock()) {
try {
@@ -234,7 +226,7 @@ class SSHTerminalTab(
}
override fun getIcon(): Icon {
return if (unread) Icons.terminalUnread else Icons.terminal
return Icons.terminal
}
override fun beforeClose() {

View File

@@ -0,0 +1,14 @@
package app.termora.plugin.internal.ssh
import org.apache.sshd.agent.local.ChannelAgentForwardingFactory
import org.apache.sshd.common.FactoryManager
import org.apache.sshd.common.channel.ChannelFactory
import org.eclipse.jgit.internal.transport.sshd.agent.JGitSshAgentFactory
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
import java.io.File
internal class SshAgentFactory(factory: ConnectorFactory, homeDir: File?) : JGitSshAgentFactory(factory, homeDir) {
override fun getChannelForwardingFactories(manager: FactoryManager?): List<ChannelFactory> {
return listOf(ChannelAgentForwardingFactory.OPENSSH, ChannelAgentForwardingFactory.IETF)
}
}

View File

@@ -56,7 +56,6 @@ import org.apache.sshd.server.forward.AcceptAllForwardingFilter
import org.apache.sshd.server.forward.RejectAllForwardingFilter
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient
import org.eclipse.jgit.internal.transport.sshd.agent.JGitSshAgentFactory
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.proxy.AbstractClientProxyConnector
@@ -112,6 +111,8 @@ object SshClients {
env.putAll(host.options.envs())
val channel = session.createShellChannel(configuration, env)
channel.isAgentForwarding = host.options.extras["forwardAgent"]?.toBoolean() == true
if (host.options.enableX11Forwarding) {
if (channel is app.termora.x11.ChannelShell) {
channel.xForwarding = true
@@ -386,7 +387,7 @@ object SshClients {
val channelFactories = mutableListOf<ChannelFactory>()
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
channelFactories.add(X11ChannelFactory.Companion.INSTANCE)
channelFactories.add(X11ChannelFactory.INSTANCE)
builder.channelFactories(channelFactories)
val sshClient = builder.build() as JGitSshClient
@@ -395,12 +396,14 @@ object SshClients {
// JGit 会尝试读取本地的私钥或缓存的私钥
sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() }
// https://github.com/TermoraDev/termora/issues/1001
if (host.authentication.type == AuthenticationType.SSHAgent || host.options.extras["forwardAgent"]?.toBoolean() == true) {
// ssh-agent
sshClient.agentFactory = SshAgentFactory(ConnectorFactory.getDefault(), null)
}
// 设置优先级
if (host.authentication.type == AuthenticationType.PublicKey || host.authentication.type == AuthenticationType.SSHAgent) {
if (host.authentication.type == AuthenticationType.SSHAgent) {
// ssh-agent
sshClient.agentFactory = JGitSshAgentFactory(ConnectorFactory.getDefault(), null)
}
CoreModuleProperties.PREFERRED_AUTHS.set(
sshClient,
listOf(

View File

@@ -1,9 +1,6 @@
package app.termora.plugin.internal.telnet
import app.termora.Host
import app.termora.ProxyType
import app.termora.PtyHostTerminalTab
import app.termora.WindowScope
import app.termora.*
import app.termora.terminal.ControlCharacters
import app.termora.terminal.KeyEncoderImpl
import app.termora.terminal.PtyConnector
@@ -71,5 +68,9 @@ class TelnetTerminalTab(
return ptyConnectorFactory.decorate(TelnetStreamPtyConnector(telnet, telnet.charset, characterMode))
}
override fun createReconnectTerminalTab(): TerminalTab {
return TelnetTerminalTab(windowScope, host)
}
}

View File

@@ -1,8 +1,11 @@
package app.termora.plugin.internal.updater
import app.termora.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import org.semver4j.Semver
import org.slf4j.LoggerFactory
import java.awt.KeyboardFocusManager
@@ -15,12 +18,12 @@ internal class MyApplicationRunnerExtension private constructor() : ApplicationR
private val log = LoggerFactory.getLogger(MyApplicationRunnerExtension::class.java)
}
private val disabledUpdater get() = Application.getLayout() == AppLayout.Appx
private val disabledUpdater get() = Application.getLayout() == AppLayout.Appx || Application.getLayout() == AppLayout.AppStore
private val updaterManager get() = UpdaterManager.getInstance()
override fun ready() {
swingCoroutineScope.launch {
swingCoroutineScope.launch(Dispatchers.IO) {
try {
delay(3.seconds)
scheduleUpdate()
@@ -31,7 +34,7 @@ internal class MyApplicationRunnerExtension private constructor() : ApplicationR
}
private fun scheduleUpdate() {
private suspend fun scheduleUpdate() {
if (disabledUpdater) return
val latestVersion = updaterManager.fetchLatestVersion()
@@ -45,13 +48,15 @@ internal class MyApplicationRunnerExtension private constructor() : ApplicationR
return
}
withContext(Dispatchers.Swing) {
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
?: TermoraFrameManager.getInstance().getWindows().firstOrNull()
if (owner == null) return
if (owner != null) {
val dialog = UpdaterDialog(owner, latestVersion)
dialog.isModal = true
dialog.isVisible = true
}
}
}
}

View File

@@ -1,9 +1,6 @@
package app.termora.plugin.internal.wsl
import app.termora.Host
import app.termora.PtyConnectorFactory
import app.termora.PtyHostTerminalTab
import app.termora.WindowScope
import app.termora.*
import app.termora.terminal.PtyConnector
import org.apache.commons.io.Charsets
import org.apache.commons.io.FileUtils
@@ -51,6 +48,10 @@ class WSLHostTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
return ptyConnector
}
override fun createReconnectTerminalTab(): TerminalTab {
return WSLHostTerminalTab(windowScope, host)
}
override fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
// Nothing

View File

@@ -332,15 +332,16 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
var top = sr.getOrElse(0) { 1 }
var bottom = sr.getOrElse(1) { terminalModel.getRows() }
if (bottom <= top || top < 1) {
if (bottom <= top) {
if (log.isWarnEnabled) {
log.warn("Set Scrolling Region Error. top: $top , bottom: $bottom")
}
top = 1
bottom = terminalModel.getRows()
}
top = max(1, top)
bottom = min(terminalModel.getRows(), bottom)
// 设置滚动区域
terminal.getTerminalModel().setData(
DataKey.ScrollingRegion,

View File

@@ -185,8 +185,9 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
this.addMouseMotionListener(mouseAdapter)
// 超链接
val hyperlinkAdapter = TerminalPanelMouseHyperlinkAdapter(this, terminal)
val hyperlinkAdapter = TerminalPanelMouseHyperlinkAdapter(this, terminalDisplay, terminal)
this.addMouseListener(hyperlinkAdapter)
this.addMouseMotionListener(hyperlinkAdapter)
// 鼠标跟踪
val trackingAdapter = TerminalPanelMouseTrackingAdapter(this, terminal, writer)

View File

@@ -2,6 +2,8 @@ package app.termora.terminal.panel
import app.termora.terminal.ClickableHighlighter
import app.termora.terminal.Terminal
import com.formdev.flatlaf.util.SystemInfo
import java.awt.Cursor
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.SwingUtilities
@@ -11,11 +13,22 @@ import javax.swing.SwingUtilities
*/
class TerminalPanelMouseHyperlinkAdapter(
private val terminalPanel: TerminalPanel,
private val terminalDisplay: TerminalDisplay,
private val terminal: Terminal,
) : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
if (SwingUtilities.isLeftMouseButton(e)) {
if (SwingUtilities.isLeftMouseButton(e).not()) {
return
}
if (SystemInfo.isMacOS) {
if (e.isMetaDown.not())
return
} else if (e.isControlDown.not()) {
return
}
val position = terminalPanel.pointToPosition(e.point)
for (highlighter in terminal.getMarkupModel().getHighlighters(position)) {
if (highlighter is ClickableHighlighter) {
@@ -23,6 +36,18 @@ class TerminalPanelMouseHyperlinkAdapter(
}
}
}
override fun mouseMoved(e: MouseEvent) {
val position = terminalPanel.pointToPosition(e.point)
var cursor = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)
for (highlighter in terminal.getMarkupModel().getHighlighters(position)) {
if (highlighter is ClickableHighlighter) {
cursor = if (SystemInfo.isMacOS) Cursor.getDefaultCursor()
else Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
break
}
}
terminalDisplay.cursor = cursor
}

View File

@@ -3,6 +3,7 @@ package app.termora.terminal.panel
import app.termora.actions.AnActionEvent
import app.termora.actions.TerminalCopyAction
import app.termora.actions.TerminalPasteAction
import app.termora.database.DatabaseManager
import app.termora.terminal.*
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionManager
@@ -27,6 +28,7 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
private val isSelectCopy get() = terminalModel.getData(TerminalPanel.SelectCopy, false)
private val selectionModel get() = terminal.getSelectionModel()
private val wordBreakIterator = BreakIterator.getWordInstance()
private val rightClickMode get() = DatabaseManager.getInstance().terminal.rightClick
companion object {
private val log = LoggerFactory.getLogger(TerminalPanelMouseSelectionAdapter::class.java)
@@ -50,7 +52,7 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
if (SwingUtilities.isRightMouseButton(e)) {
// 如果有选中并且开启了选中复制,那么右键直接是粘贴
if (selectionModel.hasSelection() && !isSelectCopy) {
if (selectionModel.hasSelection() && isSelectCopy.not()) {
triggerCopyAction(
KeyEvent(
e.component,
@@ -61,6 +63,20 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
'C'
)
)
if (rightClickMode == "CopyAndPaste") {
triggerPasteAction(
KeyEvent(
e.component,
KeyEvent.KEY_PRESSED,
e.`when`,
e.modifiersEx,
KeyEvent.VK_V,
'V'
)
)
}
} else {
// paste
triggerPasteAction(

View File

@@ -1,9 +1,7 @@
package app.termora.terminal.panel.vw
import app.termora.*
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import app.termora.actions.*
import app.termora.plugin.internal.badge.Badge
import app.termora.plugin.internal.ssh.SSHTerminalTab
import app.termora.plugin.internal.ssh.SSHTerminalTab.Companion.SSHSession
@@ -39,7 +37,7 @@ import kotlin.time.Duration.Companion.milliseconds
internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
SSHVisualWindow(tab, "Transfer", visualWindowManager) {
SSHVisualWindow(tab, "Transfer", visualWindowManager), DataProvider {
companion object {
private val log = LoggerFactory.getLogger(TransferVisualWindow::class.java)
@@ -65,7 +63,7 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
private val downloadBtn = JButton(Icons.download)
private val badgePresentation = Badge.getInstance(tab.windowScope)
.addBadge(downloadBtn).apply { visible = false }
private val support = DataProviderSupport()
init {
initViews()
@@ -82,6 +80,8 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
add(panel, BorderLayout.CENTER)
support.addData(TransportViewer.MyTransferManager, transferManager)
}
private fun initEvents() {
@@ -240,6 +240,10 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
super.dispose()
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return support.getData(dataKey)
}
override fun toolbarButtons(): List<Pair<JButton, Position>> {
return listOf(downloadBtn to Position.Left, questionBtn to Position.Right)
}

View File

@@ -1,9 +1,6 @@
package app.termora.tlog
import app.termora.Host
import app.termora.Icons
import app.termora.PtyHostTerminalTab
import app.termora.WindowScope
import app.termora.*
import app.termora.terminal.PtyConnector
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -71,6 +68,10 @@ class LogViewerTerminalTab(
return false
}
override fun createReconnectTerminalTab(): TerminalTab {
throw UnsupportedOperationException()
}
override fun canClone(): Boolean {
return false
}

View File

@@ -2,6 +2,7 @@ package app.termora.transfer
import app.termora.*
import app.termora.transfer.InternalTransferManager.TransferMode
import app.termora.transfer.TransportPanel.Companion.isWindowsFileSystem
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import kotlinx.coroutines.CoroutineScope
@@ -95,8 +96,12 @@ internal class DefaultInternalTransferManager(
val context = AskTransferContext(TransferAction.Overwrite, false)
for (pair in paths) {
if (mode == TransferMode.Transfer && context.applyAll.not()) {
var name = pair.first.name
if (targetWorkdir.fileSystem.isWindowsFileSystem()) {
name = name.replace(":", "-")
}
val action = withContext(Dispatchers.Swing) {
getTransferAction(context, targetWorkdir.resolve(pair.first.name), pair.second)
getTransferAction(context, targetWorkdir.resolve(name), pair.second)
}
if (action == null) {
break
@@ -272,8 +277,11 @@ internal class DefaultInternalTransferManager(
val isDirectory = pair.second.isDirectory
val path = pair.first
if (isDirectory.not() || mode == TransferMode.Rmrf) {
val transfer =
createTransfer(path, workdir.resolve(path.name), isDirectory, StringUtils.EMPTY, mode, action)
var name = path.name
if (workdir.fileSystem.isWindowsFileSystem()) {
name = name.replace(":", "-")
}
val transfer = createTransfer(path, workdir.resolve(name), isDirectory, StringUtils.EMPTY, mode, action)
return if (transferManager.addTransfer(transfer)) FileVisitResult.CONTINUE else FileVisitResult.TERMINATE
}

View File

@@ -106,6 +106,7 @@ internal class TransportPanel(
private val loadingPanel = LoadingPanel()
private val model = TransportTableModel()
private val table = JTable(model)
private val tableScrollPane = JScrollPane(table)
private val sorter = TableRowSorter(table.model)
private var hasParent = false
private val panel get() = this
@@ -211,7 +212,7 @@ internal class TransportPanel(
table.setDefaultRenderer(Any::class.java, MyDefaultTableCellRenderer())
val scrollPane = JScrollPane(table)
val scrollPane = tableScrollPane
scrollPane.apply { border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) }
layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER as Any)
@@ -241,7 +242,11 @@ internal class TransportPanel(
Disposer.register(this, editTransferListener)
refreshBtn.addActionListener { reload(requestFocus = true) }
refreshBtn.addActionListener {
val filename = getSelectFilename()
if (filename != null) registerSelectRow(filename)
reload(requestFocus = true)
}
prevBtn.addActionListener { navigator.back() }
nextBtn.addActionListener { navigator.forward() }
@@ -303,11 +308,16 @@ internal class TransportPanel(
if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return
}
if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) {
if (loading) {
registerNextReloadCallback { reload(requestFocus = false) }
} else {
val c = {
val filename = getSelectFilename()
if (filename != null) registerSelectRow(filename)
reload(requestFocus = false)
}
if (loading) {
registerNextReloadCallback { c.invoke() }
} else {
c.invoke()
}
}
}
}).let { Disposer.register(this, it) }
@@ -658,6 +668,9 @@ internal class TransportPanel(
}
fun registerSelectRow(name: String) {
val verticalValue = tableScrollPane.verticalScrollBar.value
val horizontalValue = tableScrollPane.horizontalScrollBar.value
registerNextReloadCallback {
for (i in 0 until model.rowCount) {
if (model.getAttributes(i).name == name) {
@@ -665,12 +678,22 @@ internal class TransportPanel(
table.clearSelection()
table.setRowSelectionInterval(c, c)
table.scrollRectToVisible(table.getCellRect(c, TransportTableModel.COLUMN_NAME, true))
tableScrollPane.verticalScrollBar.value = verticalValue
tableScrollPane.horizontalScrollBar.value = horizontalValue
break
}
}
}
}
fun getSelectFilename(): String? {
val row = table.selectedRow
if (row < 0) return null
val c = sorter.convertRowIndexToModel(row)
if (c < 0) return null
return model.getAttributes(c).name
}
private fun registerNextReloadCallback(block: () -> Unit) {
nextReloadCallbacks.computeIfAbsent(mod.get()) { mutableListOf() }
.add(block)
@@ -1083,6 +1106,8 @@ internal class TransportPanel(
} else if (actionCommand == TransportPopupMenu.ActionCommand.Delete) {
transfer(InternalTransferManager.TransferMode.Delete)
} else if (actionCommand == TransportPopupMenu.ActionCommand.Refresh) {
val filename = getSelectFilename()
if (filename != null) registerSelectRow(filename)
reload(requestFocus = true)
} else if (actionCommand == TransportPopupMenu.ActionCommand.Edit) {
edit()
@@ -1139,7 +1164,9 @@ internal class TransportPanel(
private fun edit() {
for (path in files.map { it.first }) {
val target = Application.createSubTemporaryDir().resolve(path.name)
var name = path.name
if (SystemInfo.isWindows) name = name.replace(":", "-")
val target = Application.createSubTemporaryDir().resolve(name)
val transferId = internalTransferManager.addHighTransfer(path, target)
editTransferListener.addListenTransfer(transferId)
}

View File

@@ -1,12 +1,10 @@
package app.termora.transfer
import app.termora.Application
import app.termora.ApplicationScope
import app.termora.I18n
import app.termora.OptionPane
import app.termora.*
import app.termora.plugin.ExtensionManager
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
import com.formdev.flatlaf.extras.components.FlatPopupMenu
import kotlinx.coroutines.launch
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.apache.sshd.sftp.client.fs.SftpFileSystem
@@ -149,7 +147,12 @@ internal class TransportPopupMenu(
}
private fun initEvents() {
transferMenu.addActionListener { fireActionPerformed(it, ActionCommand.Transfer) }
transferMenu.addActionListener {
swingCoroutineScope.launch {
fireActionPerformed(it, ActionCommand.Transfer)
}
}
deleteMenu.addActionListener {
if (OptionPane.showConfirmDialog(
owner,

View File

@@ -6,7 +6,7 @@
<Identity Name="TermoraDev.Termora"
Publisher="CN=C804E131-4368-4BF7-9E7F-95C681AD0AAC"
Version="@version@.0"
Version="@version@.@betaVersion@"
ProcessorArchitecture="@architecture@"/>
<Properties>

View File

@@ -45,6 +45,7 @@ termora.settings.appearance.i-want-to-translate=I want to translate
termora.settings.appearance.follow-system=Sync with OS
termora.settings.appearance.opacity=Opacity
termora.settings.appearance.background-running=Backgrounding
termora.settings.appearance.tab-order=Tab Order
termora.settings.appearance.confirm-tab-close=Confirm tab close
termora.settings.terminal=Terminal
@@ -56,6 +57,9 @@ 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.right-click=Right click
termora.settings.terminal.right-click.copy-and-paste=Copy and Paste
termora.settings.terminal.right-click.copy=${termora.copy}
termora.settings.terminal.cursor-style=Cursor type
termora.settings.terminal.cursor-blink=Cursor blink
termora.settings.terminal.local-shell=Local shell

View File

@@ -40,6 +40,7 @@ termora.settings.appearance.follow-system=Как в системе
termora.settings.appearance.opacity=Прозрачность
termora.settings.appearance.background-image=Фоновое изображение
termora.settings.appearance.background-running=Фон
termora.settings.appearance.tab-order=Порядок вкладок
termora.settings.appearance.confirm-tab-close=Подтверждение закрытия вкладки
termora.setting.security=Безопасность
@@ -59,6 +60,8 @@ termora.settings.terminal.debug=Режим отладки
termora.settings.terminal.beep=Сигнал
termora.settings.terminal.hyperlink=Ссылки
termora.settings.terminal.select-copy=Копировать выделенное
termora.settings.terminal.right-click=правой кнопкой мыши
termora.settings.terminal.right-click.copy-and-paste=Копировать и вставить
termora.settings.terminal.cursor-style=Вид курсора
termora.settings.terminal.cursor-blink=Мигать курсором
termora.settings.terminal.local-shell=Локальный терминал

View File

@@ -49,6 +49,7 @@ termora.settings.appearance.i-want-to-translate=我想要翻译
termora.settings.appearance.follow-system=跟随系统
termora.settings.appearance.opacity=透明度
termora.settings.appearance.background-running=后台运行
termora.settings.appearance.tab-order=标签序号
termora.settings.appearance.confirm-tab-close=标签关闭前确认
# Find everywhere
@@ -70,6 +71,8 @@ termora.settings.terminal.debug=调试模式
termora.settings.terminal.beep=蜂鸣声
termora.settings.terminal.hyperlink=超链接
termora.settings.terminal.select-copy=选中复制
termora.settings.terminal.right-click=右键点击
termora.settings.terminal.right-click.copy-and-paste=复制 & 粘贴
termora.settings.terminal.cursor-style=光标样式
termora.settings.terminal.cursor-blink=光标闪烁
termora.settings.terminal.local-shell=本地终端

View File

@@ -48,6 +48,7 @@ termora.settings.appearance.i-want-to-translate=我想要翻譯
termora.settings.appearance.follow-system=跟隨系統
termora.settings.appearance.opacity=透明度
termora.settings.appearance.background-running=後台運行
termora.settings.appearance.tab-order=標籤序號
termora.settings.appearance.confirm-tab-close=關閉分頁確認
termora.settings.keymap=鍵盤
@@ -79,8 +80,10 @@ termora.settings.terminal.size=大小
termora.settings.terminal.max-rows=最大行數
termora.settings.terminal.debug=偵錯模式
termora.settings.terminal.beep=超連結
termora.settings.terminal.hyperlink=Hyperlink
termora.settings.terminal.hyperlink=超連結
termora.settings.terminal.select-copy=選取複製
termora.settings.terminal.right-click=右鍵點擊
termora.settings.terminal.right-click.copy-and-paste=複製 & 貼上
termora.settings.terminal.cursor-style=遊標風格
termora.settings.terminal.cursor-blink=遊標閃爍
termora.settings.terminal.local-shell=本地端

View File

@@ -1,4 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" fill="#EBECF0" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 716 B

After

Width:  |  Height:  |  Size: 701 B

View File

@@ -1,4 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" fill="#43454A" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 716 B

After

Width:  |  Height:  |  Size: 701 B