QB menu keys: vim, nano, tmux, screen combo popups
Add menu key system to Quick Bar — tap a menu key to show a popup strip with common combos. Each item fires the right byte sequence on tap. - MenuItem model + menuItems on KeyDefinition, parsed from JSON - LongPressPopupView: menu mode with auto-sized cells, gaps, borders - QuickBarView: onMenuKeyTap callback routes menu keys - TerminalKeyboard: full-screen overlay captures tap on popup item - New "menu" style in all 4 keyboard themes (purple accent) - vim: :w, :q, :wq, :q!, :%s/, ciw, >>, <<, redo - nano: save, exit, find, repl, cut, paste, help, goto - tmux: new, nxt, prv, det, sp│, sp─, scr, zoom, kill - screen: new, nxt, prv, det, sp─, sp│, cpy, kill, list Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
678295b03b
commit
8f957ae0e7
7 changed files with 305 additions and 12 deletions
|
|
@ -68,6 +68,11 @@ class TerminalKeyboard private constructor(
|
|||
private var modLongPressRunnable: Runnable? = null
|
||||
private var modStateJob: Job? = null
|
||||
|
||||
// Menu popup state
|
||||
private var menuPopupActive = false
|
||||
private var menuPopupItems: List<MenuItem>? = null
|
||||
private var menuOverlay: View? = null
|
||||
|
||||
// Double-tap state (shared between keyboard pages and quick bar)
|
||||
private var lastTapKeyId: String? = null
|
||||
private var lastTapTime: Long = 0
|
||||
|
|
@ -187,6 +192,7 @@ class TerminalKeyboard private constructor(
|
|||
recordTap(key)
|
||||
}
|
||||
qb.onSnippetsTap = { onSnippetsTap?.invoke() }
|
||||
qb.onMenuKeyTap = { key -> showMenuPopup(key, qb) }
|
||||
qb.onKeyDown = { key ->
|
||||
hapticFeedback(qb)
|
||||
if (layout.settings.showHints) {
|
||||
|
|
@ -236,6 +242,7 @@ class TerminalKeyboard private constructor(
|
|||
fun detach() {
|
||||
modStateJob?.cancel()
|
||||
keyRepeatHandler.destroy()
|
||||
dismissMenuPopup()
|
||||
keyboardView?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
||||
quickBarView?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
||||
hintPopup?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
||||
|
|
@ -454,6 +461,92 @@ class TerminalKeyboard private constructor(
|
|||
keyboardView?.setSwipeEnabled(true) // re-enable page swiping
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun showMenuPopup(key: KeyDefinition, qbView: QuickBarView) {
|
||||
val items = key.menuItems ?: return
|
||||
val popup = longPressPopup ?: return
|
||||
val rootContainer = container ?: return
|
||||
|
||||
hapticFeedback(qbView)
|
||||
menuPopupActive = true
|
||||
menuPopupItems = items
|
||||
|
||||
popup.theme = theme
|
||||
popup.showMenu(items.map { it.label })
|
||||
popup.visibility = View.VISIBLE
|
||||
|
||||
// Position above the key
|
||||
popup.post {
|
||||
val keyBounds = key.bounds
|
||||
val qbLocation = IntArray(2)
|
||||
qbView.getLocationInWindow(qbLocation)
|
||||
val containerLocation = IntArray(2)
|
||||
(popup.parent as? View)?.getLocationInWindow(containerLocation)
|
||||
|
||||
val keyScreenX = qbLocation[0] + keyBounds.centerX()
|
||||
val keyScreenY = qbLocation[1] + keyBounds.top
|
||||
val containerWidth = (popup.parent as? View)?.width ?: popup.width
|
||||
|
||||
popup.x = (keyScreenX - containerLocation[0] - popup.width / 2f)
|
||||
.coerceIn(0f, (containerWidth - popup.width).toFloat().coerceAtLeast(0f))
|
||||
popup.y = keyScreenY - containerLocation[1] - popup.height - 8f
|
||||
}
|
||||
|
||||
// Transparent overlay on the root DecorView to capture taps anywhere on screen
|
||||
val activity = context as? android.app.Activity
|
||||
val rootView = activity?.window?.decorView as? ViewGroup ?: rootContainer
|
||||
val overlay = View(context)
|
||||
overlay.setOnTouchListener { _, event ->
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
|
||||
val popupLoc = IntArray(2)
|
||||
popup.getLocationOnScreen(popupLoc)
|
||||
val localX = event.rawX - popupLoc[0]
|
||||
val localY = event.rawY - popupLoc[1]
|
||||
if (localX >= 0 && localX <= popup.width && localY >= 0 && localY <= popup.height) {
|
||||
val idx = popup.hitTestVariant(localX)
|
||||
if (idx >= 0) popup.selectedIndex = idx
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
val popupLoc = IntArray(2)
|
||||
popup.getLocationOnScreen(popupLoc)
|
||||
val localX = event.rawX - popupLoc[0]
|
||||
val localY = event.rawY - popupLoc[1]
|
||||
if (localX >= 0 && localX <= popup.width && localY >= 0 && localY <= popup.height) {
|
||||
val idx = popup.hitTestVariant(localX)
|
||||
if (idx >= 0 && idx < items.size) {
|
||||
hapticFeedback(popup)
|
||||
val menuItem = items[idx]
|
||||
val fakeKey = KeyDefinition(
|
||||
id = "menu_${idx}",
|
||||
label = menuItem.label,
|
||||
action = menuItem.action
|
||||
)
|
||||
handleKeyAction(fakeKey)
|
||||
}
|
||||
}
|
||||
dismissMenuPopup()
|
||||
return@setOnTouchListener true
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
rootView.addView(overlay, ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
))
|
||||
menuOverlay = overlay
|
||||
}
|
||||
|
||||
private fun dismissMenuPopup() {
|
||||
longPressPopup?.visibility = View.GONE
|
||||
menuPopupActive = false
|
||||
menuPopupItems = null
|
||||
menuOverlay?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
||||
menuOverlay = null
|
||||
}
|
||||
|
||||
private fun showHintPopup(key: KeyDefinition, pageView: KeyboardPageView) {
|
||||
val hint = hintPopup ?: return
|
||||
hint.theme = theme
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@ package com.roundingmobile.keyboard.model
|
|||
|
||||
import android.graphics.RectF
|
||||
|
||||
/** A labeled action shown in a menu popup (vim/tmux/screen combo keys). */
|
||||
data class MenuItem(
|
||||
val label: String,
|
||||
val action: KeyAction
|
||||
)
|
||||
|
||||
data class KeyDefinition(
|
||||
val id: String,
|
||||
val label: String,
|
||||
|
|
@ -13,6 +19,7 @@ data class KeyDefinition(
|
|||
val style: String? = null,
|
||||
val repeatable: Boolean = false,
|
||||
val longPress: List<String>? = null,
|
||||
val menuItems: List<MenuItem>? = null,
|
||||
val visible: Boolean = true
|
||||
) {
|
||||
/** Computed pixel bounds after layout pass — used for hit testing and drawing */
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ object LayoutParser {
|
|||
style = obj.optString("style", null),
|
||||
repeatable = obj.optBoolean("repeatable", false),
|
||||
longPress = parseStringList(obj.optJSONArray("longPress")),
|
||||
menuItems = parseMenuItems(obj.optJSONArray("menuItems")),
|
||||
visible = obj.optBoolean("visible", true)
|
||||
)
|
||||
}
|
||||
|
|
@ -159,6 +160,18 @@ object LayoutParser {
|
|||
fullKeyboardScrollStyle = obj.optString("fullKeyboardScrollStyle", "viewpager")
|
||||
)
|
||||
|
||||
private fun parseMenuItems(arr: JSONArray?): List<MenuItem>? {
|
||||
arr ?: return null
|
||||
if (arr.length() == 0) return null
|
||||
return (0 until arr.length()).map { i ->
|
||||
val obj = arr.getJSONObject(i)
|
||||
MenuItem(
|
||||
label = obj.getString("label"),
|
||||
action = parseAction(obj.getJSONObject("action"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseStringList(arr: JSONArray?): List<String>? {
|
||||
arr ?: return null
|
||||
return (0 until arr.length()).map { arr.getString(it) }
|
||||
|
|
|
|||
|
|
@ -81,6 +81,12 @@ object BuiltInThemes {
|
|||
keyBg = Color.parseColor("#3A3A3D"),
|
||||
keyBgPressed = Color.parseColor("#4A4A4F"),
|
||||
keyText = Color.parseColor("#B0B0B3")
|
||||
),
|
||||
"menu" to KeyStyleOverride(
|
||||
keyBg = Color.parseColor("#2A1A3A"),
|
||||
keyBgPressed = Color.parseColor("#3A2A4A"),
|
||||
keyBorder = Color.parseColor("#6A4A8A"),
|
||||
keyText = Color.parseColor("#C8A0E8")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -124,6 +130,12 @@ object BuiltInThemes {
|
|||
keyBg = Color.parseColor("#E8E8EB"),
|
||||
keyBgPressed = Color.parseColor("#D8D8DB"),
|
||||
keyText = Color.parseColor("#666666")
|
||||
),
|
||||
"menu" to KeyStyleOverride(
|
||||
keyBg = Color.parseColor("#E8DDFF"),
|
||||
keyBgPressed = Color.parseColor("#D8CCEE"),
|
||||
keyBorder = Color.parseColor("#9977CC"),
|
||||
keyText = Color.parseColor("#6633AA")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -162,6 +174,12 @@ object BuiltInThemes {
|
|||
keyBg = Color.parseColor("#3A3A30"),
|
||||
keyBgPressed = Color.parseColor("#4A4A40"),
|
||||
keyText = Color.parseColor("#A6A68A")
|
||||
),
|
||||
"menu" to KeyStyleOverride(
|
||||
keyBg = Color.parseColor("#3A2A22"),
|
||||
keyBgPressed = Color.parseColor("#4A3A32"),
|
||||
keyBorder = Color.parseColor("#8A5A3A"),
|
||||
keyText = Color.parseColor("#E6A060")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -200,6 +218,12 @@ object BuiltInThemes {
|
|||
keyBg = Color.parseColor("#0A3A40"),
|
||||
keyBgPressed = Color.parseColor("#1A4A50"),
|
||||
keyText = Color.parseColor("#657B83")
|
||||
),
|
||||
"menu" to KeyStyleOverride(
|
||||
keyBg = Color.parseColor("#1A2A36"),
|
||||
keyBgPressed = Color.parseColor("#2A3A46"),
|
||||
keyBorder = Color.parseColor("#6C71C4"),
|
||||
keyText = Color.parseColor("#B58EDB")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,18 +7,31 @@ import android.view.View
|
|||
import com.roundingmobile.keyboard.theme.KeyboardTheme
|
||||
|
||||
/**
|
||||
* Popup that shows accent/variant characters when a key is long-pressed.
|
||||
* Popup that shows accent/variant characters when a key is long-pressed,
|
||||
* or menu items when a menu key is tapped.
|
||||
* Drawn above the key, user slides finger to select a variant.
|
||||
*/
|
||||
class LongPressPopupView(context: Context) : View(context) {
|
||||
|
||||
var variants: List<String> = emptyList()
|
||||
set(value) { field = value; selectedIndex = 0; requestLayout(); invalidate() }
|
||||
set(value) { field = value; selectedIndex = 0; _menuMode = false; requestLayout(); invalidate() }
|
||||
|
||||
var theme: KeyboardTheme? = null
|
||||
var selectedIndex: Int = 0
|
||||
set(value) { field = value; invalidate() }
|
||||
|
||||
/** Menu mode — wider cells, text-sized widths */
|
||||
private var _menuMode = false
|
||||
val isMenuMode: Boolean get() = _menuMode
|
||||
|
||||
/** Show menu-style labels (wider cells auto-sized to text) */
|
||||
fun showMenu(labels: List<String>) {
|
||||
variants = labels // triggers setter (resets _menuMode to false)
|
||||
_menuMode = true // override back — layout/draw are async, this takes effect first
|
||||
requestLayout()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val selectedPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
|
|
@ -31,33 +44,116 @@ class LongPressPopupView(context: Context) : View(context) {
|
|||
}
|
||||
|
||||
private val defaultCellWidth = dpToPx(46f)
|
||||
private val menuMinCellWidth = dpToPx(38f)
|
||||
private val menuPadding = dpToPx(14f)
|
||||
private val menuGap = dpToPx(2f)
|
||||
private val cellHeight = dpToPx(48f)
|
||||
private val radius = dpToPx(6f)
|
||||
private val menuCellRadius = dpToPx(5f)
|
||||
|
||||
private val dividerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
strokeWidth = 1f
|
||||
}
|
||||
|
||||
/** Effective cell width — shrinks when too many variants to fit the screen */
|
||||
private var cellWidth = defaultCellWidth
|
||||
/** Per-cell widths for menu mode (text-sized) */
|
||||
private var menuCellWidths: FloatArray = FloatArray(0)
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
||||
val maxPopupWidth = screenWidth - dpToPx(8f) // 4dp margin each side
|
||||
val idealWidth = defaultCellWidth * variants.size
|
||||
cellWidth = if (idealWidth > maxPopupWidth && variants.isNotEmpty()) {
|
||||
maxPopupWidth / variants.size
|
||||
|
||||
if (_menuMode && variants.isNotEmpty()) {
|
||||
// Measure each label and compute per-cell widths
|
||||
textPaint.textSize = spToPx(14f)
|
||||
menuCellWidths = FloatArray(variants.size) { i ->
|
||||
(textPaint.measureText(variants[i]) + menuPadding).coerceAtLeast(menuMinCellWidth)
|
||||
}
|
||||
val gapsTotal = menuGap * (variants.size - 1)
|
||||
val totalW = menuCellWidths.sum() + gapsTotal
|
||||
// Shrink proportionally if too wide
|
||||
if (totalW > maxPopupWidth) {
|
||||
val availForCells = maxPopupWidth - gapsTotal
|
||||
val cellsTotal = menuCellWidths.sum()
|
||||
val scale = availForCells / cellsTotal
|
||||
for (i in menuCellWidths.indices) menuCellWidths[i] *= scale
|
||||
}
|
||||
val w = (menuCellWidths.sum() + gapsTotal + dpToPx(4f)).toInt()
|
||||
val h = (cellHeight + dpToPx(4f)).toInt()
|
||||
setMeasuredDimension(w, h)
|
||||
} else {
|
||||
defaultCellWidth
|
||||
val idealWidth = defaultCellWidth * variants.size
|
||||
cellWidth = if (idealWidth > maxPopupWidth && variants.isNotEmpty()) {
|
||||
maxPopupWidth / variants.size
|
||||
} else {
|
||||
defaultCellWidth
|
||||
}
|
||||
val w = (cellWidth * variants.size + dpToPx(4f)).toInt()
|
||||
val h = (cellHeight + dpToPx(4f)).toInt()
|
||||
setMeasuredDimension(w, h)
|
||||
}
|
||||
val w = (cellWidth * variants.size + dpToPx(4f)).toInt()
|
||||
val h = (cellHeight + dpToPx(4f)).toInt()
|
||||
setMeasuredDimension(w, h)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
if (variants.isEmpty()) return
|
||||
val th = theme ?: return
|
||||
|
||||
val startY = dpToPx(2f)
|
||||
|
||||
if (_menuMode) {
|
||||
drawMenu(canvas, th, startY)
|
||||
} else {
|
||||
drawVariants(canvas, th, startY)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawMenu(canvas: Canvas, th: KeyboardTheme, startY: Float) {
|
||||
val gapsTotal = menuGap * (variants.size - 1)
|
||||
val totalW = menuCellWidths.sum() + gapsTotal
|
||||
val startX = (width - totalW) / 2f
|
||||
|
||||
// Shadow behind entire strip
|
||||
canvas.drawRoundRect(startX - 2f, startY - 2f, startX + totalW + 2f, startY + cellHeight + 2f, radius, radius, shadowPaint)
|
||||
|
||||
// Background strip
|
||||
bgPaint.color = th.keyboardBg
|
||||
canvas.drawRoundRect(startX, startY, startX + totalW, startY + cellHeight, radius, radius, bgPaint)
|
||||
|
||||
textPaint.textSize = spToPx(14f)
|
||||
val cellInset = dpToPx(1.5f)
|
||||
|
||||
var x = startX
|
||||
for (i in variants.indices) {
|
||||
val cw = menuCellWidths[i]
|
||||
val cellRect = RectF(x + cellInset, startY + cellInset, x + cw - cellInset, startY + cellHeight - cellInset)
|
||||
|
||||
// Each cell — regular key colors, highlighted when selected
|
||||
if (i == selectedIndex) {
|
||||
selectedPaint.color = th.keyBgPressed
|
||||
canvas.drawRoundRect(cellRect, menuCellRadius, menuCellRadius, selectedPaint)
|
||||
} else {
|
||||
bgPaint.color = th.keyBg
|
||||
canvas.drawRoundRect(cellRect, menuCellRadius, menuCellRadius, bgPaint)
|
||||
}
|
||||
|
||||
// Subtle border
|
||||
dividerPaint.color = th.keyBorder
|
||||
dividerPaint.style = Paint.Style.STROKE
|
||||
canvas.drawRoundRect(cellRect, menuCellRadius, menuCellRadius, dividerPaint)
|
||||
|
||||
// Text
|
||||
textPaint.color = th.keyText
|
||||
textPaint.isFakeBoldText = i == selectedIndex
|
||||
canvas.drawText(variants[i], cellRect.centerX(), cellRect.centerY() + textPaint.textSize / 3f, textPaint)
|
||||
|
||||
x += cw + menuGap
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawVariants(canvas: Canvas, th: KeyboardTheme, startY: Float) {
|
||||
val totalW = cellWidth * variants.size
|
||||
val startX = (width - totalW) / 2f
|
||||
val startY = dpToPx(2f)
|
||||
|
||||
// Shadow
|
||||
canvas.drawRoundRect(startX - 2f, startY - 2f, startX + totalW + 2f, startY + cellHeight + 2f, radius, radius, shadowPaint)
|
||||
|
|
@ -91,6 +187,17 @@ class LongPressPopupView(context: Context) : View(context) {
|
|||
*/
|
||||
fun hitTestVariant(localX: Float): Int {
|
||||
if (variants.isEmpty()) return -1
|
||||
if (_menuMode) {
|
||||
val gapsTotal = menuGap * (variants.size - 1)
|
||||
val totalW = menuCellWidths.sum() + gapsTotal
|
||||
val startX = (width - totalW) / 2f
|
||||
var x = startX
|
||||
for (i in menuCellWidths.indices) {
|
||||
if (localX < x + menuCellWidths[i]) return i
|
||||
x += menuCellWidths[i] + menuGap
|
||||
}
|
||||
return variants.size - 1
|
||||
}
|
||||
val totalW = cellWidth * variants.size
|
||||
val startX = (width - totalW) / 2f
|
||||
val relX = localX - startX
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ class QuickBarView(context: Context) : View(context) {
|
|||
var onKeyTap: ((KeyDefinition) -> Unit)? = null
|
||||
var onKeyDown: ((KeyDefinition) -> Unit)? = null
|
||||
var onKeyUp: (() -> Unit)? = null
|
||||
/** Callback for menu keys (vim/tmux/screen) — passes key and its screen bounds */
|
||||
var onMenuKeyTap: ((KeyDefinition) -> Unit)? = null
|
||||
/** Callback for the snippets button */
|
||||
var onSnippetsTap: (() -> Unit)? = null
|
||||
|
||||
|
|
@ -478,7 +480,11 @@ class QuickBarView(context: Context) : View(context) {
|
|||
pressedKeys.clear()
|
||||
invalidate()
|
||||
if (key != null) {
|
||||
onKeyTap?.invoke(key)
|
||||
if (!key.menuItems.isNullOrEmpty()) {
|
||||
onMenuKeyTap?.invoke(key)
|
||||
} else {
|
||||
onKeyTap?.invoke(key)
|
||||
}
|
||||
}
|
||||
onKeyUp?.invoke()
|
||||
velocityTracker?.recycle()
|
||||
|
|
|
|||
|
|
@ -216,7 +216,50 @@
|
|||
{ "id": "qb_down", "label": "▼", "w": 0.8, "action": { "type": "esc_seq", "seq": "[B" }, "repeatable": true },
|
||||
{ "id": "qb_left", "label": "◀", "w": 0.8, "action": { "type": "esc_seq", "seq": "[D" }, "repeatable": true },
|
||||
{ "id": "qb_right", "label": "▶", "w": 0.8, "action": { "type": "esc_seq", "seq": "[C" }, "repeatable": true },
|
||||
{ "id": "qb_stab", "label": "⇤", "w": 0.8, "action": { "type": "esc_seq", "seq": "[Z" } }
|
||||
{ "id": "qb_stab", "label": "⇤", "w": 0.8, "action": { "type": "esc_seq", "seq": "[Z" } },
|
||||
{ "id": "qb_vim", "label": "vim", "w": 1, "style": "menu", "action": { "type": "none" }, "menuItems": [
|
||||
{ "label": ":w", "action": { "type": "macro", "text": ":w\r" } },
|
||||
{ "label": ":q", "action": { "type": "macro", "text": ":q\r" } },
|
||||
{ "label": ":wq", "action": { "type": "macro", "text": ":wq\r" } },
|
||||
{ "label": ":q!", "action": { "type": "macro", "text": ":q!\r" } },
|
||||
{ "label": ":%s/", "action": { "type": "macro", "text": ":%s/" } },
|
||||
{ "label": "ciw", "action": { "type": "macro", "text": "ciw" } },
|
||||
{ "label": ">>", "action": { "type": "macro", "text": ">>" } },
|
||||
{ "label": "<<", "action": { "type": "macro", "text": "<<" } },
|
||||
{ "label": "redo", "action": { "type": "bytes", "value": [18] } }
|
||||
]},
|
||||
{ "id": "qb_nano", "label": "nano", "w": 1, "style": "menu", "action": { "type": "none" }, "menuItems": [
|
||||
{ "label": "save", "action": { "type": "bytes", "value": [15] } },
|
||||
{ "label": "exit", "action": { "type": "bytes", "value": [24] } },
|
||||
{ "label": "find", "action": { "type": "bytes", "value": [23] } },
|
||||
{ "label": "repl", "action": { "type": "bytes", "value": [28] } },
|
||||
{ "label": "cut", "action": { "type": "bytes", "value": [11] } },
|
||||
{ "label": "paste","action": { "type": "bytes", "value": [21] } },
|
||||
{ "label": "help", "action": { "type": "bytes", "value": [7] } },
|
||||
{ "label": "goto", "action": { "type": "bytes", "value": [20] } }
|
||||
]},
|
||||
{ "id": "qb_tmux", "label": "tmux", "w": 1, "style": "menu", "action": { "type": "none" }, "menuItems": [
|
||||
{ "label": "new", "action": { "type": "bytes", "value": [2, 99] } },
|
||||
{ "label": "nxt", "action": { "type": "bytes", "value": [2, 110] } },
|
||||
{ "label": "prv", "action": { "type": "bytes", "value": [2, 112] } },
|
||||
{ "label": "det", "action": { "type": "bytes", "value": [2, 100] } },
|
||||
{ "label": "sp│", "action": { "type": "bytes", "value": [2, 37] } },
|
||||
{ "label": "sp─", "action": { "type": "bytes", "value": [2, 34] } },
|
||||
{ "label": "scr", "action": { "type": "bytes", "value": [2, 91] } },
|
||||
{ "label": "zoom", "action": { "type": "bytes", "value": [2, 122] } },
|
||||
{ "label": "kill", "action": { "type": "bytes", "value": [2, 120] } }
|
||||
]},
|
||||
{ "id": "qb_screen","label": "scr", "w": 1, "style": "menu", "action": { "type": "none" }, "menuItems": [
|
||||
{ "label": "new", "action": { "type": "bytes", "value": [1, 99] } },
|
||||
{ "label": "nxt", "action": { "type": "bytes", "value": [1, 110] } },
|
||||
{ "label": "prv", "action": { "type": "bytes", "value": [1, 112] } },
|
||||
{ "label": "det", "action": { "type": "bytes", "value": [1, 100] } },
|
||||
{ "label": "sp─", "action": { "type": "bytes", "value": [1, 83] } },
|
||||
{ "label": "sp│", "action": { "type": "bytes", "value": [1, 124] } },
|
||||
{ "label": "cpy", "action": { "type": "bytes", "value": [1, 91] } },
|
||||
{ "label": "kill", "action": { "type": "bytes", "value": [1, 107] } },
|
||||
{ "label": "list", "action": { "type": "bytes", "value": [1, 34] } }
|
||||
]}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue