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 Apache License 2.0
https://github.com/apache/commons-csv/blob/master/LICENSE.txt 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 eddsa 0.3.0
Creative Commons Zero v1.0 Universal Creative Commons Zero v1.0 Universal
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt

View File

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

View File

@@ -43,6 +43,7 @@ delight-rhino-sandbox = "0.0.17"
testcontainers = "1.20.4" testcontainers = "1.20.4"
mixpanel = "1.5.3" mixpanel = "1.5.3"
jSerialComm = "2.11.0" jSerialComm = "2.11.0"
ini4j = "0.5.5-2"
[libraries] [libraries]
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } 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-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" } commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commons-compress" }
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" } 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 = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
flatlaf-extras = { group = "com.formdev", name = "flatlaf-extras", 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" } 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.CSVParser
import org.apache.commons.csv.CSVPrinter import org.apache.commons.csv.CSVPrinter
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.apache.commons.io.FilenameUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.commons.lang3.exception.ExceptionUtils
import org.ini4j.Ini
import org.jdesktop.swingx.JXTree import org.jdesktop.swingx.JXTree
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer 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 newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
val importMenu = JMenu(I18n.getString("termora.welcome.contextmenu.import")) val importMenu = JMenu(I18n.getString("termora.welcome.contextmenu.import"))
val csvMenu = importMenu.add("CSV") val csvMenu = importMenu.add("CSV")
val XshellMenu = importMenu.add("Xshell")
val windTermMenu = importMenu.add("WindTerm") val windTermMenu = importMenu.add("WindTerm")
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect")) val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect"))
@@ -385,9 +388,8 @@ class NewHostTree : JXTree() {
popupMenu.add(showMoreInfo) popupMenu.add(showMoreInfo)
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property")) val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
csvMenu.addActionListener { XshellMenu.addActionListener { importHosts(lastNode, ImportType.Xshell) }
importHosts(lastNode, ImportType.CSV) csvMenu.addActionListener { importHosts(lastNode, ImportType.CSV) }
}
windTermMenu.addActionListener { importHosts(lastNode, ImportType.WindTerm) } windTermMenu.addActionListener { importHosts(lastNode, ImportType.WindTerm) }
open.addActionListener { openHosts(it, false) } open.addActionListener { openHosts(it, false) }
openInNewWindow.addActionListener { openHosts(it, true) } openInNewWindow.addActionListener { openHosts(it, true) }
@@ -633,10 +635,14 @@ class NewHostTree : JXTree() {
chooser.isAcceptAllFileFilterUsed = false chooser.isAcceptAllFileFilterUsed = false
chooser.isMultiSelectionEnabled = false chooser.isMultiSelectionEnabled = false
if (type == ImportType.WindTerm) { when (type) {
chooser.fileFilter = FileNameExtensionFilter("WindTerm(*.sessions)", "sessions") ImportType.WindTerm -> chooser.fileFilter = FileNameExtensionFilter("WindTerm (*.sessions)", "sessions")
} else if (type == ImportType.CSV) { ImportType.CSV -> chooser.fileFilter = FileNameExtensionFilter("CSV (*.csv)", "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) val dir = properties.getString("NewHostTree.ImportHosts.defaultDir", StringUtils.EMPTY)
@@ -698,6 +704,7 @@ class NewHostTree : JXTree() {
val nodes = when (type) { val nodes = when (type) {
ImportType.WindTerm -> parseFromWindTerm(folder, file) ImportType.WindTerm -> parseFromWindTerm(folder, file)
ImportType.Xshell -> parseFromXshell(folder, file)
ImportType.CSV -> file.bufferedReader().use { parseFromCSV(folder, it) } ImportType.CSV -> file.bufferedReader().use { parseFromCSV(folder, it) }
} }
@@ -745,6 +752,34 @@ class NewHostTree : JXTree() {
return parseFromCSV(folder, StringReader(sw.toString())) 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> { private fun parseFromCSV(folderNode: HostTreeNode, sr: Reader): List<HostTreeNode> {
val records = CSVParser.builder() val records = CSVParser.builder()
.setFormat(CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).setSkipHeaderRecord(true).get()) .setFormat(CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).setSkipHeaderRecord(true).get())
@@ -823,6 +858,7 @@ class NewHostTree : JXTree() {
private enum class ImportType { private enum class ImportType {
WindTerm, WindTerm,
CSV, CSV,
Xshell,
} }
private class MoveHostTransferable(val nodes: List<HostTreeNode>) : Transferable { 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=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=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.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 # New Host
termora.new-host.title=Create a 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=您要导入还是下载模板?
termora.welcome.contextmenu.import.csv.download-template-done=下载成功 termora.welcome.contextmenu.import.csv.download-template-done=下载成功
termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下载成功, 是否需要打开所在文件夹? termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下载成功, 是否需要打开所在文件夹?
termora.welcome.contextmenu.import.xshell-folder-empty=该文件夹不包含 *.xsh 文件,请选择正确的 Xshell 会话目录
# New Host # New Host
termora.new-host.title=新建主机 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=您要匯入還是下載範本?
termora.welcome.contextmenu.import.csv.download-template-done=下載成功 termora.welcome.contextmenu.import.csv.download-template-done=下載成功
termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下載成功, 是否需要開啟所在資料夾? termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下載成功, 是否需要開啟所在資料夾?
termora.welcome.contextmenu.import.xshell-folder-empty=該資料夾不包含 *.xsh 文件,請選擇正確的 Xshell 會話目錄
# New Host # New Host
termora.new-host.title=新主機 termora.new-host.title=新主機