mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: system information (#278)
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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") }
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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())
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开一个会话
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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=跳过
|
||||
@@ -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=跳過
|
||||
4
src/main/resources/icons/locate.svg
Normal file
4
src/main/resources/icons/locate.svg
Normal 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 |
4
src/main/resources/icons/locate_dark.svg
Normal file
4
src/main/resources/icons/locate_dark.svg
Normal 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 |
4
src/main/resources/icons/openInNewWindow.svg
Normal file
4
src/main/resources/icons/openInNewWindow.svg
Normal 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 |
4
src/main/resources/icons/openInNewWindow_dark.svg
Normal file
4
src/main/resources/icons/openInNewWindow_dark.svg
Normal 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 |
6
src/main/resources/icons/openInToolWindow.svg
Normal file
6
src/main/resources/icons/openInToolWindow.svg
Normal 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 |
6
src/main/resources/icons/openInToolWindow_dark.svg
Normal file
6
src/main/resources/icons/openInToolWindow_dark.svg
Normal 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 |
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user