mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: nvidia smi (#280)
This commit is contained in:
@@ -29,6 +29,8 @@ object Icons {
|
||||
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }
|
||||
val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
|
||||
val locate by lazy { DynamicIcon("icons/locate.svg", "icons/locate_dark.svg") }
|
||||
val percentage by lazy { DynamicIcon("icons/percentage.svg", "icons/percentage_dark.svg") }
|
||||
val text by lazy { DynamicIcon("icons/text.svg", "icons/text_dark.svg") }
|
||||
val errorIntroduction by lazy { DynamicIcon("icons/errorIntroduction.svg", "icons/errorIntroduction_dark.svg") }
|
||||
val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_dark.svg") }
|
||||
val clusterRole by lazy { DynamicIcon("icons/clusterRole.svg", "icons/clusterRole_dark.svg") }
|
||||
@@ -117,5 +119,6 @@ object Icons {
|
||||
val listKey by lazy { DynamicIcon("icons/listKey.svg", "icons/listKey_dark.svg") }
|
||||
val forwardPorts by lazy { DynamicIcon("icons/forwardPorts.svg", "icons/forwardPorts_dark.svg") }
|
||||
val showWriteAccess by lazy { DynamicIcon("icons/showWriteAccess.svg", "icons/showWriteAccess_dark.svg") }
|
||||
val nvidia by lazy { DynamicIcon("icons/nvidia.svg", "icons/nvidia_dark.svg") }
|
||||
|
||||
}
|
||||
@@ -80,7 +80,9 @@ class TermoraFrameManager {
|
||||
try {
|
||||
Disposer.getTree().assertIsEmpty(true)
|
||||
} catch (e: Exception) {
|
||||
log.error(e.message)
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
exitProcess(0)
|
||||
|
||||
@@ -6,6 +6,7 @@ import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.panel.vw.NvidiaSMIVisualWindow
|
||||
import app.termora.terminal.panel.vw.SystemInformationVisualWindow
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import com.formdev.flatlaf.ui.FlatRoundBorder
|
||||
@@ -108,6 +109,9 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
// 服务器信息
|
||||
add(initServerInfoActionButton())
|
||||
|
||||
// Nvidia 显卡信息
|
||||
add(initNvidiaSMIActionButton())
|
||||
|
||||
// 重连
|
||||
add(initReconnectActionButton())
|
||||
|
||||
@@ -143,6 +147,34 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
return btn
|
||||
}
|
||||
|
||||
private fun initNvidiaSMIActionButton(): JButton {
|
||||
val btn = JButton(Icons.nvidia)
|
||||
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")
|
||||
btn.addActionListener(object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
||||
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
|
||||
|
||||
if (tab !is SSHTerminalTab) {
|
||||
terminalPanel.toast(I18n.getString("termora.floating-toolbar.not-supported"))
|
||||
return
|
||||
}
|
||||
|
||||
for (window in terminalPanel.getVisualWindows()) {
|
||||
if (window is NvidiaSMIVisualWindow) {
|
||||
terminalPanel.moveToFront(window)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val visualWindowPanel = NvidiaSMIVisualWindow(tab, terminalPanel)
|
||||
terminalPanel.addVisualWindow(visualWindowPanel)
|
||||
|
||||
}
|
||||
})
|
||||
return btn
|
||||
}
|
||||
|
||||
private fun initPinActionButton(): JButton {
|
||||
val btn = JButton(Icons.pin)
|
||||
btn.isSelected = pinAction.isSelected
|
||||
|
||||
@@ -548,8 +548,7 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
||||
override fun addVisualWindow(visualWindow: VisualWindow) {
|
||||
visualWindows = ArrayUtils.add(visualWindows, visualWindow)
|
||||
layeredPane.add(visualWindow.getJComponent(), JLayeredPane.DRAG_LAYER as Any)
|
||||
layeredPane.revalidate()
|
||||
layeredPane.repaint()
|
||||
layeredPane.moveToFront(visualWindow.getJComponent())
|
||||
}
|
||||
|
||||
override fun removeVisualWindow(visualWindow: VisualWindow) {
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.SSHTerminalTab
|
||||
import app.termora.SshClients
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.JXBusyLabel
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.xml.sax.EntityResolver
|
||||
import org.xml.sax.InputSource
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.GridLayout
|
||||
import java.io.StringReader
|
||||
import javax.swing.*
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.xpath.XPathFactory
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||
SSHVisualWindow(tab, "NVIDIA-SMI", visualWindowManager) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(NvidiaSMIVisualWindow::class.java)
|
||||
}
|
||||
|
||||
private val nvidiaSMIPanel by lazy { NvidiaSMIPanel() }
|
||||
private val busyLabel = JXBusyLabel()
|
||||
private val errorPanel = FormBuilder.create().layout(FormLayout("pref:grow", "20dlu, pref, 5dlu, pref"))
|
||||
.add(JLabel(FlatSVGIcon(Icons.warningDialog.name, 60, 60))).xy(1, 2, "center, fill")
|
||||
.add(JLabel("Not supported")).xy(1, 4, "center, fill")
|
||||
.build()
|
||||
private val loadingPanel = FormBuilder.create().layout(FormLayout("pref:grow", "20dlu, pref"))
|
||||
.add(busyLabel).xy(1, 2, "center, fill")
|
||||
.build()
|
||||
private val cardLayout = CardLayout()
|
||||
private val rootPanel = JPanel(cardLayout)
|
||||
private var isPercentage
|
||||
get() = properties.getString("VisualWindow.${id}.isPercentage", "false").toBoolean()
|
||||
set(value) = properties.putString("VisualWindow.${id}.isPercentage", value.toString())
|
||||
|
||||
private val percentageBtn by lazy { JButton(if (isPercentage) Icons.text else Icons.percentage) }
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
initVisualWindowPanel()
|
||||
}
|
||||
|
||||
override fun toolbarButtons(): List<JButton> {
|
||||
return listOf(percentageBtn)
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
title = I18n.getString("termora.visual-window.nvidia-smi")
|
||||
busyLabel.isBusy = true
|
||||
|
||||
rootPanel.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
|
||||
rootPanel.add(errorPanel, "ErrorPanel")
|
||||
rootPanel.add(loadingPanel, "LoadingPanel")
|
||||
rootPanel.add(nvidiaSMIPanel, "NvidiaSMIPanel")
|
||||
|
||||
add(rootPanel, BorderLayout.CENTER)
|
||||
|
||||
cardLayout.show(rootPanel, "LoadingPanel")
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
percentageBtn.addActionListener {
|
||||
isPercentage = !isPercentage
|
||||
percentageBtn.icon = if (isPercentage) Icons.text else Icons.percentage
|
||||
nvidiaSMIPanel.refreshPanel()
|
||||
}
|
||||
}
|
||||
|
||||
private data class GPU(
|
||||
/**
|
||||
* 名称 product_name
|
||||
*/
|
||||
val productName: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 序号 minor_number
|
||||
*/
|
||||
val minorNumber: Int = 0,
|
||||
|
||||
/**
|
||||
* 温度 temperature.gpu_temp
|
||||
*
|
||||
* 单位:C
|
||||
*/
|
||||
var temp: Double = 0.0,
|
||||
var tempText: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 使用的功率 gpu_power_readings.power_draw
|
||||
*
|
||||
* 单位:W
|
||||
*/
|
||||
var powerUsage: Double = 0.0,
|
||||
var powerUsageText: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 功率大小 gpu_power_readings.max_power_limit
|
||||
*
|
||||
* 单位:W
|
||||
*/
|
||||
var powerCap: Double = 0.0,
|
||||
var powerCapText: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 使用的显存 fb_memory_usage.used
|
||||
*
|
||||
* 单位:Mib
|
||||
*/
|
||||
var memoryUsage: Double = 0.0,
|
||||
var memoryUsageText: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 显存大小 fb_memory_usage.total
|
||||
*
|
||||
* 单位:Mib
|
||||
*/
|
||||
var memoryCap: Double = 0.0,
|
||||
var memoryCapText: String = StringUtils.EMPTY,
|
||||
|
||||
|
||||
/**
|
||||
* GPU 利用率 utilization.gpu_util
|
||||
*
|
||||
* 单位:%
|
||||
*/
|
||||
var gpu: Double = 0.0
|
||||
)
|
||||
|
||||
private class NvidiaSMI(
|
||||
val driverVersion: String = StringUtils.EMPTY,
|
||||
val cudaVersion: String = StringUtils.EMPTY,
|
||||
val gpus: MutableList<GPU> = mutableListOf<GPU>(),
|
||||
)
|
||||
|
||||
private class GPUPanel(val minorNumber: Int, title: String) : JPanel(BorderLayout()) {
|
||||
val gpuProgressBar = SmartProgressBar()
|
||||
val tempProgressBar = SmartProgressBar()
|
||||
val memProgressBar = SmartProgressBar()
|
||||
val powerProgressBar = SmartProgressBar()
|
||||
|
||||
init {
|
||||
val formMargin = "4dlu"
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val p = FormBuilder.create().debug(false)
|
||||
.layout(
|
||||
FormLayout(
|
||||
"left:pref, $formMargin, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
)
|
||||
.border(
|
||||
BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createTitledBorder(title),
|
||||
BorderFactory.createEmptyBorder(4, 4, 4, 4),
|
||||
)
|
||||
)
|
||||
.add("GPU: ").xy(1, rows)
|
||||
.add(gpuProgressBar).xy(3, rows).apply { rows += step }
|
||||
.add("Temp: ").xy(1, rows)
|
||||
.add(tempProgressBar).xy(3, rows).apply { rows += step }
|
||||
.add("Mem: ").xy(1, rows)
|
||||
.add(memProgressBar).xy(3, rows).apply { rows += step }
|
||||
.add("Power: ").xy(1, rows)
|
||||
.add(powerProgressBar).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
add(p, BorderLayout.CENTER)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class NvidiaSMIPanel : JPanel(BorderLayout()) {
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private val xPath by lazy { XPathFactory.newInstance().newXPath() }
|
||||
private val db by lazy {
|
||||
val factory = DocumentBuilderFactory.newInstance()
|
||||
factory.isValidating = false
|
||||
factory.isXIncludeAware = false
|
||||
factory.isNamespaceAware = false
|
||||
val db = factory.newDocumentBuilder()
|
||||
db.setEntityResolver(object : EntityResolver {
|
||||
override fun resolveEntity(
|
||||
publicId: String?,
|
||||
systemId: String?
|
||||
): InputSource? {
|
||||
return if (StringUtils.contains(systemId, ".dtd")) {
|
||||
InputSource(StringReader(StringUtils.EMPTY))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
db
|
||||
}
|
||||
|
||||
private var nvidiaSMI = NvidiaSMI()
|
||||
private val gpuRootPanel = JPanel()
|
||||
private val driverVersionLabel = JLabel()
|
||||
private val cudaVersionLabel = JLabel()
|
||||
private val gpusLabel = JLabel()
|
||||
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initViews() {
|
||||
|
||||
add(
|
||||
FormBuilder.create().debug(false)
|
||||
.layout(
|
||||
FormLayout(
|
||||
"default:grow, pref, default:grow, 4dlu, pref, default:grow, 4dlu, pref, default:grow, default:grow",
|
||||
"pref, 4dlu"
|
||||
)
|
||||
)
|
||||
.add(Box.createHorizontalGlue()).xy(1, 1)
|
||||
.add("Driver: ").xy(2, 1)
|
||||
.add(driverVersionLabel).xy(3, 1)
|
||||
.add("CUDA: ").xy(5, 1)
|
||||
.add(cudaVersionLabel).xy(6, 1)
|
||||
.add("GPUS: ").xy(8, 1)
|
||||
.add(gpusLabel).xy(9, 1)
|
||||
.add(Box.createHorizontalGlue()).xy(10, 1)
|
||||
.build(), BorderLayout.NORTH
|
||||
)
|
||||
|
||||
add(JScrollPane(gpuRootPanel).apply {
|
||||
verticalScrollBar.maximumSize = Dimension(0, 0)
|
||||
verticalScrollBar.preferredSize = Dimension(0, 0)
|
||||
verticalScrollBar.minimumSize = Dimension(0, 0)
|
||||
border = BorderFactory.createEmptyBorder()
|
||||
}, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
|
||||
private fun initEvents() {
|
||||
coroutineScope.launch {
|
||||
|
||||
// 首次刷新
|
||||
refresh(true)
|
||||
|
||||
while (coroutineScope.isActive) {
|
||||
delay(1000.milliseconds)
|
||||
|
||||
try {
|
||||
refresh()
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun refresh(isFirst: Boolean = false) {
|
||||
val session = tab.getData(SSHTerminalTab.SSHSession) ?: return
|
||||
|
||||
val doc = try {
|
||||
val (code, text) = SshClients.execChannel(session, "nvidia-smi -x -q")
|
||||
if (StringUtils.isNotBlank(text)) {
|
||||
db.parse(InputSource(StringReader(text)))
|
||||
} else {
|
||||
throw IllegalStateException("exit code: $code")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
if (doc == null) {
|
||||
if (isFirst) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
cardLayout.show(rootPanel, "ErrorPanel")
|
||||
}
|
||||
// 直接取消轮训
|
||||
SwingUtilities.invokeLater { coroutineScope.cancel() }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
nvidiaSMI = NvidiaSMI(
|
||||
driverVersion = xPath.compile("/nvidia_smi_log/driver_version/text()").evaluate(doc),
|
||||
cudaVersion = xPath.compile("/nvidia_smi_log/cuda_version/text()").evaluate(doc),
|
||||
)
|
||||
val attachedGPUs = xPath.compile("/nvidia_smi_log/attached_gpus/text()").evaluate(doc).toIntOrNull() ?: 0
|
||||
|
||||
for (i in 1..attachedGPUs) {
|
||||
val gpu = GPU(
|
||||
productName = xPath.compile("/nvidia_smi_log/gpu[${i}]/product_name/text()").evaluate(doc),
|
||||
minorNumber = xPath.compile("/nvidia_smi_log/gpu[${i}]/minor_number/text()").evaluate(doc)
|
||||
.toIntOrNull() ?: 0,
|
||||
tempText = xPath.compile("/nvidia_smi_log/gpu[${i}]/temperature/gpu_temp/text()").evaluate(doc),
|
||||
powerUsageText = xPath.compile("/nvidia_smi_log/gpu[${i}]/gpu_power_readings/power_draw/text()")
|
||||
.evaluate(doc),
|
||||
powerCapText = xPath.compile("/nvidia_smi_log/gpu[${i}]/gpu_power_readings/max_power_limit/text()")
|
||||
.evaluate(doc),
|
||||
memoryUsageText = xPath.compile("/nvidia_smi_log/gpu[${i}]/fb_memory_usage/used/text()")
|
||||
.evaluate(doc),
|
||||
memoryCapText = xPath.compile("/nvidia_smi_log/gpu[${i}]/fb_memory_usage/total/text()")
|
||||
.evaluate(doc),
|
||||
gpu = xPath.compile("/nvidia_smi_log/gpu[${i}]/utilization/gpu_util/text()")
|
||||
.evaluate(doc).split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
)
|
||||
|
||||
nvidiaSMI.gpus.add(
|
||||
gpu.copy(
|
||||
temp = gpu.tempText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
powerUsage = gpu.powerUsageText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
powerCap = gpu.powerCapText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
memoryUsage = gpu.memoryUsageText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
memoryCap = gpu.memoryCapText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
if (isFirst) {
|
||||
initPanel()
|
||||
cardLayout.show(rootPanel, "NvidiaSMIPanel")
|
||||
}
|
||||
|
||||
refreshPanel()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun initPanel() {
|
||||
gpuRootPanel.layout = GridLayout(
|
||||
if (nvidiaSMI.gpus.size % 2 == 0) nvidiaSMI.gpus.size / 2 else nvidiaSMI.gpus.size / 2 + 1,
|
||||
2, 4, 4
|
||||
)
|
||||
for (e in nvidiaSMI.gpus) {
|
||||
gpuRootPanel.add(GPUPanel(e.minorNumber, "${e.minorNumber} ${e.productName}"))
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshPanel() {
|
||||
cudaVersionLabel.text = nvidiaSMI.cudaVersion
|
||||
driverVersionLabel.text = nvidiaSMI.driverVersion
|
||||
gpusLabel.text = nvidiaSMI.gpus.size.toString()
|
||||
|
||||
for (c in gpuRootPanel.components) {
|
||||
if (c is GPUPanel) {
|
||||
for (g in nvidiaSMI.gpus) {
|
||||
if (c.minorNumber == g.minorNumber) {
|
||||
refreshGPUPanel(g, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshGPUPanel(gpu: GPU, g: GPUPanel) {
|
||||
g.gpuProgressBar.value = gpu.gpu.toInt()
|
||||
|
||||
g.tempProgressBar.value = gpu.temp.toInt()
|
||||
g.tempProgressBar.string = if (isPercentage) "${g.tempProgressBar.value}%" else gpu.tempText
|
||||
|
||||
g.powerProgressBar.value = (gpu.powerUsage / gpu.powerCap * 100.0).toInt()
|
||||
g.powerProgressBar.string = if (isPercentage) "${g.powerProgressBar.value}%"
|
||||
else "${gpu.powerUsageText}/${gpu.powerCapText}"
|
||||
|
||||
g.memProgressBar.value = (gpu.memoryUsage / gpu.memoryCap * 100.0).toInt()
|
||||
g.memProgressBar.string = if (isPercentage) "${g.memProgressBar.value}%"
|
||||
else "${gpu.memoryUsageText}/${gpu.memoryCapText}"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun dispose() {
|
||||
busyLabel.isBusy = false
|
||||
super.dispose()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.Disposer
|
||||
import app.termora.SSHTerminalTab
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.util.*
|
||||
|
||||
abstract class SSHVisualWindow(
|
||||
protected val tab: SSHTerminalTab,
|
||||
id: String,
|
||||
visualWindowManager: VisualWindowManager
|
||||
) : VisualWindowPanel(id, visualWindowManager) {
|
||||
|
||||
init {
|
||||
Disposer.register(tab, this)
|
||||
}
|
||||
|
||||
override fun toggleWindow() {
|
||||
val evt = AnActionEvent(tab.getJComponent(), StringUtils.EMPTY, EventObject(this))
|
||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||
|
||||
super.toggleWindow()
|
||||
|
||||
if (!isWindow()) {
|
||||
terminalTabbedManager.setSelectedTerminalTab(tab)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun getWindowTitle(): String {
|
||||
return tab.getTitle() + " - " + title
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.coroutines.*
|
||||
@@ -11,15 +9,14 @@ import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.sshd.client.session.ClientSession
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.BorderLayout
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import javax.swing.table.DefaultTableModel
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
class SystemInformationVisualWindow(private val tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||
VisualWindowPanel("SystemInformation", visualWindowManager) {
|
||||
class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||
SSHVisualWindow(tab, "SystemInformation", visualWindowManager) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(SystemInformationVisualWindow::class.java)
|
||||
@@ -41,22 +38,6 @@ class SystemInformationVisualWindow(private val tab: SSHTerminalTab, visualWindo
|
||||
|
||||
private fun initEvents() {
|
||||
Disposer.register(this, systemInformationPanel)
|
||||
Disposer.register(tab, this)
|
||||
}
|
||||
|
||||
override fun getWindowTitle(): String {
|
||||
return tab.getTitle() + " - " + title
|
||||
}
|
||||
|
||||
override fun toggleWindow() {
|
||||
val evt = AnActionEvent(tab.getJComponent(), StringUtils.EMPTY, EventObject(this))
|
||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||
|
||||
super.toggleWindow()
|
||||
|
||||
if (!isWindow()) {
|
||||
terminalTabbedManager.setSelectedTerminalTab(tab)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SystemInformationPanel : JPanel(BorderLayout()), Disposable {
|
||||
|
||||
@@ -28,6 +28,11 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
private var dialog: VisualWindowDialog? = null
|
||||
private var oldBounds = Rectangle()
|
||||
private var toggleWindowBtn = JButton(Icons.openInNewWindow)
|
||||
private var isAlwaysTop
|
||||
get() = properties.getString("VisualWindow.${id}.dialog.isAlwaysTop", "false").toBoolean()
|
||||
set(value) = properties.putString("VisualWindow.${id}.dialog.isAlwaysTop", value.toString())
|
||||
|
||||
private val alwaysTopBtn = JButton(Icons.moveUp)
|
||||
private val closeWindowListener = object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
close()
|
||||
@@ -64,6 +69,12 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
|
||||
if (w > 0 && h > 0) setSize(w, h) else setSize(400, 200)
|
||||
|
||||
alwaysTopBtn.isSelected = isAlwaysTop
|
||||
alwaysTopBtn.isVisible = false
|
||||
}
|
||||
|
||||
protected open fun toolbarButtons(): List<JButton> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
@@ -102,14 +113,29 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
alwaysTopBtn.addActionListener {
|
||||
isAlwaysTop = !isAlwaysTop
|
||||
alwaysTopBtn.isSelected = isAlwaysTop
|
||||
|
||||
if (isWindow()) {
|
||||
dialog?.isAlwaysOnTop = isAlwaysTop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initToolBar() {
|
||||
toolbar.add(JLabel(Icons.empty))
|
||||
val btns = toolbarButtons()
|
||||
val count = 2 + btns.size
|
||||
toolbar.add(alwaysTopBtn)
|
||||
toolbar.add(Box.createHorizontalStrut(count * 26))
|
||||
toolbar.add(JLabel(Icons.empty))
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
toolbar.add(titleLabel)
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
|
||||
btns.forEach { toolbar.add(it) }
|
||||
|
||||
toolbar.add(toggleWindowBtn)
|
||||
toolbar.add(JButton(Icons.close).apply { addActionListener { Disposer.dispose(visualWindow) } })
|
||||
toolbar.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor)
|
||||
@@ -157,6 +183,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
dialog?.dispose()
|
||||
dialog = null
|
||||
|
||||
alwaysTopBtn.isVisible = isWindow
|
||||
|
||||
if (isWindow) {
|
||||
oldBounds = bounds
|
||||
// 变基
|
||||
@@ -212,13 +240,11 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
|
||||
|
||||
protected open fun close() {
|
||||
SwingUtilities.invokeLater {
|
||||
if (isWindow()) {
|
||||
dialog?.dispose()
|
||||
dialog = null
|
||||
}
|
||||
visualWindowManager.removeVisualWindow(visualWindow)
|
||||
if (isWindow()) {
|
||||
dialog?.dispose()
|
||||
dialog = null
|
||||
}
|
||||
visualWindowManager.removeVisualWindow(visualWindow)
|
||||
}
|
||||
|
||||
private inner class VisualWindowDialog : DialogWrapper(null) {
|
||||
@@ -228,6 +254,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
controlsVisible = false
|
||||
isResizable = true
|
||||
title = getWindowTitle()
|
||||
isAlwaysOnTop = isAlwaysTop
|
||||
|
||||
initEvents()
|
||||
|
||||
@@ -251,8 +278,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
Disposer.register(disposable, object : Disposable {
|
||||
override fun dispose() {
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
properties.putString("VisualWindow.${id}.dialog.location.x", x.toString())
|
||||
properties.putString("VisualWindow.${id}.dialog.location.y", y.toString())
|
||||
properties.putString("VisualWindow.${id}.dialog.location.width", width.toString())
|
||||
|
||||
@@ -350,6 +350,9 @@ termora.visual-window.system-information.filesystem=Filesystem
|
||||
termora.visual-window.system-information.used-total=Used / Total
|
||||
|
||||
|
||||
termora.visual-window.nvidia-smi=NVIDIA System Management Interface
|
||||
|
||||
|
||||
termora.floating-toolbar.not-supported=This action is not supported
|
||||
|
||||
|
||||
|
||||
5
src/main/resources/icons/nvidia.svg
Normal file
5
src/main/resources/icons/nvidia.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg t="1740039296619" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9516"
|
||||
width="16" height="16">
|
||||
<path d="M381.781333 375.381333v-61.013333a285.866667 285.866667 0 0 1 18.090667-0.768c167.338667-5.290667 277.034667 143.957333 277.034667 143.957333s-118.357333 164.309333-245.333334 164.309334a156.586667 156.586667 0 0 1-49.408-7.893334v-185.429333c65.194667 7.893333 78.378667 36.565333 117.205334 101.76l87.04-73.130667s-63.658667-83.285333-170.666667-83.285333a256.682667 256.682667 0 0 0-33.962667 1.493333m0-202.026666v91.221333l18.090667-1.152c232.533333-7.893333 384.426667 190.72 384.426667 190.72s-174.08 211.797333-355.413334 211.797333a275.626667 275.626667 0 0 1-46.72-4.138666v56.533333c12.8 1.493333 26.026667 2.645333 38.826667 2.645333 168.832 0 290.986667-86.314667 409.301333-188.074666 19.584 15.829333 99.84 53.888 116.48 70.485333-112.341333 94.208-374.272 169.984-522.794666 169.984-14.293333 0-27.861333-0.768-41.429334-2.261333v79.530666H1024V173.354667z m0 440.576v48.256c-156.032-27.904-199.381333-190.293333-199.381333-190.293334s75.008-82.944 199.381333-96.512v52.778667H381.44c-65.194667-7.936-116.48 53.12-116.48 53.12s29.013333 102.912 116.864 132.693333M104.789333 465.066667s92.330667-136.405333 277.333334-150.741334V264.576C177.194667 281.173333 0 454.528 0 454.528s100.266667 290.218667 381.781333 316.586667v-52.778667c-206.506667-25.6-276.992-253.269333-276.992-253.269333z"
|
||||
p-id="9517" fill="#6C707E"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
5
src/main/resources/icons/nvidia_dark.svg
Normal file
5
src/main/resources/icons/nvidia_dark.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg t="1740039296619" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9516"
|
||||
width="16" height="16">
|
||||
<path d="M381.781333 375.381333v-61.013333a285.866667 285.866667 0 0 1 18.090667-0.768c167.338667-5.290667 277.034667 143.957333 277.034667 143.957333s-118.357333 164.309333-245.333334 164.309334a156.586667 156.586667 0 0 1-49.408-7.893334v-185.429333c65.194667 7.893333 78.378667 36.565333 117.205334 101.76l87.04-73.130667s-63.658667-83.285333-170.666667-83.285333a256.682667 256.682667 0 0 0-33.962667 1.493333m0-202.026666v91.221333l18.090667-1.152c232.533333-7.893333 384.426667 190.72 384.426667 190.72s-174.08 211.797333-355.413334 211.797333a275.626667 275.626667 0 0 1-46.72-4.138666v56.533333c12.8 1.493333 26.026667 2.645333 38.826667 2.645333 168.832 0 290.986667-86.314667 409.301333-188.074666 19.584 15.829333 99.84 53.888 116.48 70.485333-112.341333 94.208-374.272 169.984-522.794666 169.984-14.293333 0-27.861333-0.768-41.429334-2.261333v79.530666H1024V173.354667z m0 440.576v48.256c-156.032-27.904-199.381333-190.293333-199.381333-190.293334s75.008-82.944 199.381333-96.512v52.778667H381.44c-65.194667-7.936-116.48 53.12-116.48 53.12s29.013333 102.912 116.864 132.693333M104.789333 465.066667s92.330667-136.405333 277.333334-150.741334V264.576C177.194667 281.173333 0 454.528 0 454.528s100.266667 290.218667 381.781333 316.586667v-52.778667c-206.506667-25.6-276.992-253.269333-276.992-253.269333z"
|
||||
p-id="9517" fill="#CED0D6"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
5
src/main/resources/icons/percentage.svg
Normal file
5
src/main/resources/icons/percentage.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg t="1740037914635" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5141"
|
||||
width="16" height="16">
|
||||
<path d="M855.7 210.8l-42.4-42.4c-3.1-3.1-8.2-3.1-11.3 0L168.3 801.9c-3.1 3.1-3.1 8.2 0 11.3l42.4 42.4c3.1 3.1 8.2 3.1 11.3 0L855.6 222c3.2-3 3.2-8.1 0.1-11.2zM304 448c79.4 0 144-64.6 144-144s-64.6-144-144-144-144 64.6-144 144 64.6 144 144 144z m0-216c39.7 0 72 32.3 72 72s-32.3 72-72 72-72-32.3-72-72 32.3-72 72-72zM720 576c-79.4 0-144 64.6-144 144s64.6 144 144 144 144-64.6 144-144-64.6-144-144-144z m0 216c-39.7 0-72-32.3-72-72s32.3-72 72-72 72 32.3 72 72-32.3 72-72 72z"
|
||||
p-id="5142" fill="#6C707E"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 680 B |
5
src/main/resources/icons/percentage_dark.svg
Normal file
5
src/main/resources/icons/percentage_dark.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg t="1740037914635" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5141"
|
||||
width="16" height="16">
|
||||
<path d="M855.7 210.8l-42.4-42.4c-3.1-3.1-8.2-3.1-11.3 0L168.3 801.9c-3.1 3.1-3.1 8.2 0 11.3l42.4 42.4c3.1 3.1 8.2 3.1 11.3 0L855.6 222c3.2-3 3.2-8.1 0.1-11.2zM304 448c79.4 0 144-64.6 144-144s-64.6-144-144-144-144 64.6-144 144 64.6 144 144 144z m0-216c39.7 0 72 32.3 72 72s-32.3 72-72 72-72-32.3-72-72 32.3-72 72-72zM720 576c-79.4 0-144 64.6-144 144s64.6 144 144 144 144-64.6 144-144-64.6-144-144-144z m0 216c-39.7 0-72-32.3-72-72s32.3-72 72-72 72 32.3 72 72-32.3 72-72 72z"
|
||||
p-id="5142" fill="#CED0D6"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 680 B |
5
src/main/resources/icons/text.svg
Normal file
5
src/main/resources/icons/text.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg t="1740038430861" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7036"
|
||||
width="16" height="16">
|
||||
<path d="M853.333333 138.666667H170.666667c-17.066667 0-32 14.933333-32 32v128c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V202.666667h277.333333v618.666666H384c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h256c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32h-96v-618.666666h277.333333V298.666667c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V170.666667c0-17.066667-14.933333-32-32-32z"
|
||||
fill="#6C707E" p-id="7037"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 609 B |
5
src/main/resources/icons/text_dark.svg
Normal file
5
src/main/resources/icons/text_dark.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg t="1740038430861" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7036"
|
||||
width="16" height="16">
|
||||
<path d="M853.333333 138.666667H170.666667c-17.066667 0-32 14.933333-32 32v128c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V202.666667h277.333333v618.666666H384c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h256c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32h-96v-618.666666h277.333333V298.666667c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V170.666667c0-17.066667-14.933333-32-32-32z"
|
||||
fill="#CED0D6" p-id="7037"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 609 B |
Reference in New Issue
Block a user