키 (keys)
OBS는 읽기 전용입니다. 네이티브 Tauri 메인·오버레이 창은 revision coordinator를 거쳐 쓰고 같은 canonical 이벤트를 구독할 수 있습니다.
키 매핑 조회/수정
keys[mode][i]와 keyPositions[mode][i]는 인덱스로 결합되어 있습니다. 모드
집합이나 배열 길이를 바꾸는 update() 또는 updatePositions() 단독 호출은
PAIRED_UPDATE_REQUIRED로 실패할 수 있습니다. 키 추가·삭제·재정렬은
updateWithPositions()를 사용하세요.
get()
모든 키 모드의 키 매핑을 조회합니다.
type SlotMatch = 'all' | 'any';
// 슬롯은 단일 키 문자열 또는 멀티 키 바인딩입니다:
// - match: 'any': 멤버 키 중 하나만 눌러도 슬롯 활성 (택일)
// - match: 'all': 멤버 키를 전부 동시에 눌러야 슬롯 활성 (동시)
interface MultiKeySlot {
keys: string[]; // 멤버 키 2~8개
match: SlotMatch;
}
type KeySlot = string | MultiKeySlot;
type KeyMappings = Record<string, KeySlot[]>;
// { "4key": ["D", { keys: ["F", "NUMPAD 4"], match: "any" }, ...], ... }const mappings = await dmn.keys.get();
console.log('4key 매핑:', mappings['4key']);멀티 키 업데이트 이후 슬롯 값은 문자열뿐 아니라 객체(MultiKeySlot)일 수
있습니다. 모든 슬롯이 문자열이라고 가정하는 플러그인은 멀티 키 슬롯이 포함된
매핑을 다루기 전에 업데이트가 필요합니다.
canonical 슬롯 식별자
이벤트·카운터 표면은 슬롯을 canonical 문자열로 식별합니다:
- 단일 키 슬롯은 키 문자열 그대로입니다 (예:
"D"). match: 'all'슬롯은 멤버를 순서대로+로 결합합니다 (예:"LEFT CTRL+Z").match: 'any'슬롯은 멤버를|로 결합합니다 (예:"F|NUMPAD 4").
canonical 문자열은 생성과 동등 비교 전용입니다. 멤버로 되파싱하지 말고 슬롯
객체는 get()에서 읽으세요. any 슬롯의 표시 라벨은 /로 결합되는데(예:
Z/B) 이는 canonical |와 별개인 UI 관례입니다. 슬롯을 all과 any 사이에서
전환하면 canonical 식별자가 바뀌므로 기존 카운터는 이어지지 않습니다.
canonical 식별자가 쓰이는 표면: onKeyState()의 key, 카운터 맵 키
(getCounters(), onCounterChanged, onCountersChanged),
resetSingleCounter(mode, key), UI 앵커·키 메뉴의 keyCode. 매핑 표면
(get(), update(), onChanged, 에디터 문서, 프리셋 스냅샷)은 반대로
KeySlot union을 그대로 전달합니다.
수제 레거시 데이터에는 문자열 자체에 +나 |가 포함된 단일 키 슬롯(예: 한
문자열 "A+B")이 있을 수 있습니다. 이런 슬롯은 무손실로 보존되지만 데몬
입력으로 활성화될 수 없는 inert 슬롯입니다. 실제 멀티 키 슬롯과 canonical
식별자(그리고 카운터 버킷)를 겉보기로 공유할 수 있으며, 둘이 공존하면 노트
이펙트는 실제 멀티 키 슬롯을 우선합니다.
update(mappings, options?)
키 매핑을 업데이트합니다.
const current = await dmn.keys.get();
current['4key'] = ['S', 'D', 'J', 'K'];
await dmn.keys.update(current);현재 설정에 멀티 키 슬롯이 존재하는 상태에서 매핑을 쓰려면 멀티 키 지원을 명시적으로 선언해야 합니다:
await dmn.keys.update(current, { multiKey: true });선언 없이 쓰면 현재 매핑에 멀티 키 슬롯이 하나라도 있을 때
MULTI_KEY_UNSUPPORTED 오류 코드(비 retryable)로 거절됩니다. 멀티 키 슬롯을
보존하지 않는 플러그인의 왕복 쓰기가 사용자 설정을 파괴하는 것을 막는
fail-closed 게이트입니다. 같은 규칙이 updateWithPositions()와, keys를
포함한 dmn.editor.commit() 요청(커밋 요청의 multiKey: true로 선언)에도
적용됩니다.
getPositions()
모든 키 모드의 위치 정보를 조회합니다.
interface KeyPosition {
id?: string; // 요소 안정 ID (UUID). 앱이 발급·소유
dx: number;
dy: number;
width: number;
height: number;
activeImage?: string;
inactiveImage?: string;
activeTransparent?: boolean;
idleTransparent?: boolean;
imageMode?: 'replace' | 'overlay'; // 이미지 배치. 없으면 replace(이미지가 키를 대체)
idleImageTransform?: { offsetX: number; offsetY: number; rotation: number; scale: number }; // 없으면 identity
activeImageTransform?: { offsetX: number; offsetY: number; rotation: number; scale: number };
count: number;
noteColor: string | { type: 'gradient'; top: string; bottom: string };
noteOpacity: number;
noteOpacityTop?: number;
noteOpacityBottom?: number;
noteGradient?: GradientSpec | null;
noteEffectEnabled: boolean;
noteGlowEnabled: boolean;
noteGlowSyncPaint?: boolean;
noteGlowSize: number;
noteGlowOpacity: number;
noteGlowOpacityTop?: number;
noteGlowOpacityBottom?: number;
noteGlowColor?: string | { type: 'gradient'; top: string; bottom: string };
noteGlowGradient?: GradientSpec | null;
noteBorderColor?: string;
noteBorderGradient?: GradientSpec | null;
noteAutoYCorrection: boolean;
className?: string;
zIndex?: number;
counter: KeyCounterSettings;
backgroundColor?: string;
activeBackgroundColor?: string;
borderColor?: string;
activeBorderColor?: string;
borderWidth?: number; // px, 미지정 시 기본 1. 0이면 테두리 없음
backgroundGradient?: GradientSpec | null;
activeBackgroundGradient?: GradientSpec | null;
borderGradient?: GradientSpec | null;
activeBorderGradient?: GradientSpec | null;
fontColor?: string;
activeFontColor?: string;
fontGradient?: GradientSpec | null;
activeFontGradient?: GradientSpec | null;
shadow?: ElementShadowSpec;
activeShadow?: ElementShadowSpec;
}
interface KeyCounterSettings {
// ...배치, 글자, 애니메이션 필드...
fill: { idle: string; active: string };
fillIdleGradient?: GradientSpec | null;
fillActiveGradient?: GradientSpec | null;
}
interface GradientSpec {
angle: number; // 0 이상 360 미만. 360은 0으로 정규화 (CSS 기준, 0 = 위·시계 방향)
stops: { color: string; pos: number }[]; // 2–8개, pos는 0–1 오름차순
}
interface ElementShadowSpec {
enabled: boolean;
color: string; // 알파값으로 농도 조절
offsetX: number; // px, -100~100
offsetY: number; // px, -100~100
blur: number; // px, 0~100
}모든 요소 위치(keyPositions, statPositions, graphPositions,
knobPositions)는 안정 id를 가집니다. 불투명 값으로 다루세요. 읽은 값을
그대로 되돌려 보내고, 직접 만들지 마세요. id가 없거나 미확인인 쓰기는
백엔드가 새 값을 발급합니다. 같은 id가 여러 번 나오면(읽은 요소를 복사해
덧붙이는 경우처럼) 원래 자리의 요소가 신원을 지키고 나머지 사본이 새 값을
받습니다. 전체 프리셋을 불러오면 모든 id가 재발급되고, 탭 프리셋은 그
프리셋이 위치를 준 컬렉션만 재발급됩니다. 재정렬을 가로질러 요소를 추적할
때는 배열 index 대신 id를 사용하세요.
그라데이션 필드가 있으면 렌더에서 대응 단색 필드보다 우선하며, 저장 시
대응 단색 필드는 첫 스톱 색으로 자동 동기화됩니다. 단색으로 되돌리려면
그라데이션 필드를 null로 두고 단색 필드를 갱신하세요.
카운터 fill 그라데이션도 각각 counter.fill.idle과 counter.fill.active보다
우선합니다.
노트 테두리는 noteBorderGradient가 noteBorderColor보다 우선합니다.
noteBorderColor는 항상 #RRGGBB hex로 유지되므로 그라데이션 저장 시 첫
스톱 색이 hex로 변환되어 동기화됩니다. 스톱 색은 hex(#RGB, #RGBA,
#RRGGBB, #RRGGBBAA) 또는 rgb()/rgba() 형식만 허용합니다.
노트 본체와 글로우도 같은 형식을 지원합니다. noteGradient 또는
noteGlowGradient가 있으면 해당 표면은 신형 의미가 됩니다.
noteOpacity/noteGlowOpacity는 전역 배율(없으면 100)로 동작하고, 구형
필드(top/bottom 두 색 객체와 끝단 투명도)는 구버전 표시용 근사값으로 자동
동기됩니다. 스톱 색 형식 제한은 테두리와 같습니다. 그라데이션 필드를
빼거나 null로 두고 요소를 쓰면 해당 표면은 구형 의미로 되돌아갑니다.
표면을 한 번의 쓰기로 전환하려면 같은 updatePositions 호출에서 그라데이션
필드와 배율을 함께 설정하세요. 단색 필드와 구형 근사값은 저장 시 파생되어
동기화됩니다.
noteGlowSyncPaint가 true이면 글로우 페인트(noteGlowColor,
noteGlowGradient, noteGlowOpacity, noteGlowOpacityTop/Bottom)는 저장
시점에 본체 페인트와 같은 값으로 파생됩니다. 이 상태에서 글로우 필드를
직접 써도 본체 값으로 덮이므로 글로우를 따로 바꾸려면 먼저 false로
두세요. 기본값은 false입니다.
shadow와 activeShadow는 대기·입력 그림자를 각각 제어합니다. 필드를
생략하면 기존 기본 그림자가 유지되며, 그림자를 명시적으로 끄려면
enabled: false인 spec을 저장하세요.
const positions = await dmn.keys.getPositions();
console.log('4key 위치:', positions['4key']);updatePositions(positions)
키 위치 정보를 업데이트합니다.
const current = await dmn.keys.getPositions();
current['4key'][0].dx = 100;
await dmn.keys.updatePositions(current);플러그인의 위치 쓰기는 전용 큐로 직렬화되어 확정 문서 기준으로 정산됩니다.
promise는 커밋과 후속 조회까지 끝난 뒤 resolve되고, 반환 컬렉션에는 앱이
부여한 요소 안정 id가 담깁니다 (id 없이 제출한 요소는 새로 발급됨).
실패 시 원 오류가 그대로 reject되며, 커밋은 성공했는데 후속 조회만 실패했을
가능성이 있으면 무작정 재제출하지 말고 현재 값을 먼저 조회해 확인하세요.
statItems·graphItems·knobItems의 updatePositions도 동일합니다.
updateWithPositions(mappings, positions, options?)
키 매핑과 인덱스로 결합된 위치 정보를 한 번의 원자적 커밋으로 갱신합니다.
두 값의 모드 집합과 모드별 배열 길이는 같아야 합니다. 키를 추가·삭제·재정렬할
때 사용하세요. 커밋된 값이 반환되며, 저장 성공 후 keys:changed와
positions:changed 이벤트가 모두 방출됩니다.
interface KeysWithPositionsResult {
keys: KeyMappings;
positions: KeyPositions;
}
const result = await dmn.keys.updateWithPositions(mappings, positions, {
multiKey: true,
});세 번째 options 인자는 update()와 같은 { multiKey?: boolean } 선언을
받습니다. 선언 없이 쓰면 현재 매핑에 멀티 키 슬롯이 있을 때
MULTI_KEY_UNSUPPORTED로 거절됩니다.
모드 관리
setMode(mode)
현재 활성 키 모드를 변경합니다.
const result = await dmn.keys.setMode('8key');
console.log('모드 변경 성공:', result.success);resetMode(mode)
특정 키 모드와 위치, 해당 모드에 배치된 플러그인 표시 요소를 기본값으로 초기화합니다. 플러그인의 다른 저장 데이터는 유지됩니다.
await dmn.keys.resetMode('4key');resetAll()
모든 키, 위치, 카운터, 커스텀 탭과 플러그인 표시 요소 배치를 기본값으로
초기화합니다. 설치한 플러그인과 그 밖의 플러그인 저장 데이터는 유지됩니다.
초기화 대상은 저장된 defineElement 배치입니다. 플러그인 시작 코드가
displayElement.add()로 직접 만드는 런타임 요소는 플러그인이 유지되는 동안
남거나 다시 생성될 수 있습니다.
const reset = await dmn.keys.resetAll();카운터
getCounters()
현재 누적 키 카운터 스냅샷을 조회합니다. 내부 맵의 키는 canonical 슬롯 식별자입니다.
const counters = await dmn.keys.getCounters();
console.log('4key 카운터:', counters['4key']);setCounters(counters)
모든 키의 카운트를 일괄 설정합니다.
await dmn.keys.setCounters({
'4key': { D: 100, F: 200, J: 150, K: 180 },
});resetCounters()
모든 키의 누적 카운트를 초기화합니다.
await dmn.keys.resetCounters();resetCountersMode(mode)
특정 모드의 키 카운트만 초기화합니다.
await dmn.keys.resetCountersMode('4key');resetSingleCounter(mode, key)
특정 모드의 특정 키 카운트만 초기화합니다.
await dmn.keys.resetSingleCounter('4key', 'D');이벤트 구독
onKeyState(listener)
실시간 키 입력 이벤트를 구독합니다.
interface KeyStatePayload {
key: string; // canonical 슬롯 식별자 (예: "D", "F|NUMPAD 4")
state: string; // "DOWN" | "UP"
mode: string; // 현재 모드
// 입력 수신~emit 경과 시간(ms).
// `performance.now() - eventAgeMs`로 실제 입력 시각 복원
eventAgeMs?: number;
// UP 이벤트 한정. 같은 물리 입력이 canonical DOWN과 UP을 만든 경우
// 입력 데몬이 측정한 물리 눌림 지속 시간(ms). source가 다르거나
// 불명확하면 생략됨
holdDurationMs?: number;
}holdDurationMs가 있으면 전달 지연·지터의 영향을 받지 않습니다. canonical
슬롯은 여러 장치나 멤버 키의 겹친 입력 동안 계속 활성일 수 있으므로, 이 선택적
필드가 없을 때 전체 활성 시간이 필요한 소비자는 같은 canonical의 DOWN과 UP
시각을 직접 추적해야 합니다.
const unsub = dmn.keys.onKeyState(({ key, state, mode }) => {
console.log(`[${mode}] ${key} is ${state}`);
});반환된 함수에는 구독이 백엔드에 실제로 등록된 시점에 resolve되는 ready
프로미스가 함께 노출됩니다. ready 이전에 발생한 이벤트는 전달되지 않을 수
있으므로, 확실한 시작점이 필요하면(예: 스냅샷 요청 전) await 하세요:
const unsub = dmn.keys.onKeyState(handler);
await unsub.ready; // 이 시점부터 구독이 활성onKeysReset(listener)
눌림 상태 전체가 무효화될 때 발화합니다. 예를 들어 글로벌 단축키 변경으로
키보드 훅이 재시작되면 이전 onKeyState 이벤트에서 파생된 상태(눌림 유지,
활성 시각화)는 더 이상 유효하지 않으므로, 정리 후 새 스냅샷으로 재구성하세요.
interface KeysResetPayload {
reason: string; // 예: "hook_restart"
}dmn.keys.onKeysReset(({ reason }) => {
console.log(`키 상태 리셋: ${reason}`);
});onRawInput(listener)
로우 레벨 입력 이벤트를 구독합니다. 키보드, 마우스, HID 기기의 원시 입력을 감지합니다.
interface RawInputPayload {
device: 'keyboard' | 'mouse' | 'gamepad' | 'unknown';
label: string; // "D", "MOUSE1", "HIDB:1ccf:101c:9:3" 등
labels: string[]; // 모든 레이블 목록
state: string; // "DOWN" | "UP"
}const unsub = dmn.keys.onRawInput(({ device, label, state }) => {
console.log(`[${device}] ${label} ${state}`);
});HID 기기 버튼(Windows)은 device: 'gamepad'와
HIDB:vid:pid:usagePage:usage 형식의 레이블로 전달됩니다. HID 축(노브)
요소는 노브 API를 참고하세요.
구독자가 없으면 백엔드에서 이벤트를 emit하지 않아 성능 오버헤드가 없습니다.
아래 onChanged()와 onPositionsChanged()는 기존 플러그인 호환을 위해 계속
제공되지만, 신규 에디터 상태 동기화 용도로는 deprecated입니다. 여러 컬렉션의
원자적 변경은
dmn.editor.onCommitted()로
구독하세요.
onChanged(listener)
키 매핑 변경 이벤트를 구독합니다.
const unsub = dmn.keys.onChanged((mappings) => {
console.log('키 매핑 변경:', mappings);
});onPositionsChanged(listener)
키 위치 변경 이벤트를 구독합니다.
const unsub = dmn.keys.onPositionsChanged((positions) => {
console.log('키 위치 변경:', positions);
});onModeChanged(listener)
레이아웃 모드 변경 이벤트(keys:mode-changed)를 구독합니다. mode는 '4key'
같은 현재 레이아웃 모드 ID입니다.
const unsub = dmn.keys.onModeChanged(({ mode }) => {
console.log('모드 변경:', mode);
});onCounterChanged(listener)
개별 키 카운트 변경 이벤트를 구독합니다.
const unsub = dmn.keys.onCounterChanged(
({ mode, key, count, sessionId, revision }) => {
console.log(
`[${mode}] ${key}: ${count} (${sessionId}, revision ${revision})`,
);
},
);revision은 sessionId 안에서 런타임 카운터 변경의 단조 증가 순서입니다.
bootstrap 스냅샷과 실시간 이벤트를 조정할 때 dmn.app.bootstrap()의
keyCountersSessionId/keyCountersRevision과 함께 비교할 수 있습니다.
onCountersChanged(listener)
전체 키 카운터 변경 이벤트를 구독합니다.
const unsub = dmn.keys.onCountersChanged((counters) => {
console.log('카운터:', counters);
});커스텀 탭 (customTabs)
list()
커스텀 탭 목록을 조회합니다.
const tabs = await dmn.keys.customTabs.list();create(name)
새 커스텀 탭을 생성합니다.
이름은 앞뒤 공백을 다듬은 뒤 검사합니다. 비어 있거나(invalid-name), UTF-16 기준
10자를 넘거나(name-too-long), 4key·5key·6key·8key와 같거나(reserved-name),
이미 쓰는 이름이면(duplicate-name) 거절됩니다. 탭이 30개면 max-reached입니다.
const result = await dmn.keys.customTabs.create('My Keys');
if (result.error) {
// "invalid-name" | "name-too-long" | "reserved-name"
// "duplicate-name" | "max-reached"
console.error(result.error);
} else {
console.log('생성됨:', result.result);
}delete(id)
커스텀 탭을 삭제합니다.
const result = await dmn.keys.customTabs.delete('custom-123');select(id)
커스텀 탭을 선택합니다.
await dmn.keys.customTabs.select('custom-123');restore(customTabs, selectedKeyType)
이미 존재하는 커스텀 탭의 이름과 선택 모드를 원자적으로 복원합니다. 모든
탭 ID에는 짝이 맞는 keys와 keyPositions가 이미 있어야 합니다. 탭 추가·삭제나
키 구조 변경에는 create, delete, updateWithPositions를 사용하세요. 고아 탭
정보나 잘못된 입력은 저장 데이터를 바꾸지 않고 거절됩니다.
표시 순서는 tabOrder가 소유하며 restore는 건드리지 않습니다. 넘긴 배열의
순서는 무시되고 저장된 순서가 그대로 유지됩니다.
await dmn.keys.customTabs.restore(
[{ id: 'custom-123', name: 'My Keys' }],
'custom-123',
);onChanged(listener)
커스텀 탭 변경 이벤트를 구독합니다.
페이로드에는 customTabs와 selectedKeyType 외에 tabOrder, barCount,
selectionAuthoritative가 함께 실립니다. tabOrder는 내장 모드와 커스텀 탭을
합친 표시 순서로 각 탭 ID가 정확히 한 번씩 들어가고, barCount는 그중 툴바 바에
나오는 앞쪽 개수입니다(1~4). 이름 변경과 순서 변경 이벤트에서는
selectionAuthoritative가 false이며, 이때 selectedKeyType은 트랜잭션
스냅샷일 뿐 선택 변경을 뜻하지 않습니다. 선택 모드를 소유하는 이벤트에서는
selectionAuthoritative가 true입니다. list()는 배열만 돌려주므로 표시 순서는
tabOrder로 확인하세요.
const unsub = dmn.keys.customTabs.onChanged(
({
customTabs,
tabOrder,
barCount,
selectedKeyType,
selectionAuthoritative,
}) => {
console.log('선택된 탭:', selectedKeyType);
console.log('바에 있는 탭:', tabOrder.slice(0, barCount));
console.log('이벤트가 선택을 소유함:', selectionAuthoritative);
},
);통계 (stats)
키 입력 통계를 실시간으로 제공하는 API입니다. KPS(Keys Per Second)와 현재 탭의 전체 키 카운트를 조회할 수 있습니다.
구독자가 없으면 통계 계산이 수행되지 않아 성능 오버헤드가 없습니다.
subscribe(listener)
키 입력 통계를 실시간으로 구독합니다.
- KPS 관련 값(kps, kpsAvg, kpsMax): 약 50ms 간격으로 업데이트
- total: 키 카운터 변경 시 즉시 업데이트
interface KeyStatsPayload {
kps: number; // 현재 KPS (초당 키 입력 수)
kpsAvg: number; // 평균 KPS
kpsMax: number; // 최대 KPS
total: number; // 현재 탭의 전체 키 카운트 합계
}const unsub = dmn.stats.subscribe(({ kps, kpsAvg, kpsMax, total }) => {
console.log(`KPS: ${kps}, AVG: ${kpsAvg}, MAX: ${kpsMax}, TOTAL: ${total}`);
});
// 구독 해제
unsub();get()
현재 통계값을 즉시 조회합니다.
const stats = dmn.stats.get();
console.log(`현재 KPS: ${stats.kps}`);reset()
KPS 관련 통계(kps, kpsAvg, kpsMax)를 초기화합니다. total은 키 카운터 기반이므로 초기화되지 않습니다.
dmn.stats.reset();total을 초기화하려면 dmn.keys.resetCounters() 또는
dmn.keys.resetCountersMode(mode)를 사용하세요.