From 57547c95cb686a38e8fb6244b7cb70364fb69d4d Mon Sep 17 00:00:00 2001 From: hstyi Date: Wed, 19 Feb 2025 13:17:59 +0800 Subject: [PATCH] feat: blink (#273) --- src/main/kotlin/app/termora/Database.kt | 13 +- src/main/kotlin/app/termora/MyTabbedPane.kt | 20 ++- .../kotlin/app/termora/SettingsOptionsPane.kt | 12 +- .../app/termora/TerminalPanelFactory.kt | 64 ++++++++-- .../kotlin/app/termora/TermoraFrameManager.kt | 4 +- .../termora/terminal/panel/TerminalBlink.kt | 119 ++++++++++++++++++ .../termora/terminal/panel/TerminalDisplay.kt | 40 ++++-- .../termora/terminal/panel/TerminalPanel.kt | 7 +- src/main/resources/i18n/messages.properties | 3 +- .../resources/i18n/messages_zh_CN.properties | 3 +- .../resources/i18n/messages_zh_TW.properties | 3 +- 11 files changed, 251 insertions(+), 37 deletions(-) create mode 100644 src/main/kotlin/app/termora/terminal/panel/TerminalBlink.kt diff --git a/src/main/kotlin/app/termora/Database.kt b/src/main/kotlin/app/termora/Database.kt index 460a128..a156a47 100644 --- a/src/main/kotlin/app/termora/Database.kt +++ b/src/main/kotlin/app/termora/Database.kt @@ -400,10 +400,10 @@ class Database private constructor(private val env: Environment) : Disposable { protected inner class CursorStylePropertyDelegate(defaultValue: CursorStyle) : PropertyDelegate(defaultValue) { override fun convertValue(value: String): CursorStyle { - try { - return CursorStyle.valueOf(value) - } catch (e: Exception) { - return initializer.invoke() + return try { + CursorStyle.valueOf(value) + } catch (_: Exception) { + initializer.invoke() } } } @@ -458,6 +458,11 @@ class Database private constructor(private val env: Environment) : Disposable { */ var beep by BooleanPropertyDelegate(true) + /** + * 光标闪烁 + */ + var cursorBlink by BooleanPropertyDelegate(false) + /** * 选中复制 */ diff --git a/src/main/kotlin/app/termora/MyTabbedPane.kt b/src/main/kotlin/app/termora/MyTabbedPane.kt index 5ffce1d..182c0af 100644 --- a/src/main/kotlin/app/termora/MyTabbedPane.kt +++ b/src/main/kotlin/app/termora/MyTabbedPane.kt @@ -1,6 +1,7 @@ package app.termora import app.termora.actions.AnActionEvent +import app.termora.actions.DataProvider import app.termora.actions.DataProviders import com.formdev.flatlaf.extras.components.FlatTabbedPane import org.apache.commons.lang3.StringUtils @@ -13,11 +14,13 @@ import kotlin.math.abs class MyTabbedPane : FlatTabbedPane() { - private val owner: Window get() = SwingUtilities.getWindowAncestor(this) private val dragMouseAdaptor = DragMouseAdaptor() private val terminalTabbedManager get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this)) .getData(DataProviders.TerminalTabbedManager) + private val owner + get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this)) + .getData(DataProviders.TermoraFrame) as TermoraFrame init { initEvents() @@ -145,11 +148,11 @@ class MyTabbedPane : FlatTabbedPane() { // 如果等于 null 表示在空地方释放,那么单独一个窗口 if (c == null) { val window = TermoraFrameManager.getInstance().createWindow() - dragToAnotherWindow(window) + dragToAnotherWindow(owner, window) window.location = releasedPoint window.isVisible = true } else if (c != owner && c is TermoraFrame) { // 如果在某个窗口内释放,那么就移动到某个窗口内 - dragToAnotherWindow(c) + dragToAnotherWindow(owner, c) } else { val tab = this.terminalTab val terminalTabbedManager = terminalTabbedManager @@ -224,20 +227,29 @@ class MyTabbedPane : FlatTabbedPane() { } - private fun dragToAnotherWindow(frame: TermoraFrame) { + private fun dragToAnotherWindow(oldFrame: TermoraFrame, frame: TermoraFrame) { val tab = this.terminalTab ?: return + val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return val tabbedManager = frame.getData(DataProviders.TerminalTabbed) ?: return val tabbedPane = frame.getData(DataProviders.TabbedPane) ?: return + val windowScope = frame.getData(DataProviders.WindowScope) ?: return + val oldWindowScope = oldFrame.getData(DataProviders.WindowScope) ?: return val location = Point(MouseInfo.getPointerInfo().location) SwingUtilities.convertPointFromScreen(location, tabbedPane) val index = tabbedPane.indexAtLocation(location.x, location.y) + moveTab( tabbedManager, tab, index ) + TerminalPanelFactory.getInstance(oldWindowScope).removeTerminalPanel(terminalPanel) + TerminalPanelFactory.getInstance(windowScope).addTerminalPanel(terminalPanel) + + + if (frame.hasFocus()) { return } diff --git a/src/main/kotlin/app/termora/SettingsOptionsPane.kt b/src/main/kotlin/app/termora/SettingsOptionsPane.kt index 3ae4ab4..bff43b4 100644 --- a/src/main/kotlin/app/termora/SettingsOptionsPane.kt +++ b/src/main/kotlin/app/termora/SettingsOptionsPane.kt @@ -305,6 +305,7 @@ class SettingsOptionsPane : OptionsPane() { private val cursorStyleComboBox = FlatComboBox() private val debugComboBox = YesOrNoComboBox() private val beepComboBox = YesOrNoComboBox() + private val cursorBlinkComboBox = YesOrNoComboBox() private val fontComboBox = FlatComboBox() private val shellComboBox = FlatComboBox() private val maxRowsTextField = IntSpinner(0, 0) @@ -390,6 +391,12 @@ class SettingsOptionsPane : OptionsPane() { } } + cursorBlinkComboBox.addItemListener { e -> + if (e.stateChange == ItemEvent.SELECTED) { + terminalSetting.cursorBlink = cursorBlinkComboBox.selectedItem as Boolean + } + } + shellComboBox.addItemListener { if (it.stateChange == ItemEvent.SELECTED) { @@ -478,6 +485,7 @@ class SettingsOptionsPane : OptionsPane() { fontComboBox.selectedItem = terminalSetting.font debugComboBox.selectedItem = terminalSetting.debug beepComboBox.selectedItem = terminalSetting.beep + cursorBlinkComboBox.selectedItem = terminalSetting.cursorBlink cursorStyleComboBox.selectedItem = terminalSetting.cursor selectCopyComboBox.selectedItem = terminalSetting.selectCopy autoCloseTabComboBox.selectedItem = terminalSetting.autoCloseTabWhenDisconnected @@ -499,7 +507,7 @@ class SettingsOptionsPane : OptionsPane() { private fun getCenterComponent(): JComponent { val layout = FormLayout( "left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow", - "pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref" + "pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref" ) val beepBtn = JButton(Icons.run) @@ -526,6 +534,8 @@ class SettingsOptionsPane : OptionsPane() { .add(selectCopyComboBox).xy(3, rows).apply { rows += step } .add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows) .add(cursorStyleComboBox).xy(3, rows).apply { rows += step } + .add("${I18n.getString("termora.settings.terminal.cursor-blink")}:").xy(1, rows) + .add(cursorBlinkComboBox).xy(3, rows).apply { rows += step } .add("${I18n.getString("termora.settings.terminal.floating-toolbar")}:").xy(1, rows) .add(floatingToolbarComboBox).xy(3, rows).apply { rows += step } .add("${I18n.getString("termora.settings.terminal.auto-close-tab")}:").xy(1, rows) diff --git a/src/main/kotlin/app/termora/TerminalPanelFactory.kt b/src/main/kotlin/app/termora/TerminalPanelFactory.kt index 50a8862..1031f34 100644 --- a/src/main/kotlin/app/termora/TerminalPanelFactory.kt +++ b/src/main/kotlin/app/termora/TerminalPanelFactory.kt @@ -1,50 +1,67 @@ package app.termora import app.termora.highlight.KeywordHighlightPaintListener +import app.termora.terminal.DataKey import app.termora.terminal.PtyConnector import app.termora.terminal.Terminal import app.termora.terminal.panel.TerminalHyperlinkPaintListener import app.termora.terminal.panel.TerminalPanel +import kotlinx.coroutines.* import java.awt.event.ComponentEvent import java.awt.event.ComponentListener import javax.swing.SwingUtilities +import kotlin.time.Duration.Companion.milliseconds -class TerminalPanelFactory { +class TerminalPanelFactory : Disposable { private val terminalPanels = mutableListOf() companion object { + + private val Factory = DataKey(TerminalPanelFactory::class) + fun getInstance(scope: Scope): TerminalPanelFactory { return scope.getOrCreate(TerminalPanelFactory::class) { TerminalPanelFactory() } } - fun getAllTerminalPanel(): List { + fun getAllTerminalPanel(): Array { return ApplicationScope.forApplicationScope().windowScopes() .map { getInstance(it) } - .flatMap { it.getTerminalPanels() } + .flatMap { it.terminalPanels }.toTypedArray() } } + init { + // repaint + Painter.getInstance() + } + + fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel { val terminalPanel = TerminalPanel(terminal, ptyConnector) terminalPanel.addTerminalPaintListener(MultipleTerminalListener()) terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance()) terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance()) + terminal.getTerminalModel().setData(Factory, this) + Disposer.register(terminalPanel, object : Disposable { override fun dispose() { - terminalPanels.remove(terminalPanel) + if (terminal.getTerminalModel().hasData(Factory)) { + terminal.getTerminalModel().getData(Factory).removeTerminalPanel(terminalPanel) + } } }) - terminalPanels.add(terminalPanel) + + addTerminalPanel(terminalPanel) return terminalPanel } - fun getTerminalPanels(): List { - return terminalPanels + fun getTerminalPanels(): Array { + return terminalPanels.toTypedArray() } fun repaintAll() { if (SwingUtilities.isEventDispatchThread()) { - terminalPanels.forEach { it.repaintImmediate() } + getTerminalPanels().forEach { it.repaintImmediate() } } else { SwingUtilities.invokeLater { repaintAll() } } @@ -62,4 +79,35 @@ class TerminalPanelFactory { terminalPanels.remove(terminalPanel) } + fun addTerminalPanel(terminalPanel: TerminalPanel) { + terminalPanels.add(terminalPanel) + terminalPanel.terminal.getTerminalModel().setData(Factory, this) + } + + private class Painter : Disposable { + companion object { + fun getInstance(): Painter { + return ApplicationScope.forApplicationScope().getOrCreate(Painter::class) { Painter() } + } + } + + private val coroutineScope = CoroutineScope(Dispatchers.IO) + + init { + coroutineScope.launch { + while (coroutineScope.isActive) { + delay(500.milliseconds) + SwingUtilities.invokeLater { + ApplicationScope.forApplicationScope().windowScopes() + .map { getInstance(it) }.forEach { it.repaintAll() } + } + } + } + } + + override fun dispose() { + coroutineScope.cancel() + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TermoraFrameManager.kt b/src/main/kotlin/app/termora/TermoraFrameManager.kt index e6bb11a..4e7742b 100644 --- a/src/main/kotlin/app/termora/TermoraFrameManager.kt +++ b/src/main/kotlin/app/termora/TermoraFrameManager.kt @@ -45,7 +45,9 @@ class TermoraFrameManager { frames.remove(window) // dispose windowScope - Disposer.dispose(ApplicationScope.forWindowScope(e.window)) + val windowScope = ApplicationScope.forWindowScope(e.window) + Disposer.disposeChildren(windowScope, null) + Disposer.dispose(windowScope) val windowScopes = ApplicationScope.windowScopes() diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalBlink.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalBlink.kt new file mode 100644 index 0000000..6c2e1a7 --- /dev/null +++ b/src/main/kotlin/app/termora/terminal/panel/TerminalBlink.kt @@ -0,0 +1,119 @@ +package app.termora.terminal.panel + +import app.termora.ApplicationScope +import app.termora.Database +import app.termora.Disposable +import app.termora.terminal.* +import kotlinx.coroutines.* +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.time.Duration.Companion.milliseconds + +class TerminalBlink(terminal: Terminal) : Disposable { + + + private var cursorBlinkJob: Job? = null + private val terminalSettings get() = Database.getDatabase().terminal + private val isDisposed = AtomicBoolean(false) + private val globalBlink get() = GlobalBlink.getInstance() + private val coroutineScope get() = globalBlink.coroutineScope + + /** + * 返回 true 表示可以显示某些内容 [TextStyle.blink] + */ + val blink get() = globalBlink.blink + + /** + * 这个与 [blink] 不同的是它是控制光标的 + */ + @Volatile + var cursorBlink = true + private set + + init { + + reset() + + // 如果有写入,那么显示光标 N 秒 + terminal.getTerminalModel().addDataListener(object : DataListener { + override fun onChanged(key: DataKey<*>, data: Any) { + // 写入后,重置光标 + if (key == VisualTerminal.Written) { + reset() + } else if (key == TerminalPanel.Focused) { + // 获取焦点的一瞬间则立即重置 + if (data == true) { + reset() + } + } + } + }) + } + + + private fun reset() { + if (isDisposed.get()) { + return + } + + cursorBlink = true + cursorBlinkJob?.cancel() + cursorBlinkJob = coroutineScope.launch { + while (coroutineScope.isActive) { + + delay(500.milliseconds) + + if (isDisposed.get()) { + break + } + + // 如果开启了光标闪烁才闪速 + cursorBlink = if (terminalSettings.cursorBlink) { + !cursorBlink + } else { + true + } + + } + } + } + + override fun dispose() { + if (isDisposed.compareAndSet(false, true)) { + cursorBlinkJob?.cancel() + } + } + + + private class GlobalBlink : Disposable { + + companion object { + fun getInstance(): GlobalBlink { + return ApplicationScope.forApplicationScope() + .getOrCreate(GlobalBlink::class) { GlobalBlink() } + } + } + + val coroutineScope by lazy { CoroutineScope(Dispatchers.IO) } + + /** + * 返回 true 表示可以显示某些内容 [TextStyle.blink] + */ + @Volatile + var blink = true + private set + + + init { + coroutineScope.launch { + while (coroutineScope.isActive) { + delay(500) + blink = !blink + } + } + } + + override fun dispose() { + coroutineScope.cancel() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalDisplay.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalDisplay.kt index 535cbdc..33a315d 100644 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalDisplay.kt +++ b/src/main/kotlin/app/termora/terminal/panel/TerminalDisplay.kt @@ -17,6 +17,7 @@ import kotlin.time.Duration class TerminalDisplay( private val terminalPanel: TerminalPanel, private val terminal: Terminal, + private val terminalBlink: TerminalBlink ) : JComponent() { companion object { @@ -136,12 +137,13 @@ class TerminalDisplay( val lineHeight = getLineHeight() val style = if (inputMethodData.isNoTyping) terminal.getTerminalModel().getData(DataKey.CursorStyle) else CursorStyle.Bar + val hasFocus = terminal.getTerminalModel().getData(TerminalPanel.Focused, false) // background g.color = Color(colorPalette.getColor(TerminalColor.Cursor.BACKGROUND)) if (style == CursorStyle.Block) { - if (terminalPanel.hasFocus()) { + if (hasFocus) { g.fillRect(xOffset, (y - 1) * lineHeight, width, lineHeight) } else { g.drawRect(xOffset, (y - 1) * lineHeight, width, lineHeight) @@ -217,19 +219,23 @@ class TerminalDisplay( } private fun drawCharacters(g: Graphics2D) { - val reverseVideo = terminal.getTerminalModel().getData(DataKey.ReverseVideo, false) - val rows = terminal.getTerminalModel().getRows() - val cols = terminal.getTerminalModel().getCols() + val terminalModel = terminal.getTerminalModel() + val reverseVideo = terminalModel.getData(DataKey.ReverseVideo, false) + val rows = terminalModel.getRows() + val cols = terminalModel.getCols() val triple = Triple(Char.Space.toString(), TextStyle.Default, 1) val cursorPosition = terminal.getCursorModel().getPosition() val averageCharWidth = getAverageCharWidth() val maxVerticalScrollOffset = terminal.getScrollingModel().getMaxVerticalScrollOffset() val verticalScrollOffset = terminal.getScrollingModel().getVerticalScrollOffset() val selectionModel = terminal.getSelectionModel() - val cursorStyle = terminal.getTerminalModel().getData(DataKey.CursorStyle) - val showCursor = terminal.getTerminalModel().getData(DataKey.ShowCursor) + val cursorStyle = terminalModel.getData(DataKey.CursorStyle) + val showCursor = terminalModel.getData(DataKey.ShowCursor) val markupModel = terminal.getMarkupModel() val lineHeight = getLineHeight() + val blink = terminalBlink.blink + val cursorBlink = terminalBlink.cursorBlink + val hasFocus = terminalModel.getData(TerminalPanel.Focused, false) for (i in 1..rows) { @@ -269,6 +275,13 @@ class TerminalDisplay( background = colorPalette.getColor(TerminalColor.Basic.SELECTION_BACKGROUND) } + // 如果启用了闪烁 + if (textStyle.blink) { + if (!blink) { + continue + } + } + // 设置字体 g.font = getDisplayFont(text, textStyle) val charWidth = min( @@ -310,12 +323,15 @@ class TerminalDisplay( // 渲染光标 if (caret) { - drawCursor(g, i, xOffset, charWidth) - // 如果是获取焦点状态,那么颜色互换 - if (terminalPanel.hasFocus() && cursorStyle == CursorStyle.Block && inputMethodData.isNoTyping) { - g.color = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND)) - } else { - g.color = Color(foreground) + // 这几种情况光标才会渲染:输入中、闪烁中、没有焦点 + if (inputMethodData.isTyping || cursorBlink || !hasFocus) { + drawCursor(g, i, xOffset, charWidth) + // 如果是获取焦点状态,那么颜色互换 + if (hasFocus && cursorStyle == CursorStyle.Block && inputMethodData.isNoTyping) { + g.color = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND)) + } else { + g.color = Color(foreground) + } } } diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalPanel.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalPanel.kt index eaede2b..6bd93d5 100644 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalPanel.kt +++ b/src/main/kotlin/app/termora/terminal/panel/TerminalPanel.kt @@ -37,12 +37,14 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect companion object { val Debug = DataKey(Boolean::class) val Finding = DataKey(Boolean::class) + val Focused = DataKey(Boolean::class) val SelectCopy = DataKey(Boolean::class) } + private val terminalBlink = TerminalBlink(terminal) private val terminalFindPanel = TerminalFindPanel(this, terminal) private val floatingToolbar = FloatingToolbarPanel() - private val terminalDisplay = TerminalDisplay(this, terminal) + private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink) private val dataProviderSupport = DataProviderSupport() val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal) @@ -140,10 +142,12 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect this.addFocusListener(object : FocusAdapter() { override fun focusLost(e: FocusEvent) { + terminal.getTerminalModel().setData(Focused, false) repaintImmediate() } override fun focusGained(e: FocusEvent) { + terminal.getTerminalModel().setData(Focused, true) repaintImmediate() } }) @@ -386,6 +390,7 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect } override fun dispose() { + Disposer.dispose(terminalBlink) Disposer.dispose(floatingToolbar) } diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 68c0562..4dd3b24 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -69,6 +69,7 @@ termora.settings.terminal.debug=Debug mode termora.settings.terminal.beep=Beep termora.settings.terminal.select-copy=Select copy termora.settings.terminal.cursor-style=Cursor type +termora.settings.terminal.cursor-blink=Cursor blink termora.settings.terminal.local-shell=Local shell termora.settings.terminal.floating-toolbar=Floating Toolbar termora.settings.terminal.auto-close-tab=Auto Close Tab @@ -123,8 +124,6 @@ termora.find-everywhere.groups.opened-hosts=Opened hosts termora.find-everywhere.groups.tools=Tools termora.find-everywhere.groups.settings=${termora.setting} termora.find-everywhere.quick-command.local-terminal=Local Terminal -termora.find-everywhere.double-shift-deprecated=The double-click Shift shortcut will be removed in a future version -termora.find-everywhere.double-shift-deprecated-instead=${termora.find-everywhere.double-shift-deprecated}, use {0} instead # Welcome termora.welcome.my-hosts=My hosts diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 8744a4d..4708444 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -65,8 +65,6 @@ termora.find-everywhere.groups.opened-hosts=已打开的主机 termora.find-everywhere.groups.tools=工具 termora.find-everywhere.groups.settings=${termora.setting} termora.find-everywhere.quick-command.local-terminal=本地终端 -termora.find-everywhere.double-shift-deprecated=双击 Shift 快捷键将会在未来的版本中移除 -termora.find-everywhere.double-shift-deprecated-instead=${termora.find-everywhere.double-shift-deprecated},请使用 {0} 代替 termora.settings.terminal=终端 termora.settings.terminal.font=字体 @@ -76,6 +74,7 @@ termora.settings.terminal.debug=调试模式 termora.settings.terminal.beep=蜂鸣声 termora.settings.terminal.select-copy=选中复制 termora.settings.terminal.cursor-style=光标样式 +termora.settings.terminal.cursor-blink=光标闪烁 termora.settings.terminal.local-shell=本地终端 termora.settings.terminal.floating-toolbar=悬浮工具栏 termora.settings.terminal.auto-close-tab=自动关闭标签 diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index 18c4bc8..64e1f52 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -74,8 +74,6 @@ termora.find-everywhere.groups.opened-hosts=已開啟的主機 termora.find-everywhere.groups.tools=工具 termora.find-everywhere.groups.settings=${termora.setting} termora.find-everywhere.quick-command.local-terminal=本地端 -termora.find-everywhere.double-shift-deprecated=雙擊 Shift 快捷鍵將會在未來的版本中移除 -termora.find-everywhere.double-shift-deprecated-instead=${termora.find-everywhere.double-shift-deprecated},請使用 {0} 代替 termora.settings.terminal=終端 termora.settings.terminal.font=字體 @@ -85,6 +83,7 @@ termora.settings.terminal.debug=偵錯模式 termora.settings.terminal.beep=蜂鳴聲 termora.settings.terminal.select-copy=選取複製 termora.settings.terminal.cursor-style=遊標風格 +termora.settings.terminal.cursor-blink=遊標閃爍 termora.settings.terminal.local-shell=本地端 termora.settings.terminal.floating-toolbar=懸浮工具列 termora.settings.terminal.auto-close-tab=自動關閉標籤