mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc4333da21 | ||
|
|
184f6d46dc | ||
|
|
68788905fe | ||
|
|
fc46216a3f | ||
|
|
563143645e | ||
|
|
891ccb901b | ||
|
|
928a866fe7 | ||
|
|
ea25b5b46f | ||
|
|
1de10e6129 | ||
|
|
aaf9c2e8d2 | ||
|
|
b8196b5730 | ||
|
|
0a83e8beb4 |
124
build.gradle.kts
124
build.gradle.kts
@@ -14,13 +14,14 @@ plugins {
|
||||
java
|
||||
idea
|
||||
application
|
||||
`maven-publish`
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlinx.serialization)
|
||||
}
|
||||
|
||||
|
||||
group = "app.termora"
|
||||
version = "1.0.14"
|
||||
version = "1.0.15"
|
||||
|
||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||
@@ -56,67 +57,67 @@ dependencies {
|
||||
|
||||
// implementation(platform(libs.koin.bom))
|
||||
// implementation(libs.koin.core)
|
||||
implementation(libs.slf4j.api)
|
||||
implementation(libs.pty4j)
|
||||
implementation(libs.slf4j.tinylog)
|
||||
implementation(libs.tinylog.impl)
|
||||
implementation(libs.commons.codec)
|
||||
implementation(libs.commons.io)
|
||||
implementation(libs.commons.lang3)
|
||||
implementation(libs.commons.csv)
|
||||
implementation(libs.commons.net)
|
||||
implementation(libs.commons.text)
|
||||
implementation(libs.commons.compress)
|
||||
implementation(libs.commons.vfs2) { exclude(group = "*", module = "*") }
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
api(libs.slf4j.api)
|
||||
api(libs.pty4j)
|
||||
api(libs.slf4j.tinylog)
|
||||
api(libs.tinylog.impl)
|
||||
api(libs.commons.codec)
|
||||
api(libs.commons.io)
|
||||
api(libs.commons.lang3)
|
||||
api(libs.commons.csv)
|
||||
api(libs.commons.net)
|
||||
api(libs.commons.text)
|
||||
api(libs.commons.compress)
|
||||
api(libs.commons.vfs2) { exclude(group = "*", module = "*") }
|
||||
api(libs.kotlinx.coroutines.swing)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
|
||||
implementation(libs.flatlaf) {
|
||||
api(libs.flatlaf) {
|
||||
artifact {
|
||||
if (useNoNativesFlatLaf) {
|
||||
classifier = "no-natives"
|
||||
}
|
||||
}
|
||||
}
|
||||
implementation(libs.flatlaf.extras) {
|
||||
api(libs.flatlaf.extras) {
|
||||
if (useNoNativesFlatLaf) {
|
||||
exclude(group = "com.formdev", module = "flatlaf")
|
||||
}
|
||||
}
|
||||
implementation(libs.flatlaf.swingx) {
|
||||
api(libs.flatlaf.swingx) {
|
||||
if (useNoNativesFlatLaf) {
|
||||
exclude(group = "com.formdev", module = "flatlaf")
|
||||
}
|
||||
}
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.swingx)
|
||||
implementation(libs.jgoodies.forms)
|
||||
implementation(libs.jna)
|
||||
implementation(libs.jna.platform)
|
||||
implementation(libs.versioncompare)
|
||||
implementation(libs.oshi.core)
|
||||
implementation(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
||||
implementation(libs.jfa) { exclude(group = "*", module = "*") }
|
||||
implementation(libs.jbr.api)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.okhttp.logging)
|
||||
implementation(libs.sshd.core)
|
||||
implementation(libs.commonmark)
|
||||
implementation(libs.jgit)
|
||||
implementation(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||
implementation(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||
implementation(libs.eddsa)
|
||||
implementation(libs.jnafilechooser)
|
||||
implementation(libs.xodus.vfs)
|
||||
implementation(libs.xodus.openAPI)
|
||||
implementation(libs.xodus.environment)
|
||||
implementation(libs.bip39)
|
||||
implementation(libs.colorpicker)
|
||||
implementation(libs.mixpanel)
|
||||
implementation(libs.jSerialComm)
|
||||
implementation(libs.ini4j)
|
||||
implementation(libs.restart4j)
|
||||
api(libs.kotlinx.serialization.json)
|
||||
api(libs.swingx)
|
||||
api(libs.jgoodies.forms)
|
||||
api(libs.jna)
|
||||
api(libs.jna.platform)
|
||||
api(libs.versioncompare)
|
||||
api(libs.oshi.core)
|
||||
api(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
||||
api(libs.jfa) { exclude(group = "*", module = "*") }
|
||||
api(libs.jbr.api)
|
||||
api(libs.okhttp)
|
||||
api(libs.okhttp.logging)
|
||||
api(libs.sshd.core)
|
||||
api(libs.commonmark)
|
||||
api(libs.jgit)
|
||||
api(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||
api(libs.eddsa)
|
||||
api(libs.jnafilechooser)
|
||||
api(libs.xodus.vfs)
|
||||
api(libs.xodus.openAPI)
|
||||
api(libs.xodus.environment)
|
||||
api(libs.bip39)
|
||||
api(libs.colorpicker)
|
||||
api(libs.mixpanel)
|
||||
api(libs.jSerialComm)
|
||||
api(libs.ini4j)
|
||||
api(libs.restart4j)
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -147,6 +148,37 @@ application {
|
||||
mainClass = "app.termora.MainKt"
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("mavenJava") {
|
||||
from(components["java"])
|
||||
pom {
|
||||
name = project.name
|
||||
description = "Termora is a terminal emulator and SSH client for Windows, macOS and Linux"
|
||||
url = "https://github.com/TermoraDev/termora"
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name = "AGPL-3.0"
|
||||
url = "https://opensource.org/license/agpl-v3"
|
||||
}
|
||||
}
|
||||
|
||||
developers {
|
||||
developer {
|
||||
name = "hstyi"
|
||||
url = "https://github.com/hstyi"
|
||||
}
|
||||
}
|
||||
|
||||
scm {
|
||||
url = "https://github.com/TermoraDev/termora"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
kotlin = "2.1.20"
|
||||
kotlin = "2.1.21"
|
||||
slf4j = "2.0.17"
|
||||
pty4j = "0.13.4"
|
||||
tinylog = "2.7.0"
|
||||
|
||||
@@ -523,6 +523,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
*/
|
||||
var beep by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* 超链接
|
||||
*/
|
||||
var hyperlink by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* 光标闪烁
|
||||
*/
|
||||
|
||||
@@ -135,10 +135,12 @@ class NewHostTree : SimpleTree() {
|
||||
// double click
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (getPathForLocation(e.x, e.y) == null) return
|
||||
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
|
||||
if (lastNode.host.protocol != Protocol.Folder) {
|
||||
val path = tree.getClosestPathForLocation(e.x, e.y) ?: return
|
||||
val bounds = tree.getRowBounds(tree.getRowForPath(path)) ?: return
|
||||
if ((e.y >= bounds.y && e.y < (bounds.y + bounds.height)).not()) return
|
||||
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, lastNode.host, e))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,6 +422,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private val selectCopyComboBox = YesOrNoComboBox()
|
||||
private val autoCloseTabComboBox = YesOrNoComboBox()
|
||||
private val floatingToolbarComboBox = YesOrNoComboBox()
|
||||
private val hyperlinkComboBox = YesOrNoComboBox()
|
||||
|
||||
init {
|
||||
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 ->
|
||||
if (e.stateChange == ItemEvent.SELECTED) {
|
||||
terminalSetting.cursorBlink = cursorBlinkComboBox.selectedItem as Boolean
|
||||
@@ -591,6 +599,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
fontComboBox.selectedItem = terminalSetting.font
|
||||
debugComboBox.selectedItem = terminalSetting.debug
|
||||
beepComboBox.selectedItem = terminalSetting.beep
|
||||
hyperlinkComboBox.selectedItem = terminalSetting.hyperlink
|
||||
cursorBlinkComboBox.selectedItem = terminalSetting.cursorBlink
|
||||
cursorStyleComboBox.selectedItem = terminalSetting.cursor
|
||||
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
|
||||
@@ -613,7 +622,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
val beepBtn = JButton(Icons.run)
|
||||
@@ -636,6 +645,8 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.add("${I18n.getString("termora.settings.terminal.beep")}:").xy(1, rows)
|
||||
.add(beepComboBox).xy(3, rows)
|
||||
.add(beepBtn).xy(5, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.hyperlink")}:").xy(1, rows)
|
||||
.add(hyperlinkComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
||||
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
||||
|
||||
@@ -4,6 +4,7 @@ import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.actions.MultipleAction
|
||||
import app.termora.highlight.KeywordHighlightPaintListener
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.PtyConnector
|
||||
import app.termora.terminal.Terminal
|
||||
import app.termora.terminal.panel.TerminalHyperlinkPaintListener
|
||||
@@ -40,6 +41,10 @@ class TerminalPanelFactory : Disposable {
|
||||
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
||||
val writer = MyTerminalWriter(ptyConnector)
|
||||
val terminalPanel = TerminalPanel(terminal, writer)
|
||||
|
||||
// processDeviceStatusReport
|
||||
terminal.getTerminalModel().setData(DataKey.TerminalWriter, writer)
|
||||
|
||||
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
||||
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
||||
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
||||
|
||||
@@ -4,6 +4,7 @@ import app.termora.*
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.SettingsAction
|
||||
import app.termora.sftp.FileSystemViewTable.AskTransfer.Action
|
||||
import app.termora.vfs2.VFSWalker
|
||||
import app.termora.vfs2.sftp.MySftpFileObject
|
||||
import app.termora.vfs2.sftp.MySftpFileSystem
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
@@ -37,7 +38,6 @@ import java.nio.file.FileVisitor
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.nio.file.attribute.FileTime
|
||||
import java.text.MessageFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -45,6 +45,21 @@ import java.util.regex.Pattern
|
||||
import javax.swing.*
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import kotlin.collections.ArrayDeque
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.all
|
||||
import kotlin.collections.contains
|
||||
import kotlin.collections.filter
|
||||
import kotlin.collections.filterIsInstance
|
||||
import kotlin.collections.find
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.collections.isEmpty
|
||||
import kotlin.collections.isNotEmpty
|
||||
import kotlin.collections.last
|
||||
import kotlin.collections.listOf
|
||||
import kotlin.collections.map
|
||||
import kotlin.collections.mapOf
|
||||
import kotlin.collections.mutableListOf
|
||||
import kotlin.collections.sortedArray
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
@@ -218,7 +233,7 @@ class FileSystemViewTable(
|
||||
val localTarget = sftpPanel.getLocalTarget()
|
||||
val table = localTarget.getData(SFTPDataProviders.FileSystemViewTable) ?: return false
|
||||
// 委托最左侧的本地文件系统传输
|
||||
table.transfer(paths, true, targetWorkdir)
|
||||
table.transfer(paths, true, targetWorkdir, fileSystemViewPanel)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -360,34 +375,7 @@ class FileSystemViewTable(
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val last = files.last()
|
||||
if (last !is MySftpFileObject) return
|
||||
|
||||
val dialog = PosixFilePermissionDialog(
|
||||
SwingUtilities.getWindowAncestor(table),
|
||||
model.getFilePermissions(last)
|
||||
)
|
||||
val permissions = dialog.open() ?: return
|
||||
|
||||
if (fileSystemViewPanel.requestLoading()) {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
val c = runCatching { last.setPosixFilePermissions(permissions) }.onFailure {
|
||||
withContext(Dispatchers.Swing) {
|
||||
OptionPane.showMessageDialog(
|
||||
owner,
|
||||
ExceptionUtils.getMessage(it),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// stop loading
|
||||
fileSystemViewPanel.stopLoading()
|
||||
|
||||
// reload
|
||||
if (c.isSuccess) {
|
||||
fileSystemViewPanel.reload(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
changePermission(last)
|
||||
}
|
||||
})
|
||||
refresh.addActionListener { fileSystemViewPanel.reload() }
|
||||
@@ -409,6 +397,80 @@ class FileSystemViewTable(
|
||||
popupMenu.show(table, e.x, e.y)
|
||||
}
|
||||
|
||||
private fun changePermission(file: MySftpFileObject) {
|
||||
|
||||
val dialog = PosixFilePermissionDialog(
|
||||
SwingUtilities.getWindowAncestor(table),
|
||||
model.getFilePermissions(file)
|
||||
)
|
||||
val permissions = dialog.open() ?: return
|
||||
val isIncludeSubdirectories = dialog.isIncludeSubdirectories()
|
||||
|
||||
if (fileSystemViewPanel.requestLoading()) {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
val c = runCatching {
|
||||
file.setPosixFilePermissions(permissions)
|
||||
if (isIncludeSubdirectories && file.isFolder) {
|
||||
file.refresh()
|
||||
VFSWalker.walk(file, object : FileVisitor<FileObject> {
|
||||
override fun preVisitDirectory(
|
||||
dir: FileObject,
|
||||
attrs: BasicFileAttributes
|
||||
): FileVisitResult {
|
||||
dir.refresh()
|
||||
if (dir is MySftpFileObject) {
|
||||
dir.setPosixFilePermissions(permissions)
|
||||
}
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
override fun visitFile(
|
||||
file: FileObject,
|
||||
attrs: BasicFileAttributes
|
||||
): FileVisitResult {
|
||||
if (file is MySftpFileObject) {
|
||||
file.setPosixFilePermissions(permissions)
|
||||
}
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
override fun visitFileFailed(
|
||||
file: FileObject,
|
||||
exc: IOException
|
||||
): FileVisitResult {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
|
||||
override fun postVisitDirectory(
|
||||
dir: FileObject,
|
||||
exc: IOException?
|
||||
): FileVisitResult {
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}.onFailure {
|
||||
withContext(Dispatchers.Swing) {
|
||||
OptionPane.showMessageDialog(
|
||||
owner,
|
||||
ExceptionUtils.getMessage(it),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// stop loading
|
||||
fileSystemViewPanel.stopLoading()
|
||||
|
||||
// reload
|
||||
if (c.isSuccess) {
|
||||
fileSystemViewPanel.reload(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renameSelection() {
|
||||
val index = selectedRow
|
||||
if (index < 0) return
|
||||
@@ -619,12 +681,13 @@ class FileSystemViewTable(
|
||||
private fun transfer(
|
||||
files: List<FileObject>,
|
||||
fromLocalSystem: Boolean = false,
|
||||
targetWorkdir: FileObject? = null
|
||||
targetWorkdir: FileObject? = null,
|
||||
target: FileSystemViewPanel? = null,
|
||||
) {
|
||||
|
||||
assertEventDispatchThread()
|
||||
|
||||
val target = sftpPanel.getTarget(table) ?: return
|
||||
val target = (target ?: sftpPanel.getTarget(table)) ?: return
|
||||
val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return
|
||||
var isApplyAll = false
|
||||
var lastAction = Action.Overwrite
|
||||
@@ -650,7 +713,7 @@ class FileSystemViewTable(
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
doTransfer(file, lastAction, fromLocalSystem, targetWorkdir)
|
||||
doTransfer(file, lastAction, fromLocalSystem, targetWorkdir, target)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
@@ -801,10 +864,11 @@ class FileSystemViewTable(
|
||||
file: FileObject,
|
||||
action: Action,
|
||||
fromLocalSystem: Boolean,
|
||||
targetWorkdir: FileObject?
|
||||
targetWorkdir: FileObject?,
|
||||
target: FileSystemViewPanel? = null
|
||||
) {
|
||||
val sftpPanel = this.sftpPanel
|
||||
val target = sftpPanel.getTarget(table) ?: return
|
||||
val target = (target ?: sftpPanel.getTarget(table)) ?: return
|
||||
|
||||
/**
|
||||
* 定义一个添加器,它可以自动的判断导入/拖拽行为
|
||||
@@ -886,36 +950,7 @@ class FileSystemViewTable(
|
||||
dir: FileObject,
|
||||
visitor: FileVisitor<FileObject>,
|
||||
): FileVisitResult {
|
||||
|
||||
// clear cache
|
||||
if (visitor.preVisitDirectory(dir, EmptyBasicFileAttributes.INSTANCE) == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
|
||||
for (e in dir.children) {
|
||||
if (e.name.baseName == ".." || e.name.baseName == ".") continue
|
||||
if (e.isFolder) {
|
||||
if (walk(dir.resolveFile(e.name.baseName), visitor) == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
} else {
|
||||
val result = visitor.visitFile(
|
||||
dir.resolveFile(e.name.baseName),
|
||||
EmptyBasicFileAttributes.INSTANCE
|
||||
)
|
||||
if (result == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
} else if (result == FileVisitResult.SKIP_SUBTREE) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (visitor.postVisitDirectory(dir, null) == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
|
||||
return FileVisitResult.CONTINUE
|
||||
return VFSWalker.walk(dir, visitor)
|
||||
}
|
||||
|
||||
private fun addTransport(
|
||||
@@ -974,47 +1009,5 @@ class FileSystemViewTable(
|
||||
|
||||
}
|
||||
|
||||
private class EmptyBasicFileAttributes : BasicFileAttributes {
|
||||
companion object {
|
||||
val INSTANCE = EmptyBasicFileAttributes()
|
||||
}
|
||||
|
||||
override fun lastModifiedTime(): FileTime {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun lastAccessTime(): FileTime {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun creationTime(): FileTime {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isRegularFile(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isDirectory(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isSymbolicLink(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isOther(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun size(): Long {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun fileKey(): Any {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -157,7 +157,11 @@ class FileSystemViewTableModel : DefaultTableModel() {
|
||||
fun getPathNames(): Set<String> {
|
||||
val names = linkedSetOf<String>()
|
||||
for (i in 0 until rowCount) {
|
||||
names.add(getFileObject(i).name.baseName)
|
||||
if (hasParent && i == 0) {
|
||||
names.add("..")
|
||||
} else {
|
||||
names.add(getFileObject(i).name.baseName)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class PosixFilePermissionDialog(
|
||||
private val otherRead = JCheckBox(I18n.getString("termora.transport.permissions.read"))
|
||||
private val otherWrite = JCheckBox(I18n.getString("termora.transport.permissions.write"))
|
||||
private val otherExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute"))
|
||||
private val includeSubFolder = JCheckBox(I18n.getString("termora.transport.permissions.include-subfolder"))
|
||||
|
||||
private var isCancelled = false
|
||||
|
||||
@@ -60,13 +61,14 @@ class PosixFilePermissionDialog(
|
||||
otherRead.isFocusable = false
|
||||
otherWrite.isFocusable = false
|
||||
otherExecute.isFocusable = false
|
||||
includeSubFolder.isFocusable = false
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val formMargin = "7dlu"
|
||||
val layout = FormLayout(
|
||||
"default:grow, $formMargin, default:grow, $formMargin, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref"
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
||||
@@ -95,6 +97,8 @@ class PosixFilePermissionDialog(
|
||||
otherBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.others"))
|
||||
builder.add(otherBox).xy(5, 3)
|
||||
|
||||
builder.add(includeSubFolder).xyw(1, 5, 5)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@@ -103,6 +107,10 @@ class PosixFilePermissionDialog(
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
fun isIncludeSubdirectories(): Boolean {
|
||||
return includeSubFolder.isSelected
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回空表示取消了
|
||||
*/
|
||||
|
||||
@@ -56,6 +56,16 @@ class SFTPAction : AnAction("SFTP", Icons.folder) {
|
||||
}
|
||||
|
||||
val host = hostManager.getHost(hostId) ?: return
|
||||
for (i in 0 until tabbed.tabCount) {
|
||||
val c = tabbed.getComponentAt(i)
|
||||
if (c is SFTPFileSystemViewPanel) {
|
||||
if (c.state == SFTPFileSystemViewPanel.State.Initialized) {
|
||||
c.selectHost(host)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tabbed.addSFTPFileSystemViewPanelTab(host)
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.swing.*
|
||||
import javax.swing.event.TreeExpansionEvent
|
||||
import javax.swing.event.TreeExpansionListener
|
||||
|
||||
class SFTPFileSystemViewPanel(
|
||||
var host: Host? = null,
|
||||
@@ -35,17 +37,18 @@ class SFTPFileSystemViewPanel(
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(SFTPFileSystemViewPanel::class.java)
|
||||
}
|
||||
|
||||
private enum class State {
|
||||
Initialized,
|
||||
Connecting,
|
||||
Connected,
|
||||
ConnectFailed,
|
||||
}
|
||||
enum class State {
|
||||
Initialized,
|
||||
Connecting,
|
||||
Connected,
|
||||
ConnectFailed,
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var state = State.Initialized
|
||||
var state = State.Initialized
|
||||
private set
|
||||
private val cardLayout = CardLayout()
|
||||
private val cardPanel = JPanel(cardLayout)
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
@@ -283,12 +286,20 @@ class SFTPFileSystemViewPanel(
|
||||
val node = tree.getLastSelectedPathNode() ?: return
|
||||
if (node.isFolder) return
|
||||
val host = node.data as Host
|
||||
that.setTabTitle(host.name)
|
||||
that.host = host
|
||||
that.connect()
|
||||
selectHost(host)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
tree.addTreeExpansionListener(object : TreeExpansionListener {
|
||||
override fun treeExpanded(event: TreeExpansionEvent) {
|
||||
properties.putString("SFTPTabbed.Tree.state", TreeUtils.saveExpansionState(tree))
|
||||
}
|
||||
|
||||
override fun treeCollapsed(event: TreeExpansionEvent) {
|
||||
properties.putString("SFTPTabbed.Tree.state", TreeUtils.saveExpansionState(tree))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
@@ -305,6 +316,12 @@ class SFTPFileSystemViewPanel(
|
||||
}
|
||||
}
|
||||
|
||||
fun selectHost(host: Host) {
|
||||
that.setTabTitle(host.name)
|
||||
that.host = host
|
||||
that.connect()
|
||||
}
|
||||
|
||||
private fun setTabTitle(title: String) {
|
||||
val tabbed = SwingUtilities.getAncestorOfClass(JTabbedPane::class.java, that)
|
||||
if (tabbed is JTabbedPane) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import java.awt.Point
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -13,7 +12,6 @@ import javax.swing.JButton
|
||||
import javax.swing.JToolBar
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.UIManager
|
||||
import kotlin.math.max
|
||||
|
||||
@Suppress("DuplicatedCode")
|
||||
class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPane(), Disposable {
|
||||
@@ -43,23 +41,20 @@ class SFTPTabbed(private val transportManager: TransportManager) : FlatTabbedPan
|
||||
private fun initEvents() {
|
||||
addBtn.addActionListener(object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val dialog = NewHostTreeDialog(SwingUtilities.getWindowAncestor(tabbed))
|
||||
dialog.location = Point(
|
||||
max(0, addBtn.locationOnScreen.x - dialog.width / 2 + addBtn.width / 2),
|
||||
addBtn.locationOnScreen.y + max(tabHeight, addBtn.height)
|
||||
)
|
||||
dialog.setFilter { it.host.protocol == Protocol.SSH }
|
||||
dialog.setTreeName("SFTPTabbed.Tree")
|
||||
dialog.allowMulti = true
|
||||
dialog.isVisible = true
|
||||
|
||||
val hosts = dialog.hosts
|
||||
if (hosts.isEmpty()) return
|
||||
|
||||
for (host in hosts) {
|
||||
addSFTPFileSystemViewPanelTab(host)
|
||||
for (i in 0 until tabCount) {
|
||||
val c = getComponentAt(i)
|
||||
if (c !is SFTPFileSystemViewPanel) continue
|
||||
if (c.state != SFTPFileSystemViewPanel.State.Initialized) continue
|
||||
selectedIndex = i
|
||||
return
|
||||
}
|
||||
|
||||
// 添加一个新的
|
||||
addTab(
|
||||
I18n.getString("termora.transport.sftp.select-host"),
|
||||
SFTPFileSystemViewPanel(transportManager = transportManager)
|
||||
)
|
||||
selectedIndex = tabCount - 1
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
|
||||
for (e in map.entries) {
|
||||
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())))
|
||||
}
|
||||
|
||||
@@ -41,8 +41,10 @@ class SnippetPanel : JPanel(BorderLayout()), Disposable {
|
||||
private fun initViews() {
|
||||
val splitPane = JSplitPane()
|
||||
splitPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||
val scrollPane = JScrollPane(snippetTree)
|
||||
scrollPane.border = BorderFactory.createEmptyBorder()
|
||||
|
||||
leftPanel.add(snippetTree, BorderLayout.CENTER)
|
||||
leftPanel.add(scrollPane, BorderLayout.CENTER)
|
||||
leftPanel.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
|
||||
BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.termora.terminal.panel
|
||||
|
||||
import app.termora.Application
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.Database
|
||||
import app.termora.terminal.*
|
||||
import java.awt.Graphics
|
||||
import java.net.URI
|
||||
@@ -16,6 +17,7 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
||||
}
|
||||
|
||||
private val regex = Regex("https?://\\S*[^.\\s'\",()<>\\[\\]]")
|
||||
private val isEnableHyperlink get() = Database.getDatabase().terminal.hyperlink
|
||||
|
||||
override fun before(
|
||||
offset: Int,
|
||||
@@ -25,6 +27,9 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
||||
terminalDisplay: TerminalDisplay,
|
||||
terminal: Terminal
|
||||
) {
|
||||
|
||||
if (isEnableHyperlink.not()) return
|
||||
|
||||
val document = terminal.getDocument()
|
||||
var startOffset = offset
|
||||
var endOffset = startOffset + count
|
||||
@@ -91,4 +96,18 @@ class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListen
|
||||
}
|
||||
}
|
||||
|
||||
override fun after(
|
||||
offset: Int,
|
||||
count: Int,
|
||||
g: Graphics,
|
||||
terminalPanel: TerminalPanel,
|
||||
terminalDisplay: TerminalDisplay,
|
||||
terminal: Terminal
|
||||
) {
|
||||
if (isEnableHyperlink.not()) {
|
||||
// 删除之前的
|
||||
terminal.getMarkupModel().removeAllHighlighters(Highlighter.HYPERLINK)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
88
src/main/kotlin/app/termora/vfs2/VFSWalker.kt
Normal file
88
src/main/kotlin/app/termora/vfs2/VFSWalker.kt
Normal file
@@ -0,0 +1,88 @@
|
||||
package app.termora.vfs2
|
||||
|
||||
import org.apache.commons.vfs2.FileObject
|
||||
import java.nio.file.FileVisitResult
|
||||
import java.nio.file.FileVisitor
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.nio.file.attribute.FileTime
|
||||
|
||||
object VFSWalker {
|
||||
fun walk(
|
||||
dir: FileObject,
|
||||
visitor: FileVisitor<FileObject>,
|
||||
): FileVisitResult {
|
||||
|
||||
// clear cache
|
||||
if (visitor.preVisitDirectory(dir, EmptyBasicFileAttributes.INSTANCE) == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
|
||||
for (e in dir.children) {
|
||||
if (e.name.baseName == ".." || e.name.baseName == ".") continue
|
||||
if (e.isFolder) {
|
||||
if (walk(dir.resolveFile(e.name.baseName), visitor) == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
} else {
|
||||
val result = visitor.visitFile(
|
||||
dir.resolveFile(e.name.baseName),
|
||||
EmptyBasicFileAttributes.INSTANCE
|
||||
)
|
||||
if (result == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
} else if (result == FileVisitResult.SKIP_SUBTREE) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (visitor.postVisitDirectory(dir, null) == FileVisitResult.TERMINATE) {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
private class EmptyBasicFileAttributes : BasicFileAttributes {
|
||||
companion object {
|
||||
val INSTANCE = EmptyBasicFileAttributes()
|
||||
}
|
||||
|
||||
override fun lastModifiedTime(): FileTime {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun lastAccessTime(): FileTime {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun creationTime(): FileTime {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isRegularFile(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isDirectory(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isSymbolicLink(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isOther(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun size(): Long {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun fileKey(): Any {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ termora.settings.terminal.size=Size
|
||||
termora.settings.terminal.max-rows=Max rows
|
||||
termora.settings.terminal.debug=Debug mode
|
||||
termora.settings.terminal.beep=Beep
|
||||
termora.settings.terminal.hyperlink=Hyperlink
|
||||
termora.settings.terminal.select-copy=Select copy
|
||||
termora.settings.terminal.cursor-style=Cursor type
|
||||
termora.settings.terminal.cursor-blink=Cursor blink
|
||||
@@ -309,6 +310,7 @@ termora.transport.permissions.execute=Execute
|
||||
termora.transport.permissions.owner=Owner
|
||||
termora.transport.permissions.group=Group
|
||||
termora.transport.permissions.others=Others
|
||||
termora.transport.permissions.include-subfolder=Include subdirectories
|
||||
|
||||
termora.transport.sftp.retry=Retry
|
||||
termora.transport.sftp.select-another-host=Select another host
|
||||
|
||||
@@ -77,6 +77,7 @@ termora.settings.terminal.size=大小
|
||||
termora.settings.terminal.max-rows=最大行数
|
||||
termora.settings.terminal.debug=调试模式
|
||||
termora.settings.terminal.beep=蜂鸣声
|
||||
termora.settings.terminal.hyperlink=超链接
|
||||
termora.settings.terminal.select-copy=选中复制
|
||||
termora.settings.terminal.cursor-style=光标样式
|
||||
termora.settings.terminal.cursor-blink=光标闪烁
|
||||
@@ -322,6 +323,7 @@ termora.transport.permissions.execute=执行
|
||||
termora.transport.permissions.owner=所有者
|
||||
termora.transport.permissions.group=组
|
||||
termora.transport.permissions.others=其他
|
||||
termora.transport.permissions.include-subfolder=包含子目录
|
||||
|
||||
# transport job
|
||||
termora.transport.jobs.table.name=名称
|
||||
|
||||
@@ -88,7 +88,8 @@ termora.settings.terminal.font=字體
|
||||
termora.settings.terminal.size=大小
|
||||
termora.settings.terminal.max-rows=最大行數
|
||||
termora.settings.terminal.debug=偵錯模式
|
||||
termora.settings.terminal.beep=蜂鳴聲
|
||||
termora.settings.terminal.beep=超連結
|
||||
termora.settings.terminal.hyperlink=Hyperlink
|
||||
termora.settings.terminal.select-copy=選取複製
|
||||
termora.settings.terminal.cursor-style=遊標風格
|
||||
termora.settings.terminal.cursor-blink=遊標閃爍
|
||||
@@ -305,6 +306,17 @@ termora.transport.sftp.already-exists.destination=目標文件
|
||||
termora.transport.sftp.already-exists.source=原始檔
|
||||
termora.transport.sftp.already-exists.actions=操作
|
||||
|
||||
# permissions
|
||||
termora.transport.permissions=更改權限
|
||||
termora.transport.permissions.file-folder-permissions=檔案/資料夾權限
|
||||
termora.transport.permissions.read=讀取
|
||||
termora.transport.permissions.write=寫入
|
||||
termora.transport.permissions.execute=執行
|
||||
termora.transport.permissions.owner=所有者
|
||||
termora.transport.permissions.group=群組
|
||||
termora.transport.permissions.others=其他
|
||||
termora.transport.permissions.include-subfolder=包含子目錄
|
||||
|
||||
# transport job
|
||||
termora.transport.jobs.table.name=名稱
|
||||
termora.transport.jobs.table.status=狀態
|
||||
|
||||
Reference in New Issue
Block a user