Compare commits

...

36 Commits

Author SHA1 Message Date
hstyi
dc4333da21 release: 1.0.15 2025-05-19 11:34:23 +08:00
hstyi
184f6d46dc fix: snippet scroll (#587) 2025-05-16 13:17:02 +08:00
hstyi
68788905fe chore: improve sftp tab (#583) 2025-05-14 23:24:52 +08:00
dependabot[bot]
fc46216a3f chore(deps): bump kotlin from 2.1.20 to 2.1.21 (#580)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-14 11:43:24 +08:00
hstyi
563143645e fix: SFTP drag and drop upload (#578) 2025-05-13 13:50:08 +08:00
hstyi
891ccb901b chore: maven-publish 2025-05-12 16:56:01 +08:00
hstyi
928a866fe7 feat: improve SFTP (#572) 2025-05-12 15:37:39 +08:00
hstyi
ea25b5b46f feat: modify permissions to support recursion (#571) 2025-05-12 15:26:29 +08:00
hstyi
1de10e6129 fix: process Device Status Report (DSR) (#570) 2025-05-12 11:33:39 +08:00
hstyi
aaf9c2e8d2 feat: support for disabling hyperlink (#568) 2025-05-12 11:05:39 +08:00
hstyi
b8196b5730 fix: snippet unescape (#567) 2025-05-12 10:50:48 +08:00
hstyi
0a83e8beb4 fix: double-click to open the host (#566) 2025-05-12 10:49:35 +08:00
hstyi
bdf29b27e7 release: 1.0.14 2025-05-07 12:01:46 +08:00
hstyi
96da7eac41 chore: scroll to the bottom after pressed any key (#553) 2025-05-01 08:36:51 +08:00
hstyi
71c0751692 fix: test connect (#551) 2025-04-30 15:13:11 +08:00
dependabot[bot]
442f334af2 chore(deps): bump com.github.mwiede:jsch from 0.2.25 to 0.2.26 (#546)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-30 09:15:06 +08:00
hstyi
48302a519f fix: snippet i18n (#549) 2025-04-29 15:10:01 +08:00
hstyi
c00f759f15 fix: xterm CBT (#543) 2025-04-28 15:47:55 +08:00
hstyi
1736dd909e chore: folder count (#542) 2025-04-28 09:11:07 +08:00
hstyi
1f01e368dd feat: support for signature algorithms (#539) 2025-04-27 09:54:22 +08:00
hstyi
bfba958b7e feat: support for compression algorithms (#538) 2025-04-26 10:00:54 +08:00
dependabot[bot]
758121b523 chore(deps): bump org.testcontainers:testcontainers-bom from 1.20.6 to 1.21.0 (#528)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-26 09:46:37 +08:00
hstyi
06e9a89e82 fix: double-click to open the host (#529) 2025-04-26 09:45:42 +08:00
hstyi
0ba6ac3305 chore: correct typos (#537) 2025-04-26 09:44:57 +08:00
hstyi
993f220b8b feat: support RDP protocol (#524) 2025-04-20 15:33:09 +08:00
hstyi
8755c4ad23 chore: tmux 2025-04-16 16:35:03 +08:00
dependabot[bot]
77cb102dd6 chore(deps): bump com.github.oshi:oshi-core from 6.6.5 to 6.8.1 (#517)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-16 11:08:23 +08:00
hstyi
89cfb0b451 fix: snippet \ characters (#513) 2025-04-15 17:17:24 +08:00
hstyi
6bdd83f208 fix: highlighter CJK characters (#511) 2025-04-15 15:51:45 +08:00
hstyi
8f86057dcc chore: KeyShortcut toHuman text (#510) 2025-04-15 09:19:16 +08:00
hstyi
a7d7ffa2cc chore: improve dialog 2025-04-15 08:52:02 +08:00
hstyi
d51cbeee13 feat: Highlighter keywords support regex (#507) 2025-04-14 14:29:00 +08:00
hstyi
deb2a0151e fix: Linux moving window jitter 2025-04-14 13:22:25 +08:00
dependabot[bot]
e1c4e9312d chore(deps): bump org.jetbrains.pty4j:pty4j from 0.13.3 to 0.13.4
Bumps [org.jetbrains.pty4j:pty4j](https://github.com/JetBrains/pty4j) from 0.13.3 to 0.13.4.
- [Commits](https://github.com/JetBrains/pty4j/commits)

---
updated-dependencies:
- dependency-name: org.jetbrains.pty4j:pty4j
  dependency-version: 0.13.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 10:44:08 +08:00
dependabot[bot]
c7233357bd chore(deps): bump commons-io:commons-io from 2.18.0 to 2.19.0
Bumps commons-io:commons-io from 2.18.0 to 2.19.0.

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  dependency-version: 2.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 10:43:58 +08:00
hstyi
eff8d565d0 chore: upgrade flatlaf version 2025-04-14 10:42:30 +08:00
43 changed files with 692 additions and 238 deletions

View File

@@ -14,13 +14,14 @@ plugins {
java java
idea idea
application application
`maven-publish`
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlinx.serialization) alias(libs.plugins.kotlinx.serialization)
} }
group = "app.termora" group = "app.termora"
version = "1.0.13" version = "1.0.15"
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem() val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture() val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
@@ -56,67 +57,67 @@ dependencies {
// implementation(platform(libs.koin.bom)) // implementation(platform(libs.koin.bom))
// implementation(libs.koin.core) // implementation(libs.koin.core)
implementation(libs.slf4j.api) api(libs.slf4j.api)
implementation(libs.pty4j) api(libs.pty4j)
implementation(libs.slf4j.tinylog) api(libs.slf4j.tinylog)
implementation(libs.tinylog.impl) api(libs.tinylog.impl)
implementation(libs.commons.codec) api(libs.commons.codec)
implementation(libs.commons.io) api(libs.commons.io)
implementation(libs.commons.lang3) api(libs.commons.lang3)
implementation(libs.commons.csv) api(libs.commons.csv)
implementation(libs.commons.net) api(libs.commons.net)
implementation(libs.commons.text) api(libs.commons.text)
implementation(libs.commons.compress) api(libs.commons.compress)
implementation(libs.commons.vfs2) { exclude(group = "*", module = "*") } api(libs.commons.vfs2) { exclude(group = "*", module = "*") }
implementation(libs.kotlinx.coroutines.swing) api(libs.kotlinx.coroutines.swing)
implementation(libs.kotlinx.coroutines.core) api(libs.kotlinx.coroutines.core)
implementation(libs.flatlaf) { api(libs.flatlaf) {
artifact { artifact {
if (useNoNativesFlatLaf) { if (useNoNativesFlatLaf) {
classifier = "no-natives" classifier = "no-natives"
} }
} }
} }
implementation(libs.flatlaf.extras) { api(libs.flatlaf.extras) {
if (useNoNativesFlatLaf) { if (useNoNativesFlatLaf) {
exclude(group = "com.formdev", module = "flatlaf") exclude(group = "com.formdev", module = "flatlaf")
} }
} }
implementation(libs.flatlaf.swingx) { api(libs.flatlaf.swingx) {
if (useNoNativesFlatLaf) { if (useNoNativesFlatLaf) {
exclude(group = "com.formdev", module = "flatlaf") exclude(group = "com.formdev", module = "flatlaf")
} }
} }
implementation(libs.kotlinx.serialization.json) api(libs.kotlinx.serialization.json)
implementation(libs.swingx) api(libs.swingx)
implementation(libs.jgoodies.forms) api(libs.jgoodies.forms)
implementation(libs.jna) api(libs.jna)
implementation(libs.jna.platform) api(libs.jna.platform)
implementation(libs.versioncompare) api(libs.versioncompare)
implementation(libs.oshi.core) api(libs.oshi.core)
implementation(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") } api(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
implementation(libs.jfa) { exclude(group = "*", module = "*") } api(libs.jfa) { exclude(group = "*", module = "*") }
implementation(libs.jbr.api) api(libs.jbr.api)
implementation(libs.okhttp) api(libs.okhttp)
implementation(libs.okhttp.logging) api(libs.okhttp.logging)
implementation(libs.sshd.core) api(libs.sshd.core)
implementation(libs.commonmark) api(libs.commonmark)
implementation(libs.jgit) api(libs.jgit)
implementation(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") } api(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
implementation(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") } api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
implementation(libs.eddsa) api(libs.eddsa)
implementation(libs.jnafilechooser) api(libs.jnafilechooser)
implementation(libs.xodus.vfs) api(libs.xodus.vfs)
implementation(libs.xodus.openAPI) api(libs.xodus.openAPI)
implementation(libs.xodus.environment) api(libs.xodus.environment)
implementation(libs.bip39) api(libs.bip39)
implementation(libs.colorpicker) api(libs.colorpicker)
implementation(libs.mixpanel) api(libs.mixpanel)
implementation(libs.jSerialComm) api(libs.jSerialComm)
implementation(libs.ini4j) api(libs.ini4j)
implementation(libs.restart4j) api(libs.restart4j)
} }
application { application {
@@ -147,6 +148,37 @@ application {
mainClass = "app.termora.MainKt" 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 { tasks.test {
useJUnitPlatform() useJUnitPlatform()
} }

View File

@@ -1,10 +1,10 @@
[versions] [versions]
kotlin = "2.1.20" kotlin = "2.1.21"
slf4j = "2.0.17" slf4j = "2.0.17"
pty4j = "0.13.3" pty4j = "0.13.4"
tinylog = "2.7.0" tinylog = "2.7.0"
kotlinx-coroutines = "1.10.2" kotlinx-coroutines = "1.10.2"
flatlaf = "3.5.4" flatlaf = "3.6"
kotlinx-serialization-json = "1.8.1" kotlinx-serialization-json = "1.8.1"
commons-codec = "1.18.0" commons-codec = "1.18.0"
commons-lang3 = "3.17.0" commons-lang3 = "3.17.0"
@@ -16,14 +16,14 @@ commons-vfs2="2.10.0"
swingx = "1.6.5-1" swingx = "1.6.5-1"
jgoodies-forms = "1.9.0" jgoodies-forms = "1.9.0"
jfa = "1.2.0" jfa = "1.2.0"
oshi = "6.6.5" oshi = "6.8.1"
versioncompare = "1.4.1" versioncompare = "1.4.1"
jna = "5.17.0" jna = "5.17.0"
jSystemThemeDetector = "3.9.1" jSystemThemeDetector = "3.9.1"
commons-io = "2.18.0" commons-io = "2.19.0"
jbr-api = "17.1.10.1" jbr-api = "17.1.10.1"
hutool = "5.8.37" hutool = "5.8.37"
jsch = "0.2.25" jsch = "0.2.26"
okhttp = "4.12.0" okhttp = "4.12.0"
sshj = "0.39.0" sshj = "0.39.0"
sshd-core = "2.15.0" sshd-core = "2.15.0"
@@ -35,7 +35,7 @@ bip39 = "1.0.9"
colorpicker = "2.0.1" colorpicker = "2.0.1"
rhino = "1.8.0" rhino = "1.8.0"
delight-rhino-sandbox = "0.0.17" delight-rhino-sandbox = "0.0.17"
testcontainers = "1.20.6" testcontainers = "1.21.0"
mixpanel = "1.5.3" mixpanel = "1.5.3"
jSerialComm = "2.11.0" jSerialComm = "2.11.0"
ini4j = "0.5.5-2" ini4j = "0.5.5-2"

View File

@@ -8,6 +8,7 @@ import java.security.GeneralSecurityException;
import java.security.KeyPair; import java.security.KeyPair;
import java.util.*; import java.util.*;
@Deprecated
public class CombinedKeyIdentityProvider implements KeyIdentityProvider { public class CombinedKeyIdentityProvider implements KeyIdentityProvider {
private final List<KeyIdentityProvider> providers = new ArrayList<>(); private final List<KeyIdentityProvider> providers = new ArrayList<>();

View File

@@ -14,6 +14,7 @@ import static com.formdev.flatlaf.util.UIScale.scale;
/** /**
* 如果要升级 FlatLaf 需要检查是否兼容 * 如果要升级 FlatLaf 需要检查是否兼容
*/ */
@Deprecated
public class MyFlatTabbedPaneUI extends FlatTabbedPaneUI { public class MyFlatTabbedPaneUI extends FlatTabbedPaneUI {
@Override @Override
protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) { protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {

View File

@@ -523,6 +523,11 @@ class Database private constructor(private val env: Environment) : Disposable {
*/ */
var beep by BooleanPropertyDelegate(true) var beep by BooleanPropertyDelegate(true)
/**
* 超链接
*/
var hyperlink by BooleanPropertyDelegate(true)
/** /**
* 光标闪烁 * 光标闪烁
*/ */

View File

@@ -25,6 +25,7 @@ enum class Protocol {
SSH, SSH,
Local, Local,
Serial, Serial,
RDP,
/** /**
* 交互式的 SFTP此协议只在系统内部交互不应该暴露给用户也不应该持久化 * 交互式的 SFTP此协议只在系统内部交互不应该暴露给用户也不应该持久化

View File

@@ -12,6 +12,7 @@ import org.apache.sshd.client.session.ClientSession
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
import java.util.*
import javax.swing.* import javax.swing.*
class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) { class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
@@ -54,7 +55,8 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
isEnabled = false isEnabled = false
swingCoroutineScope.launch(Dispatchers.IO) { swingCoroutineScope.launch(Dispatchers.IO) {
testConnection(pane.getHost()) // 因为测试连接的时候从数据库读取会导致失效所以这里生成随机ID
testConnection(pane.getHost().copy(id = UUID.randomUUID().toSimpleString()))
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
putValue(NAME, I18n.getString("termora.new-host.test-connection")) putValue(NAME, I18n.getString("termora.new-host.test-connection"))
isEnabled = true isEnabled = true

View File

@@ -320,6 +320,7 @@ open class HostOptionsPane : OptionsPane() {
protocolTypeComboBox.addItem(Protocol.SSH) protocolTypeComboBox.addItem(Protocol.SSH)
protocolTypeComboBox.addItem(Protocol.Local) protocolTypeComboBox.addItem(Protocol.Local)
protocolTypeComboBox.addItem(Protocol.Serial) protocolTypeComboBox.addItem(Protocol.Serial)
protocolTypeComboBox.addItem(Protocol.RDP)
authenticationTypeComboBox.addItem(AuthenticationType.No) authenticationTypeComboBox.addItem(AuthenticationType.No)
authenticationTypeComboBox.addItem(AuthenticationType.Password) authenticationTypeComboBox.addItem(AuthenticationType.Password)

View File

@@ -49,6 +49,7 @@ class HostTreeNode(host: Host) : SimpleTreeNode<Host>(host) {
return when (host.protocol) { return when (host.protocol) {
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon() Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin
Protocol.RDP -> if (selected && hasFocus) Icons.microsoftWindows.dark else Icons.microsoftWindows
else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal
} }
} }

View File

@@ -64,6 +64,7 @@ object Icons {
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") } val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") } val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") } val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") }
val microsoftWindows by lazy { DynamicIcon("icons/microsoftWindows.svg", "icons/microsoftWindows_dark.svg") }
val tencent by lazy { DynamicIcon("icons/tencent.svg") } val tencent by lazy { DynamicIcon("icons/tencent.svg") }
val google by lazy { DynamicIcon("icons/google-small.svg") } val google by lazy { DynamicIcon("icons/google-small.svg") }
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") } val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }

View File

@@ -9,7 +9,6 @@ import java.awt.event.*
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.util.* import java.util.*
import javax.swing.* import javax.swing.*
import javax.swing.plaf.TabbedPaneUI
import kotlin.math.abs import kotlin.math.abs
class MyTabbedPane : FlatTabbedPane() { class MyTabbedPane : FlatTabbedPane() {
@@ -21,18 +20,12 @@ class MyTabbedPane : FlatTabbedPane() {
private val owner private val owner
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this)) get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
.getData(DataProviders.TermoraFrame) as TermoraFrame .getData(DataProviders.TermoraFrame) as TermoraFrame
private val myUI = MyFlatTabbedPaneUI()
init { init {
isFocusable = false isFocusable = false
super.setUI(myUI)
initEvents() initEvents()
} }
override fun setUI(ui: TabbedPaneUI?) {
super.setUI(myUI)
}
override fun updateUI() { override fun updateUI() {
styleMap = mapOf( styleMap = mapOf(
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"), "focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),

View File

@@ -37,6 +37,7 @@ import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathConstants import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory import javax.xml.xpath.XPathFactory
@Suppress("CascadeIf")
class NewHostTree : SimpleTree() { class NewHostTree : SimpleTree() {
companion object { companion object {
@@ -97,7 +98,7 @@ class NewHostTree : SimpleTree() {
// 是否显示更多信息 // 是否显示更多信息
if (isShowMoreInfo) { if (isShowMoreInfo) {
val color = if (sel) { val color = if (sel) {
if (tree.hasFocus()) { if (tree.hasFocus() || isPopupMenu) {
UIManager.getColor("textHighlightText") UIManager.getColor("textHighlightText")
} else { } else {
this.foreground this.foreground
@@ -110,15 +111,15 @@ class NewHostTree : SimpleTree() {
"""<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>""" """<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>"""
} }
if (host.protocol == Protocol.SSH) { // @formatter:off
text = if (host.protocol == Protocol.SSH || host.protocol == Protocol.RDP) {
"<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply("${host.username}@${host.host}")}</html>" text = "<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply("${host.username}@${host.host}")}</html>"
} else if (host.protocol == Protocol.Serial) { } else if (host.protocol == Protocol.Serial) {
text = text = "<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply(host.options.serialComm.port)}</html>"
"<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply(host.options.serialComm.port)}</html>"
} else if (host.protocol == Protocol.Folder) { } else if (host.protocol == Protocol.Folder) {
text = "<html>${host.name}${fontTag.apply(" (${node.childCount})")}</html>" text = "<html>${host.name}${fontTag.apply(" (${node.getAllChildren().size})")}</html>"
} }
// @formatter:on
} }
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus) val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
@@ -137,6 +138,9 @@ class NewHostTree : SimpleTree() {
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) { if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
if (lastNode.host.protocol != Protocol.Folder) { 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)) openHostAction?.actionPerformed(OpenHostActionEvent(e.source, lastNode.host, e))
} }
} }

View File

@@ -422,6 +422,7 @@ class SettingsOptionsPane : OptionsPane() {
private val selectCopyComboBox = YesOrNoComboBox() private val selectCopyComboBox = YesOrNoComboBox()
private val autoCloseTabComboBox = YesOrNoComboBox() private val autoCloseTabComboBox = YesOrNoComboBox()
private val floatingToolbarComboBox = YesOrNoComboBox() private val floatingToolbarComboBox = YesOrNoComboBox()
private val hyperlinkComboBox = YesOrNoComboBox()
init { init {
initView() initView()
@@ -499,6 +500,13 @@ class SettingsOptionsPane : OptionsPane() {
} }
} }
hyperlinkComboBox.addItemListener { e ->
if (e.stateChange == ItemEvent.SELECTED) {
terminalSetting.hyperlink = hyperlinkComboBox.selectedItem as Boolean
TerminalPanelFactory.getInstance().repaintAll()
}
}
cursorBlinkComboBox.addItemListener { e -> cursorBlinkComboBox.addItemListener { e ->
if (e.stateChange == ItemEvent.SELECTED) { if (e.stateChange == ItemEvent.SELECTED) {
terminalSetting.cursorBlink = cursorBlinkComboBox.selectedItem as Boolean terminalSetting.cursorBlink = cursorBlinkComboBox.selectedItem as Boolean
@@ -591,6 +599,7 @@ class SettingsOptionsPane : OptionsPane() {
fontComboBox.selectedItem = terminalSetting.font fontComboBox.selectedItem = terminalSetting.font
debugComboBox.selectedItem = terminalSetting.debug debugComboBox.selectedItem = terminalSetting.debug
beepComboBox.selectedItem = terminalSetting.beep beepComboBox.selectedItem = terminalSetting.beep
hyperlinkComboBox.selectedItem = terminalSetting.hyperlink
cursorBlinkComboBox.selectedItem = terminalSetting.cursorBlink cursorBlinkComboBox.selectedItem = terminalSetting.cursorBlink
cursorStyleComboBox.selectedItem = terminalSetting.cursor cursorStyleComboBox.selectedItem = terminalSetting.cursor
selectCopyComboBox.selectedItem = terminalSetting.selectCopy selectCopyComboBox.selectedItem = terminalSetting.selectCopy
@@ -613,7 +622,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun getCenterComponent(): JComponent { private fun getCenterComponent(): JComponent {
val layout = FormLayout( val layout = FormLayout(
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow", "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) val beepBtn = JButton(Icons.run)
@@ -636,6 +645,8 @@ class SettingsOptionsPane : OptionsPane() {
.add("${I18n.getString("termora.settings.terminal.beep")}:").xy(1, rows) .add("${I18n.getString("termora.settings.terminal.beep")}:").xy(1, rows)
.add(beepComboBox).xy(3, rows) .add(beepComboBox).xy(3, rows)
.add(beepBtn).xy(5, rows).apply { rows += step } .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("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
.add(selectCopyComboBox).xy(3, rows).apply { rows += step } .add(selectCopyComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows) .add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)

View File

@@ -34,6 +34,7 @@ import org.apache.sshd.common.channel.ChannelFactory
import org.apache.sshd.common.channel.PtyChannelConfiguration import org.apache.sshd.common.channel.PtyChannelConfiguration
import org.apache.sshd.common.channel.PtyChannelConfigurationHolder import org.apache.sshd.common.channel.PtyChannelConfigurationHolder
import org.apache.sshd.common.cipher.CipherNone import org.apache.sshd.common.cipher.CipherNone
import org.apache.sshd.common.compression.BuiltinCompressions
import org.apache.sshd.common.config.keys.KeyRandomArt import org.apache.sshd.common.config.keys.KeyRandomArt
import org.apache.sshd.common.config.keys.KeyUtils import org.apache.sshd.common.config.keys.KeyUtils
import org.apache.sshd.common.future.CloseFuture import org.apache.sshd.common.future.CloseFuture
@@ -47,6 +48,7 @@ import org.apache.sshd.common.kex.BuiltinDHFactories
import org.apache.sshd.common.keyprovider.KeyIdentityProvider import org.apache.sshd.common.keyprovider.KeyIdentityProvider
import org.apache.sshd.common.session.Session import org.apache.sshd.common.session.Session
import org.apache.sshd.common.session.SessionListener import org.apache.sshd.common.session.SessionListener
import org.apache.sshd.common.signature.BuiltinSignatures
import org.apache.sshd.common.util.net.SshdSocketAddress import org.apache.sshd.common.util.net.SshdSocketAddress
import org.apache.sshd.core.CoreModuleProperties import org.apache.sshd.core.CoreModuleProperties
import org.apache.sshd.server.forward.AcceptAllForwardingFilter import org.apache.sshd.server.forward.AcceptAllForwardingFilter
@@ -339,6 +341,24 @@ object SshClients {
) )
builder.keyExchangeFactories(keyExchangeFactories) builder.keyExchangeFactories(keyExchangeFactories)
val compressionFactories = ClientBuilder.setUpDefaultCompressionFactories(true).toMutableList()
for (compression in listOf(
BuiltinCompressions.none,
BuiltinCompressions.zlib,
BuiltinCompressions.delayedZlib
)) {
if (compressionFactories.contains(compression)) continue
compressionFactories.add(compression)
}
builder.compressionFactories(compressionFactories)
val signatureFactories = ClientBuilder.setUpDefaultSignatureFactories(true).toMutableList()
for (signature in BuiltinSignatures.entries) {
if (signatureFactories.contains(signature)) continue
signatureFactories.add(signature)
}
builder.signatureFactories(signatureFactories)
if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) { if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) {
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE) builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
} else { } else {

View File

@@ -4,6 +4,7 @@ import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders import app.termora.actions.DataProviders
import app.termora.actions.MultipleAction import app.termora.actions.MultipleAction
import app.termora.highlight.KeywordHighlightPaintListener import app.termora.highlight.KeywordHighlightPaintListener
import app.termora.terminal.DataKey
import app.termora.terminal.PtyConnector import app.termora.terminal.PtyConnector
import app.termora.terminal.Terminal import app.termora.terminal.Terminal
import app.termora.terminal.panel.TerminalHyperlinkPaintListener import app.termora.terminal.panel.TerminalHyperlinkPaintListener
@@ -40,6 +41,10 @@ class TerminalPanelFactory : Disposable {
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel { fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
val writer = MyTerminalWriter(ptyConnector) val writer = MyTerminalWriter(ptyConnector)
val terminalPanel = TerminalPanel(terminal, writer) val terminalPanel = TerminalPanel(terminal, writer)
// processDeviceStatusReport
terminal.getTerminalModel().setData(DataKey.TerminalWriter, writer)
terminalPanel.addTerminalPaintListener(MultipleTerminalListener()) terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance()) terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance()) terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())

View File

@@ -63,10 +63,9 @@ class TermoraFrame : JFrame(), DataProvider {
} }
override fun mouseDragged(e: MouseEvent) { override fun mouseDragged(e: MouseEvent) {
val mouseLayer = getMouseLayer() ?: return
getMouseMotionListener()?.mouseDragged( getMouseMotionListener()?.mouseDragged(
MouseEvent( MouseEvent(
mouseLayer, e.component,
e.id, e.id,
e.`when`, e.`when`,
e.modifiersEx, e.modifiersEx,
@@ -87,13 +86,6 @@ class TermoraFrame : JFrame(), DataProvider {
return getHandler() as? MouseMotionListener return getHandler() as? MouseMotionListener
} }
private fun getMouseLayer(): JComponent? {
val titlePane = getTitlePane() ?: return null
val handlerField = titlePane.javaClass.getDeclaredField("mouseLayer") ?: return null
handlerField.isAccessible = true
return handlerField.get(titlePane) as? JComponent
}
private fun getHandler(): Any? { private fun getHandler(): Any? {
val titlePane = getTitlePane() ?: return null val titlePane = getTitlePane() ?: return null
val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null

View File

@@ -1,6 +1,19 @@
package app.termora.actions package app.termora.actions
import app.termora.* import app.termora.*
import com.formdev.flatlaf.util.SystemInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.net.URI
import java.util.*
import javax.swing.JOptionPane
import kotlin.time.Duration.Companion.seconds
class OpenHostAction : AnAction() { class OpenHostAction : AnAction() {
companion object { companion object {
@@ -26,10 +39,70 @@ class OpenHostAction : AnAction() {
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host) Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host) Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host) Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host)
Protocol.RDP -> openRDP(windowScope, evt.host)
else -> LocalTerminalTab(windowScope, evt.host) else -> LocalTerminalTab(windowScope, evt.host)
} }
terminalTabbedManager.addTerminalTab(tab) if (tab is TerminalTab) {
tab.start() terminalTabbedManager.addTerminalTab(tab)
if (tab is PtyHostTerminalTab) {
tab.start()
}
}
}
private fun openRDP(windowScope: WindowScope, host: Host) {
if (SystemInfo.isLinux) {
OptionPane.showMessageDialog(
windowScope.window,
"Linux cannot connect to Windows Remote Server, Supported only for macOS and Windows",
messageType = JOptionPane.WARNING_MESSAGE
)
return
}
if (SystemInfo.isMacOS) {
if (!FileUtils.getFile("/Applications/Windows App.app").exists()) {
val option = OptionPane.showConfirmDialog(
windowScope.window,
"If you want to connect to a Windows Remote Server, You have to install the Windows App",
optionType = JOptionPane.OK_CANCEL_OPTION
)
if (option == JOptionPane.OK_OPTION) {
Application.browse(URI.create("https://apps.apple.com/app/windows-app/id1295203466"))
}
return
}
}
val sb = StringBuilder()
sb.append("full address:s:").append(host.host).append(':').append(host.port).appendLine()
sb.append("username:s:").append(host.username).appendLine()
val file = FileUtils.getFile(Application.getTemporaryDir(), UUID.randomUUID().toSimpleString() + ".rdp")
file.outputStream().use { IOUtils.write(sb.toString(), it, Charsets.UTF_8) }
if (host.authentication.type == AuthenticationType.Password) {
val systemClipboard = windowScope.window.toolkit.systemClipboard
val password = host.authentication.password
systemClipboard.setContents(StringSelection(password), null)
// clear password
swingCoroutineScope.launch(Dispatchers.IO) {
delay(30.seconds)
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
if (systemClipboard.getData(DataFlavor.stringFlavor) == password) {
systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
}
}
}
}
if (SystemInfo.isMacOS) {
ProcessBuilder("open", file.absolutePath).start()
} else if (SystemInfo.isWindows) {
ProcessBuilder("mstsc", file.absolutePath).start()
}
} }
} }

View File

@@ -51,6 +51,7 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
val customBtn = JButton("Custom") val customBtn = JButton("Custom")
customBtn.addActionListener { customBtn.addActionListener {
val dialog = MyColorPickerDialog(this) val dialog = MyColorPickerDialog(this)
dialog.setLocationRelativeTo(this)
dialog.colorPicker.color = defaultColor dialog.colorPicker.color = defaultColor
dialog.isVisible = true dialog.isVisible = true
val color = dialog.color val color = dialog.color

View File

@@ -24,6 +24,11 @@ data class KeywordHighlight(
*/ */
val matchCase: Boolean = false, val matchCase: Boolean = false,
/**
* 是否是正则表达式
*/
val regex: Boolean = false,
/** /**
* 0 是取前景色 * 0 是取前景色
*/ */

View File

@@ -20,10 +20,8 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
private val model = KeywordHighlightTableModel() private val model = KeywordHighlightTableModel()
private val table = FlatTable() private val table = FlatTable()
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() } private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
private val colorPalette by lazy { private val terminal by lazy { TerminalFactory.getInstance().createTerminal() }
TerminalFactory.getInstance().createTerminal().getTerminalModel() private val colorPalette by lazy { terminal.getTerminalModel().getColorPalette() }
.getColorPalette()
}
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add")) private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
private val editBtn = JButton(I18n.getString("termora.keymgr.edit")) private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
@@ -130,6 +128,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
addBtn.addActionListener { addBtn.addActionListener {
val dialog = NewKeywordHighlightDialog(this, colorPalette) val dialog = NewKeywordHighlightDialog(this, colorPalette)
dialog.setLocationRelativeTo(this)
dialog.isVisible = true dialog.isVisible = true
val keywordHighlight = dialog.keywordHighlight val keywordHighlight = dialog.keywordHighlight
if (keywordHighlight != null) { if (keywordHighlight != null) {
@@ -143,6 +142,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
if (row > -1) { if (row > -1) {
var keywordHighlight = model.getKeywordHighlight(row) var keywordHighlight = model.getKeywordHighlight(row)
val dialog = NewKeywordHighlightDialog(this, colorPalette) val dialog = NewKeywordHighlightDialog(this, colorPalette)
dialog.setLocationRelativeTo(this)
dialog.keywordTextField.text = keywordHighlight.keyword dialog.keywordTextField.text = keywordHighlight.keyword
dialog.descriptionTextField.text = keywordHighlight.description dialog.descriptionTextField.text = keywordHighlight.description
@@ -176,6 +176,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
dialog.underlineCheckBox.isSelected = keywordHighlight.underline dialog.underlineCheckBox.isSelected = keywordHighlight.underline
dialog.lineThroughCheckBox.isSelected = keywordHighlight.lineThrough dialog.lineThroughCheckBox.isSelected = keywordHighlight.lineThrough
dialog.matchCaseBtn.isSelected = keywordHighlight.matchCase dialog.matchCaseBtn.isSelected = keywordHighlight.matchCase
dialog.regexBtn.isSelected = keywordHighlight.regex
dialog.isVisible = true dialog.isVisible = true
@@ -211,6 +212,12 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
editBtn.isEnabled = table.selectedRowCount > 0 editBtn.isEnabled = table.selectedRowCount > 0
deleteBtn.isEnabled = editBtn.isEnabled deleteBtn.isEnabled = editBtn.isEnabled
} }
Disposer.register(disposable, object : Disposable {
override fun dispose() {
terminal.close()
}
})
} }
override fun createCenterPanel(): JComponent { override fun createCenterPanel(): JComponent {

View File

@@ -5,6 +5,7 @@ import app.termora.terminal.*
import app.termora.terminal.panel.TerminalDisplay import app.termora.terminal.panel.TerminalDisplay
import app.termora.terminal.panel.TerminalPaintListener import app.termora.terminal.panel.TerminalPaintListener
import app.termora.terminal.panel.TerminalPanel import app.termora.terminal.panel.TerminalPanel
import org.slf4j.LoggerFactory
import java.awt.Graphics import java.awt.Graphics
import kotlin.math.min import kotlin.math.min
import kotlin.random.Random import kotlin.random.Random
@@ -18,9 +19,10 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
} }
private val tag = Random.nextInt() private val tag = Random.nextInt()
private val log = LoggerFactory.getLogger(KeywordHighlightPaintListener::class.java)
} }
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() } private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
override fun before( override fun before(
offset: Int, offset: Int,
@@ -36,7 +38,8 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
} }
val document = terminal.getDocument() val document = terminal.getDocument()
val kinds = SubstrFinder(object : Iterator<TerminalLine> { val kinds = mutableListOf<FindKind>()
val iterator = object : Iterator<TerminalLine> {
private var index = offset + 1 private var index = offset + 1
private val maxCount = min(index + count, document.getLineCount()) private val maxCount = min(index + count, document.getLineCount())
override fun hasNext(): Boolean { override fun hasNext(): Boolean {
@@ -46,8 +49,24 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
override fun next(): TerminalLine { override fun next(): TerminalLine {
return document.getLine(index++) return document.getLine(index++)
} }
}
}, CharArraySubstr(highlight.keyword.toCharArray())).find(!highlight.matchCase) if (highlight.regex) {
try {
val regex = if (highlight.matchCase)
highlight.keyword.toRegex()
else highlight.keyword.toRegex(RegexOption.IGNORE_CASE)
RegexFinder(regex, iterator).find()
.apply { kinds.addAll(this) }
} catch (e: Exception) {
if (log.isDebugEnabled) {
log.error(e.message, e)
}
}
} else {
SubstrFinder(iterator, CharArraySubstr(highlight.keyword.toCharArray())).find(!highlight.matchCase)
.apply { kinds.addAll(this) }
}
for (kind in kinds) { for (kind in kinds) {
terminal.getMarkupModel().addHighlighter( terminal.getMarkupModel().addHighlighter(
@@ -77,6 +96,74 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
terminal.getMarkupModel().removeAllHighlighters(tag) terminal.getMarkupModel().removeAllHighlighters(tag)
} }
private class RegexFinder(
private val regex: Regex,
private val iterator: Iterator<TerminalLine>
) {
private data class Coords(val row: Int, val col: Int)
private data class MatchResultWithCoords(
val match: String,
val coords: List<Coords>
)
fun find(): List<FindKind> {
val lines = mutableListOf<TerminalLine>()
val kinds = mutableListOf<FindKind>()
for ((index, line) in iterator.withIndex()) {
lines.add(line)
if (line.wrapped) continue
val data = mutableListOf<MutableList<Char>>()
for (e in lines) {
data.add(mutableListOf())
for (c in e.chars()) {
if (c.first.isNull) break
data.last().add(c.first)
}
}
lines.clear()
val resultWithCoords = findMatchesWithCoords(data)
if (resultWithCoords.isEmpty()) continue
val offset = index - data.size + 1
for (e in resultWithCoords) {
val coords = e.coords
if (coords.isEmpty()) continue
kinds.add(
FindKind(
startPosition = Position(coords.first().row + offset + 1, coords.first().col + 1),
endPosition = Position(coords.last().row + offset + 1, coords.last().col + 1)
)
)
}
}
return kinds
}
private fun findMatchesWithCoords(data: List<List<Char>>): List<MatchResultWithCoords> {
val flatChars = StringBuilder()
val indexMap = mutableListOf<Coords>()
// 拉平成字符串,并记录每个字符的位置
for ((rowIndex, row) in data.withIndex()) {
for ((colIndex, char) in row.withIndex()) {
flatChars.append(char)
indexMap.add(Coords(rowIndex, colIndex))
}
}
return regex.findAll(flatChars.toString())
.map { MatchResultWithCoords(it.value, indexMap.subList(it.range.first, it.range.last + 1)) }
.toList()
}
}
private class KeywordHighlightHighlighter( private class KeywordHighlightHighlighter(
range: HighlighterRange, terminal: Terminal, range: HighlighterRange, terminal: Terminal,
@@ -93,4 +180,6 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
) )
} }
} }
} }

View File

@@ -1,10 +1,6 @@
package app.termora.highlight package app.termora.highlight
import app.termora.DialogWrapper import app.termora.*
import app.termora.DynamicColor
import app.termora.I18n
import app.termora.Icons
import app.termora.Database
import app.termora.terminal.ColorPalette import app.termora.terminal.ColorPalette
import app.termora.terminal.TerminalColor import app.termora.terminal.TerminalColor
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
@@ -46,6 +42,7 @@ class NewKeywordHighlightDialog(
I18n.getString("termora.highlight.background-color") I18n.getString("termora.highlight.background-color")
) )
val matchCaseBtn = JToggleButton(Icons.matchCase) val matchCaseBtn = JToggleButton(Icons.matchCase)
val regexBtn = JToggleButton(Icons.regex)
private val textColorRevert = JButton(Icons.revert) private val textColorRevert = JButton(Icons.revert)
@@ -85,6 +82,7 @@ class NewKeywordHighlightDialog(
val box = FlatToolBar() val box = FlatToolBar()
box.add(matchCaseBtn) box.add(matchCaseBtn)
box.add(regexBtn)
keywordTextField.trailingComponent = box keywordTextField.trailingComponent = box
repaintKeywordHighlightView() repaintKeywordHighlightView()
@@ -187,6 +185,7 @@ class NewKeywordHighlightDialog(
} }
private fun createColorPanel(color: Color, title: String): ColorPanel { private fun createColorPanel(color: Color, title: String): ColorPanel {
val owner = this
val arc = UIManager.getInt("Component.arc") val arc = UIManager.getInt("Component.arc")
val lineBorder = FlatLineBorder(Insets(1, 1, 1, 1), DynamicColor.BorderColor, 1f, arc) val lineBorder = FlatLineBorder(Insets(1, 1, 1, 1), DynamicColor.BorderColor, 1f, arc)
val colorPanel = ColorPanel(color) val colorPanel = ColorPanel(color)
@@ -195,7 +194,8 @@ class NewKeywordHighlightDialog(
colorPanel.addMouseListener(object : MouseAdapter() { colorPanel.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { override fun mouseClicked(e: MouseEvent) {
if (SwingUtilities.isLeftMouseButton(e)) { if (SwingUtilities.isLeftMouseButton(e)) {
val dialog = ChooseColorTemplateDialog(this@NewKeywordHighlightDialog, title) val dialog = ChooseColorTemplateDialog(owner, title)
dialog.setLocationRelativeTo(owner)
dialog.defaultColor = colorPanel.color dialog.defaultColor = colorPanel.color
dialog.isVisible = true dialog.isVisible = true
colorPanel.color = dialog.color ?: return colorPanel.color = dialog.color ?: return
@@ -218,6 +218,7 @@ class NewKeywordHighlightDialog(
keyword = keywordTextField.text, keyword = keywordTextField.text,
description = descriptionTextField.text, description = descriptionTextField.text,
matchCase = matchCaseBtn.isSelected, matchCase = matchCaseBtn.isSelected,
regex = regexBtn.isSelected,
textColor = if (textColor.colorIndex != -1) textColor.colorIndex else textColor.color.toRGB(), textColor = if (textColor.colorIndex != -1) textColor.colorIndex else textColor.color.toRGB(),
backgroundColor = if (backgroundColor.colorIndex != -1) backgroundColor.colorIndex else backgroundColor.color.toRGB(), backgroundColor = if (backgroundColor.colorIndex != -1) backgroundColor.colorIndex else backgroundColor.color.toRGB(),
bold = boldCheckBox.isSelected, bold = boldCheckBox.isSelected,

View File

@@ -1,5 +1,6 @@
package app.termora.keymap package app.termora.keymap
import com.formdev.flatlaf.util.SystemInfo
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import javax.swing.KeyStroke import javax.swing.KeyStroke
@@ -23,7 +24,14 @@ class KeyShortcut(val keyStroke: KeyStroke) : Shortcut() {
text = text.replace("MINUS", "-") text = text.replace("MINUS", "-")
} }
return text.toCharArray().joinToString(" + ") text = text.toCharArray().joinToString(" + ")
if (SystemInfo.isWindows || SystemInfo.isLinux) {
text = text.replace("", "Shift")
text = text.replace("", "Ctrl")
text = text.replace("", "Alt")
}
return text
} }
} }

View File

@@ -4,6 +4,7 @@ import app.termora.*
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import app.termora.actions.SettingsAction import app.termora.actions.SettingsAction
import app.termora.sftp.FileSystemViewTable.AskTransfer.Action import app.termora.sftp.FileSystemViewTable.AskTransfer.Action
import app.termora.vfs2.VFSWalker
import app.termora.vfs2.sftp.MySftpFileObject import app.termora.vfs2.sftp.MySftpFileObject
import app.termora.vfs2.sftp.MySftpFileSystem import app.termora.vfs2.sftp.MySftpFileSystem
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
@@ -37,7 +38,6 @@ import java.nio.file.FileVisitor
import java.nio.file.Paths import java.nio.file.Paths
import java.nio.file.StandardOpenOption import java.nio.file.StandardOpenOption
import java.nio.file.attribute.BasicFileAttributes import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.attribute.FileTime
import java.text.MessageFormat import java.text.MessageFormat
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -45,6 +45,21 @@ import java.util.regex.Pattern
import javax.swing.* import javax.swing.*
import javax.swing.table.DefaultTableCellRenderer import javax.swing.table.DefaultTableCellRenderer
import kotlin.collections.ArrayDeque 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.io.path.absolutePathString
import kotlin.math.max import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@@ -218,7 +233,7 @@ class FileSystemViewTable(
val localTarget = sftpPanel.getLocalTarget() val localTarget = sftpPanel.getLocalTarget()
val table = localTarget.getData(SFTPDataProviders.FileSystemViewTable) ?: return false val table = localTarget.getData(SFTPDataProviders.FileSystemViewTable) ?: return false
// 委托最左侧的本地文件系统传输 // 委托最左侧的本地文件系统传输
table.transfer(paths, true, targetWorkdir) table.transfer(paths, true, targetWorkdir, fileSystemViewPanel)
return true return true
} }
return false return false
@@ -360,34 +375,7 @@ class FileSystemViewTable(
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
val last = files.last() val last = files.last()
if (last !is MySftpFileObject) return if (last !is MySftpFileObject) return
changePermission(last)
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)
}
}
}
} }
}) })
refresh.addActionListener { fileSystemViewPanel.reload() } refresh.addActionListener { fileSystemViewPanel.reload() }
@@ -409,6 +397,80 @@ class FileSystemViewTable(
popupMenu.show(table, e.x, e.y) 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() { private fun renameSelection() {
val index = selectedRow val index = selectedRow
if (index < 0) return if (index < 0) return
@@ -619,12 +681,13 @@ class FileSystemViewTable(
private fun transfer( private fun transfer(
files: List<FileObject>, files: List<FileObject>,
fromLocalSystem: Boolean = false, fromLocalSystem: Boolean = false,
targetWorkdir: FileObject? = null targetWorkdir: FileObject? = null,
target: FileSystemViewPanel? = null,
) { ) {
assertEventDispatchThread() assertEventDispatchThread()
val target = sftpPanel.getTarget(table) ?: return val target = (target ?: sftpPanel.getTarget(table)) ?: return
val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return
var isApplyAll = false var isApplyAll = false
var lastAction = Action.Overwrite var lastAction = Action.Overwrite
@@ -650,7 +713,7 @@ class FileSystemViewTable(
coroutineScope.launch { coroutineScope.launch {
try { try {
doTransfer(file, lastAction, fromLocalSystem, targetWorkdir) doTransfer(file, lastAction, fromLocalSystem, targetWorkdir, target)
} catch (e: Exception) { } catch (e: Exception) {
if (log.isErrorEnabled) { if (log.isErrorEnabled) {
log.error(e.message, e) log.error(e.message, e)
@@ -801,10 +864,11 @@ class FileSystemViewTable(
file: FileObject, file: FileObject,
action: Action, action: Action,
fromLocalSystem: Boolean, fromLocalSystem: Boolean,
targetWorkdir: FileObject? targetWorkdir: FileObject?,
target: FileSystemViewPanel? = null
) { ) {
val sftpPanel = this.sftpPanel 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, dir: FileObject,
visitor: FileVisitor<FileObject>, visitor: FileVisitor<FileObject>,
): FileVisitResult { ): FileVisitResult {
return VFSWalker.walk(dir, visitor)
// 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 fun addTransport( 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")
}
}
} }

View File

@@ -157,7 +157,11 @@ class FileSystemViewTableModel : DefaultTableModel() {
fun getPathNames(): Set<String> { fun getPathNames(): Set<String> {
val names = linkedSetOf<String>() val names = linkedSetOf<String>()
for (i in 0 until rowCount) { 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 return names
} }

View File

@@ -25,6 +25,7 @@ class PosixFilePermissionDialog(
private val otherRead = JCheckBox(I18n.getString("termora.transport.permissions.read")) private val otherRead = JCheckBox(I18n.getString("termora.transport.permissions.read"))
private val otherWrite = JCheckBox(I18n.getString("termora.transport.permissions.write")) private val otherWrite = JCheckBox(I18n.getString("termora.transport.permissions.write"))
private val otherExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute")) 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 private var isCancelled = false
@@ -60,13 +61,14 @@ class PosixFilePermissionDialog(
otherRead.isFocusable = false otherRead.isFocusable = false
otherWrite.isFocusable = false otherWrite.isFocusable = false
otherExecute.isFocusable = false otherExecute.isFocusable = false
includeSubFolder.isFocusable = false
} }
override fun createCenterPanel(): JComponent { override fun createCenterPanel(): JComponent {
val formMargin = "7dlu" val formMargin = "7dlu"
val layout = FormLayout( val layout = FormLayout(
"default:grow, $formMargin, default:grow, $formMargin, default:grow", "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") 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")) otherBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.others"))
builder.add(otherBox).xy(5, 3) builder.add(otherBox).xy(5, 3)
builder.add(includeSubFolder).xyw(1, 5, 5)
return builder.build() return builder.build()
} }
@@ -103,6 +107,10 @@ class PosixFilePermissionDialog(
super.doCancelAction() super.doCancelAction()
} }
fun isIncludeSubdirectories(): Boolean {
return includeSubFolder.isSelected
}
/** /**
* @return 返回空表示取消了 * @return 返回空表示取消了
*/ */

View File

@@ -56,6 +56,16 @@ class SFTPAction : AnAction("SFTP", Icons.folder) {
} }
val host = hostManager.getHost(hostId) ?: return 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) tabbed.addSFTPFileSystemViewPanelTab(host)
} }

View File

@@ -27,6 +27,8 @@ import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.* import javax.swing.*
import javax.swing.event.TreeExpansionEvent
import javax.swing.event.TreeExpansionListener
class SFTPFileSystemViewPanel( class SFTPFileSystemViewPanel(
var host: Host? = null, var host: Host? = null,
@@ -35,17 +37,18 @@ class SFTPFileSystemViewPanel(
companion object { companion object {
private val log = LoggerFactory.getLogger(SFTPFileSystemViewPanel::class.java) private val log = LoggerFactory.getLogger(SFTPFileSystemViewPanel::class.java)
}
private enum class State { enum class State {
Initialized, Initialized,
Connecting, Connecting,
Connected, Connected,
ConnectFailed, ConnectFailed,
}
} }
@Volatile @Volatile
private var state = State.Initialized var state = State.Initialized
private set
private val cardLayout = CardLayout() private val cardLayout = CardLayout()
private val cardPanel = JPanel(cardLayout) private val cardPanel = JPanel(cardLayout)
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@@ -283,12 +286,20 @@ class SFTPFileSystemViewPanel(
val node = tree.getLastSelectedPathNode() ?: return val node = tree.getLastSelectedPathNode() ?: return
if (node.isFolder) return if (node.isFolder) return
val host = node.data as Host val host = node.data as Host
that.setTabTitle(host.name) selectHost(host)
that.host = host
that.connect()
} }
} }
}) })
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() { 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) { private fun setTabTitle(title: String) {
val tabbed = SwingUtilities.getAncestorOfClass(JTabbedPane::class.java, that) val tabbed = SwingUtilities.getAncestorOfClass(JTabbedPane::class.java, that)
if (tabbed is JTabbedPane) { if (tabbed is JTabbedPane) {

View File

@@ -5,7 +5,6 @@ import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.extras.components.FlatTabbedPane import com.formdev.flatlaf.extras.components.FlatTabbedPane
import java.awt.Point
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -13,7 +12,6 @@ import javax.swing.JButton
import javax.swing.JToolBar import javax.swing.JToolBar
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
import javax.swing.UIManager import javax.swing.UIManager
import kotlin.math.max
@Suppress("DuplicatedCode") @Suppress("DuplicatedCode")
class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPane(), Disposable { class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPane(), Disposable {
@@ -43,23 +41,20 @@ class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPan
private fun initEvents() { private fun initEvents() {
addBtn.addActionListener(object : AnAction() { addBtn.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
val dialog = NewHostTreeDialog(SwingUtilities.getWindowAncestor(tabbed)) for (i in 0 until tabCount) {
dialog.location = Point( val c = getComponentAt(i)
max(0, addBtn.locationOnScreen.x - dialog.width / 2 + addBtn.width / 2), if (c !is SFTPFileSystemViewPanel) continue
addBtn.locationOnScreen.y + max(tabHeight, addBtn.height) if (c.state != SFTPFileSystemViewPanel.State.Initialized) continue
) selectedIndex = i
dialog.setFilter { it.host.protocol == Protocol.SSH } return
dialog.setTreeName("SFTPTabbed.Tree")
dialog.allowMulti = true
dialog.isVisible = true
val hosts = dialog.hosts
if (hosts.isEmpty()) return
for (host in hosts) {
addSFTPFileSystemViewPanelTab(host)
} }
// 添加一个新的
addTab(
I18n.getString("termora.transport.sftp.select-host"),
SFTPFileSystemViewPanel(transportManager = transportManager)
)
selectedIndex = tabCount - 1
} }
}) })

View File

@@ -6,7 +6,9 @@ import app.termora.Icons
import app.termora.actions.AnAction import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import app.termora.terminal.ControlCharacters import app.termora.terminal.ControlCharacters
import app.termora.terminal.Null
import app.termora.terminal.panel.TerminalWriter import app.termora.terminal.panel.TerminalWriter
import org.apache.commons.lang3.StringUtils
import org.apache.commons.text.StringEscapeUtils import org.apache.commons.text.StringEscapeUtils
class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) { class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) {
@@ -33,11 +35,23 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
"\\a" to ControlCharacters.BEL, "\\a" to ControlCharacters.BEL,
"\\e" to ControlCharacters.ESC, "\\e" to ControlCharacters.ESC,
) )
val chars = snippet.snippet.toCharArray()
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
}
var text = StringEscapeUtils.unescapeJava(snippet.snippet) var text = chars.joinToString(StringUtils.EMPTY)
text = StringEscapeUtils.unescapeJava(text)
for (e in map.entries) { for (e in map.entries) {
text = text.replace(e.key, e.value.toString()) text = text.replace(e.key, e.value.toString())
} }
text = text.replace(Char.Null, '\\')
writer.write(TerminalWriter.WriteRequest.fromBytes(text.toByteArray(writer.getCharset()))) writer.write(TerminalWriter.WriteRequest.fromBytes(text.toByteArray(writer.getCharset())))
} }
} }

View File

@@ -41,8 +41,10 @@ class SnippetPanel : JPanel(BorderLayout()), Disposable {
private fun initViews() { private fun initViews() {
val splitPane = JSplitPane() val splitPane = JSplitPane()
splitPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) 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( leftPanel.border = BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor), BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
BorderFactory.createEmptyBorder(4, 4, 4, 4) BorderFactory.createEmptyBorder(4, 4, 4, 4)

View File

@@ -1,5 +1,6 @@
package app.termora.snippet package app.termora.snippet
import app.termora.I18n
import app.termora.SimpleTreeModel import app.termora.SimpleTreeModel
import javax.swing.tree.MutableTreeNode import javax.swing.tree.MutableTreeNode
import javax.swing.tree.TreeNode import javax.swing.tree.TreeNode
@@ -8,7 +9,7 @@ class SnippetTreeModel : SimpleTreeModel<Snippet>(
SnippetTreeNode( SnippetTreeNode(
Snippet( Snippet(
id = "0", id = "0",
name = "全部片段", name = I18n.getString("termora.snippet.title"),
type = SnippetType.Folder type = SnippetType.Folder
) )
) )

View File

@@ -399,6 +399,16 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
} }
} }
// CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
'Z' -> {
val count = args.toInt(1)
val cursorModel = terminal.getCursorModel()
for (i in 0 until count) {
val x = terminal.getTabulator().previousTab(cursorModel.getPosition().x - 1) + 1
terminal.getCursorModel().move(cursorModel.getPosition().y, x)
}
}
// split // split
';' -> { ';' -> {
args.append(ch) args.append(ch)

View File

@@ -2,6 +2,7 @@ package app.termora.terminal.panel
import app.termora.Application import app.termora.Application
import app.termora.ApplicationScope import app.termora.ApplicationScope
import app.termora.Database
import app.termora.terminal.* import app.termora.terminal.*
import java.awt.Graphics import java.awt.Graphics
import java.net.URI import java.net.URI
@@ -16,6 +17,7 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
} }
private val regex = Regex("https?://\\S*[^.\\s'\",()<>\\[\\]]") private val regex = Regex("https?://\\S*[^.\\s'\",()<>\\[\\]]")
private val isEnableHyperlink get() = Database.getDatabase().terminal.hyperlink
override fun before( override fun before(
offset: Int, offset: Int,
@@ -25,6 +27,9 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
terminalDisplay: TerminalDisplay, terminalDisplay: TerminalDisplay,
terminal: Terminal terminal: Terminal
) { ) {
if (isEnableHyperlink.not()) return
val document = terminal.getDocument() val document = terminal.getDocument()
var startOffset = offset var startOffset = offset
var endOffset = startOffset + count 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)
}
}
} }

View File

@@ -79,6 +79,8 @@ class TerminalPanelKeyAdapter(
val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e)) val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e))
if (encode.isNotEmpty()) { if (encode.isNotEmpty()) {
writer.write(TerminalWriter.WriteRequest.fromBytes(encode.toByteArray(writer.getCharset()))) writer.write(TerminalWriter.WriteRequest.fromBytes(encode.toByteArray(writer.getCharset())))
// scroll to bottom
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
e.consume() e.consume()
} }
@@ -91,6 +93,8 @@ class TerminalPanelKeyAdapter(
if (isAltPressedOnly(e) && Character.isDefined(e.keyChar)) { if (isAltPressedOnly(e) && Character.isDefined(e.keyChar)) {
val c = String(charArrayOf(ASCII_ESC, simpleMapKeyCodeToChar(e))) val c = String(charArrayOf(ASCII_ESC, simpleMapKeyCodeToChar(e)))
writer.write(TerminalWriter.WriteRequest.fromBytes(c.toByteArray(writer.getCharset()))) writer.write(TerminalWriter.WriteRequest.fromBytes(c.toByteArray(writer.getCharset())))
// scroll to bottom
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
e.consume() e.consume()
return return
} }

View File

@@ -2,10 +2,12 @@ package app.termora.terminal.panel.vw
import app.termora.* import app.termora.*
import com.formdev.flatlaf.extras.components.FlatToolBar import com.formdev.flatlaf.extras.components.FlatToolBar
import com.formdev.flatlaf.util.SystemInfo
import java.awt.* import java.awt.*
import java.awt.event.* import java.awt.event.*
import java.beans.PropertyChangeEvent import java.beans.PropertyChangeEvent
import java.beans.PropertyChangeListener import java.beans.PropertyChangeListener
import javax.imageio.ImageIO
import javax.swing.* import javax.swing.*
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -333,6 +335,15 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
title = getWindowTitle() title = getWindowTitle()
isAlwaysOnTop = isAlwaysTop isAlwaysOnTop = isAlwaysTop
if (SystemInfo.isWindows || SystemInfo.isLinux) {
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
val loader = TermoraFrame::class.java.classLoader
val images = sizes.mapNotNull { e ->
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
}
iconImages = images
}
initEvents() initEvents()
init() init()

View 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")
}
}
}

View File

@@ -73,6 +73,7 @@ termora.settings.terminal.size=Size
termora.settings.terminal.max-rows=Max rows termora.settings.terminal.max-rows=Max rows
termora.settings.terminal.debug=Debug mode termora.settings.terminal.debug=Debug mode
termora.settings.terminal.beep=Beep termora.settings.terminal.beep=Beep
termora.settings.terminal.hyperlink=Hyperlink
termora.settings.terminal.select-copy=Select copy termora.settings.terminal.select-copy=Select copy
termora.settings.terminal.cursor-style=Cursor type termora.settings.terminal.cursor-style=Cursor type
termora.settings.terminal.cursor-blink=Cursor blink termora.settings.terminal.cursor-blink=Cursor blink
@@ -309,6 +310,7 @@ termora.transport.permissions.execute=Execute
termora.transport.permissions.owner=Owner termora.transport.permissions.owner=Owner
termora.transport.permissions.group=Group termora.transport.permissions.group=Group
termora.transport.permissions.others=Others termora.transport.permissions.others=Others
termora.transport.permissions.include-subfolder=Include subdirectories
termora.transport.sftp.retry=Retry termora.transport.sftp.retry=Retry
termora.transport.sftp.select-another-host=Select another host termora.transport.sftp.select-another-host=Select another host

View File

@@ -77,6 +77,7 @@ termora.settings.terminal.size=大小
termora.settings.terminal.max-rows=最大行数 termora.settings.terminal.max-rows=最大行数
termora.settings.terminal.debug=调试模式 termora.settings.terminal.debug=调试模式
termora.settings.terminal.beep=蜂鸣声 termora.settings.terminal.beep=蜂鸣声
termora.settings.terminal.hyperlink=超链接
termora.settings.terminal.select-copy=选中复制 termora.settings.terminal.select-copy=选中复制
termora.settings.terminal.cursor-style=光标样式 termora.settings.terminal.cursor-style=光标样式
termora.settings.terminal.cursor-blink=光标闪烁 termora.settings.terminal.cursor-blink=光标闪烁
@@ -299,7 +300,7 @@ termora.transport.sftp.status.done=已完成
termora.transport.sftp.status.failed=已失败 termora.transport.sftp.status.failed=已失败
termora.transport.sftp.already-exists.message1=此文件夹已包含下名称的对象 termora.transport.sftp.already-exists.message1=此文件夹已包含下名称的对象
termora.transport.sftp.already-exists.message2=请选择要执行的操作 termora.transport.sftp.already-exists.message2=请选择要执行的操作
termora.transport.sftp.already-exists.overwrite=覆盖 termora.transport.sftp.already-exists.overwrite=覆盖
termora.transport.sftp.already-exists.append=追加 termora.transport.sftp.already-exists.append=追加
@@ -322,6 +323,7 @@ termora.transport.permissions.execute=执行
termora.transport.permissions.owner=所有者 termora.transport.permissions.owner=所有者
termora.transport.permissions.group= termora.transport.permissions.group=
termora.transport.permissions.others=其他 termora.transport.permissions.others=其他
termora.transport.permissions.include-subfolder=包含子目录
# transport job # transport job
termora.transport.jobs.table.name=名称 termora.transport.jobs.table.name=名称

View File

@@ -88,7 +88,8 @@ termora.settings.terminal.font=字體
termora.settings.terminal.size=大小 termora.settings.terminal.size=大小
termora.settings.terminal.max-rows=最大行數 termora.settings.terminal.max-rows=最大行數
termora.settings.terminal.debug=偵錯模式 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.select-copy=選取複製
termora.settings.terminal.cursor-style=遊標風格 termora.settings.terminal.cursor-style=遊標風格
termora.settings.terminal.cursor-blink=遊標閃爍 termora.settings.terminal.cursor-blink=遊標閃爍
@@ -294,7 +295,7 @@ termora.transport.sftp.status.waiting=等待中
termora.transport.sftp.status.done=已完成 termora.transport.sftp.status.done=已完成
termora.transport.sftp.status.failed=已失敗 termora.transport.sftp.status.failed=已失敗
termora.transport.sftp.already-exists.message1=此資料夾已包含下名稱的對象 termora.transport.sftp.already-exists.message1=此資料夾已包含下名稱的對象
termora.transport.sftp.already-exists.message2=請選擇要執行的操作 termora.transport.sftp.already-exists.message2=請選擇要執行的操作
termora.transport.sftp.already-exists.overwrite=覆蓋 termora.transport.sftp.already-exists.overwrite=覆蓋
termora.transport.sftp.already-exists.append=追加 termora.transport.sftp.already-exists.append=追加
@@ -305,6 +306,17 @@ termora.transport.sftp.already-exists.destination=目標文件
termora.transport.sftp.already-exists.source=原始檔 termora.transport.sftp.already-exists.source=原始檔
termora.transport.sftp.already-exists.actions=操作 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 # transport job
termora.transport.jobs.table.name=名稱 termora.transport.jobs.table.name=名稱
termora.transport.jobs.table.status=狀態 termora.transport.jobs.table.status=狀態

View File

@@ -0,0 +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="M2.00098 2H7.00195L7.00098 7H2L2.00098 2ZM8.00293 2H13V7H8.00293V2ZM2 7.99902L7 8V13.001L2 13V7.99902ZM8.00195 8H12.999L12.998 13.001H8.00195" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 399 B

View File

@@ -0,0 +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="M2.00098 2H7.00195L7.00098 7H2L2.00098 2ZM8.00293 2H13V7H8.00293V2ZM2 7.99902L7 8V13.001L2 13V7.99902ZM8.00195 8H12.999L12.998 13.001H8.00195" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 399 B

View File

@@ -1,6 +1,6 @@
FROM linuxserver/openssh-server FROM linuxserver/openssh-server
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update && apk add wget gcc g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \ && apk update && apk add wget tmux gcc g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \ && tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz && ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/sshd_config RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/sshd_config