diff --git a/THIRDPARTY b/THIRDPARTY index 4bf1918..ce43a80 100644 --- a/THIRDPARTY +++ b/THIRDPARTY @@ -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 diff --git a/build.gradle.kts b/build.gradle.kts index 8d272e3..2b6f21e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -108,6 +108,7 @@ dependencies { implementation(libs.colorpicker) implementation(libs.mixpanel) implementation(libs.jSerialComm) + implementation(libs.ini4j) } application { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 11bb96a..efe29f5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/src/main/kotlin/app/termora/NewHostTree.kt b/src/main/kotlin/app/termora/NewHostTree.kt index d4be85a..141d9f0 100644 --- a/src/main/kotlin/app/termora/NewHostTree.kt +++ b/src/main/kotlin/app/termora/NewHostTree.kt @@ -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 { + 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 { 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) : Transferable { diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index e0ec891..b9e469b 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -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 diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 21d830e..073a908 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -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=新建主机 diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index 0666740..e188c70 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -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=新主機