feat: system information (#278)

This commit is contained in:
hstyi
2025-02-20 12:05:45 +08:00
committed by GitHub
parent 0b84d3271c
commit 33a359fcbf
24 changed files with 1073 additions and 14 deletions

View File

@@ -54,7 +54,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
protected fun init() {
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
defaultCloseOperation = DISPOSE_ON_CLOSE
initTitleBar()
initEvents()
@@ -158,12 +158,14 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
openPopup = true
}
val window = SwingUtilities.windowForComponent(c)
val windows = window.ownedWindows
for (w in windows) {
if (w.isVisible && w.javaClass.getName().endsWith("HeavyWeightWindow")) {
openPopup = true
w.dispose()
val window = c as? Window ?: SwingUtilities.windowForComponent(c)
if (window != null) {
val windows = window.ownedWindows
for (w in windows) {
if (w.isVisible && w.javaClass.getName().endsWith("HeavyWeightWindow")) {
openPopup = true
w.dispose()
}
}
}

View File

@@ -10,6 +10,8 @@ object Icons {
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
val openInNewWindow by lazy { DynamicIcon("icons/openInNewWindow.svg", "icons/openInNewWindow_dark.svg") }
val openInToolWindow by lazy { DynamicIcon("icons/openInToolWindow.svg", "icons/openInToolWindow_dark.svg") }
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
val eye by lazy { DynamicIcon("icons/eye.svg", "icons/eye_dark.svg") }
val eyeClose by lazy { DynamicIcon("icons/eyeClose.svg", "icons/eyeClose_dark.svg") }
@@ -26,6 +28,7 @@ object Icons {
val empty by lazy { DynamicIcon("icons/empty.svg") }
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 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") }

View File

@@ -26,7 +26,6 @@ import org.apache.sshd.common.channel.ChannelListener
import org.apache.sshd.common.session.Session
import org.apache.sshd.common.session.SessionListener
import org.apache.sshd.common.session.SessionListener.Event
import org.slf4j.LoggerFactory
import java.nio.charset.StandardCharsets
import java.util.*
import javax.swing.JComponent
@@ -36,7 +35,7 @@ import javax.swing.SwingUtilities
class SSHTerminalTab(windowScope: WindowScope, host: Host) :
PtyHostTerminalTab(windowScope, host) {
companion object {
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
val SSHSession = DataKey(ClientSession::class)
}
private val mutex = Mutex()
@@ -201,6 +200,13 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
}
}
@Suppress("UNCHECKED_CAST")
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
if (dataKey == SSHSession) {
return sshSession as T?
}
return super.getData(dataKey)
}
override fun stop() {
if (mutex.tryLock()) {

View File

@@ -2,10 +2,12 @@ package app.termora
import app.termora.keymgr.OhKeyPairKeyPairProvider
import app.termora.terminal.TerminalSize
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.apache.sshd.client.ClientBuilder
import org.apache.sshd.client.SshClient
import org.apache.sshd.client.channel.ChannelShell
import org.apache.sshd.client.channel.ClientChannelEvent
import org.apache.sshd.client.config.hosts.HostConfigEntry
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
import org.apache.sshd.client.config.hosts.KnownHostEntry
@@ -31,6 +33,7 @@ import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
import org.eclipse.jgit.transport.sshd.ProxyData
import org.slf4j.LoggerFactory
import java.awt.Window
import java.io.ByteArrayOutputStream
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.SocketAddress
@@ -38,6 +41,7 @@ import java.nio.file.Path
import java.nio.file.Paths
import java.security.PublicKey
import java.time.Duration
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JOptionPane
import javax.swing.SwingUtilities
@@ -75,6 +79,34 @@ object SshClients {
}
/**
* 执行一个命令
*
* @return first: exitCode , second: response
*/
fun execChannel(
session: ClientSession,
command: String
): Pair<Int, String> {
val baos = ByteArrayOutputStream()
val channel = session.createExecChannel(command)
channel.out = baos
if (channel.open().verify(timeout).await(timeout)) {
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), timeout)
}
IOUtils.closeQuietly(channel)
if (channel.exitStatus == null) {
return Pair(-1, baos.toString())
}
return Pair(channel.exitStatus, baos.toString())
}
/**
* 打开一个会话
*/

View File

@@ -73,12 +73,17 @@ class TerminalTabbed(
tabbedPane.addPropertyChangeListener("selectedIndex") { evt ->
val oldIndex = evt.oldValue as Int
val newIndex = evt.newValue as Int
if (oldIndex >= 0 && tabs.size > newIndex) {
tabs[oldIndex].onLostFocus()
}
if (newIndex >= 0 && tabs.size > newIndex) {
tabs[newIndex].onGrabFocus()
}
SwingUtilities.invokeLater { tabbedPane.getComponentAt(newIndex).requestFocusInWindow() }
}
// 选择变动
@@ -174,6 +179,9 @@ class TerminalTabbed(
// 新的获取到焦点
tabs[tabbedPane.selectedIndex].onGrabFocus()
// 新的真正获取焦点
tabbedPane.getComponentAt(tabbedPane.selectedIndex).requestFocusInWindow()
if (disposable) {
Disposer.dispose(tab)
}

View File

@@ -3,8 +3,10 @@ package app.termora.terminal.panel
import app.termora.*
import app.termora.actions.AnAction
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.SystemInformationVisualWindow
import com.formdev.flatlaf.extras.components.FlatToolBar
import com.formdev.flatlaf.ui.FlatRoundBorder
import org.apache.commons.lang3.StringUtils
@@ -70,6 +72,11 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
initActions()
}
override fun updateUI() {
super.updateUI()
border = FlatRoundBorder()
}
fun triggerShow() {
if (!floatingToolbarEnable || closed) {
return
@@ -98,6 +105,9 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
// Pin
add(initPinActionButton())
// 服务器信息
add(initServerInfoActionButton())
// 重连
add(initReconnectActionButton())
@@ -105,6 +115,34 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
add(initCloseActionButton())
}
private fun initServerInfoActionButton(): JButton {
val btn = JButton(Icons.infoOutline)
btn.toolTipText = I18n.getString("termora.visual-window.system-information")
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 SystemInformationVisualWindow) {
terminalPanel.moveToFront(window)
return
}
}
val visualWindowPanel = SystemInformationVisualWindow(tab, terminalPanel)
terminalPanel.addVisualWindow(visualWindowPanel)
}
})
return btn
}
private fun initPinActionButton(): JButton {
val btn = JButton(Icons.pin)
btn.isSelected = pinAction.isSelected

View File

@@ -6,7 +6,10 @@ import app.termora.actions.DataProvider
import app.termora.actions.DataProviderSupport
import app.termora.actions.DataProviders
import app.termora.terminal.*
import app.termora.terminal.panel.vw.VisualWindow
import app.termora.terminal.panel.vw.VisualWindowManager
import com.formdev.flatlaf.util.SystemInfo
import org.apache.commons.lang3.ArrayUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.SystemUtils
import java.awt.*
@@ -32,7 +35,7 @@ import kotlin.time.Duration.Companion.milliseconds
class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnector) :
JPanel(BorderLayout()), DataProvider, Disposable {
JPanel(BorderLayout()), DataProvider, Disposable, VisualWindowManager {
companion object {
val Debug = DataKey(Boolean::class)
@@ -46,6 +49,8 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
private val floatingToolbar = FloatingToolbarPanel()
private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink)
private val dataProviderSupport = DataProviderSupport()
private val layeredPane = TerminalLayeredPane()
private var visualWindows = emptyArray<VisualWindow>()
val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal)
@@ -118,8 +123,6 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
scrollBar.blockIncrement = 1
background = Color.black
val layeredPane = TerminalLayeredPane()
layeredPane.add(terminalDisplay, JLayeredPane.DEFAULT_LAYER as Any)
layeredPane.add(terminalFindPanel, JLayeredPane.POPUP_LAYER as Any)
layeredPane.add(floatingToolbar, JLayeredPane.POPUP_LAYER as Any)
@@ -503,6 +506,23 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
height
)
}
is VisualWindow -> {
var location = c.location
val dimension = getDimension()
if (location.x > dimension.width) {
location = Point(dimension.width - c.preferredSize.width, location.y)
}
if (location.y > dimension.height) {
location = Point(location.x, dimension.height - c.preferredSize.height)
}
c.setBounds(
location.x,
location.y,
c.width,
c.height
)
}
}
}
}
@@ -512,4 +532,42 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return dataProviderSupport.getData(dataKey)
}
override fun moveToFront(visualWindow: VisualWindow) {
if (visualWindow.isWindow()) {
visualWindow.getWindow()?.requestFocus()
return
}
layeredPane.moveToFront(visualWindow.getJComponent())
}
override fun getVisualWindows(): Array<VisualWindow> {
return visualWindows
}
override fun addVisualWindow(visualWindow: VisualWindow) {
visualWindows = ArrayUtils.add(visualWindows, visualWindow)
layeredPane.add(visualWindow.getJComponent(), JLayeredPane.DRAG_LAYER as Any)
layeredPane.revalidate()
layeredPane.repaint()
}
override fun removeVisualWindow(visualWindow: VisualWindow) {
rebaseVisualWindow(visualWindow)
visualWindows = ArrayUtils.removeElement(visualWindows, visualWindow)
}
override fun rebaseVisualWindow(visualWindow: VisualWindow) {
layeredPane.remove(visualWindow.getJComponent())
layeredPane.revalidate()
layeredPane.repaint()
requestFocusInWindow()
}
override fun getDimension(): Dimension {
return Dimension(
terminalDisplay.size.width + padding.left + padding.right,
terminalDisplay.size.height + padding.bottom + padding.top
)
}
}

View File

@@ -42,6 +42,8 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
override fun mousePressed(e: MouseEvent) {
terminalPanel.requestFocusInWindow()
if (isMouseTracking) {
return
}
@@ -77,7 +79,6 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
mousePressedPoint.y = e.y
}
terminalPanel.requestFocusInWindow()
// 如果只有 Shift 键按下,那么应该追加选中
if (selectionModel.hasSelection() && SwingUtilities.isLeftMouseButton(e) && e.modifiersEx == 1088) {

View File

@@ -0,0 +1,31 @@
package app.termora.terminal.panel.vw
import com.formdev.flatlaf.extras.components.FlatProgressBar
import java.awt.Dimension
import javax.swing.UIManager
class SmartProgressBar : FlatProgressBar() {
init {
preferredSize = Dimension(-1, UIManager.getInt("Table.rowHeight") - 6)
isStringPainted = true
maximum = 100
minimum = 0
}
override fun setValue(n: Int) {
super.setValue(n)
foreground = if (value < 60) {
UIManager.getColor("Component.accentColor")
} else if (value < 85) {
UIManager.getColor("Component.warning.focusedBorderColor")
} else {
UIManager.getColor("Component.error.focusedBorderColor")
}
}
override fun updateUI() {
super.updateUI()
value = value
}
}

View File

@@ -0,0 +1,435 @@
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.*
import kotlinx.coroutines.swing.Swing
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) {
companion object {
private val log = LoggerFactory.getLogger(SystemInformationVisualWindow::class.java)
}
private val systemInformationPanel by lazy { SystemInformationPanel() }
init {
initViews()
initEvents()
initVisualWindowPanel()
}
private fun initViews() {
title = I18n.getString("termora.visual-window.system-information")
add(systemInformationPanel, BorderLayout.CENTER)
}
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 {
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private val cpuProgressBar = SmartProgressBar()
private val memoryProgressBar = SmartProgressBar()
private val swapProgressBar = SmartProgressBar()
private val mem = Mem()
private val cpu = CPU()
private val swap = Swap()
private val tableModel = object : DefaultTableModel() {
override fun isCellEditable(row: Int, column: Int): Boolean {
return false
}
}
init {
initViews()
initEvents()
}
private fun initViews() {
add(createPanel(), BorderLayout.CENTER)
}
private fun createPanel(): JComponent {
val formMargin = "4dlu"
var rows = 1
val step = 2
val p = JPanel(BorderLayout())
val n = FormBuilder.create().debug(false).layout(
FormLayout(
"left:pref, $formMargin, default:grow",
"pref, $formMargin, pref, $formMargin, pref, $formMargin"
)
)
.add("CPU: ").xy(1, rows)
.add(cpuProgressBar).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.visual-window.system-information.mem")}: ").xy(1, rows)
.add(memoryProgressBar).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.visual-window.system-information.swap")}: ").xy(1, rows)
.add(swapProgressBar).xy(3, rows).apply { rows += step }
.build()
val table = JTable(tableModel)
table.tableHeader.isEnabled = false
table.showVerticalLines = true
table.showHorizontalLines = true
table.fillsViewportHeight = true
tableModel.addColumn(I18n.getString("termora.visual-window.system-information.filesystem"))
tableModel.addColumn(I18n.getString("termora.visual-window.system-information.used-total"))
val centerRenderer = DefaultTableCellRenderer()
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
table.columnModel.getColumn(1).cellRenderer = centerRenderer
p.add(n, BorderLayout.NORTH)
p.add(JScrollPane(table).apply {
border = BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.BorderColor)
}, BorderLayout.CENTER)
p.border = BorderFactory.createEmptyBorder(6, 6, 6, 6)
return p
}
private fun initEvents() {
coroutineScope.launch {
while (coroutineScope.isActive) {
try {
refresh()
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
} finally {
delay(1000.milliseconds)
}
}
}
}
private suspend fun refresh() {
val session = tab.getData(SSHTerminalTab.SSHSession) ?: return
try {
// 刷新 CPU 和 内存
refreshCPUAndMem(session)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error("refreshCPUAndMem", e)
}
}
try {
// 刷新磁盘
refreshDisk(session)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error("refreshDisk", e)
}
}
}
private suspend fun refreshCPUAndMem(session: ClientSession) {
// top
var pair = SshClients.execChannel(session, "top -bn1")
if (pair.first != 0) {
return
}
val lines = pair.second.split(StringUtils.LF)
for (line in lines) {
val isCPU = line.startsWith("%Cpu(s):", true)
val isMibMem = line.startsWith("MiB Mem :", true)
val isKibMem = line.startsWith("KiB Mem :", true)
val isMibSwap = line.startsWith("MiB Swap:", true)
val isKibSwap = line.startsWith("KiB Swap:", true)
val unit = if (isKibSwap || isKibMem) 'K' else 'M'
if (isCPU) {
val parts = StringUtils.removeStartIgnoreCase(line, "%Cpu(s):").split(",").map { it.trim() }
for (part in parts) {
if (part.endsWith("us")) {
cpu.us = StringUtils.removeEnd(part, "us").trim().toDoubleOrNull() ?: 0.0
} else if (part.endsWith("sy")) {
cpu.sy = StringUtils.removeEnd(part, "sy").trim().toDoubleOrNull() ?: 0.0
} else if (part.endsWith("ni")) {
cpu.ni = StringUtils.removeEnd(part, "ni").trim().toDoubleOrNull() ?: 0.0
} else if (part.endsWith("id")) {
cpu.id = StringUtils.removeEnd(part, "id").trim().toDoubleOrNull() ?: 0.0
} else if (part.endsWith("wa")) {
cpu.wa = StringUtils.removeEnd(part, "wa").trim().toDoubleOrNull() ?: 0.0
} else if (part.endsWith("hi")) {
cpu.hi = StringUtils.removeEnd(part, "hi").trim().toDoubleOrNull() ?: 0.0
} else if (part.endsWith("si")) {
cpu.si = StringUtils.removeEnd(part, "si").trim().toDoubleOrNull() ?: 0.0
} else if (part.endsWith("st")) {
cpu.st = StringUtils.removeEnd(part, "st").trim().toDoubleOrNull() ?: 0.0
}
}
} else if (isMibMem || isKibMem) {
val parts = StringUtils.removeStartIgnoreCase(line, "${unit}iB Mem :")
.split(",")
.map { it.trim() }
for (part in parts) {
if (part.endsWith("total")) {
mem.total = StringUtils.removeEnd(part, "total").trim().toDoubleOrNull() ?: 0.0
} else if (part.endsWith("free")) {
mem.free = StringUtils.removeEnd(part, "free").trim().toDoubleOrNull() ?: 0.0
} else if (part.endsWith("used")) {
mem.used = StringUtils.removeEnd(part, "used").trim().toDoubleOrNull() ?: 0.0
} else if (part.endsWith("buff/cache")) {
mem.buffCache = StringUtils.removeEnd(part, "buff/cache").trim().toDoubleOrNull() ?: 0.0
}
}
if (isKibMem) {
mem.total = mem.total / 1024.0
mem.free = mem.free / 1024.0
mem.used = mem.used / 1024.0
mem.buffCache = mem.buffCache / 1024.0
}
} else if (isMibSwap || isKibSwap) {
val parts = StringUtils.removeStartIgnoreCase(line, "${unit}iB Swap:")
.split(",")
.map { it.trim() }
for (part in parts) {
if (part.contains("total")) {
swap.total = StringUtils.removeEnd(part, "total").trim().toDoubleOrNull() ?: 0.0
} else if (part.contains("free")) {
swap.free = StringUtils.removeEnd(part, "free").trim().toDoubleOrNull() ?: 0.0
} else if (part.contains("used.")) {
swap.used = part.substringBefore("used.").trim().toDoubleOrNull() ?: 0.0
}
}
if (isKibSwap) {
swap.total = swap.total / 1024.0
swap.free = swap.free / 1024.0
swap.used = swap.used / 1024.0
}
}
}
withContext(Dispatchers.Swing) {
cpuProgressBar.value = (100.0 - cpu.id).toInt()
memoryProgressBar.value = (mem.used / mem.total * 100.0).toInt()
memoryProgressBar.string =
"${formatBytes((mem.used * 1024 * 1024).toLong())} / ${formatBytes((mem.total * 1024 * 1024).toLong())}"
swapProgressBar.value = (swap.used / swap.total * 100.0).toInt()
swapProgressBar.string =
"${formatBytes((swap.used * 1024 * 1024).toLong())} / ${formatBytes((swap.total * 1024 * 1024).toLong())}"
}
}
private suspend fun refreshDisk(session: ClientSession) {
// df -h
var pair = SshClients.execChannel(session, "df -B1")
if (pair.first != 0) {
return
}
val disks = mutableListOf<Disk>()
val lines = pair.second.split(StringUtils.LF)
for (line in lines) {
if (!line.startsWith("/dev/")) {
continue
}
val parts = line.split("\\s+".toRegex())
if (parts.size < 6) {
continue
}
disks.add(
Disk(
filesystem = parts[0],
size = parts[1].toLong(),
used = parts[2].toLong(),
avail = parts[3].toLong(),
usePercentage = StringUtils.removeEnd(parts[4], "%").toIntOrNull() ?: 0,
mountedOn = parts[5],
)
)
}
withContext(Dispatchers.Swing) {
while (tableModel.rowCount > 0) {
tableModel.removeRow(0)
}
for (disk in disks) {
tableModel.addRow(
arrayOf(
" ${disk.filesystem}",
formatBytes(disk.used) + " / " + formatBytes(disk.size),
)
)
}
}
}
override fun dispose() {
coroutineScope.cancel()
}
}
private data class Mem(
/**
* 总内存
*/
var total: Double = 0.0,
/**
* 空闲内存
*/
var free: Double = 0.0,
/**
* 已用内存
*/
var used: Double = 0.0,
/**
* 缓存和缓冲区占用的内存
*/
var buffCache: Double = 0.0,
)
private data class Swap(
/**
* 交换空间的总大小
*/
var total: Double = 0.0,
/**
* 已使用的交换空间
*/
var free: Double = 0.0,
/**
* 未使用的交换空间
*/
var used: Double = 0.0,
)
private data class CPU(
/**
* 用户空间 CPU 占用时间百分比。
* 该值表示 CPU 用于执行用户进程的时间比例。
* 示例:如果系统中 CPU 用于执行用户程序的时间占总 CPU 时间的 40%,则该值为 40.0。
*/
var us: Double = 0.0,
/**
* 系统空间 CPU 占用时间百分比。
* 该值表示 CPU 用于执行内核进程的时间比例。
* 示例:如果内核进程占用 CPU 时间的 20%,则该值为 20.0。
*/
var sy: Double = 0.0,
/**
* 优先级调整过的进程Nice占用的 CPU 时间百分比。
* 该值表示 CPU 用于执行“优先级较低的”进程的时间比例。
* 示例:如果优先级调整过的进程占用 CPU 时间的 10%,则该值为 10.0。
*/
var ni: Double = 0.0,
/**
* CPU 空闲时间百分比。
* 该值表示 CPU 在空闲状态下没有执行任何任务的时间比例。
* 示例:如果 CPU 95% 处于空闲状态,该值为 95.0。
*/
var id: Double = 0.0,
/**
* I/O 等待时间百分比。
* 该值表示 CPU 正在等待 I/O 操作完成的时间比例。
* 示例:如果 CPU 由于 I/O 操作等待占用 5% 的时间,则该值为 5.0。
*/
var wa: Double = 0.0,
/**
* 硬件中断处理时间百分比。
* 该值表示 CPU 用于处理中断请求的时间比例,通常由硬件触发。
* 示例:如果 CPU 处理硬件中断占用 2% 的时间,则该值为 2.0。
*/
var hi: Double = 0.0,
/**
* 软件中断处理时间百分比。
* 该值表示 CPU 用于处理由软件触发的中断的时间比例。
* 示例:如果 CPU 处理软件中断占用 3% 的时间,则该值为 3.0。
*/
var si: Double = 0.0,
/**
* 虚拟化环境中的 CPU 抢占时间百分比。
* 该值表示 CPU 在虚拟化环境中被其他虚拟机抢占的时间比例。
* 示例:如果虚拟化环境中的 CPU 抢占占用 0.5% 的时间,则该值为 0.5。
*/
var st: Double = 0.0,
)
private data class Disk(
var filesystem: String = StringUtils.EMPTY,
/**
* 总大小
*/
var size: Long = 0L,
/**
* 已经使用的空间
*/
var used: Long = 0L,
/**
* 可用空间
*/
var avail: Long = 0L,
/**
* 已经使用的百分比
*/
var usePercentage: Int = 0,
/**
* 挂载点
*/
var mountedOn: String = StringUtils.EMPTY
)
}

View File

@@ -0,0 +1,31 @@
package app.termora.terminal.panel.vw
import app.termora.Disposable
import java.awt.Window
import javax.swing.JComponent
/**
* 虚拟窗口
*/
interface VisualWindow : Disposable {
/**
* 虚拟窗口内容
*/
fun getJComponent(): JComponent
/**
* 是否是独立窗口(独立成一个 Window
*/
fun isWindow(): Boolean
/**
* 如果是独立窗口,那么可以返回
*/
fun getWindow(): Window? = null
/**
* 切换独立模式
*/
fun toggleWindow()
}

View File

@@ -0,0 +1,36 @@
package app.termora.terminal.panel.vw
import java.awt.Dimension
interface VisualWindowManager {
/**
* 将窗口移动到最前面
*/
fun moveToFront(visualWindow: VisualWindow)
/**
* 添加虚拟窗口
*/
fun addVisualWindow(visualWindow: VisualWindow)
/**
* 移除虚拟窗口
*/
fun removeVisualWindow(visualWindow: VisualWindow)
/**
* 变基,仅仅从 LayeredPane 移除,但是不从 [getVisualWindows] 中移除
*/
fun rebaseVisualWindow(visualWindow: VisualWindow)
/**
* 获取管理的所有窗口
*/
fun getVisualWindows(): Array<VisualWindow>
/**
* 获取管理器的宽高
*/
fun getDimension(): Dimension
}

View File

@@ -0,0 +1,273 @@
package app.termora.terminal.panel.vw
import app.termora.*
import com.formdev.flatlaf.extras.components.FlatToolBar
import java.awt.*
import java.awt.event.*
import java.beans.PropertyChangeEvent
import java.beans.PropertyChangeListener
import javax.swing.*
import kotlin.math.max
import kotlin.math.min
open class VisualWindowPanel(protected val id: String, protected val visualWindowManager: VisualWindowManager) :
JPanel(BorderLayout()), VisualWindow {
protected val properties get() = Database.getDatabase().properties
private val titleLabel = JLabel()
private val toolbar = FlatToolBar()
private val visualWindow = this
private val resizer = VisualWindowResizer(this) { !isWindow }
private var isWindow = false
set(value) {
val oldValue = field
field = value
firePropertyChange("isWindow", oldValue, value)
}
private var dialog: VisualWindowDialog? = null
private var oldBounds = Rectangle()
private var toggleWindowBtn = JButton(Icons.openInNewWindow)
private val closeWindowListener = object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
close()
}
}
var title: String
set(value) {
titleLabel.text = value
}
get() = titleLabel.text
protected fun initVisualWindowPanel() {
initViews()
initEvents()
initToolBar()
}
private fun initViews() {
border = BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.BorderColor)
val x = properties.getString("VisualWindow.${id}.location.x", "-1").toIntOrNull() ?: -1
val y = properties.getString("VisualWindow.${id}.location.y", "-1").toIntOrNull() ?: -1
val w = properties.getString("VisualWindow.${id}.location.width", "-1").toIntOrNull() ?: -1
val h = properties.getString("VisualWindow.${id}.location.height", "-1").toIntOrNull() ?: -1
if (x >= 0 && y >= 0) {
setLocation(x, y)
} else {
setLocation(200, 200)
}
if (w > 0 && h > 0) setSize(w, h) else setSize(400, 200)
}
private fun initEvents() {
val dragListener = DragListener()
toolbar.addMouseListener(dragListener)
toolbar.addMouseMotionListener(dragListener)
// 监听全局事件
Toolkit.getDefaultToolkit().addAWTEventListener(object : AWTEventListener {
override fun eventDispatched(event: AWTEvent) {
if (event is MouseEvent) {
if (event.id == MouseEvent.MOUSE_PRESSED) {
val c = event.component ?: return
if (SwingUtilities.isDescendingFrom(c, visualWindow)) {
visualWindowManager.moveToFront(visualWindow)
}
}
}
}
}, MouseEvent.MOUSE_EVENT_MASK)
// 阻止事件穿透
addMouseListener(object : MouseAdapter() {})
toggleWindowBtn.addActionListener { toggleWindow() }
addPropertyChangeListener("isWindow", object : PropertyChangeListener {
override fun propertyChange(evt: PropertyChangeEvent) {
if (isWindow) {
border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
toggleWindowBtn.icon = Icons.openInToolWindow
} else {
border = BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.BorderColor)
toggleWindowBtn.icon = Icons.openInNewWindow
}
}
})
}
private fun initToolBar() {
toolbar.add(JLabel(Icons.empty))
toolbar.add(JLabel(Icons.empty))
toolbar.add(Box.createHorizontalGlue())
toolbar.add(titleLabel)
toolbar.add(Box.createHorizontalGlue())
toolbar.add(toggleWindowBtn)
toolbar.add(JButton(Icons.close).apply { addActionListener { Disposer.dispose(visualWindow) } })
toolbar.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor)
add(toolbar, BorderLayout.NORTH)
}
override fun dispose() {
val bounds = if (isWindow) oldBounds else bounds
properties.putString("VisualWindow.${id}.location.x", bounds.x.toString())
properties.putString("VisualWindow.${id}.location.y", bounds.y.toString())
properties.putString("VisualWindow.${id}.location.width", bounds.width.toString())
properties.putString("VisualWindow.${id}.location.height", bounds.height.toString())
resizer.uninstall()
this.close()
}
final override fun getJComponent(): JComponent {
return this
}
override fun isWindow(): Boolean {
return isWindow
}
override fun getWindow(): Window? {
return dialog
}
protected open fun getWindowTitle(): String {
return id
}
override fun toggleWindow() {
if (isWindow) {
// 提前移除 dialog 的关闭事件
dialog?.removeWindowListener(closeWindowListener)
}
isWindow = !isWindow
dialog?.dispose()
dialog = null
if (isWindow) {
oldBounds = bounds
// 变基
visualWindowManager.rebaseVisualWindow(this)
val dialog = VisualWindowDialog().apply { dialog = this }
dialog.addWindowListener(closeWindowListener)
dialog.isVisible = true
} else {
bounds = oldBounds
visualWindowManager.removeVisualWindow(visualWindow)
visualWindowManager.addVisualWindow(visualWindow)
}
}
private inner class DragListener() : MouseAdapter() {
private var startPoint: Point? = null
override fun mousePressed(e: MouseEvent) {
if (isWindow) {
startPoint = null
return
}
startPoint = SwingUtilities.convertPoint(visualWindow, e.getPoint(), visualWindow.getParent())
}
override fun mouseDragged(e: MouseEvent) {
val startPoint = this.startPoint ?: return
val newPoint = SwingUtilities.convertPoint(visualWindow, e.getPoint(), visualWindow.getParent())
val dimension = visualWindowManager.getDimension()
val x = min(
visualWindow.getX() + (newPoint.x - startPoint.x),
dimension.width - visualWindow.width
)
val y = min(
visualWindow.getY() + (newPoint.y - startPoint.y),
dimension.height - visualWindow.height
)
visualWindow.setBounds(max(x, 0), max(y, 0), visualWindow.getWidth(), visualWindow.getHeight())
this.startPoint = newPoint
}
override fun mouseReleased(e: MouseEvent) {
visualWindowManager.moveToFront(visualWindow)
}
}
protected open fun close() {
SwingUtilities.invokeLater {
if (isWindow()) {
dialog?.dispose()
dialog = null
}
visualWindowManager.removeVisualWindow(visualWindow)
}
}
private inner class VisualWindowDialog : DialogWrapper(null) {
init {
isModal = false
controlsVisible = false
isResizable = true
title = getWindowTitle()
initEvents()
init()
val x = properties.getString("VisualWindow.${id}.dialog.location.x", "-1").toIntOrNull() ?: -1
val y = properties.getString("VisualWindow.${id}.dialog.location.y", "-1").toIntOrNull() ?: -1
val w = properties.getString("VisualWindow.${id}.dialog.location.width", "-1").toIntOrNull() ?: -1
val h = properties.getString("VisualWindow.${id}.dialog.location.height", "-1").toIntOrNull() ?: -1
if (w > 0 && h > 0) setSize(w, h) else pack()
if (x >= 0 && y >= 0) {
setLocation(x, y)
} else {
setLocationRelativeTo(null)
}
}
private fun initEvents() {
Disposer.register(disposable, object : Disposable {
override fun dispose() {
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())
properties.putString("VisualWindow.${id}.dialog.location.height", height.toString())
}
})
}
override fun createCenterPanel(): JComponent {
return getJComponent()
}
override fun createSouthPanel(): JComponent? {
return null
}
}
}

View File

@@ -0,0 +1,48 @@
package app.termora.terminal.panel.vw
import com.formdev.flatlaf.ui.FlatWindowResizer
import java.awt.Dimension
import java.awt.Rectangle
import javax.swing.JComponent
class VisualWindowResizer(resizeComp: JComponent, private val windowResizable: () -> Boolean = { true }) :
FlatWindowResizer(resizeComp) {
override fun isWindowResizable(): Boolean {
return windowResizable.invoke()
}
override fun getWindowBounds(): Rectangle {
return resizeComp.bounds
}
override fun setWindowBounds(r: Rectangle) {
resizeComp.bounds = r
resizeComp.revalidate()
resizeComp.repaint()
}
override fun limitToParentBounds(): Boolean {
return true
}
override fun getParentBounds(): Rectangle {
return resizeComp.getParent().bounds
}
override fun honorMinimumSizeOnResize(): Boolean {
return true
}
override fun honorMaximumSizeOnResize(): Boolean {
return true
}
override fun getWindowMinimumSize(): Dimension {
return resizeComp.minimumSize
}
override fun getWindowMaximumSize(): Dimension {
return resizeComp.maximumSize
}
}

View File

@@ -342,6 +342,16 @@ termora.terminal.copied=Copied
termora.terminal.channel-disconnected=Channel has been disconnected.\u0020
termora.terminal.channel-reconnect=Type {0} to reconnect.
# Visual Window
termora.visual-window.system-information=System information
termora.visual-window.system-information.mem=Mem
termora.visual-window.system-information.swap=Swap
termora.visual-window.system-information.filesystem=Filesystem
termora.visual-window.system-information.used-total=Used / Total
termora.floating-toolbar.not-supported=This action is not supported
# zmodem
termora.addons.zmodem.skip=SKIP

View File

@@ -328,5 +328,16 @@ termora.actions.open-new-window=打开新窗口
termora.actions.clear-screen=清除终端屏幕
termora.actions.switch-tab=切换到特定标签页 [1..9]
# Visual Window
termora.visual-window.system-information=系统信息
termora.visual-window.system-information.mem=内存
termora.visual-window.system-information.swap=交换
termora.visual-window.system-information.filesystem=文件系统
termora.visual-window.system-information.used-total=使用 / 大小
termora.floating-toolbar.not-supported=不允许此操作
# zmodem
termora.addons.zmodem.skip=跳过

View File

@@ -309,6 +309,14 @@ termora.actions.open-new-window=開啟新視窗
termora.actions.clear-screen=清除終端機螢幕
termora.actions.switch-tab=切換到特定分頁 [1..9]
# Visual Window
termora.visual-window.system-information=系統訊息
termora.visual-window.system-information.mem=內存
termora.visual-window.system-information.swap=交換
termora.visual-window.system-information.filesystem=檔案系統
termora.visual-window.system-information.used-total=使用 / 大小
termora.floating-toolbar.not-supported=不允許此操作
# zmodem
termora.addons.zmodem.skip=跳過

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.5 5V2.02054C11.4149 2.26101 13.739 4.5851 13.9795 7.5H11C10.7239 7.5 10.5 7.72386 10.5 8C10.5 8.27614 10.7239 8.5 11 8.5H13.9795C13.739 11.4149 11.4149 13.739 8.5 13.9795V11C8.5 10.7239 8.27614 10.5 8 10.5C7.72386 10.5 7.5 10.7239 7.5 11V13.9795C4.5851 13.739 2.26101 11.4149 2.02054 8.5H5C5.27614 8.5 5.5 8.27614 5.5 8C5.5 7.72386 5.27614 7.5 5 7.5H2.02054C2.26101 4.5851 4.5851 2.26101 7.5 2.02054V5C7.5 5.27614 7.72386 5.5 8 5.5C8.27614 5.5 8.5 5.27614 8.5 5ZM1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8C15 11.866 11.866 15 8 15C4.13401 15 1 11.866 1 8Z" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 866 B

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.5 5V2.02054C11.4149 2.26101 13.739 4.5851 13.9795 7.5H11C10.7239 7.5 10.5 7.72386 10.5 8C10.5 8.27614 10.7239 8.5 11 8.5H13.9795C13.739 11.4149 11.4149 13.739 8.5 13.9795V11C8.5 10.7239 8.27614 10.5 8 10.5C7.72386 10.5 7.5 10.7239 7.5 11V13.9795C4.5851 13.739 2.26101 11.4149 2.02054 8.5H5C5.27614 8.5 5.5 8.27614 5.5 8C5.5 7.72386 5.27614 7.5 5 7.5H2.02054C2.26101 4.5851 4.5851 2.26101 7.5 2.02054V5C7.5 5.27614 7.72386 5.5 8 5.5C8.27614 5.5 8.5 5.27614 8.5 5ZM1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8C15 11.866 11.866 15 8 15C4.13401 15 1 11.866 1 8Z" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 866 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 14C2.89543 14 2 13.1046 2 12L2 4C2 2.89543 2.89543 2 4 2L6.5 2C6.77614 2 7 2.22386 7 2.5C7 2.77614 6.77614 3 6.5 3L4 3C3.44772 3 3 3.44772 3 4L3 12C3 12.5523 3.44772 13 4 13H12C12.5523 13 13 12.5523 13 12V9.5C13 9.22386 13.2239 9 13.5 9C13.7761 9 14 9.22386 14 9.5V12C14 13.1046 13.1046 14 12 14H4Z" fill="#6C707E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 2V6.5C14 6.77614 13.7761 7 13.5 7C13.2239 7 13 6.77614 13 6.5V3.70711L8.85355 7.85355C8.65829 8.04882 8.34171 8.04882 8.14645 7.85355C7.95118 7.65829 7.95118 7.34171 8.14645 7.14645L12.2929 3L9.5 3C9.22386 3 9 2.77614 9 2.5C9 2.22386 9.22386 2 9.5 2L14 2Z" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 799 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 14C2.89543 14 2 13.1046 2 12L2 4C2 2.89543 2.89543 2 4 2L6.5 2C6.77614 2 7 2.22386 7 2.5C7 2.77614 6.77614 3 6.5 3L4 3C3.44772 3 3 3.44772 3 4L3 12C3 12.5523 3.44772 13 4 13H12C12.5523 13 13 12.5523 13 12V9.5C13 9.22386 13.2239 9 13.5 9C13.7761 9 14 9.22386 14 9.5V12C14 13.1046 13.1046 14 12 14H4Z" fill="#CED0D6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 2V6.5C14 6.77614 13.7761 7 13.5 7C13.2239 7 13 6.77614 13 6.5V3.70711L8.85355 7.85355C8.65829 8.04882 8.34171 8.04882 8.14645 7.85355C7.95118 7.65829 7.95118 7.34171 8.14645 7.14645L12.2929 3L9.5 3C9.22386 3 9 2.77614 9 2.5C9 2.22386 9.22386 2 9.5 2L14 2Z" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 799 B

View File

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 7L5.5 10.5" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.5 10.5L5.5 10.5L5.5 7.5" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 508 B

View File

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 7L5.5 10.5" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.5 10.5L5.5 10.5L5.5 7.5" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -1,6 +1,6 @@
FROM linuxserver/openssh-server
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update && apk add wget gcc g++ git make zsh htop inetutils-telnet && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
&& apk update && apk add wget gcc g++ git make zsh htop stress-ng inetutils-telnet && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz