Compare commits

...

24 Commits

Author SHA1 Message Date
hstyi
bdf29b27e7 release: 1.0.14 2025-05-07 12:01:46 +08:00
hstyi
96da7eac41 chore: scroll to the bottom after pressed any key (#553) 2025-05-01 08:36:51 +08:00
hstyi
71c0751692 fix: test connect (#551) 2025-04-30 15:13:11 +08:00
dependabot[bot]
442f334af2 chore(deps): bump com.github.mwiede:jsch from 0.2.25 to 0.2.26 (#546)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-30 09:15:06 +08:00
hstyi
48302a519f fix: snippet i18n (#549) 2025-04-29 15:10:01 +08:00
hstyi
c00f759f15 fix: xterm CBT (#543) 2025-04-28 15:47:55 +08:00
hstyi
1736dd909e chore: folder count (#542) 2025-04-28 09:11:07 +08:00
hstyi
1f01e368dd feat: support for signature algorithms (#539) 2025-04-27 09:54:22 +08:00
hstyi
bfba958b7e feat: support for compression algorithms (#538) 2025-04-26 10:00:54 +08:00
dependabot[bot]
758121b523 chore(deps): bump org.testcontainers:testcontainers-bom from 1.20.6 to 1.21.0 (#528)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-26 09:46:37 +08:00
hstyi
06e9a89e82 fix: double-click to open the host (#529) 2025-04-26 09:45:42 +08:00
hstyi
0ba6ac3305 chore: correct typos (#537) 2025-04-26 09:44:57 +08:00
hstyi
993f220b8b feat: support RDP protocol (#524) 2025-04-20 15:33:09 +08:00
hstyi
8755c4ad23 chore: tmux 2025-04-16 16:35:03 +08:00
dependabot[bot]
77cb102dd6 chore(deps): bump com.github.oshi:oshi-core from 6.6.5 to 6.8.1 (#517)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-16 11:08:23 +08:00
hstyi
89cfb0b451 fix: snippet \ characters (#513) 2025-04-15 17:17:24 +08:00
hstyi
6bdd83f208 fix: highlighter CJK characters (#511) 2025-04-15 15:51:45 +08:00
hstyi
8f86057dcc chore: KeyShortcut toHuman text (#510) 2025-04-15 09:19:16 +08:00
hstyi
a7d7ffa2cc chore: improve dialog 2025-04-15 08:52:02 +08:00
hstyi
d51cbeee13 feat: Highlighter keywords support regex (#507) 2025-04-14 14:29:00 +08:00
hstyi
deb2a0151e fix: Linux moving window jitter 2025-04-14 13:22:25 +08:00
dependabot[bot]
e1c4e9312d chore(deps): bump org.jetbrains.pty4j:pty4j from 0.13.3 to 0.13.4
Bumps [org.jetbrains.pty4j:pty4j](https://github.com/JetBrains/pty4j) from 0.13.3 to 0.13.4.
- [Commits](https://github.com/JetBrains/pty4j/commits)

---
updated-dependencies:
- dependency-name: org.jetbrains.pty4j:pty4j
  dependency-version: 0.13.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 10:44:08 +08:00
dependabot[bot]
c7233357bd chore(deps): bump commons-io:commons-io from 2.18.0 to 2.19.0
Bumps commons-io:commons-io from 2.18.0 to 2.19.0.

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  dependency-version: 2.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 10:43:58 +08:00
hstyi
eff8d565d0 chore: upgrade flatlaf version 2025-04-14 10:42:30 +08:00
30 changed files with 300 additions and 53 deletions

View File

@@ -20,7 +20,7 @@ plugins {
group = "app.termora"
version = "1.0.13"
version = "1.0.14"
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()

View File

@@ -1,10 +1,10 @@
[versions]
kotlin = "2.1.20"
slf4j = "2.0.17"
pty4j = "0.13.3"
pty4j = "0.13.4"
tinylog = "2.7.0"
kotlinx-coroutines = "1.10.2"
flatlaf = "3.5.4"
flatlaf = "3.6"
kotlinx-serialization-json = "1.8.1"
commons-codec = "1.18.0"
commons-lang3 = "3.17.0"
@@ -16,14 +16,14 @@ commons-vfs2="2.10.0"
swingx = "1.6.5-1"
jgoodies-forms = "1.9.0"
jfa = "1.2.0"
oshi = "6.6.5"
oshi = "6.8.1"
versioncompare = "1.4.1"
jna = "5.17.0"
jSystemThemeDetector = "3.9.1"
commons-io = "2.18.0"
commons-io = "2.19.0"
jbr-api = "17.1.10.1"
hutool = "5.8.37"
jsch = "0.2.25"
jsch = "0.2.26"
okhttp = "4.12.0"
sshj = "0.39.0"
sshd-core = "2.15.0"
@@ -35,7 +35,7 @@ bip39 = "1.0.9"
colorpicker = "2.0.1"
rhino = "1.8.0"
delight-rhino-sandbox = "0.0.17"
testcontainers = "1.20.6"
testcontainers = "1.21.0"
mixpanel = "1.5.3"
jSerialComm = "2.11.0"
ini4j = "0.5.5-2"

View File

@@ -8,6 +8,7 @@ import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.util.*;
@Deprecated
public class CombinedKeyIdentityProvider implements KeyIdentityProvider {
private final List<KeyIdentityProvider> providers = new ArrayList<>();

View File

@@ -14,6 +14,7 @@ import static com.formdev.flatlaf.util.UIScale.scale;
/**
* 如果要升级 FlatLaf 需要检查是否兼容
*/
@Deprecated
public class MyFlatTabbedPaneUI extends FlatTabbedPaneUI {
@Override
protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {

View File

@@ -25,6 +25,7 @@ enum class Protocol {
SSH,
Local,
Serial,
RDP,
/**
* 交互式的 SFTP此协议只在系统内部交互不应该暴露给用户也不应该持久化

View File

@@ -12,6 +12,7 @@ import org.apache.sshd.client.session.ClientSession
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.Window
import java.util.*
import javax.swing.*
class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
@@ -54,7 +55,8 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
isEnabled = false
swingCoroutineScope.launch(Dispatchers.IO) {
testConnection(pane.getHost())
// 因为测试连接的时候从数据库读取会导致失效所以这里生成随机ID
testConnection(pane.getHost().copy(id = UUID.randomUUID().toSimpleString()))
withContext(Dispatchers.Swing) {
putValue(NAME, I18n.getString("termora.new-host.test-connection"))
isEnabled = true

View File

@@ -320,6 +320,7 @@ open class HostOptionsPane : OptionsPane() {
protocolTypeComboBox.addItem(Protocol.SSH)
protocolTypeComboBox.addItem(Protocol.Local)
protocolTypeComboBox.addItem(Protocol.Serial)
protocolTypeComboBox.addItem(Protocol.RDP)
authenticationTypeComboBox.addItem(AuthenticationType.No)
authenticationTypeComboBox.addItem(AuthenticationType.Password)

View File

@@ -49,6 +49,7 @@ class HostTreeNode(host: Host) : SimpleTreeNode<Host>(host) {
return when (host.protocol) {
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin
Protocol.RDP -> if (selected && hasFocus) Icons.microsoftWindows.dark else Icons.microsoftWindows
else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal
}
}

View File

@@ -64,6 +64,7 @@ object Icons {
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") }
val microsoftWindows by lazy { DynamicIcon("icons/microsoftWindows.svg", "icons/microsoftWindows_dark.svg") }
val tencent by lazy { DynamicIcon("icons/tencent.svg") }
val google by lazy { DynamicIcon("icons/google-small.svg") }
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }

View File

@@ -9,7 +9,6 @@ import java.awt.event.*
import java.awt.image.BufferedImage
import java.util.*
import javax.swing.*
import javax.swing.plaf.TabbedPaneUI
import kotlin.math.abs
class MyTabbedPane : FlatTabbedPane() {
@@ -21,18 +20,12 @@ class MyTabbedPane : FlatTabbedPane() {
private val owner
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
.getData(DataProviders.TermoraFrame) as TermoraFrame
private val myUI = MyFlatTabbedPaneUI()
init {
isFocusable = false
super.setUI(myUI)
initEvents()
}
override fun setUI(ui: TabbedPaneUI?) {
super.setUI(myUI)
}
override fun updateUI() {
styleMap = mapOf(
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),

View File

@@ -37,6 +37,7 @@ import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory
@Suppress("CascadeIf")
class NewHostTree : SimpleTree() {
companion object {
@@ -97,7 +98,7 @@ class NewHostTree : SimpleTree() {
// 是否显示更多信息
if (isShowMoreInfo) {
val color = if (sel) {
if (tree.hasFocus()) {
if (tree.hasFocus() || isPopupMenu) {
UIManager.getColor("textHighlightText")
} else {
this.foreground
@@ -110,15 +111,15 @@ class NewHostTree : SimpleTree() {
"""<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>"""
}
if (host.protocol == Protocol.SSH) {
text =
"<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply("${host.username}@${host.host}")}</html>"
// @formatter:off
if (host.protocol == Protocol.SSH || host.protocol == Protocol.RDP) {
text = "<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply("${host.username}@${host.host}")}</html>"
} else if (host.protocol == Protocol.Serial) {
text =
"<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply(host.options.serialComm.port)}</html>"
text = "<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply(host.options.serialComm.port)}</html>"
} else if (host.protocol == Protocol.Folder) {
text = "<html>${host.name}${fontTag.apply(" (${node.childCount})")}</html>"
text = "<html>${host.name}${fontTag.apply(" (${node.getAllChildren().size})")}</html>"
}
// @formatter:on
}
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
@@ -134,6 +135,7 @@ class NewHostTree : SimpleTree() {
// double click
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
if (getPathForLocation(e.x, e.y) == null) return
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
if (lastNode.host.protocol != Protocol.Folder) {

View File

@@ -34,6 +34,7 @@ import org.apache.sshd.common.channel.ChannelFactory
import org.apache.sshd.common.channel.PtyChannelConfiguration
import org.apache.sshd.common.channel.PtyChannelConfigurationHolder
import org.apache.sshd.common.cipher.CipherNone
import org.apache.sshd.common.compression.BuiltinCompressions
import org.apache.sshd.common.config.keys.KeyRandomArt
import org.apache.sshd.common.config.keys.KeyUtils
import org.apache.sshd.common.future.CloseFuture
@@ -47,6 +48,7 @@ import org.apache.sshd.common.kex.BuiltinDHFactories
import org.apache.sshd.common.keyprovider.KeyIdentityProvider
import org.apache.sshd.common.session.Session
import org.apache.sshd.common.session.SessionListener
import org.apache.sshd.common.signature.BuiltinSignatures
import org.apache.sshd.common.util.net.SshdSocketAddress
import org.apache.sshd.core.CoreModuleProperties
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
@@ -339,6 +341,24 @@ object SshClients {
)
builder.keyExchangeFactories(keyExchangeFactories)
val compressionFactories = ClientBuilder.setUpDefaultCompressionFactories(true).toMutableList()
for (compression in listOf(
BuiltinCompressions.none,
BuiltinCompressions.zlib,
BuiltinCompressions.delayedZlib
)) {
if (compressionFactories.contains(compression)) continue
compressionFactories.add(compression)
}
builder.compressionFactories(compressionFactories)
val signatureFactories = ClientBuilder.setUpDefaultSignatureFactories(true).toMutableList()
for (signature in BuiltinSignatures.entries) {
if (signatureFactories.contains(signature)) continue
signatureFactories.add(signature)
}
builder.signatureFactories(signatureFactories)
if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) {
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
} else {

View File

@@ -63,10 +63,9 @@ class TermoraFrame : JFrame(), DataProvider {
}
override fun mouseDragged(e: MouseEvent) {
val mouseLayer = getMouseLayer() ?: return
getMouseMotionListener()?.mouseDragged(
MouseEvent(
mouseLayer,
e.component,
e.id,
e.`when`,
e.modifiersEx,
@@ -87,13 +86,6 @@ class TermoraFrame : JFrame(), DataProvider {
return getHandler() as? MouseMotionListener
}
private fun getMouseLayer(): JComponent? {
val titlePane = getTitlePane() ?: return null
val handlerField = titlePane.javaClass.getDeclaredField("mouseLayer") ?: return null
handlerField.isAccessible = true
return handlerField.get(titlePane) as? JComponent
}
private fun getHandler(): Any? {
val titlePane = getTitlePane() ?: return null
val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null

View File

@@ -1,6 +1,19 @@
package app.termora.actions
import app.termora.*
import com.formdev.flatlaf.util.SystemInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.net.URI
import java.util.*
import javax.swing.JOptionPane
import kotlin.time.Duration.Companion.seconds
class OpenHostAction : AnAction() {
companion object {
@@ -26,10 +39,70 @@ class OpenHostAction : AnAction() {
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host)
Protocol.RDP -> openRDP(windowScope, evt.host)
else -> LocalTerminalTab(windowScope, evt.host)
}
terminalTabbedManager.addTerminalTab(tab)
tab.start()
if (tab is TerminalTab) {
terminalTabbedManager.addTerminalTab(tab)
if (tab is PtyHostTerminalTab) {
tab.start()
}
}
}
private fun openRDP(windowScope: WindowScope, host: Host) {
if (SystemInfo.isLinux) {
OptionPane.showMessageDialog(
windowScope.window,
"Linux cannot connect to Windows Remote Server, Supported only for macOS and Windows",
messageType = JOptionPane.WARNING_MESSAGE
)
return
}
if (SystemInfo.isMacOS) {
if (!FileUtils.getFile("/Applications/Windows App.app").exists()) {
val option = OptionPane.showConfirmDialog(
windowScope.window,
"If you want to connect to a Windows Remote Server, You have to install the Windows App",
optionType = JOptionPane.OK_CANCEL_OPTION
)
if (option == JOptionPane.OK_OPTION) {
Application.browse(URI.create("https://apps.apple.com/app/windows-app/id1295203466"))
}
return
}
}
val sb = StringBuilder()
sb.append("full address:s:").append(host.host).append(':').append(host.port).appendLine()
sb.append("username:s:").append(host.username).appendLine()
val file = FileUtils.getFile(Application.getTemporaryDir(), UUID.randomUUID().toSimpleString() + ".rdp")
file.outputStream().use { IOUtils.write(sb.toString(), it, Charsets.UTF_8) }
if (host.authentication.type == AuthenticationType.Password) {
val systemClipboard = windowScope.window.toolkit.systemClipboard
val password = host.authentication.password
systemClipboard.setContents(StringSelection(password), null)
// clear password
swingCoroutineScope.launch(Dispatchers.IO) {
delay(30.seconds)
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
if (systemClipboard.getData(DataFlavor.stringFlavor) == password) {
systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
}
}
}
}
if (SystemInfo.isMacOS) {
ProcessBuilder("open", file.absolutePath).start()
} else if (SystemInfo.isWindows) {
ProcessBuilder("mstsc", file.absolutePath).start()
}
}
}

View File

@@ -51,6 +51,7 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
val customBtn = JButton("Custom")
customBtn.addActionListener {
val dialog = MyColorPickerDialog(this)
dialog.setLocationRelativeTo(this)
dialog.colorPicker.color = defaultColor
dialog.isVisible = true
val color = dialog.color

View File

@@ -24,6 +24,11 @@ data class KeywordHighlight(
*/
val matchCase: Boolean = false,
/**
* 是否是正则表达式
*/
val regex: Boolean = false,
/**
* 0 是取前景色
*/

View File

@@ -20,10 +20,8 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
private val model = KeywordHighlightTableModel()
private val table = FlatTable()
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
private val colorPalette by lazy {
TerminalFactory.getInstance().createTerminal().getTerminalModel()
.getColorPalette()
}
private val terminal by lazy { TerminalFactory.getInstance().createTerminal() }
private val colorPalette by lazy { terminal.getTerminalModel().getColorPalette() }
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
@@ -130,6 +128,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
addBtn.addActionListener {
val dialog = NewKeywordHighlightDialog(this, colorPalette)
dialog.setLocationRelativeTo(this)
dialog.isVisible = true
val keywordHighlight = dialog.keywordHighlight
if (keywordHighlight != null) {
@@ -143,6 +142,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
if (row > -1) {
var keywordHighlight = model.getKeywordHighlight(row)
val dialog = NewKeywordHighlightDialog(this, colorPalette)
dialog.setLocationRelativeTo(this)
dialog.keywordTextField.text = keywordHighlight.keyword
dialog.descriptionTextField.text = keywordHighlight.description
@@ -176,6 +176,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
dialog.underlineCheckBox.isSelected = keywordHighlight.underline
dialog.lineThroughCheckBox.isSelected = keywordHighlight.lineThrough
dialog.matchCaseBtn.isSelected = keywordHighlight.matchCase
dialog.regexBtn.isSelected = keywordHighlight.regex
dialog.isVisible = true
@@ -211,6 +212,12 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
editBtn.isEnabled = table.selectedRowCount > 0
deleteBtn.isEnabled = editBtn.isEnabled
}
Disposer.register(disposable, object : Disposable {
override fun dispose() {
terminal.close()
}
})
}
override fun createCenterPanel(): JComponent {

View File

@@ -5,6 +5,7 @@ import app.termora.terminal.*
import app.termora.terminal.panel.TerminalDisplay
import app.termora.terminal.panel.TerminalPaintListener
import app.termora.terminal.panel.TerminalPanel
import org.slf4j.LoggerFactory
import java.awt.Graphics
import kotlin.math.min
import kotlin.random.Random
@@ -18,9 +19,10 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
}
private val tag = Random.nextInt()
private val log = LoggerFactory.getLogger(KeywordHighlightPaintListener::class.java)
}
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
override fun before(
offset: Int,
@@ -36,7 +38,8 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
}
val document = terminal.getDocument()
val kinds = SubstrFinder(object : Iterator<TerminalLine> {
val kinds = mutableListOf<FindKind>()
val iterator = object : Iterator<TerminalLine> {
private var index = offset + 1
private val maxCount = min(index + count, document.getLineCount())
override fun hasNext(): Boolean {
@@ -46,8 +49,24 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
override fun next(): TerminalLine {
return document.getLine(index++)
}
}
}, CharArraySubstr(highlight.keyword.toCharArray())).find(!highlight.matchCase)
if (highlight.regex) {
try {
val regex = if (highlight.matchCase)
highlight.keyword.toRegex()
else highlight.keyword.toRegex(RegexOption.IGNORE_CASE)
RegexFinder(regex, iterator).find()
.apply { kinds.addAll(this) }
} catch (e: Exception) {
if (log.isDebugEnabled) {
log.error(e.message, e)
}
}
} else {
SubstrFinder(iterator, CharArraySubstr(highlight.keyword.toCharArray())).find(!highlight.matchCase)
.apply { kinds.addAll(this) }
}
for (kind in kinds) {
terminal.getMarkupModel().addHighlighter(
@@ -77,6 +96,74 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
terminal.getMarkupModel().removeAllHighlighters(tag)
}
private class RegexFinder(
private val regex: Regex,
private val iterator: Iterator<TerminalLine>
) {
private data class Coords(val row: Int, val col: Int)
private data class MatchResultWithCoords(
val match: String,
val coords: List<Coords>
)
fun find(): List<FindKind> {
val lines = mutableListOf<TerminalLine>()
val kinds = mutableListOf<FindKind>()
for ((index, line) in iterator.withIndex()) {
lines.add(line)
if (line.wrapped) continue
val data = mutableListOf<MutableList<Char>>()
for (e in lines) {
data.add(mutableListOf())
for (c in e.chars()) {
if (c.first.isNull) break
data.last().add(c.first)
}
}
lines.clear()
val resultWithCoords = findMatchesWithCoords(data)
if (resultWithCoords.isEmpty()) continue
val offset = index - data.size + 1
for (e in resultWithCoords) {
val coords = e.coords
if (coords.isEmpty()) continue
kinds.add(
FindKind(
startPosition = Position(coords.first().row + offset + 1, coords.first().col + 1),
endPosition = Position(coords.last().row + offset + 1, coords.last().col + 1)
)
)
}
}
return kinds
}
private fun findMatchesWithCoords(data: List<List<Char>>): List<MatchResultWithCoords> {
val flatChars = StringBuilder()
val indexMap = mutableListOf<Coords>()
// 拉平成字符串,并记录每个字符的位置
for ((rowIndex, row) in data.withIndex()) {
for ((colIndex, char) in row.withIndex()) {
flatChars.append(char)
indexMap.add(Coords(rowIndex, colIndex))
}
}
return regex.findAll(flatChars.toString())
.map { MatchResultWithCoords(it.value, indexMap.subList(it.range.first, it.range.last + 1)) }
.toList()
}
}
private class KeywordHighlightHighlighter(
range: HighlighterRange, terminal: Terminal,
@@ -93,4 +180,6 @@ class KeywordHighlightPaintListener private constructor() : TerminalPaintListene
)
}
}
}
}

View File

@@ -1,10 +1,6 @@
package app.termora.highlight
import app.termora.DialogWrapper
import app.termora.DynamicColor
import app.termora.I18n
import app.termora.Icons
import app.termora.Database
import app.termora.*
import app.termora.terminal.ColorPalette
import app.termora.terminal.TerminalColor
import com.formdev.flatlaf.FlatClientProperties
@@ -46,6 +42,7 @@ class NewKeywordHighlightDialog(
I18n.getString("termora.highlight.background-color")
)
val matchCaseBtn = JToggleButton(Icons.matchCase)
val regexBtn = JToggleButton(Icons.regex)
private val textColorRevert = JButton(Icons.revert)
@@ -85,6 +82,7 @@ class NewKeywordHighlightDialog(
val box = FlatToolBar()
box.add(matchCaseBtn)
box.add(regexBtn)
keywordTextField.trailingComponent = box
repaintKeywordHighlightView()
@@ -187,6 +185,7 @@ class NewKeywordHighlightDialog(
}
private fun createColorPanel(color: Color, title: String): ColorPanel {
val owner = this
val arc = UIManager.getInt("Component.arc")
val lineBorder = FlatLineBorder(Insets(1, 1, 1, 1), DynamicColor.BorderColor, 1f, arc)
val colorPanel = ColorPanel(color)
@@ -195,7 +194,8 @@ class NewKeywordHighlightDialog(
colorPanel.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
if (SwingUtilities.isLeftMouseButton(e)) {
val dialog = ChooseColorTemplateDialog(this@NewKeywordHighlightDialog, title)
val dialog = ChooseColorTemplateDialog(owner, title)
dialog.setLocationRelativeTo(owner)
dialog.defaultColor = colorPanel.color
dialog.isVisible = true
colorPanel.color = dialog.color ?: return
@@ -218,6 +218,7 @@ class NewKeywordHighlightDialog(
keyword = keywordTextField.text,
description = descriptionTextField.text,
matchCase = matchCaseBtn.isSelected,
regex = regexBtn.isSelected,
textColor = if (textColor.colorIndex != -1) textColor.colorIndex else textColor.color.toRGB(),
backgroundColor = if (backgroundColor.colorIndex != -1) backgroundColor.colorIndex else backgroundColor.color.toRGB(),
bold = boldCheckBox.isSelected,

View File

@@ -1,5 +1,6 @@
package app.termora.keymap
import com.formdev.flatlaf.util.SystemInfo
import org.apache.commons.lang3.StringUtils
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
@@ -23,7 +24,14 @@ class KeyShortcut(val keyStroke: KeyStroke) : Shortcut() {
text = text.replace("MINUS", "-")
}
return text.toCharArray().joinToString(" + ")
text = text.toCharArray().joinToString(" + ")
if (SystemInfo.isWindows || SystemInfo.isLinux) {
text = text.replace("", "Shift")
text = text.replace("", "Ctrl")
text = text.replace("", "Alt")
}
return text
}
}

View File

@@ -6,7 +6,9 @@ import app.termora.Icons
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.terminal.ControlCharacters
import app.termora.terminal.Null
import app.termora.terminal.panel.TerminalWriter
import org.apache.commons.lang3.StringUtils
import org.apache.commons.text.StringEscapeUtils
class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) {
@@ -33,11 +35,23 @@ class SnippetAction private constructor() : AnAction(I18n.getString("termora.sni
"\\a" to ControlCharacters.BEL,
"\\e" to ControlCharacters.ESC,
)
val chars = snippet.snippet.toCharArray()
for (i in chars.indices) {
val c = chars[i]
if (i == 0) continue
if (c != '\n') continue
if (chars[i - 1] != '\\') continue
// 每一行的最后一个 \ 比较特殊,先转成 null 然后再去 unescapeJava
chars[i - 1] = Char.Null
}
var text = StringEscapeUtils.unescapeJava(snippet.snippet)
var text = chars.joinToString(StringUtils.EMPTY)
text = StringEscapeUtils.unescapeJava(text)
for (e in map.entries) {
text = text.replace(e.key, e.value.toString())
}
text = snippet.snippet.replace(Char.Null, '\\')
writer.write(TerminalWriter.WriteRequest.fromBytes(text.toByteArray(writer.getCharset())))
}
}

View File

@@ -1,5 +1,6 @@
package app.termora.snippet
import app.termora.I18n
import app.termora.SimpleTreeModel
import javax.swing.tree.MutableTreeNode
import javax.swing.tree.TreeNode
@@ -8,7 +9,7 @@ class SnippetTreeModel : SimpleTreeModel<Snippet>(
SnippetTreeNode(
Snippet(
id = "0",
name = "全部片段",
name = I18n.getString("termora.snippet.title"),
type = SnippetType.Folder
)
)

View File

@@ -399,6 +399,16 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
}
}
// CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
'Z' -> {
val count = args.toInt(1)
val cursorModel = terminal.getCursorModel()
for (i in 0 until count) {
val x = terminal.getTabulator().previousTab(cursorModel.getPosition().x - 1) + 1
terminal.getCursorModel().move(cursorModel.getPosition().y, x)
}
}
// split
';' -> {
args.append(ch)

View File

@@ -79,6 +79,8 @@ class TerminalPanelKeyAdapter(
val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e))
if (encode.isNotEmpty()) {
writer.write(TerminalWriter.WriteRequest.fromBytes(encode.toByteArray(writer.getCharset())))
// scroll to bottom
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
e.consume()
}
@@ -91,6 +93,8 @@ class TerminalPanelKeyAdapter(
if (isAltPressedOnly(e) && Character.isDefined(e.keyChar)) {
val c = String(charArrayOf(ASCII_ESC, simpleMapKeyCodeToChar(e)))
writer.write(TerminalWriter.WriteRequest.fromBytes(c.toByteArray(writer.getCharset())))
// scroll to bottom
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
e.consume()
return
}

View File

@@ -2,10 +2,12 @@ package app.termora.terminal.panel.vw
import app.termora.*
import com.formdev.flatlaf.extras.components.FlatToolBar
import com.formdev.flatlaf.util.SystemInfo
import java.awt.*
import java.awt.event.*
import java.beans.PropertyChangeEvent
import java.beans.PropertyChangeListener
import javax.imageio.ImageIO
import javax.swing.*
import kotlin.math.max
import kotlin.math.min
@@ -333,6 +335,15 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
title = getWindowTitle()
isAlwaysOnTop = isAlwaysTop
if (SystemInfo.isWindows || SystemInfo.isLinux) {
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
val loader = TermoraFrame::class.java.classLoader
val images = sizes.mapNotNull { e ->
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
}
iconImages = images
}
initEvents()
init()

View File

@@ -299,7 +299,7 @@ termora.transport.sftp.status.done=已完成
termora.transport.sftp.status.failed=已失败
termora.transport.sftp.already-exists.message1=此文件夹已包含下名称的对象
termora.transport.sftp.already-exists.message1=此文件夹已包含下名称的对象
termora.transport.sftp.already-exists.message2=请选择要执行的操作
termora.transport.sftp.already-exists.overwrite=覆盖
termora.transport.sftp.already-exists.append=追加

View File

@@ -294,7 +294,7 @@ termora.transport.sftp.status.waiting=等待中
termora.transport.sftp.status.done=已完成
termora.transport.sftp.status.failed=已失敗
termora.transport.sftp.already-exists.message1=此資料夾已包含下名稱的對象
termora.transport.sftp.already-exists.message1=此資料夾已包含下名稱的對象
termora.transport.sftp.already-exists.message2=請選擇要執行的操作
termora.transport.sftp.already-exists.overwrite=覆蓋
termora.transport.sftp.already-exists.append=追加

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2023 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="M2.00098 2H7.00195L7.00098 7H2L2.00098 2ZM8.00293 2H13V7H8.00293V2ZM2 7.99902L7 8V13.001L2 13V7.99902ZM8.00195 8H12.999L12.998 13.001H8.00195" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 399 B

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2023 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="M2.00098 2H7.00195L7.00098 7H2L2.00098 2ZM8.00293 2H13V7H8.00293V2ZM2 7.99902L7 8V13.001L2 13V7.99902ZM8.00195 8H12.999L12.998 13.001H8.00195" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 399 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 stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
&& apk update && apk add wget tmux gcc g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && 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
RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/sshd_config