Compare commits

...

12 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
19 changed files with 395 additions and 188 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.14" 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,5 +1,5 @@
[versions] [versions]
kotlin = "2.1.20" kotlin = "2.1.21"
slf4j = "2.0.17" slf4j = "2.0.17"
pty4j = "0.13.4" pty4j = "0.13.4"
tinylog = "2.7.0" tinylog = "2.7.0"

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

@@ -135,10 +135,12 @@ class NewHostTree : SimpleTree() {
// double click // double click
addMouseListener(object : MouseAdapter() { addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { override fun mouseClicked(e: MouseEvent) {
if (getPathForLocation(e.x, e.y) == null) return
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

@@ -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

@@ -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,8 +157,12 @@ 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) {
if (hasParent && i == 0) {
names.add("..")
} else {
names.add(getFileObject(i).name.baseName) 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

@@ -50,7 +50,7 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
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 = snippet.snippet.replace(Char.Null, '\\') 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

@@ -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

@@ -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=光标闪烁
@@ -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=遊標閃爍
@@ -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=狀態