feat: supports importing hosts from Xshell (#292)

This commit is contained in:
hstyi
2025-02-22 13:15:45 +08:00
committed by GitHub
parent adabaf8f2d
commit 034ee3791d
7 changed files with 53 additions and 7 deletions

View File

@@ -42,6 +42,10 @@ commons-csv 1.13.0
Apache License 2.0
https://github.com/apache/commons-csv/blob/master/LICENSE.txt
ini4j 0.5.5-2
Apache License 2.0
http://www.apache.org/licenses/LICENSE-2.0.txt
eddsa 0.3.0
Creative Commons Zero v1.0 Universal
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt

View File

@@ -108,6 +108,7 @@ dependencies {
implementation(libs.colorpicker)
implementation(libs.mixpanel)
implementation(libs.jSerialComm)
implementation(libs.ini4j)
}
application {

View File

@@ -43,6 +43,7 @@ delight-rhino-sandbox = "0.0.17"
testcontainers = "1.20.4"
mixpanel = "1.5.3"
jSerialComm = "2.11.0"
ini4j = "0.5.5-2"
[libraries]
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
@@ -58,6 +59,7 @@ commons-csv = { group = "org.apache.commons", name = "commons-csv", version.ref
commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" }
commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commons-compress" }
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }
ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" }
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
flatlaf-extras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
trove4j = { group = "org.jetbrains.intellij.deps", name = "trove4j", version.ref = "trove4j" }

View File

@@ -15,8 +15,10 @@ import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVParser
import org.apache.commons.csv.CSVPrinter
import org.apache.commons.io.FileUtils
import org.apache.commons.io.FilenameUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.exception.ExceptionUtils
import org.ini4j.Ini
import org.jdesktop.swingx.JXTree
import org.jdesktop.swingx.action.ActionManager
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
@@ -357,6 +359,7 @@ class NewHostTree : JXTree() {
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
val importMenu = JMenu(I18n.getString("termora.welcome.contextmenu.import"))
val csvMenu = importMenu.add("CSV")
val XshellMenu = importMenu.add("Xshell")
val windTermMenu = importMenu.add("WindTerm")
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect"))
@@ -385,9 +388,8 @@ class NewHostTree : JXTree() {
popupMenu.add(showMoreInfo)
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
csvMenu.addActionListener {
importHosts(lastNode, ImportType.CSV)
}
XshellMenu.addActionListener { importHosts(lastNode, ImportType.Xshell) }
csvMenu.addActionListener { importHosts(lastNode, ImportType.CSV) }
windTermMenu.addActionListener { importHosts(lastNode, ImportType.WindTerm) }
open.addActionListener { openHosts(it, false) }
openInNewWindow.addActionListener { openHosts(it, true) }
@@ -633,10 +635,14 @@ class NewHostTree : JXTree() {
chooser.isAcceptAllFileFilterUsed = false
chooser.isMultiSelectionEnabled = false
if (type == ImportType.WindTerm) {
chooser.fileFilter = FileNameExtensionFilter("WindTerm(*.sessions)", "sessions")
} else if (type == ImportType.CSV) {
chooser.fileFilter = FileNameExtensionFilter("CSV(*.csv)", "csv")
when (type) {
ImportType.WindTerm -> chooser.fileFilter = FileNameExtensionFilter("WindTerm (*.sessions)", "sessions")
ImportType.CSV -> chooser.fileFilter = FileNameExtensionFilter("CSV (*.csv)", "csv")
ImportType.Xshell -> {
chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
chooser.dialogTitle = "Xshell Sessions"
chooser.isAcceptAllFileFilterUsed = true
}
}
val dir = properties.getString("NewHostTree.ImportHosts.defaultDir", StringUtils.EMPTY)
@@ -698,6 +704,7 @@ class NewHostTree : JXTree() {
val nodes = when (type) {
ImportType.WindTerm -> parseFromWindTerm(folder, file)
ImportType.Xshell -> parseFromXshell(folder, file)
ImportType.CSV -> file.bufferedReader().use { parseFromCSV(folder, it) }
}
@@ -745,6 +752,34 @@ class NewHostTree : JXTree() {
return parseFromCSV(folder, StringReader(sw.toString()))
}
private fun parseFromXshell(folder: HostTreeNode, dir: File): List<HostTreeNode> {
val files = FileUtils.listFiles(dir, arrayOf("xsh"), true)
if (files.isEmpty()) {
OptionPane.showMessageDialog(
owner,
I18n.getString("termora.welcome.contextmenu.import.xshell-folder-empty")
)
return emptyList()
}
val sw = StringWriter()
CSVPrinter(sw, CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()).use { printer ->
for (file in files) {
val ini = Ini(file)
val protocol = ini.get("CONNECTION", "Protocol") ?: "SSH"
if (!StringUtils.equalsIgnoreCase("SSH", protocol)) continue
val folders = FilenameUtils.separatorsToUnix(file.parentFile.relativeTo(dir).toString())
val hostname = ini.get("CONNECTION", "Host") ?: StringUtils.EMPTY
val label = file.nameWithoutExtension
val port = ini.get("CONNECTION", "Port")?.toIntOrNull() ?: 22
val username = ini.get("CONNECTION:AUTHENTICATION", "UserName") ?: StringUtils.EMPTY
printer.printRecord(folders, label, hostname, port, username, "SSH")
}
}
return parseFromCSV(folder, StringReader(sw.toString()))
}
private fun parseFromCSV(folderNode: HostTreeNode, sr: Reader): List<HostTreeNode> {
val records = CSVParser.builder()
.setFormat(CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).setSkipHeaderRecord(true).get())
@@ -823,6 +858,7 @@ class NewHostTree : JXTree() {
private enum class ImportType {
WindTerm,
CSV,
Xshell,
}
private class MoveHostTransferable(val nodes: List<HostTreeNode>) : Transferable {

View File

@@ -148,6 +148,7 @@ termora.welcome.contextmenu.download=Download
termora.welcome.contextmenu.import.csv.download-template=Do you want to import or download the template?
termora.welcome.contextmenu.import.csv.download-template-done=Download the template successfully
termora.welcome.contextmenu.import.csv.download-template-done-open-folder=Download the template successfully, Do you want to open the folder?
termora.welcome.contextmenu.import.xshell-folder-empty=The folder does not contain any *.xsh files, Please select the correct Xshell Sessions directory
# New Host
termora.new-host.title=Create a new host

View File

@@ -136,6 +136,7 @@ termora.welcome.contextmenu.download=下载
termora.welcome.contextmenu.import.csv.download-template=您要导入还是下载模板?
termora.welcome.contextmenu.import.csv.download-template-done=下载成功
termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下载成功, 是否需要打开所在文件夹?
termora.welcome.contextmenu.import.xshell-folder-empty=该文件夹不包含 *.xsh 文件,请选择正确的 Xshell 会话目录
# New Host
termora.new-host.title=新建主机

View File

@@ -134,6 +134,7 @@ termora.welcome.contextmenu.download=下載
termora.welcome.contextmenu.import.csv.download-template=您要匯入還是下載範本?
termora.welcome.contextmenu.import.csv.download-template-done=下載成功
termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下載成功, 是否需要開啟所在資料夾?
termora.welcome.contextmenu.import.xshell-folder-empty=該資料夾不包含 *.xsh 文件,請選擇正確的 Xshell 會話目錄
# New Host
termora.new-host.title=新主機