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() { protected fun init() {
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE defaultCloseOperation = DISPOSE_ON_CLOSE
initTitleBar() initTitleBar()
initEvents() initEvents()
@@ -158,12 +158,14 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
openPopup = true openPopup = true
} }
val window = SwingUtilities.windowForComponent(c) val window = c as? Window ?: SwingUtilities.windowForComponent(c)
val windows = window.ownedWindows if (window != null) {
for (w in windows) { val windows = window.ownedWindows
if (w.isVisible && w.javaClass.getName().endsWith("HeavyWeightWindow")) { for (w in windows) {
openPopup = true if (w.isVisible && w.javaClass.getName().endsWith("HeavyWeightWindow")) {
w.dispose() 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 down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_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 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 searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
val eye by lazy { DynamicIcon("icons/eye.svg", "icons/eye_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") } 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 empty by lazy { DynamicIcon("icons/empty.svg") }
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.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 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 errorIntroduction by lazy { DynamicIcon("icons/errorIntroduction.svg", "icons/errorIntroduction_dark.svg") }
val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_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") } 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.Session
import org.apache.sshd.common.session.SessionListener import org.apache.sshd.common.session.SessionListener
import org.apache.sshd.common.session.SessionListener.Event import org.apache.sshd.common.session.SessionListener.Event
import org.slf4j.LoggerFactory
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.* import java.util.*
import javax.swing.JComponent import javax.swing.JComponent
@@ -36,7 +35,7 @@ import javax.swing.SwingUtilities
class SSHTerminalTab(windowScope: WindowScope, host: Host) : class SSHTerminalTab(windowScope: WindowScope, host: Host) :
PtyHostTerminalTab(windowScope, host) { PtyHostTerminalTab(windowScope, host) {
companion object { companion object {
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java) val SSHSession = DataKey(ClientSession::class)
} }
private val mutex = Mutex() 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() { override fun stop() {
if (mutex.tryLock()) { if (mutex.tryLock()) {

View File

@@ -2,10 +2,12 @@ package app.termora
import app.termora.keymgr.OhKeyPairKeyPairProvider import app.termora.keymgr.OhKeyPairKeyPairProvider
import app.termora.terminal.TerminalSize import app.termora.terminal.TerminalSize
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.apache.sshd.client.ClientBuilder import org.apache.sshd.client.ClientBuilder
import org.apache.sshd.client.SshClient import org.apache.sshd.client.SshClient
import org.apache.sshd.client.channel.ChannelShell 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.HostConfigEntry
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
import org.apache.sshd.client.config.hosts.KnownHostEntry 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.eclipse.jgit.transport.sshd.ProxyData
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.Window import java.awt.Window
import java.io.ByteArrayOutputStream
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Proxy import java.net.Proxy
import java.net.SocketAddress import java.net.SocketAddress
@@ -38,6 +41,7 @@ import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration import java.time.Duration
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JOptionPane import javax.swing.JOptionPane
import javax.swing.SwingUtilities 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 -> tabbedPane.addPropertyChangeListener("selectedIndex") { evt ->
val oldIndex = evt.oldValue as Int val oldIndex = evt.oldValue as Int
val newIndex = evt.newValue as Int val newIndex = evt.newValue as Int
if (oldIndex >= 0 && tabs.size > newIndex) { if (oldIndex >= 0 && tabs.size > newIndex) {
tabs[oldIndex].onLostFocus() tabs[oldIndex].onLostFocus()
} }
if (newIndex >= 0 && tabs.size > newIndex) { if (newIndex >= 0 && tabs.size > newIndex) {
tabs[newIndex].onGrabFocus() tabs[newIndex].onGrabFocus()
} }
SwingUtilities.invokeLater { tabbedPane.getComponentAt(newIndex).requestFocusInWindow() }
} }
// 选择变动 // 选择变动
@@ -174,6 +179,9 @@ class TerminalTabbed(
// 新的获取到焦点 // 新的获取到焦点
tabs[tabbedPane.selectedIndex].onGrabFocus() tabs[tabbedPane.selectedIndex].onGrabFocus()
// 新的真正获取焦点
tabbedPane.getComponentAt(tabbedPane.selectedIndex).requestFocusInWindow()
if (disposable) { if (disposable) {
Disposer.dispose(tab) Disposer.dispose(tab)
} }

View File

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

View File

@@ -6,7 +6,10 @@ import app.termora.actions.DataProvider
import app.termora.actions.DataProviderSupport import app.termora.actions.DataProviderSupport
import app.termora.actions.DataProviders import app.termora.actions.DataProviders
import app.termora.terminal.* 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 com.formdev.flatlaf.util.SystemInfo
import org.apache.commons.lang3.ArrayUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.SystemUtils
import java.awt.* import java.awt.*
@@ -32,7 +35,7 @@ import kotlin.time.Duration.Companion.milliseconds
class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnector) : class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnector) :
JPanel(BorderLayout()), DataProvider, Disposable { JPanel(BorderLayout()), DataProvider, Disposable, VisualWindowManager {
companion object { companion object {
val Debug = DataKey(Boolean::class) val Debug = DataKey(Boolean::class)
@@ -46,6 +49,8 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
private val floatingToolbar = FloatingToolbarPanel() private val floatingToolbar = FloatingToolbarPanel()
private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink) private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink)
private val dataProviderSupport = DataProviderSupport() private val dataProviderSupport = DataProviderSupport()
private val layeredPane = TerminalLayeredPane()
private var visualWindows = emptyArray<VisualWindow>()
val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal) val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal)
@@ -118,8 +123,6 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
scrollBar.blockIncrement = 1 scrollBar.blockIncrement = 1
background = Color.black background = Color.black
val layeredPane = TerminalLayeredPane()
layeredPane.add(terminalDisplay, JLayeredPane.DEFAULT_LAYER as Any) layeredPane.add(terminalDisplay, JLayeredPane.DEFAULT_LAYER as Any)
layeredPane.add(terminalFindPanel, JLayeredPane.POPUP_LAYER as Any) layeredPane.add(terminalFindPanel, JLayeredPane.POPUP_LAYER as Any)
layeredPane.add(floatingToolbar, 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 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? { override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return dataProviderSupport.getData(dataKey) 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) { override fun mousePressed(e: MouseEvent) {
terminalPanel.requestFocusInWindow()
if (isMouseTracking) { if (isMouseTracking) {
return return
} }
@@ -77,7 +79,6 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
mousePressedPoint.y = e.y mousePressedPoint.y = e.y
} }
terminalPanel.requestFocusInWindow()
// 如果只有 Shift 键按下,那么应该追加选中 // 如果只有 Shift 键按下,那么应该追加选中
if (selectionModel.hasSelection() && SwingUtilities.isLeftMouseButton(e) && e.modifiersEx == 1088) { 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-disconnected=Channel has been disconnected.\u0020
termora.terminal.channel-reconnect=Type {0} to reconnect. 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 # zmodem
termora.addons.zmodem.skip=SKIP termora.addons.zmodem.skip=SKIP

View File

@@ -328,5 +328,16 @@ termora.actions.open-new-window=打开新窗口
termora.actions.clear-screen=清除终端屏幕 termora.actions.clear-screen=清除终端屏幕
termora.actions.switch-tab=切换到特定标签页 [1..9] 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 # zmodem
termora.addons.zmodem.skip=跳过 termora.addons.zmodem.skip=跳过

View File

@@ -309,6 +309,14 @@ termora.actions.open-new-window=開啟新視窗
termora.actions.clear-screen=清除終端機螢幕 termora.actions.clear-screen=清除終端機螢幕
termora.actions.switch-tab=切換到特定分頁 [1..9] 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 # zmodem
termora.addons.zmodem.skip=跳過 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 FROM linuxserver/openssh-server
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ 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 \ && 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 && ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz