feat: support keymap sync

This commit is contained in:
hstyi
2025-01-15 20:03:57 +08:00
committed by hstyi
parent d321e766b1
commit e30316eab3
6 changed files with 105 additions and 35 deletions

View File

@@ -2,7 +2,6 @@ package app.termora
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.highlight.KeywordHighlight import app.termora.highlight.KeywordHighlight
import app.termora.keymap.KeyShortcut
import app.termora.keymap.Keymap import app.termora.keymap.Keymap
import app.termora.keymgr.OhKeyPair import app.termora.keymgr.OhKeyPair
import app.termora.macro.Macro import app.termora.macro.Macro
@@ -14,12 +13,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.*
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.File import java.io.File
import java.util.* import java.util.*
import javax.swing.KeyStroke
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.collections.set import kotlin.collections.set
@@ -65,36 +62,17 @@ class Database private constructor(private val env: Environment) : Disposable {
fun getKeymaps(): Collection<Keymap> { fun getKeymaps(): Collection<Keymap> {
val array = env.computeInTransaction { tx -> val array = env.computeInTransaction { tx ->
openCursor<JsonObject>(tx, KEYMAP_STORE) { _, value -> openCursor<String>(tx, KEYMAP_STORE) { _, value ->
ohMyJson.decodeFromString<JsonObject>(value) value
}.values }.values
} }
val shortcuts = mutableListOf<Keymap>() val keymaps = mutableListOf<Keymap>()
for (json in array.iterator()) { for (text in array.iterator()) {
val name = json["name"]?.jsonPrimitive?.content ?: continue keymaps.add(Keymap.fromJSON(text) ?: continue)
val readonly = json["readonly"]?.jsonPrimitive?.booleanOrNull ?: false
val keymap = Keymap(name, null, readonly)
for (shortcut in (json["shortcuts"]?.jsonArray ?: emptyList()).map { it.jsonObject }) {
val keyStroke = shortcut["keyStroke"]?.jsonPrimitive?.contentOrNull ?: continue
val keyboard = shortcut["keyboard"]?.jsonPrimitive?.booleanOrNull ?: true
val actionIds = ohMyJson.decodeFromJsonElement<List<String>>(
shortcut["actionIds"]?.jsonArray
?: continue
)
if (keyboard) {
val keyShortcut = KeyShortcut(KeyStroke.getKeyStroke(keyStroke))
for (actionId in actionIds) {
keymap.addShortcut(actionId, keyShortcut)
}
}
} }
shortcuts.add(keymap) return keymaps
}
return shortcuts
} }
fun addKeymap(keymap: Keymap) { fun addKeymap(keymap: Keymap) {
@@ -599,6 +577,7 @@ class Database private constructor(private val env: Environment) : Disposable {
var rangeKeyPairs by BooleanPropertyDelegate(true) var rangeKeyPairs by BooleanPropertyDelegate(true)
var rangeKeywordHighlights by BooleanPropertyDelegate(true) var rangeKeywordHighlights by BooleanPropertyDelegate(true)
var rangeMacros by BooleanPropertyDelegate(true) var rangeMacros by BooleanPropertyDelegate(true)
var rangeKeymap by BooleanPropertyDelegate(true)
/** /**
* Token * Token

View File

@@ -402,6 +402,7 @@ class SettingsOptionsPane : OptionsPane() {
val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys")) val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys"))
val keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights")) val keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights"))
val macrosCheckBox = JCheckBox(I18n.getString("termora.macro")) val macrosCheckBox = JCheckBox(I18n.getString("termora.macro"))
val keymapCheckBox = JCheckBox(I18n.getString("termora.settings.keymap"))
val visitGistBtn = JButton(Icons.externalLink) val visitGistBtn = JButton(Icons.externalLink)
val getTokenBtn = JButton(Icons.externalLink) val getTokenBtn = JButton(Icons.externalLink)
@@ -593,6 +594,9 @@ class SettingsOptionsPane : OptionsPane() {
if (macrosCheckBox.isSelected) { if (macrosCheckBox.isSelected) {
range.add(SyncRange.Macros) range.add(SyncRange.Macros)
} }
if (keymapCheckBox.isSelected) {
range.add(SyncRange.Keymap)
}
return SyncConfig( return SyncConfig(
type = typeComboBox.selectedItem as SyncType, type = typeComboBox.selectedItem as SyncType,
token = String(tokenTextField.password), token = String(tokenTextField.password),
@@ -664,6 +668,7 @@ class SettingsOptionsPane : OptionsPane() {
tokenTextField.isEnabled = false tokenTextField.isEnabled = false
keysCheckBox.isEnabled = false keysCheckBox.isEnabled = false
macrosCheckBox.isEnabled = false macrosCheckBox.isEnabled = false
keymapCheckBox.isEnabled = false
keywordHighlightsCheckBox.isEnabled = false keywordHighlightsCheckBox.isEnabled = false
hostsCheckBox.isEnabled = false hostsCheckBox.isEnabled = false
domainTextField.isEnabled = false domainTextField.isEnabled = false
@@ -696,6 +701,7 @@ class SettingsOptionsPane : OptionsPane() {
hostsCheckBox.isEnabled = true hostsCheckBox.isEnabled = true
typeComboBox.isEnabled = true typeComboBox.isEnabled = true
macrosCheckBox.isEnabled = true macrosCheckBox.isEnabled = true
keymapCheckBox.isEnabled = true
gistTextField.isEnabled = true gistTextField.isEnabled = true
tokenTextField.isEnabled = true tokenTextField.isEnabled = true
domainTextField.isEnabled = true domainTextField.isEnabled = true
@@ -756,11 +762,13 @@ class SettingsOptionsPane : OptionsPane() {
keysCheckBox.isFocusable = false keysCheckBox.isFocusable = false
keywordHighlightsCheckBox.isFocusable = false keywordHighlightsCheckBox.isFocusable = false
macrosCheckBox.isFocusable = false macrosCheckBox.isFocusable = false
keymapCheckBox.isFocusable = false
hostsCheckBox.isSelected = sync.rangeHosts hostsCheckBox.isSelected = sync.rangeHosts
keysCheckBox.isSelected = sync.rangeKeyPairs keysCheckBox.isSelected = sync.rangeKeyPairs
keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights
macrosCheckBox.isSelected = sync.rangeMacros macrosCheckBox.isSelected = sync.rangeMacros
keymapCheckBox.isSelected = sync.rangeKeymap
typeComboBox.selectedItem = sync.type typeComboBox.selectedItem = sync.type
gistTextField.text = sync.gist gistTextField.text = sync.gist
@@ -823,6 +831,7 @@ class SettingsOptionsPane : OptionsPane() {
.add(keysCheckBox).xy(3, 1) .add(keysCheckBox).xy(3, 1)
.add(keywordHighlightsCheckBox).xy(5, 1) .add(keywordHighlightsCheckBox).xy(5, 1)
.add(macrosCheckBox).xy(1, 3) .add(macrosCheckBox).xy(1, 3)
.add(keymapCheckBox).xy(3, 3)
.build() .build()
var rows = 1 var rows = 1

View File

@@ -1,5 +1,6 @@
package app.termora.actions package app.termora.actions
import app.termora.ApplicationScope
import app.termora.I18n import app.termora.I18n
import app.termora.Icons import app.termora.Icons
import app.termora.SettingsDialog import app.termora.SettingsDialog
@@ -27,7 +28,8 @@ class SettingsAction : AnAction(
init { init {
FlatDesktop.setPreferencesHandler { FlatDesktop.setPreferencesHandler {
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
if (owner != null) { // Doorman 的情况下不允许打开
if (owner != null && ApplicationScope.windowScopes().isNotEmpty()) {
actionPerformed(ActionEvent(owner, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY)) actionPerformed(ActionEvent(owner, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
} }
} }

View File

@@ -2,10 +2,8 @@ package app.termora.keymap
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.*
import kotlinx.serialization.json.buildJsonObject import javax.swing.KeyStroke
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.put
open class Keymap( open class Keymap(
val name: String, val name: String,
@@ -16,6 +14,37 @@ open class Keymap(
val isReadonly: Boolean = false, val isReadonly: Boolean = false,
) { ) {
companion object {
fun fromJSON(text: String): Keymap? {
return fromJSON(ohMyJson.decodeFromString<JsonObject>(text))
}
fun fromJSON(json: JsonObject): Keymap? {
val shortcuts = mutableListOf<Keymap>()
val name = json["name"]?.jsonPrimitive?.content ?: return null
val readonly = json["readonly"]?.jsonPrimitive?.booleanOrNull ?: return null
val keymap = Keymap(name, null, readonly)
for (shortcut in (json["shortcuts"]?.jsonArray ?: emptyList()).map { it.jsonObject }) {
val keyStroke = shortcut["keyStroke"]?.jsonPrimitive?.contentOrNull ?: continue
val keyboard = shortcut["keyboard"]?.jsonPrimitive?.booleanOrNull ?: true
val actionIds = ohMyJson.decodeFromJsonElement<List<String>>(
shortcut["actionIds"]?.jsonArray
?: continue
)
if (keyboard) {
val keyShortcut = KeyShortcut(KeyStroke.getKeyStroke(keyStroke))
for (actionId in actionIds) {
keymap.addShortcut(actionId, keyShortcut)
}
}
}
shortcuts.add(keymap)
return keymap
}
}
private val shortcuts = mutableMapOf<Shortcut, MutableList<String>>() private val shortcuts = mutableMapOf<Shortcut, MutableList<String>>()
open fun addShortcut(actionId: String, shortcut: Shortcut) { open fun addShortcut(actionId: String, shortcut: Shortcut) {
@@ -66,7 +95,11 @@ open class Keymap(
fun toJSON(): String { fun toJSON(): String {
return ohMyJson.encodeToString(buildJsonObject { return ohMyJson.encodeToString(toJSONObject())
}
fun toJSONObject(): JsonObject {
return buildJsonObject {
put("name", name) put("name", name)
put("readonly", isReadonly) put("readonly", isReadonly)
parent?.let { put("parent", it.name) } parent?.let { put("parent", it.name) }
@@ -81,7 +114,7 @@ open class Keymap(
}) })
} }
}) })
}) }
} }
} }

View File

@@ -8,11 +8,14 @@ import app.termora.AES.encodeBase64String
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.highlight.KeywordHighlight import app.termora.highlight.KeywordHighlight
import app.termora.highlight.KeywordHighlightManager import app.termora.highlight.KeywordHighlightManager
import app.termora.keymap.Keymap
import app.termora.keymap.KeymapManager
import app.termora.keymgr.KeyManager import app.termora.keymgr.KeyManager
import app.termora.keymgr.OhKeyPair import app.termora.keymgr.OhKeyPair
import app.termora.macro.Macro import app.termora.macro.Macro
import app.termora.macro.MacroManager import app.termora.macro.MacroManager
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Request import okhttp3.Request
@@ -32,6 +35,7 @@ abstract class GitSyncer : Syncer {
protected val keyManager get() = KeyManager.getInstance() protected val keyManager get() = KeyManager.getInstance()
protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance() protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
protected val macroManager get() = MacroManager.getInstance() protected val macroManager get() = MacroManager.getInstance()
protected val keymapManager get() = KeymapManager.getInstance()
override fun pull(config: SyncConfig): GistResponse { override fun pull(config: SyncConfig): GistResponse {
@@ -74,6 +78,13 @@ abstract class GitSyncer : Syncer {
} }
} }
// decode keymaps
if (config.ranges.contains(SyncRange.Macros)) {
gistResponse.gists.findLast { it.filename == "Keymaps" }?.let {
decodeKeymaps(it.content, config)
}
}
if (log.isInfoEnabled) { if (log.isInfoEnabled) {
log.info("Type: ${config.type} , Gist: ${config.gistId} Pulled") log.info("Type: ${config.type} , Gist: ${config.gistId} Pulled")
} }
@@ -231,6 +242,17 @@ abstract class GitSyncer : Syncer {
} }
} }
private fun decodeKeymaps(text: String, config: SyncConfig) {
for (keymap in ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }) {
keymapManager.addKeymap(keymap)
}
if (log.isDebugEnabled) {
log.debug("Decode Keymaps: {}", text)
}
}
private fun getKey(config: SyncConfig): ByteArray { private fun getKey(config: SyncConfig): ByteArray {
return ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16) return ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16)
} }
@@ -244,6 +266,7 @@ abstract class GitSyncer : Syncer {
// aes key // aes key
val key = ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16) val key = ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16)
// Hosts
if (config.ranges.contains(SyncRange.Hosts)) { if (config.ranges.contains(SyncRange.Hosts)) {
val encryptedHosts = mutableListOf<EncryptedHost>() val encryptedHosts = mutableListOf<EncryptedHost>()
for (host in hostManager.hosts()) { for (host in hostManager.hosts()) {
@@ -282,6 +305,7 @@ abstract class GitSyncer : Syncer {
} }
// KeyPairs
if (config.ranges.contains(SyncRange.KeyPairs)) { if (config.ranges.contains(SyncRange.KeyPairs)) {
val encryptedKeys = mutableListOf<OhKeyPair>() val encryptedKeys = mutableListOf<OhKeyPair>()
for (keyPair in keyManager.getOhKeyPairs()) { for (keyPair in keyManager.getOhKeyPairs()) {
@@ -306,6 +330,7 @@ abstract class GitSyncer : Syncer {
gistFiles.add(GistFile("KeyPairs", keysContent)) gistFiles.add(GistFile("KeyPairs", keysContent))
} }
// Highlights
if (config.ranges.contains(SyncRange.KeywordHighlights)) { if (config.ranges.contains(SyncRange.KeywordHighlights)) {
val keywordHighlights = mutableListOf<KeywordHighlight>() val keywordHighlights = mutableListOf<KeywordHighlight>()
for (keywordHighlight in keywordHighlightManager.getKeywordHighlights()) { for (keywordHighlight in keywordHighlightManager.getKeywordHighlights()) {
@@ -324,6 +349,7 @@ abstract class GitSyncer : Syncer {
gistFiles.add(GistFile("KeywordHighlights", keywordHighlightsContent)) gistFiles.add(GistFile("KeywordHighlights", keywordHighlightsContent))
} }
// Macros
if (config.ranges.contains(SyncRange.Macros)) { if (config.ranges.contains(SyncRange.Macros)) {
val macros = mutableListOf<Macro>() val macros = mutableListOf<Macro>()
for (macro in macroManager.getMacros()) { for (macro in macroManager.getMacros()) {
@@ -342,6 +368,26 @@ abstract class GitSyncer : Syncer {
gistFiles.add(GistFile("Macros", macrosContent)) gistFiles.add(GistFile("Macros", macrosContent))
} }
// Keymap
if (config.ranges.contains(SyncRange.Keymap)) {
val keymaps = mutableListOf<JsonObject>()
for (keymap in keymapManager.getKeymaps()) {
// 只读的是内置的
if (keymap.isReadonly) {
continue
}
keymaps.add(keymap.toJSONObject())
}
if (keymaps.isNotEmpty()) {
val keymapsContent = ohMyJson.encodeToString(keymaps)
if (log.isDebugEnabled) {
log.debug("Push keymaps: {}", keymapsContent)
}
gistFiles.add(GistFile("Keymaps", keymapsContent))
}
}
if (gistFiles.isEmpty()) { if (gistFiles.isEmpty()) {
throw IllegalArgumentException("No gist files found") throw IllegalArgumentException("No gist files found")
} }

View File

@@ -11,6 +11,7 @@ enum class SyncRange {
KeyPairs, KeyPairs,
KeywordHighlights, KeywordHighlights,
Macros, Macros,
Keymap,
} }
data class SyncConfig( data class SyncConfig(