feat: add combo mode with combo counter, score and escalating effects
A 'Combo mode' toggle turns the display into a game-style counter. Chords containing a modifier (Super/Ctrl/Alt/Shift/Menu/AltGr) are combos: they build a combo counter, apply a multiplier (x2 at 5, x3 at 10 ... x8 at 50) and escalate the effects. Plain characters and shifted chars typed alone are hits: +10 score only, they never touch the combo counter or its window. Scoring: hit = 10, combo = (20 + 15*(mods-1) + 5*(keys-1)) * multiplier. Counted once per physical chord on the Lua's empty payload (release), so the intermediate growing emits of a chord never double-count. The banner (COMBO 12 x3 - 3,450) sits below the history for bottom positions and above it for top positions, with a floating +N pop per press. Effects scale with modifiers and chain length: banner pulse, hue shift, screen shake (up to 5px), continuous hue cycling at 10+ combos and a constant subtle vibration at 20+. The combo resets after comboWindowMs (2s) without a new combo; the score accumulates for the shell session. Also fixes the README Behavior section: the history bullet was never applied (a failed multi-edit call), now restored alongside the combo mode docs. Manifest bumped to 1.7.0.
This commit is contained in:
+312
-8
@@ -12,7 +12,8 @@ import qs.Ui
|
||||
// and renders. No images, no animations: the combo appears while held and
|
||||
// lingers briefly after release (like keyviz's Duration), then vanishes.
|
||||
// With historyCount > 1 the last few combos stack as a fading history,
|
||||
// keyviz-style.
|
||||
// keyviz-style. Combo mode ("game mode") adds a score banner and effects
|
||||
// on top of the same display.
|
||||
//
|
||||
// On first load the panel also appends a small guarded block to
|
||||
// ~/.config/hypr/hyprland.lua that dofiles the capture script, so install
|
||||
@@ -38,6 +39,25 @@ Item {
|
||||
// show": entries never expire, so the stack only shrinks when newer
|
||||
// combos push old ones out of the history window.
|
||||
property int lingerMs: 1000
|
||||
|
||||
// Combo mode — "game mode". Toggle in the panel. Chords that contain a
|
||||
// modifier (Super/Ctrl/Alt/Shift/Menu/AltGr) are COMBOs: they build a
|
||||
// combo counter, apply a multiplier and escalate the effects. Plain
|
||||
// characters (and shifted chars typed alone, which the Lua folds into
|
||||
// the character) are HITs: basic score only, they never touch the combo
|
||||
// counter or its window. Counted when the chord completes (the Lua's
|
||||
// empty payload), so one physical chord is exactly one press even though
|
||||
// the Lua emits intermediate growing states while keys are added.
|
||||
property bool comboMode: false
|
||||
property int comboCount: 0
|
||||
property int comboScore: 0
|
||||
property int multiplier: 1
|
||||
property real comboHue: 0.12
|
||||
property real bannerScale: 1.0
|
||||
property real bannerPulseTo: 1.2
|
||||
property real shakeX: 0.0
|
||||
property real shakeY: 0.0
|
||||
property real popOffsetY: 0.0
|
||||
// Drop state written more than this long ago (e.g. from a previous shell
|
||||
// session after a restart) so a stale combo never sticks on screen.
|
||||
readonly property int maxStateAgeMs: 1500
|
||||
@@ -152,6 +172,134 @@ Item {
|
||||
return list
|
||||
}
|
||||
|
||||
// --------------------------------------------------- combo mode
|
||||
|
||||
// Combo mode tuning — all adjustable:
|
||||
readonly property int comboWindowMs: 2000 // max gap between combos before the counter resets
|
||||
readonly property int hitPoints: 10 // score for a plain character (HIT)
|
||||
readonly property int comboBasePoints: 20 // score for a 1-mod COMBO before the multiplier
|
||||
readonly property int comboPerMod: 15 // extra points per additional modifier
|
||||
readonly property int comboPerKey: 5 // extra points per key in the chord
|
||||
readonly property int bannerPadX: Style.space(14)
|
||||
readonly property int bannerPadY: Style.space(6)
|
||||
readonly property int bannerGap: Style.space(8)
|
||||
readonly property int bannerHeight: Math.ceil(bannerFontMetrics.height) + 2 * bannerPadY
|
||||
|
||||
function multiplierFor(count) {
|
||||
if (count >= 50) return 8
|
||||
if (count >= 40) return 7
|
||||
if (count >= 30) return 6
|
||||
if (count >= 20) return 5
|
||||
if (count >= 15) return 4
|
||||
if (count >= 10) return 3
|
||||
if (count >= 5) return 2
|
||||
return 1
|
||||
}
|
||||
|
||||
function modCountOf(keys) {
|
||||
var n = 0
|
||||
for (var i = 0; i < keys.length; i++) if (root.modLabels.indexOf(keys[i]) !== -1) n++
|
||||
return n
|
||||
}
|
||||
|
||||
function tierOf(count) {
|
||||
if (count >= 20) return 3
|
||||
if (count >= 10) return 2
|
||||
if (count >= 5) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
function formatScore(n) {
|
||||
var s = String(n)
|
||||
var out = ""
|
||||
var c = 0
|
||||
for (var i = s.length - 1; i >= 0; i--) {
|
||||
out = s[i] + out
|
||||
c++
|
||||
if (c % 3 === 0 && i > 0) out = "," + out
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Called once per completed physical chord (see apply()). Classifies the
|
||||
// chord as HIT (no modifiers) or COMBO (1+ modifiers) and scores it.
|
||||
function pressCombo(keys) {
|
||||
if (!root.comboMode) return
|
||||
var mods = root.modCountOf(keys)
|
||||
if (mods === 0) {
|
||||
// HIT: plain characters never build the combo, they just score the
|
||||
// basics — the "normal punch" of the game.
|
||||
root.comboScore += root.hitPoints
|
||||
root.bannerPulseTo = 1.08
|
||||
bannerPulseAnim.restart()
|
||||
root.popScore("+" + root.hitPoints, -1)
|
||||
return
|
||||
}
|
||||
// A chord made only of modifiers is not a real combo — modifiers of
|
||||
// nothing. It scores zero and touches neither the counter nor the
|
||||
// window. A valid combo always ends on a non-modifier key.
|
||||
if (mods >= keys.length) return
|
||||
// COMBO: modifiers present plus at least one real key — the real deal.
|
||||
root.comboCount++
|
||||
root.multiplier = root.multiplierFor(root.comboCount)
|
||||
var pts = (root.comboBasePoints + root.comboPerMod * (mods - 1)
|
||||
+ root.comboPerKey * (keys.length - 1)) * root.multiplier
|
||||
root.comboScore += pts
|
||||
comboWindowTimer.restart()
|
||||
var tier = root.tierOf(root.comboCount)
|
||||
// Hue shifts with modifiers and chain length; the colorTimer cycles it
|
||||
// continuously once the combo is hot.
|
||||
root.comboHue = (0.12 + mods * 0.06 + root.comboCount * 0.008) % 1
|
||||
root.bannerPulseTo = Math.min(1.6, 1.2 + mods * 0.06 + root.comboCount * 0.004)
|
||||
bannerPulseAnim.restart()
|
||||
root.popScore("+" + pts, root.comboHue)
|
||||
root.triggerShake(mods, tier)
|
||||
}
|
||||
|
||||
// Floating "+N" pop above the banner. hue < 0 renders in the normal text
|
||||
// color (hits); otherwise in the animated combo hue.
|
||||
function popScore(text, hue) {
|
||||
scorePop.text = text
|
||||
scorePop.color = hue < 0 ? Color.popups.text : Qt.hsva(hue, 0.85, 1)
|
||||
scorePop.visible = true
|
||||
popAnim.restart()
|
||||
}
|
||||
|
||||
// Screen shake on combo presses. Amplitude grows with modifiers and the
|
||||
// combo tier; at tier 3 (20+ combos) the continuous jitterTimer takes
|
||||
// over instead so the two never fight.
|
||||
function triggerShake(mods, tier) {
|
||||
if (tier >= 3) return
|
||||
var amp = Math.min(5, 1 + mods * 0.8 + tier * 1.2)
|
||||
shake1.to = amp
|
||||
shake2.to = -amp
|
||||
shake3.to = -amp
|
||||
shake4.to = amp
|
||||
shakeAnim.restart()
|
||||
}
|
||||
|
||||
function comboBannerText() {
|
||||
if (root.comboCount > 0) {
|
||||
var t = "COMBO " + root.comboCount
|
||||
if (root.multiplier > 1) t += " ×" + root.multiplier
|
||||
return t + " · " + root.formatScore(root.comboScore)
|
||||
}
|
||||
return "SCORE " + root.formatScore(root.comboScore)
|
||||
}
|
||||
|
||||
function comboBannerWidth() {
|
||||
return Math.ceil(bannerFontMetrics.advanceWidth(root.comboBannerText())) + 2 * bannerPadX
|
||||
}
|
||||
|
||||
function comboBannerColor() {
|
||||
if (root.comboCount <= 0) return Color.popups.text
|
||||
return Qt.hsva(root.comboHue, 0.85, 1)
|
||||
}
|
||||
|
||||
function bannerVisible() {
|
||||
return root.comboMode && root.comboScore > 0
|
||||
}
|
||||
|
||||
FontMetrics {
|
||||
id: chipFontMetrics
|
||||
font: chipFont
|
||||
@@ -163,6 +311,17 @@ Item {
|
||||
bold: true
|
||||
})
|
||||
|
||||
readonly property var bannerFont: Qt.font({
|
||||
family: Style.font.family,
|
||||
pixelSize: Math.round(Style.font.title * 1.35),
|
||||
bold: true
|
||||
})
|
||||
|
||||
FontMetrics {
|
||||
id: bannerFontMetrics
|
||||
font: bannerFont
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- state
|
||||
|
||||
function apply() {
|
||||
@@ -185,9 +344,14 @@ Item {
|
||||
var es = root.entries.slice()
|
||||
if (next.length === 0) {
|
||||
// All keys released: the newest combo enters its linger window; the
|
||||
// history tick prunes it once lingerMs passes.
|
||||
// history tick prunes it once lingerMs passes. This empty payload is
|
||||
// also the chord-completion signal: the full combo that just ended is
|
||||
// the one being pushed into its linger window, so count it exactly
|
||||
// once here (never on the intermediate growing emits).
|
||||
if (es.length > 0 && es[0].releasedAt === 0) {
|
||||
var completed = es[0].keys.slice()
|
||||
es[0] = { keys: es[0].keys, releasedAt: Date.now() }
|
||||
root.pressCombo(completed)
|
||||
}
|
||||
} else if (es.length > 0 && root.sameKeys(es[0].keys, next)) {
|
||||
// Same combo re-pressed (or the state file re-fired): refresh it,
|
||||
@@ -222,9 +386,54 @@ Item {
|
||||
}
|
||||
root.entries = root.trimEntries(kept)
|
||||
root.opened = root.entries.length > 0
|
||||
// The linger window passed and the display cleared: the run is over,
|
||||
// so the score resets with it (the banner hides again).
|
||||
if (root.entries.length === 0 && root.comboScore !== 0) {
|
||||
root.comboScore = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Combo window: when no new COMBO arrives in time, the counter resets
|
||||
// (the score stays). Hits never touch this timer.
|
||||
Timer {
|
||||
id: comboWindowTimer
|
||||
interval: root.comboWindowMs
|
||||
onTriggered: {
|
||||
root.comboCount = 0
|
||||
root.multiplier = 1
|
||||
root.shakeX = 0
|
||||
root.shakeY = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 4 (20+ combos): continuous subtle vibration while the combo is
|
||||
// hot. Press shakes are skipped at this tier so they never fight.
|
||||
Timer {
|
||||
id: jitterTimer
|
||||
interval: 80
|
||||
running: root.comboMode && root.comboCount >= 20
|
||||
onTriggered: {
|
||||
root.shakeX = (Math.random() - 0.5) * 3
|
||||
root.shakeY = (Math.random() - 0.5) * 3
|
||||
}
|
||||
}
|
||||
|
||||
// Hot combos cycle the hue continuously instead of only on presses.
|
||||
Timer {
|
||||
id: colorTimer
|
||||
interval: 60
|
||||
running: root.comboMode && root.comboCount >= 10
|
||||
onTriggered: root.comboHue = (root.comboHue + 0.012) % 1
|
||||
}
|
||||
|
||||
onComboModeChanged: if (!root.comboMode) {
|
||||
root.comboCount = 0
|
||||
root.multiplier = 1
|
||||
root.shakeX = 0
|
||||
root.shakeY = 0
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: stateFile
|
||||
path: root.statePath
|
||||
@@ -279,6 +488,7 @@ Item {
|
||||
if (isFinite(cfg.margin) && cfg.margin >= 0) root.margin = Math.round(cfg.margin)
|
||||
if (isFinite(cfg.lingerMs) && cfg.lingerMs >= 0) root.lingerMs = Math.round(cfg.lingerMs)
|
||||
if (isFinite(cfg.historyCount)) root.historyCount = Math.max(1, Math.min(5, Math.round(cfg.historyCount)))
|
||||
root.comboMode = cfg.comboMode === true
|
||||
}
|
||||
|
||||
function migrateConfig() {
|
||||
@@ -412,17 +622,29 @@ Item {
|
||||
height: card.borderTop + root.cardPad + root.contentHeight() + root.cardPad + card.borderBottom
|
||||
x: {
|
||||
var p = root.position
|
||||
if (p.indexOf("left") !== -1) return root.margin
|
||||
if (p.indexOf("right") !== -1) return parent.width - width - root.margin
|
||||
return Math.round((parent.width - width) / 2)
|
||||
var bx = 0
|
||||
if (p.indexOf("left") !== -1) bx = root.margin
|
||||
else if (p.indexOf("right") !== -1) bx = panel.width - width - root.margin
|
||||
else bx = Math.round((panel.width - width) / 2)
|
||||
return bx + root.shakeX
|
||||
}
|
||||
// Top positions anchor the stack's first row at the top edge (the
|
||||
// history grows downward); bottom positions anchor the last row at
|
||||
// the bottom edge (the history grows upward).
|
||||
// the bottom edge (the history grows upward). In combo mode the
|
||||
// banner sits below the card for bottom positions and above it for
|
||||
// top positions, and the card makes room for it.
|
||||
y: {
|
||||
var p = root.position
|
||||
if (p.indexOf("top") !== -1) return root.margin
|
||||
return parent.height - height - root.margin
|
||||
var by = 0
|
||||
if (root.bannerVisible()) {
|
||||
if (p.indexOf("top") !== -1) by = root.margin + root.bannerHeight + root.bannerGap
|
||||
else by = panel.height - root.margin - root.bannerHeight - root.bannerGap - height
|
||||
} else if (p.indexOf("top") !== -1) {
|
||||
by = root.margin
|
||||
} else {
|
||||
by = panel.height - height - root.margin
|
||||
}
|
||||
return by + root.shakeY
|
||||
}
|
||||
color: Util.alpha(Color.popups.background, 0.97)
|
||||
borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border, Math.max(1, Style.space(2)))
|
||||
@@ -468,5 +690,87 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Combo mode banner — a separate visual stacked against the history
|
||||
// card (below it for bottom positions, above it for top positions).
|
||||
// Shows the combo counter, multiplier and running score; hue and
|
||||
// border follow the combo color, and the whole banner pulses on each
|
||||
// press. Shakes with the card via the shared shakeX/shakeY offsets.
|
||||
BorderSurface {
|
||||
id: banner
|
||||
visible: root.bannerVisible()
|
||||
width: root.comboBannerWidth()
|
||||
height: root.bannerHeight
|
||||
x: {
|
||||
var bx = card.x - root.shakeX + (card.width - width) / 2
|
||||
return bx + root.shakeX
|
||||
}
|
||||
// Bottom positions: banner at the bottom edge, card above it.
|
||||
// Top positions: banner at the top edge, card below it.
|
||||
y: {
|
||||
var by = root.position.indexOf("top") !== -1
|
||||
? root.margin
|
||||
: panel.height - height - root.margin
|
||||
return by + root.shakeY
|
||||
}
|
||||
color: Util.alpha(Color.popups.background, 0.97)
|
||||
borderSpec: Border.surfaceSpec("popups", "border", root.comboBannerColor(), Math.max(1, Style.space(2)))
|
||||
radius: Style.cornerRadius
|
||||
scale: root.bannerScale
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Text {
|
||||
id: bannerText
|
||||
anchors.centerIn: parent
|
||||
text: root.comboBannerText()
|
||||
font: root.bannerFont
|
||||
color: root.comboBannerColor()
|
||||
}
|
||||
}
|
||||
|
||||
// Transient "+N" pop of the points just scored, floating up from the
|
||||
// banner. Reused for every press (hit or combo).
|
||||
Text {
|
||||
id: scorePop
|
||||
visible: false
|
||||
font: root.bannerFont
|
||||
x: banner.x + (banner.width - width) / 2
|
||||
y: banner.y - height - Style.space(8)
|
||||
opacity: 0
|
||||
transform: Translate { id: popTranslate; y: root.popOffsetY }
|
||||
}
|
||||
|
||||
// Banner pulse on each press (hits pulse small, combos grow with mods).
|
||||
SequentialAnimation {
|
||||
id: bannerPulseAnim
|
||||
running: false
|
||||
NumberAnimation { target: root; property: "bannerScale"; from: 1.0; to: root.bannerPulseTo; duration: 70 }
|
||||
NumberAnimation { target: root; property: "bannerScale"; to: 1.0; duration: 220; easing.type: Easing.OutBack }
|
||||
}
|
||||
|
||||
// Press shake: a few quick offset steps around the base position. The
|
||||
// amplitudes are set by triggerShake() before restarting.
|
||||
SequentialAnimation {
|
||||
id: shakeAnim
|
||||
running: false
|
||||
NumberAnimation { id: shake1; target: root; property: "shakeX"; to: 2; duration: 30 }
|
||||
NumberAnimation { id: shake2; target: root; property: "shakeY"; to: -2; duration: 30 }
|
||||
NumberAnimation { id: shake3; target: root; property: "shakeX"; to: -2; duration: 30 }
|
||||
NumberAnimation { id: shake4; target: root; property: "shakeY"; to: 2; duration: 30 }
|
||||
NumberAnimation { target: root; property: "shakeX"; to: 0; duration: 40 }
|
||||
NumberAnimation { target: root; property: "shakeY"; to: 0; duration: 40 }
|
||||
}
|
||||
|
||||
// The +N score pop: snap in, then float up while fading and shrinking.
|
||||
SequentialAnimation {
|
||||
id: popAnim
|
||||
running: false
|
||||
ScriptAction { script: { scorePop.opacity = 1; scorePop.scale = 1.45; root.popOffsetY = 0 } }
|
||||
ParallelAnimation {
|
||||
NumberAnimation { target: scorePop; property: "opacity"; to: 0; duration: 500; easing.type: Easing.OutQuad }
|
||||
NumberAnimation { target: scorePop; property: "scale"; to: 1.0; duration: 500; easing.type: Easing.OutQuad }
|
||||
NumberAnimation { target: root; property: "popOffsetY"; to: -24; duration: 500; easing.type: Easing.OutQuad }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ Panel {
|
||||
property int margin: 67
|
||||
property int lingerMs: 1000
|
||||
property int historyCount: 1
|
||||
property bool comboMode: false
|
||||
|
||||
// Config lives outside the plugin folder (the shell reloads all plugin
|
||||
// code on any file change under ~/.config/omarchy/plugins/), so editing it
|
||||
@@ -76,6 +77,7 @@ Panel {
|
||||
if (isFinite(cfg.margin) && cfg.margin >= 0) root.margin = Math.round(cfg.margin)
|
||||
if (isFinite(cfg.lingerMs) && cfg.lingerMs >= 0) root.lingerMs = Math.round(cfg.lingerMs)
|
||||
if (isFinite(cfg.historyCount)) root.historyCount = Math.max(1, Math.min(5, Math.round(cfg.historyCount)))
|
||||
root.comboMode = cfg.comboMode === true
|
||||
}
|
||||
|
||||
// Round-trips every option so a menu edit never drops values the display
|
||||
@@ -86,12 +88,14 @@ Panel {
|
||||
position: root.position,
|
||||
margin: root.margin,
|
||||
lingerMs: root.lingerMs,
|
||||
historyCount: root.historyCount
|
||||
historyCount: root.historyCount,
|
||||
comboMode: root.comboMode
|
||||
}
|
||||
for (var k in update) cfg[k] = update[k]
|
||||
if (update.mode !== undefined) root.mode = update.mode
|
||||
if (update.position !== undefined) root.position = update.position
|
||||
if (update.historyCount !== undefined) root.historyCount = update.historyCount
|
||||
if (update.comboMode !== undefined) root.comboMode = update.comboMode
|
||||
writeProc.command = ["sh", "-c",
|
||||
"printf '%s\\n' '" + JSON.stringify(cfg) + "' > " + Util.shellQuote(root.configPath)]
|
||||
writeProc.running = true
|
||||
@@ -168,6 +172,34 @@ Panel {
|
||||
}
|
||||
}
|
||||
|
||||
// Combo mode -----------------------------------------------------
|
||||
// Game mode: combo counter + score with escalating effects (pulse,
|
||||
// hue, screen shake). Chords with modifiers build the combo; plain
|
||||
// characters are hits that only add score.
|
||||
Item {
|
||||
width: parent.width
|
||||
height: comboSwitch.implicitHeight
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Combo mode"
|
||||
font.family: Style.font.family
|
||||
font.pixelSize: Style.font.body
|
||||
color: Color.popups.text
|
||||
}
|
||||
|
||||
ToggleSwitch {
|
||||
id: comboSwitch
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: root.comboMode
|
||||
foreground: Color.popups.text
|
||||
accent: Color.accent
|
||||
onToggled: root.writeConfig({ comboMode: !root.comboMode })
|
||||
}
|
||||
}
|
||||
|
||||
// Mode ----------------------------------------------------------
|
||||
Item {
|
||||
width: parent.width
|
||||
|
||||
@@ -125,6 +125,35 @@ plugin, not just this one.
|
||||
lingering combo is always the last full one, never a partial release state.
|
||||
`lingerMs: 0` means "always show": the last combo stays on screen until
|
||||
the next key replaces it.
|
||||
- **History** (`historyCount`, 1–5, default 1) — the last few combos stack
|
||||
on screen instead of vanishing. Each new combo pushes the previous one
|
||||
down the stack; the current combo is fully opaque and every older entry
|
||||
fades step by step, so a `Super + G` → `Super + Shift + G` sequence shows
|
||||
both, with the older one visibly dimmer. The stack direction follows the
|
||||
position: top positions grow downward (newest on top), bottom positions
|
||||
grow upward (newest at the bottom edge) — not configurable, by design.
|
||||
With `lingerMs: 0` entries never expire, so the stack keeps the last few
|
||||
combos on screen until newer ones push them out.
|
||||
- **Combo mode** (toggle in the panel, `comboMode` in config) — turns the
|
||||
display into a game-style counter. Chords that contain a modifier
|
||||
(`Super`, `Ctrl`, `Alt`, `Shift`, `Menu`, `AltGr`) are **combos**: they
|
||||
build a combo counter, apply a multiplier (`×2` at 5, `×3` at 10, … up to
|
||||
`×8` at 50) and escalate the effects. Plain characters (and shifted chars
|
||||
typed alone, which render as the character itself) are **hits**: +10
|
||||
score only, they never touch the combo counter. Scoring:
|
||||
`hit = +10`, `combo = (20 + 15×(mods−1) + 5×(keys−1)) × multiplier`.
|
||||
The banner (`COMBO 12 ×3 · 3,450`) sits below the history for bottom
|
||||
positions and above it for top positions, with a floating `+N` pop per
|
||||
press. Effects scale with modifiers and chain length: banner pulse,
|
||||
hue shift, screen shake (up to 5px) and, at 10+ combos, continuous hue
|
||||
cycling — at 20+, a constant subtle vibration while the combo is hot.
|
||||
The combo counter resets after `comboWindowMs` (2s) without a new combo,
|
||||
and when the linger window then passes (the display clears) the **score
|
||||
resets with it**, so every run starts from zero. A chord made only of
|
||||
modifiers (`Super` alone, `Super+Ctrl+Shift`) is not a valid combo —
|
||||
modifiers of nothing — and scores zero; a valid combo always ends on a
|
||||
non-modifier key. Counted once
|
||||
per physical chord (on release), so growing key presses never double-count.
|
||||
- In `mode: "bindings"` only combos with a modifier are shown; plain typing
|
||||
stays off screen.
|
||||
|
||||
@@ -142,7 +171,8 @@ the plugin on every edit. Editing this file updates the display live:
|
||||
"position": "bottom-center",
|
||||
"margin": 67,
|
||||
"lingerMs": 1000,
|
||||
"historyCount": 1
|
||||
"historyCount": 1,
|
||||
"comboMode": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -153,6 +183,7 @@ the plugin on every edit. Editing this file updates the display live:
|
||||
| `margin` | px from the screen edge | `67` |
|
||||
| `lingerMs` | ms a released combo stays; `0` keeps it until the next key (keyviz defaults to `5000`) | `1000` |
|
||||
| `historyCount` | how many combos stack on screen (1–5); older entries fade | `1` |
|
||||
| `comboMode` | `true`/`false` — game-style combo counter, score and effects | `false` |
|
||||
|
||||
Deeper tweaks still live in the QML/Lua sources:
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 1,
|
||||
"id": "felixzsh.key-visualizer",
|
||||
"name": "Key Visualizer",
|
||||
"version": "1.6.0",
|
||||
"version": "1.7.0",
|
||||
"author": "felixzsh",
|
||||
"description": "Shows the keys you press on screen. Great for keybinding tutorials, demos, and screencasts.",
|
||||
"kinds": [
|
||||
|
||||
Reference in New Issue
Block a user