diff --git a/proto/cormoran/zmk/custom_settings/custom_settings.proto b/proto/cormoran/zmk/custom_settings/custom_settings.proto
new file mode 100644
index 0000000..24f98bd
--- /dev/null
+++ b/proto/cormoran/zmk/custom_settings/custom_settings.proto
@@ -0,0 +1,204 @@
+syntax = "proto3";
+
+package cormoran.zmk.custom_settings;
+
+enum SettingWriteMode {
+ // Update RAM only. The value can later be saved or discarded.
+ SETTING_WRITE_MODE_MEMORY = 0;
+ // Update RAM and save the value to persistent storage.
+ SETTING_WRITE_MODE_PERSIST = 1;
+}
+
+enum SettingConfidentiality {
+ // Do not expose the setting value over RPC.
+ SETTING_CONFIDENTIALITY_DEVICE_PRIVATE = 0;
+ // RPC may expose the value, but the web UI should treat it as personal data.
+ SETTING_CONFIDENTIALITY_RPC_PERSONAL = 1;
+ // RPC may expose the value and the web UI may publish it.
+ SETTING_CONFIDENTIALITY_RPC_PUBLIC = 2;
+}
+
+enum SettingPermission {
+ SETTING_PERMISSION_UNSECURE = 0;
+ SETTING_PERMISSION_SECURE = 1;
+}
+
+enum SettingNotificationKind {
+ SETTING_NOTIFICATION_KIND_LIST_ITEM = 0;
+ SETTING_NOTIFICATION_KIND_VALUE_UPDATED = 1;
+ SETTING_NOTIFICATION_KIND_SAVED = 2;
+ SETTING_NOTIFICATION_KIND_DISCARDED = 3;
+ SETTING_NOTIFICATION_KIND_RESET = 4;
+}
+
+message SettingScalarValue {
+ oneof value_type {
+ bytes bytes_value = 1;
+ int32 int32_value = 2;
+ bool bool_value = 3;
+ string string_value = 4;
+ }
+}
+
+// Array values encode one active element plus the active array length.
+message SettingArrayValue {
+ uint32 index = 1;
+ uint32 size = 2;
+ SettingScalarValue value = 3;
+ reserved 4;
+}
+
+// One setting value. Array elements wrap a scalar with array metadata.
+message SettingValue {
+ oneof value_type {
+ bytes bytes_value = 1;
+ int32 int32_value = 2;
+ bool bool_value = 3;
+ string string_value = 4;
+ SettingArrayValue array_value = 5;
+ }
+}
+
+message SettingConstraintRange {
+ SettingScalarValue min = 1;
+ SettingScalarValue max = 2;
+}
+
+message SettingConstraintOptions {
+ repeated SettingScalarValue values = 1;
+ repeated string labels = 2;
+}
+
+message SettingConstraintHidUsage {
+ uint32 usage_page = 1;
+ uint32 usage_min = 2;
+ uint32 usage_max = 3;
+}
+
+message SettingConstraintLayerId {}
+
+message SettingConstraintBehaviorId {}
+
+message SettingConstraint {
+ oneof constraint_type {
+ SettingConstraintRange range = 1;
+ SettingConstraintOptions options = 2;
+ SettingConstraintHidUsage hid_usage = 3;
+ SettingConstraintLayerId layer_id = 4;
+ SettingConstraintBehaviorId behavior_id = 5;
+ }
+}
+
+// References one setting. custom_subsystem_index is the Studio custom subsystem index.
+message SettingRef {
+ optional uint32 custom_subsystem_index = 1;
+ optional string key = 2;
+ // Omitted source means local side only. UINT32_MAX means all split sides.
+ optional uint32 source = 3;
+ // Optional index for array settings when value.array_value is not used.
+ optional uint32 array_index = 4;
+}
+
+// Selects a group of settings for list/save/discard/reset.
+message SettingScope {
+ optional uint32 custom_subsystem_index = 1;
+ optional string key = 2;
+ optional string key_prefix = 3;
+ // Omitted source means local side only. UINT32_MAX means all split sides.
+ optional uint32 source = 4;
+}
+
+// Static setting metadata included only when requests set require_meta.
+message SettingMeta {
+ SettingConfidentiality confidentiality = 1;
+ SettingPermission read_permission = 2;
+ SettingPermission write_permission = 3;
+ repeated SettingConstraint constraints = 4;
+}
+
+message Setting {
+ uint32 custom_subsystem_index = 1;
+ string key = 2;
+ // Present only when the request asked for metadata.
+ optional SettingMeta meta = 3;
+ bool has_unsaved_value = 8;
+ // Omitted for DEVICE_PRIVATE settings and secure settings while locked.
+ SettingValue value = 9;
+ uint32 source = 10;
+}
+
+message ListSettingsRequest {
+ SettingScope scope = 1;
+ // Include static metadata in each list item notification.
+ bool require_meta = 2;
+}
+
+message GetSettingRequest {
+ SettingRef setting = 1;
+ // Include static metadata in the get response.
+ bool require_meta = 2;
+}
+
+message WriteSettingRequest {
+ SettingRef setting = 1;
+ SettingValue value = 2;
+ SettingWriteMode mode = 3;
+}
+
+// Append one scalar value to the end of an array setting.
+message PushBackArrayRequest {
+ SettingRef setting = 1;
+ SettingScalarValue value = 2;
+ SettingWriteMode mode = 3;
+}
+
+// Remove the last active value from an array setting.
+message PopBackArrayRequest {
+ SettingRef setting = 1;
+ SettingWriteMode mode = 2;
+}
+
+message SaveSettingsRequest { SettingScope scope = 1; }
+
+message DiscardSettingsRequest { SettingScope scope = 1; }
+
+message ResetSettingsRequest { SettingScope scope = 1; }
+
+message Request {
+ oneof request_type {
+ ListSettingsRequest list_settings = 1;
+ GetSettingRequest get_setting = 2;
+ WriteSettingRequest write_setting = 3;
+ SaveSettingsRequest save_settings = 4;
+ DiscardSettingsRequest discard_settings = 5;
+ ResetSettingsRequest reset_settings = 6;
+ PushBackArrayRequest push_back_array = 7;
+ PopBackArrayRequest pop_back_array = 8;
+ }
+}
+
+message ErrorResponse { string message = 1; }
+
+message StatusResponse {
+ uint32 affected_count = 1;
+ string message = 2;
+}
+
+message GetSettingResponse { Setting setting = 1; }
+
+message Response {
+ oneof response_type {
+ ErrorResponse error = 1;
+ StatusResponse status = 2;
+ GetSettingResponse get_setting = 3;
+ }
+}
+
+message SettingNotification {
+ SettingNotificationKind kind = 1;
+ Setting setting = 2;
+}
+
+message Notification {
+ oneof notification_type { SettingNotification setting = 1; }
+}
diff --git a/src/components/CustomSettingsSection.tsx b/src/components/CustomSettingsSection.tsx
new file mode 100644
index 0000000..15abe4c
--- /dev/null
+++ b/src/components/CustomSettingsSection.tsx
@@ -0,0 +1,980 @@
+import { useContext, useMemo, useState } from "react";
+import * as Dialog from "@radix-ui/react-dialog";
+import { IconRefresh, IconRotate, IconX } from "@tabler/icons-react";
+import {
+ CUSTOM_SETTINGS_SOURCE_ALL,
+ type UseCustomSettingsReturn,
+} from "../hooks/useCustomSettings";
+import { useDebouncedSave } from "../hooks/useDebouncedSave";
+import { useKeymap } from "../hooks/useKeymap";
+import { KeyboardLayoutContext } from "../contexts/KeyboardLayoutContext";
+import { KeycodeValueSelector } from "./KeycodeValueSelector";
+import {
+ type Setting,
+ type SettingConstraint,
+ type SettingConstraintHidUsage,
+ type SettingConstraintOptions,
+ type SettingConstraintRange,
+ type SettingScalarValue,
+ type SettingValue,
+} from "../proto/cormoran/zmk/custom_settings/custom_settings";
+import { formatKeycodeWithModifiers } from "../lib/keycodes";
+
+interface CustomSettingsSectionProps {
+ customSettings: UseCustomSettingsReturn;
+}
+
+interface CustomSettingGroup {
+ id: string;
+ subsystemLabel: string;
+ key: string;
+ settings: Setting[];
+}
+
+interface CustomSubsystemGroup {
+ id: string;
+ label: string;
+ customSubsystemIndex: number;
+ settings: CustomSettingGroup[];
+}
+
+type ScalarValueKind = "bytes" | "int32" | "bool" | "string";
+
+export function CustomSettingsSection({
+ customSettings,
+}: CustomSettingsSectionProps) {
+ const {
+ isAvailable,
+ settings,
+ isLoading,
+ error,
+ loadSettings,
+ updateSettingMemory,
+ saveSubsystemSettings,
+ discardSubsystemSettings,
+ resetSubsystemSettings,
+ subsystemIdentifierForIndex,
+ } = customSettings;
+ const { keymap } = useKeymap();
+ const { layout: keyboardLayout } = useContext(KeyboardLayoutContext);
+ const layers = keymap?.layers ?? [];
+
+ const groupedSettings = useMemo(
+ () => groupSettings(settings, subsystemIdentifierForIndex),
+ [settings, subsystemIdentifierForIndex],
+ );
+
+ if (!isAvailable) {
+ return null;
+ }
+
+ return (
+
+
+
+ Custom Settings
+
+
+
+
+ {error && (
+
+ )}
+
+ {isLoading && settings.length === 0 ? (
+
+ Loading custom settings...
+
+ ) : groupedSettings.length > 0 ? (
+
+ {groupedSettings.map((subsystem) => (
+
+
+
+ {subsystem.label}
+
+
+
+
+ {subsystem.settings.map((group) => (
+
+ ))}
+
+
+ ))}
+
+ ) : (
+
+ No custom settings are exposed by this keyboard.
+
+ )}
+
+ );
+}
+
+function SubsystemActions({
+ subsystem,
+ isLoading,
+ onSave,
+ onDiscard,
+ onReset,
+}: {
+ subsystem: CustomSubsystemGroup;
+ isLoading: boolean;
+ onSave: (customSubsystemIndex: number) => Promise;
+ onDiscard: (customSubsystemIndex: number) => Promise;
+ onReset: (customSubsystemIndex: number) => Promise;
+}) {
+ const [confirmReset, setConfirmReset] = useState(false);
+ const hasChanges = subsystem.settings.some((group) =>
+ group.settings.some((setting) => setting.hasUnsavedValue),
+ );
+
+ return (
+
+
+ {subsystem.settings.length} settings
+
+
+
+ {!confirmReset ? (
+
+ ) : (
+
+
+
+
+ )}
+
+ );
+}
+
+function CustomSettingRow({
+ group,
+ isLoading,
+ layers,
+ keyboardLayout,
+ onUpdate,
+}: {
+ group: CustomSettingGroup;
+ isLoading: boolean;
+ layers: Array<{ id: number; name: string }>;
+ keyboardLayout: Parameters[1];
+ onUpdate: (
+ setting: Setting,
+ value: SettingValue,
+ source: number,
+ ) => Promise;
+}) {
+ const arrayIndices = getArrayIndices(group.settings);
+ const [selectedSource, setSelectedSource] = useState(
+ CUSTOM_SETTINGS_SOURCE_ALL,
+ );
+ const [selectedArrayIndexState, setSelectedArrayIndex] = useState(
+ arrayIndices[0] ?? 0,
+ );
+ const selectedArrayIndex = arrayIndices.includes(selectedArrayIndexState)
+ ? selectedArrayIndexState
+ : (arrayIndices[0] ?? 0);
+ const selectedSetting = getSelectedSetting(
+ group.settings,
+ selectedArrayIndex,
+ selectedSource,
+ );
+ const selectedScalar = scalarFromSetting(selectedSetting);
+ const selectedKind = scalarValueKind(selectedScalar);
+ const layerConstraint = selectedSetting?.meta?.constraints.some(
+ (constraint) => constraint.layerId,
+ );
+ const selectedValueKey = `${selectedSetting?.source ?? "none"}:${
+ selectedSetting?.customSubsystemIndex ?? "none"
+ }:${selectedSetting?.key ?? "none"}:${selectedArrayIndex}:${
+ selectedScalar ? scalarValueToInput(selectedScalar) : ""
+ }`;
+ const initialValueText = selectedScalar
+ ? scalarValueToInput(selectedScalar)
+ : "";
+ const [valueDraft, setValueDraft] = useState({ key: "", value: "" });
+ const valueText =
+ valueDraft.key === selectedValueKey ? valueDraft.value : initialValueText;
+ const setValueText = (value: string) =>
+ setValueDraft({ key: selectedValueKey, value });
+ const debouncedSave = useDebouncedSave({
+ delay: 500,
+ savedStatusDuration: 800,
+ });
+
+ const sourceOptions = getSourceOptions(group.settings);
+ const optionConstraint = getOptionsConstraint(selectedSetting);
+ const rangeConstraint = getRangeConstraint(selectedSetting);
+ const hidConstraint = selectedSetting?.meta?.constraints.find(
+ (constraint) => constraint.hidUsage,
+ )?.hidUsage;
+ const validationMessage = validateInput(
+ valueText,
+ selectedKind,
+ selectedSetting?.meta?.constraints ?? [],
+ );
+ const isHidden = selectedSetting !== undefined && !selectedSetting.value;
+ const canUpdate =
+ selectedSetting !== undefined &&
+ selectedKind !== undefined &&
+ !isHidden &&
+ !validationMessage &&
+ !isLoading;
+ const hasPendingChange = group.settings.some(
+ (setting) => setting.hasUnsavedValue,
+ );
+
+ const commitValue = async (nextText: string) => {
+ const nextValidation = validateInput(
+ nextText,
+ selectedKind,
+ selectedSetting?.meta?.constraints ?? [],
+ );
+ if (!selectedSetting || !selectedKind || nextValidation) {
+ return;
+ }
+
+ const scalarValue = parseScalarValue(nextText, selectedKind);
+ const arrayValue = selectedSetting.value?.arrayValue;
+ const value: SettingValue = arrayValue
+ ? {
+ arrayValue: {
+ index: arrayValue.index,
+ size: arrayValue.size,
+ value: scalarValue,
+ },
+ }
+ : scalarValue;
+
+ await onUpdate(selectedSetting, value, selectedSource);
+ };
+
+ const handleInputChange = (nextText: string) => {
+ setValueText(nextText);
+ if (!selectedSetting || !selectedKind) {
+ return;
+ }
+ const nextValidation = validateInput(
+ nextText,
+ selectedKind,
+ selectedSetting.meta?.constraints ?? [],
+ );
+ if (!nextValidation) {
+ debouncedSave.setPendingValue(nextText, commitValue);
+ }
+ };
+
+ const handleImmediateChange = (nextText: string) => {
+ debouncedSave.cancel();
+ setValueText(nextText);
+ void commitValue(nextText);
+ };
+
+ return (
+
+
+
+
+
+ {group.key}
+ {arrayIndices.length > 0 ? "[]" : ""}
+
+ {group.settings.some((setting) => setting.hasUnsavedValue) && (
+
+ unsaved
+
+ )}
+
+
+ {getValuesBySource(group.settings).map((entry) => (
+
+
+ {formatSource(entry.source)}
+
+
+ {entry.value}
+ {entry.hasUnsavedValue && (
+
+ pending
+
+ )}
+
+
+ ))}
+
+
+
+
+
+
+
+ {arrayIndices.length > 0 ? (
+
+ ) : (
+
+ Type
+
+ {selectedKind ?? "hidden"}
+
+
+ )}
+
+
+
+
+ {constraintSummary(selectedSetting) && (
+
+ {constraintSummary(selectedSetting)}
+
+ )}
+ {hidConstraint && (
+
+ HID page {hidConstraint.usagePage}, usage{" "}
+ {hidConstraint.usageMin}-{hidConstraint.usageMax}
+
+ )}
+ {validationMessage && (
+
+ {validationMessage}
+
+ )}
+
+
+ {!canUpdate && validationMessage && (
+
+ Fix the value before it can be updated.
+
+ )}
+
+
+
+ );
+}
+
+function renderValueInput({
+ valueText,
+ selectedKind,
+ optionConstraint,
+ rangeConstraint,
+ hidConstraint,
+ layerConstraint,
+ layers,
+ keyboardLayout,
+ isHidden,
+ onChange,
+ onImmediateChange,
+}: {
+ valueText: string;
+ selectedKind: ScalarValueKind | undefined;
+ optionConstraint: SettingConstraintOptions | undefined;
+ rangeConstraint: SettingConstraintRange | undefined;
+ hidConstraint: SettingConstraintHidUsage | undefined;
+ layerConstraint: boolean | undefined;
+ layers: Array<{ id: number; name: string }>;
+ keyboardLayout: Parameters[1];
+ isHidden: boolean;
+ onChange: (value: string) => void;
+ onImmediateChange: (value: string) => void;
+}) {
+ if (isHidden || !selectedKind) {
+ return (
+
+ );
+ }
+
+ if (optionConstraint) {
+ return (
+
+ );
+ }
+
+ if (selectedKind === "bool") {
+ return (
+
+ );
+ }
+
+ if (selectedKind === "int32") {
+ if (layerConstraint) {
+ return (
+
+ );
+ }
+
+ if (hidConstraint) {
+ return (
+
+ );
+ }
+
+ return (
+ onChange(event.target.value)}
+ />
+ );
+ }
+
+ return (
+ onChange(event.target.value)}
+ />
+ );
+}
+
+function HidUsagePicker({
+ valueText,
+ hidConstraint,
+ keyboardLayout,
+ onChange,
+}: {
+ valueText: string;
+ hidConstraint: SettingConstraintHidUsage;
+ keyboardLayout: Parameters[1];
+ onChange: (value: string) => void;
+}) {
+ const [open, setOpen] = useState(false);
+ const value = Number(valueText) || 0;
+ const formatted = formatKeycodeWithModifiers(value, keyboardLayout);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ Select HID Usage
+
+
+ Page {hidConstraint.usagePage}, usage {hidConstraint.usageMin}
+ -{hidConstraint.usageMax}
+
+
+
+
+
+
+
+ {
+ onChange(`${nextValue}`);
+ if (!shouldNotClose) {
+ setOpen(false);
+ }
+ }}
+ />
+
+
+
+
+ >
+ );
+}
+
+function groupSettings(
+ settings: Setting[],
+ subsystemIdentifierForIndex: (index: number) => string,
+): CustomSubsystemGroup[] {
+ const subsystemMap = new Map();
+
+ for (const setting of settings) {
+ const subsystemLabel = subsystemIdentifierForIndex(
+ setting.customSubsystemIndex,
+ );
+ const subsystemId = `${setting.customSubsystemIndex}:${subsystemLabel}`;
+ const subsystemGroup =
+ subsystemMap.get(subsystemId) ??
+ ({
+ id: subsystemId,
+ label: subsystemLabel,
+ customSubsystemIndex: setting.customSubsystemIndex,
+ settings: [],
+ } satisfies CustomSubsystemGroup);
+ subsystemMap.set(subsystemId, subsystemGroup);
+
+ const settingId = `${subsystemId}:${setting.key}`;
+ let settingGroup = subsystemGroup.settings.find(
+ (candidate) => candidate.id === settingId,
+ );
+ if (!settingGroup) {
+ settingGroup = {
+ id: settingId,
+ subsystemLabel,
+ key: setting.key,
+ settings: [],
+ };
+ subsystemGroup.settings.push(settingGroup);
+ }
+ settingGroup.settings.push(setting);
+ }
+
+ return Array.from(subsystemMap.values()).map((subsystem) => ({
+ ...subsystem,
+ settings: subsystem.settings.sort((a, b) => a.key.localeCompare(b.key)),
+ }));
+}
+
+function getSourceOptions(settings: Setting[]): number[] {
+ return [
+ CUSTOM_SETTINGS_SOURCE_ALL,
+ ...Array.from(new Set(settings.map((setting) => setting.source))).sort(
+ (a, b) => sourceSortValue(a) - sourceSortValue(b),
+ ),
+ ];
+}
+
+function getArrayIndices(settings: Setting[]): number[] {
+ return Array.from(
+ new Set(
+ settings
+ .map((setting) => setting.value?.arrayValue?.index)
+ .filter((index): index is number => index !== undefined),
+ ),
+ ).sort((a, b) => a - b);
+}
+
+function getSelectedSetting(
+ settings: Setting[],
+ selectedArrayIndex: number,
+ selectedSource: number,
+): Setting | undefined {
+ const candidates = settings.filter((setting) => {
+ const arrayIndex = setting.value?.arrayValue?.index;
+ return arrayIndex === undefined || arrayIndex === selectedArrayIndex;
+ });
+
+ if (selectedSource !== CUSTOM_SETTINGS_SOURCE_ALL) {
+ const sourceSetting = candidates.find(
+ (setting) => setting.source === selectedSource,
+ );
+ if (sourceSetting) {
+ return sourceSetting;
+ }
+ }
+
+ return candidates.find((setting) => setting.source === 0) ?? candidates[0];
+}
+
+function getValuesBySource(
+ settings: Setting[],
+): { source: number; value: string; hasUnsavedValue: boolean }[] {
+ const bySource = new Map();
+
+ for (const setting of settings) {
+ const sourceSettings = bySource.get(setting.source) ?? [];
+ sourceSettings.push(setting);
+ bySource.set(setting.source, sourceSettings);
+ }
+
+ return Array.from(bySource.entries())
+ .sort((a, b) => sourceSortValue(a[0]) - sourceSortValue(b[0]))
+ .map(([source, sourceSettings]) => ({
+ source,
+ value: formatSourceSettings(sourceSettings),
+ hasUnsavedValue: sourceSettings.some(
+ (setting) => setting.hasUnsavedValue,
+ ),
+ }));
+}
+
+function formatSourceSettings(settings: Setting[]): string {
+ const sorted = [...settings].sort(
+ (a, b) =>
+ (a.value?.arrayValue?.index ?? -1) - (b.value?.arrayValue?.index ?? -1),
+ );
+
+ if (sorted.some((setting) => setting.value?.arrayValue)) {
+ return sorted
+ .map((setting) =>
+ setting.value?.arrayValue
+ ? `[${setting.value.arrayValue.index}] ${formatScalarValue(
+ setting.value.arrayValue.value ?? {},
+ )}`
+ : formatValue(setting.value),
+ )
+ .join(", ");
+ }
+
+ return formatValue(sorted[0]?.value);
+}
+
+function formatValue(value: SettingValue | undefined): string {
+ if (!value) {
+ return "(hidden)";
+ }
+ return formatScalarValue(value.arrayValue?.value ?? value);
+}
+
+function scalarFromSetting(
+ setting: Setting | undefined,
+): SettingScalarValue | undefined {
+ return setting?.value?.arrayValue?.value ?? setting?.value;
+}
+
+function scalarValueKind(
+ value: SettingScalarValue | undefined,
+): ScalarValueKind | undefined {
+ if (!value) {
+ return undefined;
+ }
+ if (value.int32Value !== undefined) return "int32";
+ if (value.boolValue !== undefined) return "bool";
+ if (value.stringValue !== undefined) return "string";
+ if (value.bytesValue !== undefined) return "bytes";
+ return undefined;
+}
+
+function scalarValueToInput(value: SettingScalarValue): string {
+ if (value.int32Value !== undefined) return `${value.int32Value}`;
+ if (value.boolValue !== undefined) return value.boolValue ? "true" : "false";
+ if (value.stringValue !== undefined) return value.stringValue;
+ if (value.bytesValue !== undefined) return formatBytesValue(value.bytesValue);
+ return "";
+}
+
+function formatScalarValue(value: SettingScalarValue): string {
+ if (value.int32Value !== undefined) return `${value.int32Value}`;
+ if (value.boolValue !== undefined) return value.boolValue ? "true" : "false";
+ if (value.stringValue !== undefined) return value.stringValue;
+ if (value.bytesValue !== undefined) return formatBytesValue(value.bytesValue);
+ return "";
+}
+
+function parseScalarValue(
+ value: string,
+ kind: ScalarValueKind,
+): SettingScalarValue {
+ switch (kind) {
+ case "bytes":
+ return { bytesValue: parseBytesValue(value) };
+ case "bool":
+ return { boolValue: value === "true" || value === "1" };
+ case "string":
+ return { stringValue: value };
+ case "int32":
+ return { int32Value: Number.parseInt(value, 10) };
+ }
+}
+
+function parseBytesValue(value: string): Uint8Array {
+ const trimmed = value.trim();
+ if (trimmed.length === 0) return new Uint8Array();
+
+ const tokens = trimmed.split(/[\s,]+/).filter(Boolean);
+ if (
+ tokens.length > 0 &&
+ tokens.every((token) => /^[0-9a-fA-F]{1,2}$/.test(token))
+ ) {
+ return Uint8Array.from(tokens.map((token) => Number.parseInt(token, 16)));
+ }
+
+ return new TextEncoder().encode(value);
+}
+
+function formatBytesValue(value: Uint8Array): string {
+ return Array.from(value)
+ .map((byte) => byte.toString(16).padStart(2, "0"))
+ .join(" ");
+}
+
+function getOptionsConstraint(
+ setting: Setting | undefined,
+): SettingConstraintOptions | undefined {
+ return setting?.meta?.constraints.find((constraint) => constraint.options)
+ ?.options;
+}
+
+function getRangeConstraint(
+ setting: Setting | undefined,
+): SettingConstraintRange | undefined {
+ return setting?.meta?.constraints.find((constraint) => constraint.range)
+ ?.range;
+}
+
+function validateInput(
+ value: string,
+ kind: ScalarValueKind | undefined,
+ constraints: SettingConstraint[],
+): string | null {
+ if (!kind) {
+ return null;
+ }
+
+ const options = constraints.find((constraint) => constraint.options)?.options;
+ if (options) {
+ const allowed = new Set(options.values.map(scalarValueToInput));
+ if (!allowed.has(value)) {
+ return "Value is not one of the allowed options.";
+ }
+ }
+
+ if (kind === "int32") {
+ const intValue = Number(value);
+ if (!Number.isInteger(intValue)) {
+ return "Value must be an integer.";
+ }
+
+ const range = constraints.find((constraint) => constraint.range)?.range;
+ const min = range?.min?.int32Value;
+ const max = range?.max?.int32Value;
+ if (min !== undefined && intValue < min) {
+ return `Value must be at least ${min}.`;
+ }
+ if (max !== undefined && intValue > max) {
+ return `Value must be at most ${max}.`;
+ }
+
+ const hidUsage = constraints.find(
+ (constraint) => constraint.hidUsage,
+ )?.hidUsage;
+ if (hidUsage) {
+ const usagePage = (intValue >>> 16) & 0xffff;
+ const usage = intValue & 0xffff;
+ if (
+ usagePage !== hidUsage.usagePage ||
+ usage < hidUsage.usageMin ||
+ usage > hidUsage.usageMax
+ ) {
+ return "Value must match the HID usage constraint.";
+ }
+ }
+ }
+
+ if (kind === "string" && new TextEncoder().encode(value).length > 64) {
+ return "Value must be 64 bytes or fewer.";
+ }
+
+ if (kind === "bytes" && parseBytesValue(value).length > 64) {
+ return "Value must be 64 bytes or fewer.";
+ }
+
+ return null;
+}
+
+function constraintSummary(setting: Setting | undefined): string | null {
+ const constraints = setting?.meta?.constraints ?? [];
+ const range = constraints.find((constraint) => constraint.range)?.range;
+ if (
+ range?.min?.int32Value !== undefined &&
+ range.max?.int32Value !== undefined
+ ) {
+ return `Range ${range.min.int32Value}-${range.max.int32Value}`;
+ }
+
+ const options = constraints.find((constraint) => constraint.options)?.options;
+ if (options) {
+ return `${options.values.length} options`;
+ }
+
+ if (constraints.some((constraint) => constraint.layerId)) {
+ return "Layer ID";
+ }
+
+ if (constraints.some((constraint) => constraint.behaviorId)) {
+ return "Behavior ID";
+ }
+
+ return null;
+}
+
+function formatSource(source: number): string {
+ if (source === CUSTOM_SETTINGS_SOURCE_ALL) return "All";
+ if (source === 0) return "Central";
+ return `Peripheral ${source}`;
+}
+
+function sourceSortValue(source: number): number {
+ if (source === 0) return -1;
+ if (source === CUSTOM_SETTINGS_SOURCE_ALL) return Number.MAX_SAFE_INTEGER;
+ return source;
+}
diff --git a/src/components/KeycodeValueSelector.tsx b/src/components/KeycodeValueSelector.tsx
index b29b0a0..5d2bdaf 100644
--- a/src/components/KeycodeValueSelector.tsx
+++ b/src/components/KeycodeValueSelector.tsx
@@ -12,7 +12,10 @@ import {
type KeycodeCategory,
type KeycodeDefinition,
HID_USAGE_PAGE_KEYBOARD,
+ MAX_BASIC_KEYCODE,
createHidUsage,
+ getHidUsagePage,
+ getHidUsageCode,
MODIFIER_FLAGS,
NO_PARAM_VALUE,
extractModifierFlags,
@@ -45,6 +48,11 @@ interface KeycodeValueSelectorProps {
onChange: (value: number, shouldNotClose?: boolean) => void;
showModifiers?: boolean;
keyboardLayout?: KeyboardLayoutType;
+ hidUsageConstraint?: {
+ usagePage: number;
+ usageMin: number;
+ usageMax: number;
+ };
}
export function KeycodeValueSelector({
@@ -52,6 +60,7 @@ export function KeycodeValueSelector({
onChange,
keyboardLayout,
showModifiers = true,
+ hidUsageConstraint,
}: KeycodeValueSelectorProps) {
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] =
@@ -87,11 +96,18 @@ export function KeycodeValueSelector({
}
}, []);
const filteredKeycodes = useMemo((): KeycodeDefinition[] => {
- if (searchQuery.trim()) {
- return searchKeycodes(searchQuery, keyboardLayout);
+ const keycodes = searchQuery.trim()
+ ? searchKeycodes(searchQuery, keyboardLayout)
+ : getKeycodesByCategory(selectedCategory, keyboardLayout);
+
+ if (!hidUsageConstraint) {
+ return keycodes;
}
- return getKeycodesByCategory(selectedCategory, keyboardLayout);
- }, [searchQuery, selectedCategory, keyboardLayout]);
+
+ return keycodes.filter((keycode) =>
+ matchesHidUsageConstraint(keycode, hidUsageConstraint),
+ );
+ }, [searchQuery, selectedCategory, keyboardLayout, hidUsageConstraint]);
const handleKeycodeSelect = useCallback(
(keycode: KeycodeDefinition) => {
@@ -260,3 +276,20 @@ export function KeycodeValueSelector({
);
}
+
+function matchesHidUsageConstraint(
+ keycode: KeycodeDefinition,
+ constraint: NonNullable,
+): boolean {
+ const usage =
+ keycode.code <= MAX_BASIC_KEYCODE
+ ? createHidUsage(HID_USAGE_PAGE_KEYBOARD, keycode.code)
+ : keycode.code;
+ const usagePage = getHidUsagePage(usage);
+ const usageCode = getHidUsageCode(usage);
+ return (
+ usagePage === constraint.usagePage &&
+ usageCode >= constraint.usageMin &&
+ usageCode <= constraint.usageMax
+ );
+}
diff --git a/src/hooks/__tests__/useCustomSettings.test.tsx b/src/hooks/__tests__/useCustomSettings.test.tsx
new file mode 100644
index 0000000..78927d4
--- /dev/null
+++ b/src/hooks/__tests__/useCustomSettings.test.tsx
@@ -0,0 +1,142 @@
+import { renderHook, act } from "@testing-library/react";
+import type { ReactNode } from "react";
+import { ZMKAppContext } from "@cormoran/zmk-studio-react-hook";
+import { useCustomSettings } from "../useCustomSettings";
+import {
+ Notification,
+ Response,
+ SettingNotificationKind,
+} from "../../proto/cormoran/zmk/custom_settings/custom_settings";
+
+const mockCallRPC = jest.fn();
+const mockOnNotification = jest.fn();
+
+jest.mock("@cormoran/zmk-studio-react-hook", () => ({
+ ...jest.requireActual("@cormoran/zmk-studio-react-hook"),
+ ZMKCustomSubsystem: jest.fn().mockImplementation(() => ({
+ callRPC: mockCallRPC,
+ })),
+}));
+
+function createWrapper(zmkAppValue: {
+ state: {
+ connection: unknown;
+ customSubsystems: unknown[];
+ };
+ findSubsystem: (id: string) => { index: number; identifier: string } | null;
+ onNotification: (subscription: unknown) => () => void;
+}) {
+ return function Wrapper({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ );
+ };
+}
+
+function listItemNotification({
+ key,
+ source,
+ value,
+}: {
+ key: string;
+ source: number;
+ value: number;
+}) {
+ return {
+ payload: Notification.encode(
+ Notification.create({
+ setting: {
+ kind: SettingNotificationKind.SETTING_NOTIFICATION_KIND_LIST_ITEM,
+ setting: {
+ customSubsystemIndex: 2,
+ key,
+ source,
+ hasUnsavedValue: false,
+ value: { int32Value: value },
+ },
+ },
+ }),
+ ).finish(),
+ };
+}
+
+describe("useCustomSettings", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it("waits for quiet notifications before finishing an all-source list", async () => {
+ const response = Response.create({
+ status: { affectedCount: 1, message: "OK" },
+ });
+ mockCallRPC.mockResolvedValue(Response.encode(response).finish());
+
+ let notificationCallback:
+ | ((notification: { payload: Uint8Array }) => void)
+ | null = null;
+ mockOnNotification.mockImplementation(
+ (subscription: {
+ callback: (notification: { payload: Uint8Array }) => void;
+ }) => {
+ notificationCallback = subscription.callback;
+ return () => {};
+ },
+ );
+
+ const wrapper = createWrapper({
+ state: {
+ connection: { isConnected: true },
+ customSubsystems: [
+ { index: 0, identifier: "cormoran_custom_settings" },
+ ],
+ },
+ findSubsystem: (id: string) =>
+ id === "cormoran_custom_settings"
+ ? { index: 0, identifier: "cormoran_custom_settings" }
+ : null,
+ onNotification: mockOnNotification,
+ });
+
+ const { result } = renderHook(() => useCustomSettings(), { wrapper });
+
+ await act(async () => {
+ await Promise.resolve();
+ });
+
+ expect(mockCallRPC).toHaveBeenCalledTimes(1);
+ expect(notificationCallback).not.toBeNull();
+
+ await act(async () => {
+ notificationCallback?.(
+ listItemNotification({ key: "feature/value", source: 0, value: 10 }),
+ );
+ jest.advanceTimersByTime(1000);
+ });
+
+ expect(result.current.settings).toHaveLength(0);
+
+ await act(async () => {
+ notificationCallback?.(
+ listItemNotification({ key: "feature/value", source: 1, value: 20 }),
+ );
+ jest.advanceTimersByTime(1499);
+ });
+
+ expect(result.current.settings).toHaveLength(0);
+
+ await act(async () => {
+ jest.advanceTimersByTime(1);
+ });
+
+ expect(result.current.settings.map((setting) => setting.source)).toEqual([
+ 0, 1,
+ ]);
+ });
+});
diff --git a/src/hooks/useCustomSettings.ts b/src/hooks/useCustomSettings.ts
new file mode 100644
index 0000000..6b6bdd4
--- /dev/null
+++ b/src/hooks/useCustomSettings.ts
@@ -0,0 +1,459 @@
+import { useCallback, useContext, useEffect, useMemo, useState } from "react";
+import {
+ ZMKCustomSubsystem,
+ ZMKAppContext,
+} from "@cormoran/zmk-studio-react-hook";
+import {
+ Notification,
+ Request,
+ Response,
+ Setting,
+ SettingNotificationKind,
+ SettingValue,
+ SettingWriteMode,
+} from "../proto/cormoran/zmk/custom_settings/custom_settings";
+
+const SUBSYSTEM_IDENTIFIERS = [
+ "cormoran_custom_settings",
+ "zmk__custom_settings",
+];
+
+export const CUSTOM_SETTINGS_SOURCE_ALL = 0xffffffff;
+
+const LIST_NOTIFICATION_TIMEOUT_MS = 1500;
+const LIST_REQUEST_TIMEOUT_MS = 5000;
+
+export interface UseCustomSettingsReturn {
+ isAvailable: boolean;
+ settings: Setting[];
+ isLoading: boolean;
+ error: string | null;
+ loadSettings: () => Promise;
+ updateSettingMemory: (
+ setting: Setting,
+ value: SettingValue,
+ source: number,
+ ) => Promise;
+ saveSubsystemSettings: (customSubsystemIndex: number) => Promise;
+ discardSubsystemSettings: (customSubsystemIndex: number) => Promise;
+ resetSubsystemSettings: (customSubsystemIndex: number) => Promise;
+ subsystemIdentifierForIndex: (index: number) => string;
+}
+
+interface ListedSubsystem {
+ index: number;
+ identifier: string;
+}
+
+export function useCustomSettings(): UseCustomSettingsReturn {
+ const zmkApp = useContext(ZMKAppContext);
+ const [settings, setSettings] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const subsystem = useMemo(
+ () =>
+ SUBSYSTEM_IDENTIFIERS.map((identifier) =>
+ zmkApp?.findSubsystem(identifier),
+ ).find((candidate) => candidate !== undefined && candidate !== null),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [zmkApp?.state.customSubsystems],
+ );
+ const subsystemIndex = subsystem?.index;
+
+ const subsystemIdentifierForIndex = useCallback(
+ (index: number) => {
+ const subsystems = getSubsystems(zmkApp?.state.customSubsystems);
+ return (
+ subsystems.find((candidate) => candidate.index === index)?.identifier ??
+ `${index}`
+ );
+ },
+ [zmkApp?.state.customSubsystems],
+ );
+
+ const callCustomRequest = useCallback(
+ async (request: Request): Promise => {
+ if (!zmkApp?.state.connection || subsystemIndex === undefined) {
+ throw new Error("Custom settings subsystem is not available");
+ }
+
+ const service = new ZMKCustomSubsystem(
+ zmkApp.state.connection,
+ subsystemIndex,
+ );
+ const payload = Request.encode(request).finish();
+ const responsePayload = await service.callRPC(payload);
+ if (!responsePayload) {
+ throw new Error("Empty response");
+ }
+
+ const decoded = Response.decode(responsePayload);
+ if (decoded.error) {
+ throw new Error(
+ decoded.error.message || "Custom settings request failed",
+ );
+ }
+ return decoded;
+ },
+ [zmkApp, subsystemIndex],
+ );
+
+ const collectListSettings = useCallback(async (): Promise => {
+ if (!zmkApp || subsystemIndex === undefined) {
+ return [];
+ }
+
+ const collected: Setting[] = [];
+ let quietTimeout: ReturnType | undefined;
+ let isComplete = false;
+ let resolveList: () => void = () => {};
+
+ const listComplete = new Promise((resolve) => {
+ resolveList = resolve;
+ });
+
+ const completeList = () => {
+ if (isComplete) {
+ return;
+ }
+ isComplete = true;
+ if (quietTimeout) {
+ clearTimeout(quietTimeout);
+ }
+ resolveList();
+ };
+
+ const scheduleQuietResolve = () => {
+ if (quietTimeout) {
+ clearTimeout(quietTimeout);
+ }
+ quietTimeout = setTimeout(completeList, LIST_NOTIFICATION_TIMEOUT_MS);
+ };
+
+ const unsubscribe = zmkApp.onNotification({
+ type: "custom",
+ subsystemIndex,
+ callback: (customNotification) => {
+ try {
+ const notification = Notification.decode(customNotification.payload);
+ if (
+ notification.setting?.kind ===
+ SettingNotificationKind.SETTING_NOTIFICATION_KIND_LIST_ITEM &&
+ notification.setting.setting
+ ) {
+ collected.push(notification.setting.setting);
+ scheduleQuietResolve();
+ }
+ } catch (err) {
+ console.error("Failed to decode custom settings notification:", err);
+ }
+ },
+ });
+
+ try {
+ await withTimeout(
+ callCustomRequest(
+ Request.create({
+ listSettings: {
+ scope: {
+ source: CUSTOM_SETTINGS_SOURCE_ALL,
+ },
+ requireMeta: true,
+ },
+ }),
+ ),
+ LIST_REQUEST_TIMEOUT_MS,
+ "Custom settings list request timed out",
+ );
+
+ // Split peripheral settings can arrive after the central response.
+ // For ALL-scope list requests, wait for notifications to settle.
+ scheduleQuietResolve();
+
+ await listComplete;
+ return sortSettings(dedupeSettings(collected));
+ } finally {
+ unsubscribe();
+ if (quietTimeout) {
+ clearTimeout(quietTimeout);
+ }
+ }
+ }, [zmkApp, subsystemIndex, callCustomRequest]);
+
+ const loadSettings = useCallback(async () => {
+ if (!zmkApp?.state.connection || subsystemIndex === undefined) {
+ setSettings([]);
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ setSettings(await collectListSettings());
+ } catch (err) {
+ console.error("Failed to load custom settings:", err);
+ setError(
+ `Failed to load custom settings: ${
+ err instanceof Error ? err.message : "Unknown error"
+ }`,
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ }, [zmkApp?.state.connection, subsystemIndex, collectListSettings]);
+
+ const updateSettingMemory = useCallback(
+ async (setting: Setting, value: SettingValue, source: number) => {
+ if (!zmkApp?.state.connection || subsystemIndex === undefined) {
+ setError("Not connected to device or subsystem not found");
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const arrayIndex = setting.value?.arrayValue?.index;
+ await callCustomRequest(
+ Request.create({
+ writeSetting: {
+ setting: {
+ customSubsystemIndex: setting.customSubsystemIndex,
+ key: setting.key,
+ source,
+ arrayIndex,
+ },
+ value,
+ mode: SettingWriteMode.SETTING_WRITE_MODE_MEMORY,
+ },
+ }),
+ );
+ setSettings(await collectListSettings());
+ } catch (err) {
+ console.error("Failed to update custom setting:", err);
+ setError(
+ `Failed to update custom setting: ${
+ err instanceof Error ? err.message : "Unknown error"
+ }`,
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [
+ zmkApp?.state.connection,
+ subsystemIndex,
+ callCustomRequest,
+ collectListSettings,
+ ],
+ );
+
+ const saveSubsystemSettings = useCallback(
+ async (customSubsystemIndex: number) => {
+ await sendScopeRequest(
+ customSubsystemIndex,
+ (scope) => Request.create({ saveSettings: { scope } }),
+ "save",
+ zmkApp?.state.connection,
+ subsystemIndex,
+ callCustomRequest,
+ collectListSettings,
+ setSettings,
+ setIsLoading,
+ setError,
+ );
+ },
+ [
+ zmkApp?.state.connection,
+ subsystemIndex,
+ callCustomRequest,
+ collectListSettings,
+ ],
+ );
+
+ const discardSubsystemSettings = useCallback(
+ async (customSubsystemIndex: number) => {
+ await sendScopeRequest(
+ customSubsystemIndex,
+ (scope) => Request.create({ discardSettings: { scope } }),
+ "discard",
+ zmkApp?.state.connection,
+ subsystemIndex,
+ callCustomRequest,
+ collectListSettings,
+ setSettings,
+ setIsLoading,
+ setError,
+ );
+ },
+ [
+ zmkApp?.state.connection,
+ subsystemIndex,
+ callCustomRequest,
+ collectListSettings,
+ ],
+ );
+
+ const resetSubsystemSettings = useCallback(
+ async (customSubsystemIndex: number) => {
+ await sendScopeRequest(
+ customSubsystemIndex,
+ (scope) => Request.create({ resetSettings: { scope } }),
+ "reset",
+ zmkApp?.state.connection,
+ subsystemIndex,
+ callCustomRequest,
+ collectListSettings,
+ setSettings,
+ setIsLoading,
+ setError,
+ );
+ },
+ [
+ zmkApp?.state.connection,
+ subsystemIndex,
+ callCustomRequest,
+ collectListSettings,
+ ],
+ );
+
+ useEffect(() => {
+ if (subsystemIndex !== undefined && zmkApp?.state.connection) {
+ void loadSettings();
+ } else {
+ setSettings([]);
+ }
+ }, [subsystemIndex, zmkApp?.state.connection, loadSettings]);
+
+ return {
+ isAvailable: subsystemIndex !== undefined,
+ settings,
+ isLoading,
+ error,
+ loadSettings,
+ updateSettingMemory,
+ saveSubsystemSettings,
+ discardSubsystemSettings,
+ resetSubsystemSettings,
+ subsystemIdentifierForIndex,
+ };
+}
+
+async function sendScopeRequest(
+ customSubsystemIndex: number,
+ createRequest: (scope: {
+ customSubsystemIndex: number;
+ source: number;
+ }) => Request,
+ action: string,
+ connection: unknown,
+ subsystemIndex: number | undefined,
+ callCustomRequest: (request: Request) => Promise,
+ collectListSettings: () => Promise,
+ setSettings: (settings: Setting[]) => void,
+ setIsLoading: (value: boolean) => void,
+ setError: (value: string | null) => void,
+) {
+ if (!connection || subsystemIndex === undefined) {
+ setError("Not connected to device or subsystem not found");
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ await callCustomRequest(
+ createRequest({
+ customSubsystemIndex,
+ source: CUSTOM_SETTINGS_SOURCE_ALL,
+ }),
+ );
+ setSettings(await collectListSettings());
+ } catch (err) {
+ console.error(`Failed to ${action} custom settings:`, err);
+ setError(
+ `Failed to ${action} custom settings: ${
+ err instanceof Error ? err.message : "Unknown error"
+ }`,
+ );
+ } finally {
+ setIsLoading(false);
+ }
+}
+
+function getSubsystems(value: unknown): ListedSubsystem[] {
+ const direct = Array.isArray(value) ? value : undefined;
+ const nested =
+ typeof value === "object" && value !== null && "subsystems" in value
+ ? (value as { subsystems?: unknown }).subsystems
+ : undefined;
+ const candidates = direct ?? (Array.isArray(nested) ? nested : []);
+
+ return candidates.filter(
+ (candidate): candidate is ListedSubsystem =>
+ typeof candidate === "object" &&
+ candidate !== null &&
+ "index" in candidate &&
+ "identifier" in candidate &&
+ typeof (candidate as ListedSubsystem).index === "number" &&
+ typeof (candidate as ListedSubsystem).identifier === "string",
+ );
+}
+
+function sortSettings(settings: Setting[]): Setting[] {
+ return [...settings].sort(
+ (a, b) =>
+ a.customSubsystemIndex - b.customSubsystemIndex ||
+ a.key.localeCompare(b.key) ||
+ (a.value?.arrayValue?.index ?? -1) - (b.value?.arrayValue?.index ?? -1) ||
+ sourceSortValue(a.source) - sourceSortValue(b.source),
+ );
+}
+
+function dedupeSettings(settings: Setting[]): Setting[] {
+ const bySettingRef = new Map();
+
+ for (const setting of settings) {
+ bySettingRef.set(settingListKey(setting), setting);
+ }
+
+ return [...bySettingRef.values()];
+}
+
+function settingListKey(setting: Setting): string {
+ return [
+ setting.customSubsystemIndex,
+ setting.key,
+ setting.source,
+ setting.value?.arrayValue?.index ?? "",
+ ].join(":");
+}
+
+function sourceSortValue(source: number): number {
+ if (source === 0) return -1;
+ if (source === CUSTOM_SETTINGS_SOURCE_ALL) return Number.MAX_SAFE_INTEGER;
+ return source;
+}
+
+async function withTimeout(
+ promise: Promise,
+ timeoutMs: number,
+ message: string,
+): Promise {
+ let timeout: ReturnType | undefined;
+ try {
+ return await Promise.race([
+ promise,
+ new Promise((_, reject) => {
+ timeout = setTimeout(() => reject(new Error(message)), timeoutMs);
+ }),
+ ]);
+ } finally {
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+ }
+}
diff --git a/src/lib/transport/demo-custom-settings.ts b/src/lib/transport/demo-custom-settings.ts
new file mode 100644
index 0000000..149664b
--- /dev/null
+++ b/src/lib/transport/demo-custom-settings.ts
@@ -0,0 +1,323 @@
+import {
+ Notification,
+ Request,
+ Response,
+ Setting,
+ SettingConstraint,
+ SettingNotificationKind,
+ SettingScalarValue,
+ SettingScope,
+ SettingValue,
+} from "../../proto/cormoran/zmk/custom_settings/custom_settings";
+import { CUSTOM_SETTINGS_SOURCE_ALL } from "../../hooks/useCustomSettings";
+
+export const CUSTOM_SETTINGS_IDENTIFIER = "cormoran_custom_settings";
+export const CUSTOM_SETTINGS_SAMPLE_IDENTIFIER = "zmk_config_sample";
+
+const SAMPLE_SUBSYSTEM_INDEX = 7;
+const SOURCES = [0, 1, 2];
+
+const RANGE_0_100: SettingConstraint = {
+ range: {
+ min: { int32Value: 0 },
+ max: { int32Value: 100 },
+ },
+};
+
+const LAYER_ID: SettingConstraint = {
+ layerId: {},
+};
+
+const KEYBOARD_HID_USAGE: SettingConstraint = {
+ hidUsage: {
+ usagePage: 0x07,
+ usageMin: 0x04,
+ usageMax: 0x73,
+ },
+};
+
+const MODE_OPTIONS: SettingConstraint = {
+ options: {
+ values: [
+ { stringValue: "slow" },
+ { stringValue: "normal" },
+ { stringValue: "fast" },
+ ],
+ labels: ["Slow", "Normal", "Fast"],
+ },
+};
+
+interface DemoSettingTemplate {
+ key: string;
+ valueForSource: (source: number) => SettingValue;
+ constraints?: SettingConstraint[];
+}
+
+const TEMPLATES: DemoSettingTemplate[] = [
+ {
+ key: "speed",
+ valueForSource: (source) => ({ int32Value: source === 2 ? 45 : 35 }),
+ constraints: [RANGE_0_100],
+ },
+ {
+ key: "enabled",
+ valueForSource: (source) => ({ boolValue: source !== 2 }),
+ },
+ {
+ key: "mode",
+ valueForSource: () => ({ stringValue: "normal" }),
+ constraints: [MODE_OPTIONS],
+ },
+ {
+ key: "layers",
+ valueForSource: (source) => ({
+ arrayValue: {
+ index: 0,
+ size: 2,
+ value: { int32Value: source },
+ },
+ }),
+ constraints: [LAYER_ID],
+ },
+ {
+ key: "layers",
+ valueForSource: (source) => ({
+ arrayValue: {
+ index: 1,
+ size: 2,
+ value: { int32Value: source + 1 },
+ },
+ }),
+ constraints: [LAYER_ID],
+ },
+ {
+ key: "tap_key",
+ valueForSource: () => ({ int32Value: 0x00070004 }),
+ constraints: [KEYBOARD_HID_USAGE],
+ },
+];
+
+export class CustomSettingsHandler {
+ private defaults = TEMPLATES.flatMap((template) =>
+ SOURCES.map((source) => createSetting(template, source)),
+ );
+ private persistedSettings = cloneSettings(this.defaults);
+ private settings = TEMPLATES.flatMap((template) =>
+ SOURCES.map((source) => createSetting(template, source)),
+ );
+ private callbacks: ((data: Uint8Array) => void)[] = [];
+
+ process(request: Request): Response {
+ if (request.listSettings) {
+ const listed = this.filterSettings(request.listSettings.scope);
+ listed.forEach((setting, index) => {
+ setTimeout(() => {
+ this.callbacks.forEach((callback) =>
+ callback(
+ Notification.encode({
+ setting: {
+ kind: SettingNotificationKind.SETTING_NOTIFICATION_KIND_LIST_ITEM,
+ setting,
+ },
+ }).finish(),
+ ),
+ );
+ }, 25 * index);
+ });
+
+ return {
+ status: {
+ affectedCount: listed.length,
+ message: "OK",
+ },
+ };
+ }
+
+ if (request.writeSetting?.setting && request.writeSetting.value) {
+ const settingRef = request.writeSetting.setting;
+ const targetSources =
+ settingRef.source === CUSTOM_SETTINGS_SOURCE_ALL
+ ? SOURCES
+ : [settingRef.source ?? 0];
+ let affectedCount = 0;
+
+ this.settings = this.settings.map((setting) => {
+ if (
+ setting.customSubsystemIndex !== settingRef.customSubsystemIndex ||
+ setting.key !== settingRef.key ||
+ !targetSources.includes(setting.source) ||
+ arrayIndex(setting) !== settingRef.arrayIndex
+ ) {
+ return setting;
+ }
+
+ affectedCount += 1;
+ return {
+ ...setting,
+ value: normalizeValueForSetting(
+ setting,
+ request.writeSetting!.value!,
+ ),
+ hasUnsavedValue:
+ request.writeSetting!.mode === 0 ? true : setting.hasUnsavedValue,
+ };
+ });
+
+ if (request.writeSetting.mode === 1) {
+ this.persistedSettings = cloneSettings(this.settings);
+ this.settings = this.settings.map((setting) => ({
+ ...setting,
+ hasUnsavedValue: false,
+ }));
+ }
+
+ return {
+ status: {
+ affectedCount,
+ message: affectedCount > 0 ? "OK" : "No matching setting",
+ },
+ };
+ }
+
+ if (request.saveSettings) {
+ const scoped = this.filterSettings(request.saveSettings.scope);
+ this.persistedSettings = this.persistedSettings.map((persisted) => {
+ const current = scoped.find((setting) =>
+ sameSetting(setting, persisted),
+ );
+ return current ? { ...current, hasUnsavedValue: false } : persisted;
+ });
+ this.settings = this.settings.map((setting) =>
+ scoped.some((candidate) => sameSetting(candidate, setting))
+ ? { ...setting, hasUnsavedValue: false }
+ : setting,
+ );
+ return { status: { affectedCount: scoped.length, message: "OK" } };
+ }
+
+ if (request.discardSettings) {
+ const scoped = this.filterSettings(request.discardSettings.scope);
+ this.settings = this.settings.map((setting) => {
+ if (!scoped.some((candidate) => sameSetting(candidate, setting))) {
+ return setting;
+ }
+ return (
+ this.persistedSettings.find((persisted) =>
+ sameSetting(persisted, setting),
+ ) ?? setting
+ );
+ });
+ return { status: { affectedCount: scoped.length, message: "OK" } };
+ }
+
+ if (request.resetSettings) {
+ const scoped = this.filterSettings(request.resetSettings.scope);
+ this.settings = this.settings.map((setting) => {
+ if (!scoped.some((candidate) => sameSetting(candidate, setting))) {
+ return setting;
+ }
+ const defaultSetting = this.defaults.find((candidate) =>
+ sameSetting(candidate, setting),
+ );
+ return defaultSetting
+ ? { ...defaultSetting, hasUnsavedValue: true }
+ : setting;
+ });
+ return { status: { affectedCount: scoped.length, message: "OK" } };
+ }
+
+ return { error: { message: "Not implemented" } };
+ }
+
+ notify(callback: (data: Uint8Array) => void) {
+ this.callbacks.push(callback);
+ }
+
+ private filterSettings(scope: SettingScope | undefined = {}) {
+ return this.settings.filter((setting) => {
+ if (
+ scope.customSubsystemIndex !== undefined &&
+ setting.customSubsystemIndex !== scope.customSubsystemIndex
+ ) {
+ return false;
+ }
+ if (scope.key !== undefined && setting.key !== scope.key) {
+ return false;
+ }
+ if (
+ scope.keyPrefix !== undefined &&
+ !setting.key.startsWith(scope.keyPrefix)
+ ) {
+ return false;
+ }
+ if (
+ scope.source !== undefined &&
+ scope.source !== CUSTOM_SETTINGS_SOURCE_ALL &&
+ setting.source !== scope.source
+ ) {
+ return false;
+ }
+ return true;
+ });
+ }
+}
+
+function sameSetting(a: Setting, b: Setting): boolean {
+ return (
+ a.customSubsystemIndex === b.customSubsystemIndex &&
+ a.key === b.key &&
+ a.source === b.source &&
+ arrayIndex(a) === arrayIndex(b)
+ );
+}
+
+function cloneSettings(settings: Setting[]): Setting[] {
+ return JSON.parse(JSON.stringify(settings)) as Setting[];
+}
+
+function createSetting(template: DemoSettingTemplate, source: number): Setting {
+ return {
+ customSubsystemIndex: SAMPLE_SUBSYSTEM_INDEX,
+ key: template.key,
+ source,
+ hasUnsavedValue: false,
+ meta: {
+ confidentiality: 2,
+ readPermission: 0,
+ writePermission: 0,
+ constraints: template.constraints ?? [],
+ },
+ value: template.valueForSource(source),
+ };
+}
+
+function arrayIndex(setting: Setting): number | undefined {
+ return setting.value?.arrayValue?.index;
+}
+
+function normalizeValueForSetting(
+ setting: Setting,
+ value: SettingValue,
+): SettingValue {
+ const existingArray = setting.value?.arrayValue;
+ if (!existingArray) {
+ return scalarValue(value);
+ }
+
+ return {
+ arrayValue: {
+ index: existingArray.index,
+ size: existingArray.size,
+ value: scalarValue(value.arrayValue?.value ?? value),
+ },
+ };
+}
+
+function scalarValue(value: SettingScalarValue): SettingScalarValue {
+ if (value.int32Value !== undefined) return { int32Value: value.int32Value };
+ if (value.boolValue !== undefined) return { boolValue: value.boolValue };
+ if (value.stringValue !== undefined)
+ return { stringValue: value.stringValue };
+ if (value.bytesValue !== undefined) return { bytesValue: value.bytesValue };
+ return {};
+}
diff --git a/src/lib/transport/demo.ts b/src/lib/transport/demo.ts
index e9335a3..c12ba34 100644
--- a/src/lib/transport/demo.ts
+++ b/src/lib/transport/demo.ts
@@ -29,6 +29,11 @@ import {
PhysicalLayoutsHandler,
PHYSICAL_LAYOUTS_IDENTIFIER,
} from "./demo-physical-layouts";
+import {
+ CustomSettingsHandler,
+ CUSTOM_SETTINGS_IDENTIFIER,
+ CUSTOM_SETTINGS_SAMPLE_IDENTIFIER,
+} from "./demo-custom-settings";
import {
Request as BLERequest,
Response as BLEResponse,
@@ -53,6 +58,10 @@ import {
Request as PhysicalLayoutsRequest,
Response as PhysicalLayoutsResponse,
} from "../../proto/zmk/physical_layouts/physical_layouts";
+import {
+ Request as CustomSettingsRequest,
+ Response as CustomSettingsResponse,
+} from "../../proto/cormoran/zmk/custom_settings/custom_settings";
import {
ANSI60,
ORTHO,
@@ -161,6 +170,7 @@ class Keyboard {
private runtimeInputProcessorHandler = new RuntimeInputProcessorHandler();
private runtimeSensorRotateHandler = new RuntimeSensorRotateHandler();
private physicalLayoutsHandler = new PhysicalLayoutsHandler();
+ private customSettingsHandler = new CustomSettingsHandler();
// Custom subsystems registry
private readonly BLE_SUBSYSTEM_INDEX = 0;
@@ -169,6 +179,8 @@ class Keyboard {
private readonly RUNTIME_INPUT_PROCESSOR_SUBSYSTEM_INDEX = 3;
private readonly RUNTIME_SENSOR_ROTATE_SUBSYSTEM_INDEX = 4;
private readonly PHYSICAL_LAYOUTS_SUBSYSTEM_INDEX = 5;
+ private readonly CUSTOM_SETTINGS_SUBSYSTEM_INDEX = 6;
+ private readonly CUSTOM_SETTINGS_SAMPLE_SUBSYSTEM_INDEX = 7;
private customSubsystems = [
{
@@ -201,6 +213,16 @@ class Keyboard {
identifier: PHYSICAL_LAYOUTS_IDENTIFIER,
uiUrl: [],
},
+ {
+ index: this.CUSTOM_SETTINGS_SUBSYSTEM_INDEX,
+ identifier: CUSTOM_SETTINGS_IDENTIFIER,
+ uiUrl: [],
+ },
+ {
+ index: this.CUSTOM_SETTINGS_SAMPLE_SUBSYSTEM_INDEX,
+ identifier: CUSTOM_SETTINGS_SAMPLE_IDENTIFIER,
+ uiUrl: [],
+ },
];
process(req: Request): Response {
@@ -413,6 +435,17 @@ class Keyboard {
} catch (e) {
console.error("Physical Layouts subsystem error:", e);
}
+ } else if (subsystemIndex === this.CUSTOM_SETTINGS_SUBSYSTEM_INDEX) {
+ // Custom Settings
+ try {
+ const customSettingsReq = CustomSettingsRequest.decode(data);
+ const customSettingsResp =
+ this.customSettingsHandler.process(customSettingsReq);
+ responseData =
+ CustomSettingsResponse.encode(customSettingsResp).finish();
+ } catch (e) {
+ console.error("Custom Settings subsystem error:", e);
+ }
}
if (responseData) {
@@ -497,6 +530,21 @@ class Keyboard {
}).finish(),
);
});
+
+ this.customSettingsHandler.notify((payload: Uint8Array) => {
+ callback(
+ Response.encode({
+ notification: {
+ custom: {
+ customNotification: {
+ subsystemIndex: this.CUSTOM_SETTINGS_SUBSYSTEM_INDEX,
+ payload: payload,
+ },
+ },
+ },
+ }).finish(),
+ );
+ });
}
}
diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx
index 08c7d12..f948a24 100644
--- a/src/pages/SettingsPage.tsx
+++ b/src/pages/SettingsPage.tsx
@@ -6,6 +6,8 @@ import {
IconAlertTriangleFilled,
} from "@tabler/icons-react";
import { useSettings } from "../hooks/useSettings";
+import { useCustomSettings } from "../hooks/useCustomSettings";
+import { CustomSettingsSection } from "../components/CustomSettingsSection";
// Helper to format milliseconds to human readable
function formatMs(ms: number): string {
@@ -221,6 +223,7 @@ function TimeDropdown({ value, onChange, presets }: TimeDropdownProps) {
export function SettingsPage() {
const { isAvailable, devices, isLoading, error, setActivitySettings } =
useSettings();
+ const customSettings = useCustomSettings();
// Track if user has edited the form
const [hasEdits, setHasEdits] = useState(false);
const [editedIdleTimeout, setEditedIdleTimeout] = useState(0);
@@ -275,27 +278,30 @@ export function SettingsPage() {
- {!isAvailable && !isLoading && !error && (
-
-
-
+ {!isAvailable &&
+ !customSettings.isAvailable &&
+ !isLoading &&
+ !error && (
+
-
- Settings RPC subsystem is not available for your keyboard.
-
- Make sure your firmware has the
-
- cormoran/zmk-module-settings-rpc
-
- enabled.
-
-
- )}
+ )}
{/* Error Display */}
{error && (
@@ -305,7 +311,7 @@ export function SettingsPage() {
)}
{/* Loading State */}
- {isLoading && !centralSettings && (
+ {isLoading && !centralSettings && !customSettings.isAvailable && (
Loading settings...
@@ -314,79 +320,85 @@ export function SettingsPage() {
)}
{/* Settings Groups */}
- {centralSettings ? (
+ {centralSettings || customSettings.isAvailable ? (
{/* Power Management */}
-
-
- Power Management
-
+ {centralSettings && (
+
+
+ Power Management
+
+
+
+
+
+
+ Idle Timeout
+
+
+ Time before keyboard enters idle mode
+
+
+
+
-
-
-
-
- Idle Timeout
-
-
- Time before keyboard enters idle mode
-
+
+
+
+ Sleep Timeout
+
+
+ Time before entering deep sleep
+
+
+
-
-
-
-
-
- Sleep Timeout
-
-
- Time before entering deep sleep
-
+
+
-
-
-
-
+ {/* Show all devices status - moved after inputs */}
+ {devices.length > 0 && (
+
+
+ Current Settings by Device:
+
+
+ {devices.map((device) => (
+
+
+ {device.deviceName}:
+ {" "}
+ Idle: {formatMs(device.idleMs)}, Sleep:{" "}
+ {formatMs(device.sleepMs)}
+
+ ))}
+
+
+ )}
+ )}
- {/* Show all devices status - moved after inputs */}
- {devices.length > 0 && (
-
-
- Current Settings by Device:
-
-
- {devices.map((device) => (
-
- {device.deviceName}:{" "}
- Idle: {formatMs(device.idleMs)}, Sleep:{" "}
- {formatMs(device.sleepMs)}
-
- ))}
-
-
- )}
-
+
{/* Danger Zone
diff --git a/src/pages/__tests__/SettingsPage.test.tsx b/src/pages/__tests__/SettingsPage.test.tsx
new file mode 100644
index 0000000..819f981
--- /dev/null
+++ b/src/pages/__tests__/SettingsPage.test.tsx
@@ -0,0 +1,232 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { SettingsPage } from "../SettingsPage";
+import { useSettings } from "../../hooks/useSettings";
+import { useCustomSettings } from "../../hooks/useCustomSettings";
+import { useKeymap } from "../../hooks/useKeymap";
+import type { Setting } from "../../proto/cormoran/zmk/custom_settings/custom_settings";
+
+jest.mock("../../hooks/useSettings");
+jest.mock("../../hooks/useKeymap");
+jest.mock("../../hooks/useCustomSettings", () => ({
+ CUSTOM_SETTINGS_SOURCE_ALL: 0xffffffff,
+ useCustomSettings: jest.fn(),
+}));
+
+const mockUseSettings = useSettings as jest.MockedFunction
;
+const mockUseCustomSettings = useCustomSettings as jest.MockedFunction<
+ typeof useCustomSettings
+>;
+const mockUseKeymap = useKeymap as jest.MockedFunction;
+
+const customSettings: Setting[] = [
+ {
+ customSubsystemIndex: 4,
+ key: "speed",
+ source: 0,
+ hasUnsavedValue: false,
+ value: { int32Value: 35 },
+ meta: {
+ confidentiality: 2,
+ readPermission: 0,
+ writePermission: 0,
+ constraints: [
+ {
+ range: {
+ min: { int32Value: 0 },
+ max: { int32Value: 100 },
+ },
+ },
+ ],
+ },
+ },
+ {
+ customSubsystemIndex: 4,
+ key: "speed",
+ source: 1,
+ hasUnsavedValue: false,
+ value: { int32Value: 45 },
+ meta: {
+ confidentiality: 2,
+ readPermission: 0,
+ writePermission: 0,
+ constraints: [
+ {
+ range: {
+ min: { int32Value: 0 },
+ max: { int32Value: 100 },
+ },
+ },
+ ],
+ },
+ },
+];
+
+describe("SettingsPage", () => {
+ const updateSettingMemory = jest.fn();
+ const saveSubsystemSettings = jest.fn();
+ const discardSubsystemSettings = jest.fn();
+ const resetSubsystemSettings = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ updateSettingMemory.mockResolvedValue(undefined);
+ saveSubsystemSettings.mockResolvedValue(undefined);
+ discardSubsystemSettings.mockResolvedValue(undefined);
+ resetSubsystemSettings.mockResolvedValue(undefined);
+
+ mockUseSettings.mockReturnValue({
+ isAvailable: true,
+ devices: [
+ { sourceId: 0, deviceName: "Central", idleMs: 30000, sleepMs: 900000 },
+ ],
+ isLoading: false,
+ error: null,
+ loadAllSettings: jest.fn(),
+ setActivitySettings: jest.fn(),
+ resetToDefaults: jest.fn(),
+ });
+
+ mockUseCustomSettings.mockReturnValue({
+ isAvailable: true,
+ settings: customSettings,
+ isLoading: false,
+ error: null,
+ loadSettings: jest.fn(),
+ updateSettingMemory,
+ saveSubsystemSettings,
+ discardSubsystemSettings,
+ resetSubsystemSettings,
+ subsystemIdentifierForIndex: (index) =>
+ index === 4 ? "zmk_config_sample" : `${index}`,
+ });
+
+ mockUseKeymap.mockReturnValue({
+ keymap: {
+ layers: [
+ { id: 0, name: "Base", bindings: [] },
+ { id: 1, name: "Lower", bindings: [] },
+ ],
+ availableLayers: 2,
+ },
+ physicalLayouts: null,
+ behaviors: new Map(),
+ originalBindings: new Map(),
+ hasUnsavedChanges: false,
+ isLoading: false,
+ error: null,
+ unlockRequired: false,
+ loadKeymapData: jest.fn(),
+ setBinding: jest.fn(),
+ resetBinding: jest.fn(),
+ moveLayer: jest.fn(),
+ addLayer: jest.fn(),
+ removeLayer: jest.fn(),
+ restoreLayer: jest.fn(),
+ availableLayers: 2,
+ removedLayerIds: [],
+ saveChanges: jest.fn(),
+ discardChanges: jest.fn(),
+ setActiveLayout: jest.fn(),
+ getOriginalBinding: jest.fn(),
+ isBindingModified: jest.fn(),
+ getBehavior: jest.fn(),
+ getBindingDisplayName: jest.fn(),
+ clearUnlockRequired: jest.fn(),
+ });
+ });
+
+ it("shows custom settings after power management", () => {
+ render();
+
+ const powerHeading = screen.getByText("Power Management");
+ const customHeading = screen.getByText("Custom Settings");
+
+ expect(
+ powerHeading.compareDocumentPosition(customHeading) &
+ Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy();
+ expect(screen.getByText("zmk_config_sample")).toBeInTheDocument();
+ expect(screen.getAllByText("Central").length).toBeGreaterThan(0);
+ expect(screen.getAllByText("Peripheral 1").length).toBeGreaterThan(0);
+ });
+
+ it("updates custom settings memory with selectable split target", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const targetSelect = screen.getByLabelText("Target");
+ expect(targetSelect).toHaveTextContent("All");
+ expect(targetSelect).toHaveTextContent("Central");
+ expect(targetSelect).toHaveTextContent("Peripheral 1");
+
+ const valueInput = screen.getByLabelText("Value");
+ expect(valueInput).toHaveAttribute("min", "0");
+ expect(valueInput).toHaveAttribute("max", "100");
+
+ await user.clear(valueInput);
+ await user.type(valueInput, "42");
+
+ await waitFor(() =>
+ expect(updateSettingMemory).toHaveBeenCalledWith(
+ customSettings[0],
+ { int32Value: 42 },
+ 0xffffffff,
+ ),
+ );
+ });
+
+ it("enables subsystem save and discard when a custom setting is pending", async () => {
+ const user = userEvent.setup();
+ mockUseCustomSettings.mockReturnValue({
+ isAvailable: true,
+ settings: [{ ...customSettings[0], hasUnsavedValue: true }],
+ isLoading: false,
+ error: null,
+ loadSettings: jest.fn(),
+ updateSettingMemory,
+ saveSubsystemSettings,
+ discardSubsystemSettings,
+ resetSubsystemSettings,
+ subsystemIdentifierForIndex: (index) =>
+ index === 4 ? "zmk_config_sample" : `${index}`,
+ });
+
+ render();
+
+ await user.click(screen.getByRole("button", { name: "Save" }));
+ expect(saveSubsystemSettings).toHaveBeenCalledWith(4);
+
+ await user.click(screen.getByRole("button", { name: "Discard" }));
+ expect(discardSubsystemSettings).toHaveBeenCalledWith(4);
+ expect(screen.getAllByText("pending").length).toBeGreaterThan(0);
+ });
+
+ it("confirms reset before resetting subsystem settings", async () => {
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByRole("button", { name: "Reset" }));
+ expect(resetSubsystemSettings).not.toHaveBeenCalled();
+
+ await user.click(screen.getByRole("button", { name: "Confirm Reset" }));
+ expect(resetSubsystemSettings).toHaveBeenCalledWith(4);
+ });
+
+ it("shows custom settings even when the power settings subsystem is absent", () => {
+ mockUseSettings.mockReturnValue({
+ isAvailable: false,
+ devices: [],
+ isLoading: false,
+ error: null,
+ loadAllSettings: jest.fn(),
+ setActivitySettings: jest.fn(),
+ resetToDefaults: jest.fn(),
+ });
+
+ render();
+
+ expect(screen.queryByText("Power Management")).not.toBeInTheDocument();
+ expect(screen.getByText("Custom Settings")).toBeInTheDocument();
+ });
+});
diff --git a/src/proto/cormoran/zmk/custom_settings/custom_settings.ts b/src/proto/cormoran/zmk/custom_settings/custom_settings.ts
new file mode 100644
index 0000000..9777005
--- /dev/null
+++ b/src/proto/cormoran/zmk/custom_settings/custom_settings.ts
@@ -0,0 +1,2216 @@
+// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
+// versions:
+// protoc-gen-ts_proto v2.11.0
+// protoc unknown
+// source: cormoran/zmk/custom_settings/custom_settings.proto
+
+/* eslint-disable */
+import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
+
+export const protobufPackage = "cormoran.zmk.custom_settings";
+
+export const SettingWriteMode = {
+ /** SETTING_WRITE_MODE_MEMORY - Update RAM only. The value can later be saved or discarded. */
+ SETTING_WRITE_MODE_MEMORY: 0,
+ /** SETTING_WRITE_MODE_PERSIST - Update RAM and save the value to persistent storage. */
+ SETTING_WRITE_MODE_PERSIST: 1,
+ UNRECOGNIZED: -1,
+} as const;
+
+export type SettingWriteMode = typeof SettingWriteMode[keyof typeof SettingWriteMode];
+
+export namespace SettingWriteMode {
+ export type SETTING_WRITE_MODE_MEMORY = typeof SettingWriteMode.SETTING_WRITE_MODE_MEMORY;
+ export type SETTING_WRITE_MODE_PERSIST = typeof SettingWriteMode.SETTING_WRITE_MODE_PERSIST;
+ export type UNRECOGNIZED = typeof SettingWriteMode.UNRECOGNIZED;
+}
+
+export const SettingConfidentiality = {
+ /** SETTING_CONFIDENTIALITY_DEVICE_PRIVATE - Do not expose the setting value over RPC. */
+ SETTING_CONFIDENTIALITY_DEVICE_PRIVATE: 0,
+ /** SETTING_CONFIDENTIALITY_RPC_PERSONAL - RPC may expose the value, but the web UI should treat it as personal data. */
+ SETTING_CONFIDENTIALITY_RPC_PERSONAL: 1,
+ /** SETTING_CONFIDENTIALITY_RPC_PUBLIC - RPC may expose the value and the web UI may publish it. */
+ SETTING_CONFIDENTIALITY_RPC_PUBLIC: 2,
+ UNRECOGNIZED: -1,
+} as const;
+
+export type SettingConfidentiality = typeof SettingConfidentiality[keyof typeof SettingConfidentiality];
+
+export namespace SettingConfidentiality {
+ export type SETTING_CONFIDENTIALITY_DEVICE_PRIVATE =
+ typeof SettingConfidentiality.SETTING_CONFIDENTIALITY_DEVICE_PRIVATE;
+ export type SETTING_CONFIDENTIALITY_RPC_PERSONAL = typeof SettingConfidentiality.SETTING_CONFIDENTIALITY_RPC_PERSONAL;
+ export type SETTING_CONFIDENTIALITY_RPC_PUBLIC = typeof SettingConfidentiality.SETTING_CONFIDENTIALITY_RPC_PUBLIC;
+ export type UNRECOGNIZED = typeof SettingConfidentiality.UNRECOGNIZED;
+}
+
+export const SettingPermission = {
+ SETTING_PERMISSION_UNSECURE: 0,
+ SETTING_PERMISSION_SECURE: 1,
+ UNRECOGNIZED: -1,
+} as const;
+
+export type SettingPermission = typeof SettingPermission[keyof typeof SettingPermission];
+
+export namespace SettingPermission {
+ export type SETTING_PERMISSION_UNSECURE = typeof SettingPermission.SETTING_PERMISSION_UNSECURE;
+ export type SETTING_PERMISSION_SECURE = typeof SettingPermission.SETTING_PERMISSION_SECURE;
+ export type UNRECOGNIZED = typeof SettingPermission.UNRECOGNIZED;
+}
+
+export const SettingNotificationKind = {
+ SETTING_NOTIFICATION_KIND_LIST_ITEM: 0,
+ SETTING_NOTIFICATION_KIND_VALUE_UPDATED: 1,
+ SETTING_NOTIFICATION_KIND_SAVED: 2,
+ SETTING_NOTIFICATION_KIND_DISCARDED: 3,
+ SETTING_NOTIFICATION_KIND_RESET: 4,
+ UNRECOGNIZED: -1,
+} as const;
+
+export type SettingNotificationKind = typeof SettingNotificationKind[keyof typeof SettingNotificationKind];
+
+export namespace SettingNotificationKind {
+ export type SETTING_NOTIFICATION_KIND_LIST_ITEM = typeof SettingNotificationKind.SETTING_NOTIFICATION_KIND_LIST_ITEM;
+ export type SETTING_NOTIFICATION_KIND_VALUE_UPDATED =
+ typeof SettingNotificationKind.SETTING_NOTIFICATION_KIND_VALUE_UPDATED;
+ export type SETTING_NOTIFICATION_KIND_SAVED = typeof SettingNotificationKind.SETTING_NOTIFICATION_KIND_SAVED;
+ export type SETTING_NOTIFICATION_KIND_DISCARDED = typeof SettingNotificationKind.SETTING_NOTIFICATION_KIND_DISCARDED;
+ export type SETTING_NOTIFICATION_KIND_RESET = typeof SettingNotificationKind.SETTING_NOTIFICATION_KIND_RESET;
+ export type UNRECOGNIZED = typeof SettingNotificationKind.UNRECOGNIZED;
+}
+
+export interface SettingScalarValue {
+ bytesValue?: Uint8Array | undefined;
+ int32Value?: number | undefined;
+ boolValue?: boolean | undefined;
+ stringValue?: string | undefined;
+}
+
+/** Array values encode one active element plus the active array length. */
+export interface SettingArrayValue {
+ index: number;
+ size: number;
+ value: SettingScalarValue | undefined;
+}
+
+/** One setting value. Array elements wrap a scalar with array metadata. */
+export interface SettingValue {
+ bytesValue?: Uint8Array | undefined;
+ int32Value?: number | undefined;
+ boolValue?: boolean | undefined;
+ stringValue?: string | undefined;
+ arrayValue?: SettingArrayValue | undefined;
+}
+
+export interface SettingConstraintRange {
+ min: SettingScalarValue | undefined;
+ max: SettingScalarValue | undefined;
+}
+
+export interface SettingConstraintOptions {
+ values: SettingScalarValue[];
+ labels: string[];
+}
+
+export interface SettingConstraintHidUsage {
+ usagePage: number;
+ usageMin: number;
+ usageMax: number;
+}
+
+export interface SettingConstraintLayerId {
+}
+
+export interface SettingConstraintBehaviorId {
+}
+
+export interface SettingConstraint {
+ range?: SettingConstraintRange | undefined;
+ options?: SettingConstraintOptions | undefined;
+ hidUsage?: SettingConstraintHidUsage | undefined;
+ layerId?: SettingConstraintLayerId | undefined;
+ behaviorId?: SettingConstraintBehaviorId | undefined;
+}
+
+/** References one setting. custom_subsystem_index is the Studio custom subsystem index. */
+export interface SettingRef {
+ customSubsystemIndex?: number | undefined;
+ key?:
+ | string
+ | undefined;
+ /** Omitted source means local side only. UINT32_MAX means all split sides. */
+ source?:
+ | number
+ | undefined;
+ /** Optional index for array settings when value.array_value is not used. */
+ arrayIndex?: number | undefined;
+}
+
+/** Selects a group of settings for list/save/discard/reset. */
+export interface SettingScope {
+ customSubsystemIndex?: number | undefined;
+ key?: string | undefined;
+ keyPrefix?:
+ | string
+ | undefined;
+ /** Omitted source means local side only. UINT32_MAX means all split sides. */
+ source?: number | undefined;
+}
+
+/** Static setting metadata included only when requests set require_meta. */
+export interface SettingMeta {
+ confidentiality: SettingConfidentiality;
+ readPermission: SettingPermission;
+ writePermission: SettingPermission;
+ constraints: SettingConstraint[];
+}
+
+export interface Setting {
+ customSubsystemIndex: number;
+ key: string;
+ /** Present only when the request asked for metadata. */
+ meta?: SettingMeta | undefined;
+ hasUnsavedValue: boolean;
+ /** Omitted for DEVICE_PRIVATE settings and secure settings while locked. */
+ value: SettingValue | undefined;
+ source: number;
+}
+
+export interface ListSettingsRequest {
+ scope:
+ | SettingScope
+ | undefined;
+ /** Include static metadata in each list item notification. */
+ requireMeta: boolean;
+}
+
+export interface GetSettingRequest {
+ setting:
+ | SettingRef
+ | undefined;
+ /** Include static metadata in the get response. */
+ requireMeta: boolean;
+}
+
+export interface WriteSettingRequest {
+ setting: SettingRef | undefined;
+ value: SettingValue | undefined;
+ mode: SettingWriteMode;
+}
+
+/** Append one scalar value to the end of an array setting. */
+export interface PushBackArrayRequest {
+ setting: SettingRef | undefined;
+ value: SettingScalarValue | undefined;
+ mode: SettingWriteMode;
+}
+
+/** Remove the last active value from an array setting. */
+export interface PopBackArrayRequest {
+ setting: SettingRef | undefined;
+ mode: SettingWriteMode;
+}
+
+export interface SaveSettingsRequest {
+ scope: SettingScope | undefined;
+}
+
+export interface DiscardSettingsRequest {
+ scope: SettingScope | undefined;
+}
+
+export interface ResetSettingsRequest {
+ scope: SettingScope | undefined;
+}
+
+export interface Request {
+ listSettings?: ListSettingsRequest | undefined;
+ getSetting?: GetSettingRequest | undefined;
+ writeSetting?: WriteSettingRequest | undefined;
+ saveSettings?: SaveSettingsRequest | undefined;
+ discardSettings?: DiscardSettingsRequest | undefined;
+ resetSettings?: ResetSettingsRequest | undefined;
+ pushBackArray?: PushBackArrayRequest | undefined;
+ popBackArray?: PopBackArrayRequest | undefined;
+}
+
+export interface ErrorResponse {
+ message: string;
+}
+
+export interface StatusResponse {
+ affectedCount: number;
+ message: string;
+}
+
+export interface GetSettingResponse {
+ setting: Setting | undefined;
+}
+
+export interface Response {
+ error?: ErrorResponse | undefined;
+ status?: StatusResponse | undefined;
+ getSetting?: GetSettingResponse | undefined;
+}
+
+export interface SettingNotification {
+ kind: SettingNotificationKind;
+ setting: Setting | undefined;
+}
+
+export interface Notification {
+ setting?: SettingNotification | undefined;
+}
+
+function createBaseSettingScalarValue(): SettingScalarValue {
+ return { bytesValue: undefined, int32Value: undefined, boolValue: undefined, stringValue: undefined };
+}
+
+export const SettingScalarValue: MessageFns = {
+ encode(message: SettingScalarValue, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.bytesValue !== undefined) {
+ writer.uint32(10).bytes(message.bytesValue);
+ }
+ if (message.int32Value !== undefined) {
+ writer.uint32(16).int32(message.int32Value);
+ }
+ if (message.boolValue !== undefined) {
+ writer.uint32(24).bool(message.boolValue);
+ }
+ if (message.stringValue !== undefined) {
+ writer.uint32(34).string(message.stringValue);
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): SettingScalarValue {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSettingScalarValue();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.bytesValue = reader.bytes();
+ continue;
+ }
+ case 2: {
+ if (tag !== 16) {
+ break;
+ }
+
+ message.int32Value = reader.int32();
+ continue;
+ }
+ case 3: {
+ if (tag !== 24) {
+ break;
+ }
+
+ message.boolValue = reader.bool();
+ continue;
+ }
+ case 4: {
+ if (tag !== 34) {
+ break;
+ }
+
+ message.stringValue = reader.string();
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): SettingScalarValue {
+ return SettingScalarValue.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): SettingScalarValue {
+ const message = createBaseSettingScalarValue();
+ message.bytesValue = object.bytesValue ?? undefined;
+ message.int32Value = object.int32Value ?? undefined;
+ message.boolValue = object.boolValue ?? undefined;
+ message.stringValue = object.stringValue ?? undefined;
+ return message;
+ },
+};
+
+function createBaseSettingArrayValue(): SettingArrayValue {
+ return { index: 0, size: 0, value: undefined };
+}
+
+export const SettingArrayValue: MessageFns = {
+ encode(message: SettingArrayValue, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.index !== 0) {
+ writer.uint32(8).uint32(message.index);
+ }
+ if (message.size !== 0) {
+ writer.uint32(16).uint32(message.size);
+ }
+ if (message.value !== undefined) {
+ SettingScalarValue.encode(message.value, writer.uint32(26).fork()).join();
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): SettingArrayValue {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSettingArrayValue();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 8) {
+ break;
+ }
+
+ message.index = reader.uint32();
+ continue;
+ }
+ case 2: {
+ if (tag !== 16) {
+ break;
+ }
+
+ message.size = reader.uint32();
+ continue;
+ }
+ case 3: {
+ if (tag !== 26) {
+ break;
+ }
+
+ message.value = SettingScalarValue.decode(reader, reader.uint32());
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): SettingArrayValue {
+ return SettingArrayValue.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): SettingArrayValue {
+ const message = createBaseSettingArrayValue();
+ message.index = object.index ?? 0;
+ message.size = object.size ?? 0;
+ message.value = (object.value !== undefined && object.value !== null)
+ ? SettingScalarValue.fromPartial(object.value)
+ : undefined;
+ return message;
+ },
+};
+
+function createBaseSettingValue(): SettingValue {
+ return {
+ bytesValue: undefined,
+ int32Value: undefined,
+ boolValue: undefined,
+ stringValue: undefined,
+ arrayValue: undefined,
+ };
+}
+
+export const SettingValue: MessageFns = {
+ encode(message: SettingValue, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.bytesValue !== undefined) {
+ writer.uint32(10).bytes(message.bytesValue);
+ }
+ if (message.int32Value !== undefined) {
+ writer.uint32(16).int32(message.int32Value);
+ }
+ if (message.boolValue !== undefined) {
+ writer.uint32(24).bool(message.boolValue);
+ }
+ if (message.stringValue !== undefined) {
+ writer.uint32(34).string(message.stringValue);
+ }
+ if (message.arrayValue !== undefined) {
+ SettingArrayValue.encode(message.arrayValue, writer.uint32(42).fork()).join();
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): SettingValue {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSettingValue();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.bytesValue = reader.bytes();
+ continue;
+ }
+ case 2: {
+ if (tag !== 16) {
+ break;
+ }
+
+ message.int32Value = reader.int32();
+ continue;
+ }
+ case 3: {
+ if (tag !== 24) {
+ break;
+ }
+
+ message.boolValue = reader.bool();
+ continue;
+ }
+ case 4: {
+ if (tag !== 34) {
+ break;
+ }
+
+ message.stringValue = reader.string();
+ continue;
+ }
+ case 5: {
+ if (tag !== 42) {
+ break;
+ }
+
+ message.arrayValue = SettingArrayValue.decode(reader, reader.uint32());
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): SettingValue {
+ return SettingValue.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): SettingValue {
+ const message = createBaseSettingValue();
+ message.bytesValue = object.bytesValue ?? undefined;
+ message.int32Value = object.int32Value ?? undefined;
+ message.boolValue = object.boolValue ?? undefined;
+ message.stringValue = object.stringValue ?? undefined;
+ message.arrayValue = (object.arrayValue !== undefined && object.arrayValue !== null)
+ ? SettingArrayValue.fromPartial(object.arrayValue)
+ : undefined;
+ return message;
+ },
+};
+
+function createBaseSettingConstraintRange(): SettingConstraintRange {
+ return { min: undefined, max: undefined };
+}
+
+export const SettingConstraintRange: MessageFns = {
+ encode(message: SettingConstraintRange, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.min !== undefined) {
+ SettingScalarValue.encode(message.min, writer.uint32(10).fork()).join();
+ }
+ if (message.max !== undefined) {
+ SettingScalarValue.encode(message.max, writer.uint32(18).fork()).join();
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): SettingConstraintRange {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSettingConstraintRange();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.min = SettingScalarValue.decode(reader, reader.uint32());
+ continue;
+ }
+ case 2: {
+ if (tag !== 18) {
+ break;
+ }
+
+ message.max = SettingScalarValue.decode(reader, reader.uint32());
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): SettingConstraintRange {
+ return SettingConstraintRange.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): SettingConstraintRange {
+ const message = createBaseSettingConstraintRange();
+ message.min = (object.min !== undefined && object.min !== null)
+ ? SettingScalarValue.fromPartial(object.min)
+ : undefined;
+ message.max = (object.max !== undefined && object.max !== null)
+ ? SettingScalarValue.fromPartial(object.max)
+ : undefined;
+ return message;
+ },
+};
+
+function createBaseSettingConstraintOptions(): SettingConstraintOptions {
+ return { values: [], labels: [] };
+}
+
+export const SettingConstraintOptions: MessageFns = {
+ encode(message: SettingConstraintOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ for (const v of message.values) {
+ SettingScalarValue.encode(v!, writer.uint32(10).fork()).join();
+ }
+ for (const v of message.labels) {
+ writer.uint32(18).string(v!);
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): SettingConstraintOptions {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSettingConstraintOptions();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.values.push(SettingScalarValue.decode(reader, reader.uint32()));
+ continue;
+ }
+ case 2: {
+ if (tag !== 18) {
+ break;
+ }
+
+ message.labels.push(reader.string());
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): SettingConstraintOptions {
+ return SettingConstraintOptions.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): SettingConstraintOptions {
+ const message = createBaseSettingConstraintOptions();
+ message.values = object.values?.map((e) => SettingScalarValue.fromPartial(e)) || [];
+ message.labels = object.labels?.map((e) => e) || [];
+ return message;
+ },
+};
+
+function createBaseSettingConstraintHidUsage(): SettingConstraintHidUsage {
+ return { usagePage: 0, usageMin: 0, usageMax: 0 };
+}
+
+export const SettingConstraintHidUsage: MessageFns = {
+ encode(message: SettingConstraintHidUsage, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.usagePage !== 0) {
+ writer.uint32(8).uint32(message.usagePage);
+ }
+ if (message.usageMin !== 0) {
+ writer.uint32(16).uint32(message.usageMin);
+ }
+ if (message.usageMax !== 0) {
+ writer.uint32(24).uint32(message.usageMax);
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): SettingConstraintHidUsage {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSettingConstraintHidUsage();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 8) {
+ break;
+ }
+
+ message.usagePage = reader.uint32();
+ continue;
+ }
+ case 2: {
+ if (tag !== 16) {
+ break;
+ }
+
+ message.usageMin = reader.uint32();
+ continue;
+ }
+ case 3: {
+ if (tag !== 24) {
+ break;
+ }
+
+ message.usageMax = reader.uint32();
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): SettingConstraintHidUsage {
+ return SettingConstraintHidUsage.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): SettingConstraintHidUsage {
+ const message = createBaseSettingConstraintHidUsage();
+ message.usagePage = object.usagePage ?? 0;
+ message.usageMin = object.usageMin ?? 0;
+ message.usageMax = object.usageMax ?? 0;
+ return message;
+ },
+};
+
+function createBaseSettingConstraintLayerId(): SettingConstraintLayerId {
+ return {};
+}
+
+export const SettingConstraintLayerId: MessageFns = {
+ encode(_: SettingConstraintLayerId, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): SettingConstraintLayerId {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSettingConstraintLayerId();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): SettingConstraintLayerId {
+ return SettingConstraintLayerId.fromPartial(base ?? {});
+ },
+ fromPartial(_: DeepPartial): SettingConstraintLayerId {
+ const message = createBaseSettingConstraintLayerId();
+ return message;
+ },
+};
+
+function createBaseSettingConstraintBehaviorId(): SettingConstraintBehaviorId {
+ return {};
+}
+
+export const SettingConstraintBehaviorId: MessageFns = {
+ encode(_: SettingConstraintBehaviorId, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): SettingConstraintBehaviorId {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSettingConstraintBehaviorId();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): SettingConstraintBehaviorId {
+ return SettingConstraintBehaviorId.fromPartial(base ?? {});
+ },
+ fromPartial(_: DeepPartial): SettingConstraintBehaviorId {
+ const message = createBaseSettingConstraintBehaviorId();
+ return message;
+ },
+};
+
+function createBaseSettingConstraint(): SettingConstraint {
+ return { range: undefined, options: undefined, hidUsage: undefined, layerId: undefined, behaviorId: undefined };
+}
+
+export const SettingConstraint: MessageFns = {
+ encode(message: SettingConstraint, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.range !== undefined) {
+ SettingConstraintRange.encode(message.range, writer.uint32(10).fork()).join();
+ }
+ if (message.options !== undefined) {
+ SettingConstraintOptions.encode(message.options, writer.uint32(18).fork()).join();
+ }
+ if (message.hidUsage !== undefined) {
+ SettingConstraintHidUsage.encode(message.hidUsage, writer.uint32(26).fork()).join();
+ }
+ if (message.layerId !== undefined) {
+ SettingConstraintLayerId.encode(message.layerId, writer.uint32(34).fork()).join();
+ }
+ if (message.behaviorId !== undefined) {
+ SettingConstraintBehaviorId.encode(message.behaviorId, writer.uint32(42).fork()).join();
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): SettingConstraint {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSettingConstraint();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.range = SettingConstraintRange.decode(reader, reader.uint32());
+ continue;
+ }
+ case 2: {
+ if (tag !== 18) {
+ break;
+ }
+
+ message.options = SettingConstraintOptions.decode(reader, reader.uint32());
+ continue;
+ }
+ case 3: {
+ if (tag !== 26) {
+ break;
+ }
+
+ message.hidUsage = SettingConstraintHidUsage.decode(reader, reader.uint32());
+ continue;
+ }
+ case 4: {
+ if (tag !== 34) {
+ break;
+ }
+
+ message.layerId = SettingConstraintLayerId.decode(reader, reader.uint32());
+ continue;
+ }
+ case 5: {
+ if (tag !== 42) {
+ break;
+ }
+
+ message.behaviorId = SettingConstraintBehaviorId.decode(reader, reader.uint32());
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): SettingConstraint {
+ return SettingConstraint.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): SettingConstraint {
+ const message = createBaseSettingConstraint();
+ message.range = (object.range !== undefined && object.range !== null)
+ ? SettingConstraintRange.fromPartial(object.range)
+ : undefined;
+ message.options = (object.options !== undefined && object.options !== null)
+ ? SettingConstraintOptions.fromPartial(object.options)
+ : undefined;
+ message.hidUsage = (object.hidUsage !== undefined && object.hidUsage !== null)
+ ? SettingConstraintHidUsage.fromPartial(object.hidUsage)
+ : undefined;
+ message.layerId = (object.layerId !== undefined && object.layerId !== null)
+ ? SettingConstraintLayerId.fromPartial(object.layerId)
+ : undefined;
+ message.behaviorId = (object.behaviorId !== undefined && object.behaviorId !== null)
+ ? SettingConstraintBehaviorId.fromPartial(object.behaviorId)
+ : undefined;
+ return message;
+ },
+};
+
+function createBaseSettingRef(): SettingRef {
+ return { customSubsystemIndex: undefined, key: undefined, source: undefined, arrayIndex: undefined };
+}
+
+export const SettingRef: MessageFns = {
+ encode(message: SettingRef, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.customSubsystemIndex !== undefined) {
+ writer.uint32(8).uint32(message.customSubsystemIndex);
+ }
+ if (message.key !== undefined) {
+ writer.uint32(18).string(message.key);
+ }
+ if (message.source !== undefined) {
+ writer.uint32(24).uint32(message.source);
+ }
+ if (message.arrayIndex !== undefined) {
+ writer.uint32(32).uint32(message.arrayIndex);
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): SettingRef {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSettingRef();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 8) {
+ break;
+ }
+
+ message.customSubsystemIndex = reader.uint32();
+ continue;
+ }
+ case 2: {
+ if (tag !== 18) {
+ break;
+ }
+
+ message.key = reader.string();
+ continue;
+ }
+ case 3: {
+ if (tag !== 24) {
+ break;
+ }
+
+ message.source = reader.uint32();
+ continue;
+ }
+ case 4: {
+ if (tag !== 32) {
+ break;
+ }
+
+ message.arrayIndex = reader.uint32();
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): SettingRef {
+ return SettingRef.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): SettingRef {
+ const message = createBaseSettingRef();
+ message.customSubsystemIndex = object.customSubsystemIndex ?? undefined;
+ message.key = object.key ?? undefined;
+ message.source = object.source ?? undefined;
+ message.arrayIndex = object.arrayIndex ?? undefined;
+ return message;
+ },
+};
+
+function createBaseSettingScope(): SettingScope {
+ return { customSubsystemIndex: undefined, key: undefined, keyPrefix: undefined, source: undefined };
+}
+
+export const SettingScope: MessageFns = {
+ encode(message: SettingScope, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.customSubsystemIndex !== undefined) {
+ writer.uint32(8).uint32(message.customSubsystemIndex);
+ }
+ if (message.key !== undefined) {
+ writer.uint32(18).string(message.key);
+ }
+ if (message.keyPrefix !== undefined) {
+ writer.uint32(26).string(message.keyPrefix);
+ }
+ if (message.source !== undefined) {
+ writer.uint32(32).uint32(message.source);
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): SettingScope {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSettingScope();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 8) {
+ break;
+ }
+
+ message.customSubsystemIndex = reader.uint32();
+ continue;
+ }
+ case 2: {
+ if (tag !== 18) {
+ break;
+ }
+
+ message.key = reader.string();
+ continue;
+ }
+ case 3: {
+ if (tag !== 26) {
+ break;
+ }
+
+ message.keyPrefix = reader.string();
+ continue;
+ }
+ case 4: {
+ if (tag !== 32) {
+ break;
+ }
+
+ message.source = reader.uint32();
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): SettingScope {
+ return SettingScope.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): SettingScope {
+ const message = createBaseSettingScope();
+ message.customSubsystemIndex = object.customSubsystemIndex ?? undefined;
+ message.key = object.key ?? undefined;
+ message.keyPrefix = object.keyPrefix ?? undefined;
+ message.source = object.source ?? undefined;
+ return message;
+ },
+};
+
+function createBaseSettingMeta(): SettingMeta {
+ return { confidentiality: 0, readPermission: 0, writePermission: 0, constraints: [] };
+}
+
+export const SettingMeta: MessageFns = {
+ encode(message: SettingMeta, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.confidentiality !== 0) {
+ writer.uint32(8).int32(message.confidentiality);
+ }
+ if (message.readPermission !== 0) {
+ writer.uint32(16).int32(message.readPermission);
+ }
+ if (message.writePermission !== 0) {
+ writer.uint32(24).int32(message.writePermission);
+ }
+ for (const v of message.constraints) {
+ SettingConstraint.encode(v!, writer.uint32(34).fork()).join();
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): SettingMeta {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSettingMeta();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 8) {
+ break;
+ }
+
+ message.confidentiality = reader.int32() as any;
+ continue;
+ }
+ case 2: {
+ if (tag !== 16) {
+ break;
+ }
+
+ message.readPermission = reader.int32() as any;
+ continue;
+ }
+ case 3: {
+ if (tag !== 24) {
+ break;
+ }
+
+ message.writePermission = reader.int32() as any;
+ continue;
+ }
+ case 4: {
+ if (tag !== 34) {
+ break;
+ }
+
+ message.constraints.push(SettingConstraint.decode(reader, reader.uint32()));
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): SettingMeta {
+ return SettingMeta.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): SettingMeta {
+ const message = createBaseSettingMeta();
+ message.confidentiality = object.confidentiality ?? 0;
+ message.readPermission = object.readPermission ?? 0;
+ message.writePermission = object.writePermission ?? 0;
+ message.constraints = object.constraints?.map((e) => SettingConstraint.fromPartial(e)) || [];
+ return message;
+ },
+};
+
+function createBaseSetting(): Setting {
+ return { customSubsystemIndex: 0, key: "", meta: undefined, hasUnsavedValue: false, value: undefined, source: 0 };
+}
+
+export const Setting: MessageFns = {
+ encode(message: Setting, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.customSubsystemIndex !== 0) {
+ writer.uint32(8).uint32(message.customSubsystemIndex);
+ }
+ if (message.key !== "") {
+ writer.uint32(18).string(message.key);
+ }
+ if (message.meta !== undefined) {
+ SettingMeta.encode(message.meta, writer.uint32(26).fork()).join();
+ }
+ if (message.hasUnsavedValue !== false) {
+ writer.uint32(64).bool(message.hasUnsavedValue);
+ }
+ if (message.value !== undefined) {
+ SettingValue.encode(message.value, writer.uint32(74).fork()).join();
+ }
+ if (message.source !== 0) {
+ writer.uint32(80).uint32(message.source);
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): Setting {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSetting();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 8) {
+ break;
+ }
+
+ message.customSubsystemIndex = reader.uint32();
+ continue;
+ }
+ case 2: {
+ if (tag !== 18) {
+ break;
+ }
+
+ message.key = reader.string();
+ continue;
+ }
+ case 3: {
+ if (tag !== 26) {
+ break;
+ }
+
+ message.meta = SettingMeta.decode(reader, reader.uint32());
+ continue;
+ }
+ case 8: {
+ if (tag !== 64) {
+ break;
+ }
+
+ message.hasUnsavedValue = reader.bool();
+ continue;
+ }
+ case 9: {
+ if (tag !== 74) {
+ break;
+ }
+
+ message.value = SettingValue.decode(reader, reader.uint32());
+ continue;
+ }
+ case 10: {
+ if (tag !== 80) {
+ break;
+ }
+
+ message.source = reader.uint32();
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): Setting {
+ return Setting.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): Setting {
+ const message = createBaseSetting();
+ message.customSubsystemIndex = object.customSubsystemIndex ?? 0;
+ message.key = object.key ?? "";
+ message.meta = (object.meta !== undefined && object.meta !== null)
+ ? SettingMeta.fromPartial(object.meta)
+ : undefined;
+ message.hasUnsavedValue = object.hasUnsavedValue ?? false;
+ message.value = (object.value !== undefined && object.value !== null)
+ ? SettingValue.fromPartial(object.value)
+ : undefined;
+ message.source = object.source ?? 0;
+ return message;
+ },
+};
+
+function createBaseListSettingsRequest(): ListSettingsRequest {
+ return { scope: undefined, requireMeta: false };
+}
+
+export const ListSettingsRequest: MessageFns = {
+ encode(message: ListSettingsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.scope !== undefined) {
+ SettingScope.encode(message.scope, writer.uint32(10).fork()).join();
+ }
+ if (message.requireMeta !== false) {
+ writer.uint32(16).bool(message.requireMeta);
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): ListSettingsRequest {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseListSettingsRequest();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.scope = SettingScope.decode(reader, reader.uint32());
+ continue;
+ }
+ case 2: {
+ if (tag !== 16) {
+ break;
+ }
+
+ message.requireMeta = reader.bool();
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): ListSettingsRequest {
+ return ListSettingsRequest.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): ListSettingsRequest {
+ const message = createBaseListSettingsRequest();
+ message.scope = (object.scope !== undefined && object.scope !== null)
+ ? SettingScope.fromPartial(object.scope)
+ : undefined;
+ message.requireMeta = object.requireMeta ?? false;
+ return message;
+ },
+};
+
+function createBaseGetSettingRequest(): GetSettingRequest {
+ return { setting: undefined, requireMeta: false };
+}
+
+export const GetSettingRequest: MessageFns = {
+ encode(message: GetSettingRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.setting !== undefined) {
+ SettingRef.encode(message.setting, writer.uint32(10).fork()).join();
+ }
+ if (message.requireMeta !== false) {
+ writer.uint32(16).bool(message.requireMeta);
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): GetSettingRequest {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseGetSettingRequest();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.setting = SettingRef.decode(reader, reader.uint32());
+ continue;
+ }
+ case 2: {
+ if (tag !== 16) {
+ break;
+ }
+
+ message.requireMeta = reader.bool();
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): GetSettingRequest {
+ return GetSettingRequest.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): GetSettingRequest {
+ const message = createBaseGetSettingRequest();
+ message.setting = (object.setting !== undefined && object.setting !== null)
+ ? SettingRef.fromPartial(object.setting)
+ : undefined;
+ message.requireMeta = object.requireMeta ?? false;
+ return message;
+ },
+};
+
+function createBaseWriteSettingRequest(): WriteSettingRequest {
+ return { setting: undefined, value: undefined, mode: 0 };
+}
+
+export const WriteSettingRequest: MessageFns = {
+ encode(message: WriteSettingRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.setting !== undefined) {
+ SettingRef.encode(message.setting, writer.uint32(10).fork()).join();
+ }
+ if (message.value !== undefined) {
+ SettingValue.encode(message.value, writer.uint32(18).fork()).join();
+ }
+ if (message.mode !== 0) {
+ writer.uint32(24).int32(message.mode);
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): WriteSettingRequest {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseWriteSettingRequest();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.setting = SettingRef.decode(reader, reader.uint32());
+ continue;
+ }
+ case 2: {
+ if (tag !== 18) {
+ break;
+ }
+
+ message.value = SettingValue.decode(reader, reader.uint32());
+ continue;
+ }
+ case 3: {
+ if (tag !== 24) {
+ break;
+ }
+
+ message.mode = reader.int32() as any;
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): WriteSettingRequest {
+ return WriteSettingRequest.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): WriteSettingRequest {
+ const message = createBaseWriteSettingRequest();
+ message.setting = (object.setting !== undefined && object.setting !== null)
+ ? SettingRef.fromPartial(object.setting)
+ : undefined;
+ message.value = (object.value !== undefined && object.value !== null)
+ ? SettingValue.fromPartial(object.value)
+ : undefined;
+ message.mode = object.mode ?? 0;
+ return message;
+ },
+};
+
+function createBasePushBackArrayRequest(): PushBackArrayRequest {
+ return { setting: undefined, value: undefined, mode: 0 };
+}
+
+export const PushBackArrayRequest: MessageFns = {
+ encode(message: PushBackArrayRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.setting !== undefined) {
+ SettingRef.encode(message.setting, writer.uint32(10).fork()).join();
+ }
+ if (message.value !== undefined) {
+ SettingScalarValue.encode(message.value, writer.uint32(18).fork()).join();
+ }
+ if (message.mode !== 0) {
+ writer.uint32(24).int32(message.mode);
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): PushBackArrayRequest {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBasePushBackArrayRequest();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.setting = SettingRef.decode(reader, reader.uint32());
+ continue;
+ }
+ case 2: {
+ if (tag !== 18) {
+ break;
+ }
+
+ message.value = SettingScalarValue.decode(reader, reader.uint32());
+ continue;
+ }
+ case 3: {
+ if (tag !== 24) {
+ break;
+ }
+
+ message.mode = reader.int32() as any;
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): PushBackArrayRequest {
+ return PushBackArrayRequest.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): PushBackArrayRequest {
+ const message = createBasePushBackArrayRequest();
+ message.setting = (object.setting !== undefined && object.setting !== null)
+ ? SettingRef.fromPartial(object.setting)
+ : undefined;
+ message.value = (object.value !== undefined && object.value !== null)
+ ? SettingScalarValue.fromPartial(object.value)
+ : undefined;
+ message.mode = object.mode ?? 0;
+ return message;
+ },
+};
+
+function createBasePopBackArrayRequest(): PopBackArrayRequest {
+ return { setting: undefined, mode: 0 };
+}
+
+export const PopBackArrayRequest: MessageFns = {
+ encode(message: PopBackArrayRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.setting !== undefined) {
+ SettingRef.encode(message.setting, writer.uint32(10).fork()).join();
+ }
+ if (message.mode !== 0) {
+ writer.uint32(16).int32(message.mode);
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): PopBackArrayRequest {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBasePopBackArrayRequest();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.setting = SettingRef.decode(reader, reader.uint32());
+ continue;
+ }
+ case 2: {
+ if (tag !== 16) {
+ break;
+ }
+
+ message.mode = reader.int32() as any;
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): PopBackArrayRequest {
+ return PopBackArrayRequest.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): PopBackArrayRequest {
+ const message = createBasePopBackArrayRequest();
+ message.setting = (object.setting !== undefined && object.setting !== null)
+ ? SettingRef.fromPartial(object.setting)
+ : undefined;
+ message.mode = object.mode ?? 0;
+ return message;
+ },
+};
+
+function createBaseSaveSettingsRequest(): SaveSettingsRequest {
+ return { scope: undefined };
+}
+
+export const SaveSettingsRequest: MessageFns = {
+ encode(message: SaveSettingsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.scope !== undefined) {
+ SettingScope.encode(message.scope, writer.uint32(10).fork()).join();
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): SaveSettingsRequest {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSaveSettingsRequest();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.scope = SettingScope.decode(reader, reader.uint32());
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): SaveSettingsRequest {
+ return SaveSettingsRequest.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): SaveSettingsRequest {
+ const message = createBaseSaveSettingsRequest();
+ message.scope = (object.scope !== undefined && object.scope !== null)
+ ? SettingScope.fromPartial(object.scope)
+ : undefined;
+ return message;
+ },
+};
+
+function createBaseDiscardSettingsRequest(): DiscardSettingsRequest {
+ return { scope: undefined };
+}
+
+export const DiscardSettingsRequest: MessageFns = {
+ encode(message: DiscardSettingsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.scope !== undefined) {
+ SettingScope.encode(message.scope, writer.uint32(10).fork()).join();
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): DiscardSettingsRequest {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseDiscardSettingsRequest();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.scope = SettingScope.decode(reader, reader.uint32());
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): DiscardSettingsRequest {
+ return DiscardSettingsRequest.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): DiscardSettingsRequest {
+ const message = createBaseDiscardSettingsRequest();
+ message.scope = (object.scope !== undefined && object.scope !== null)
+ ? SettingScope.fromPartial(object.scope)
+ : undefined;
+ return message;
+ },
+};
+
+function createBaseResetSettingsRequest(): ResetSettingsRequest {
+ return { scope: undefined };
+}
+
+export const ResetSettingsRequest: MessageFns = {
+ encode(message: ResetSettingsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.scope !== undefined) {
+ SettingScope.encode(message.scope, writer.uint32(10).fork()).join();
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): ResetSettingsRequest {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseResetSettingsRequest();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.scope = SettingScope.decode(reader, reader.uint32());
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): ResetSettingsRequest {
+ return ResetSettingsRequest.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): ResetSettingsRequest {
+ const message = createBaseResetSettingsRequest();
+ message.scope = (object.scope !== undefined && object.scope !== null)
+ ? SettingScope.fromPartial(object.scope)
+ : undefined;
+ return message;
+ },
+};
+
+function createBaseRequest(): Request {
+ return {
+ listSettings: undefined,
+ getSetting: undefined,
+ writeSetting: undefined,
+ saveSettings: undefined,
+ discardSettings: undefined,
+ resetSettings: undefined,
+ pushBackArray: undefined,
+ popBackArray: undefined,
+ };
+}
+
+export const Request: MessageFns = {
+ encode(message: Request, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.listSettings !== undefined) {
+ ListSettingsRequest.encode(message.listSettings, writer.uint32(10).fork()).join();
+ }
+ if (message.getSetting !== undefined) {
+ GetSettingRequest.encode(message.getSetting, writer.uint32(18).fork()).join();
+ }
+ if (message.writeSetting !== undefined) {
+ WriteSettingRequest.encode(message.writeSetting, writer.uint32(26).fork()).join();
+ }
+ if (message.saveSettings !== undefined) {
+ SaveSettingsRequest.encode(message.saveSettings, writer.uint32(34).fork()).join();
+ }
+ if (message.discardSettings !== undefined) {
+ DiscardSettingsRequest.encode(message.discardSettings, writer.uint32(42).fork()).join();
+ }
+ if (message.resetSettings !== undefined) {
+ ResetSettingsRequest.encode(message.resetSettings, writer.uint32(50).fork()).join();
+ }
+ if (message.pushBackArray !== undefined) {
+ PushBackArrayRequest.encode(message.pushBackArray, writer.uint32(58).fork()).join();
+ }
+ if (message.popBackArray !== undefined) {
+ PopBackArrayRequest.encode(message.popBackArray, writer.uint32(66).fork()).join();
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): Request {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseRequest();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.listSettings = ListSettingsRequest.decode(reader, reader.uint32());
+ continue;
+ }
+ case 2: {
+ if (tag !== 18) {
+ break;
+ }
+
+ message.getSetting = GetSettingRequest.decode(reader, reader.uint32());
+ continue;
+ }
+ case 3: {
+ if (tag !== 26) {
+ break;
+ }
+
+ message.writeSetting = WriteSettingRequest.decode(reader, reader.uint32());
+ continue;
+ }
+ case 4: {
+ if (tag !== 34) {
+ break;
+ }
+
+ message.saveSettings = SaveSettingsRequest.decode(reader, reader.uint32());
+ continue;
+ }
+ case 5: {
+ if (tag !== 42) {
+ break;
+ }
+
+ message.discardSettings = DiscardSettingsRequest.decode(reader, reader.uint32());
+ continue;
+ }
+ case 6: {
+ if (tag !== 50) {
+ break;
+ }
+
+ message.resetSettings = ResetSettingsRequest.decode(reader, reader.uint32());
+ continue;
+ }
+ case 7: {
+ if (tag !== 58) {
+ break;
+ }
+
+ message.pushBackArray = PushBackArrayRequest.decode(reader, reader.uint32());
+ continue;
+ }
+ case 8: {
+ if (tag !== 66) {
+ break;
+ }
+
+ message.popBackArray = PopBackArrayRequest.decode(reader, reader.uint32());
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): Request {
+ return Request.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): Request {
+ const message = createBaseRequest();
+ message.listSettings = (object.listSettings !== undefined && object.listSettings !== null)
+ ? ListSettingsRequest.fromPartial(object.listSettings)
+ : undefined;
+ message.getSetting = (object.getSetting !== undefined && object.getSetting !== null)
+ ? GetSettingRequest.fromPartial(object.getSetting)
+ : undefined;
+ message.writeSetting = (object.writeSetting !== undefined && object.writeSetting !== null)
+ ? WriteSettingRequest.fromPartial(object.writeSetting)
+ : undefined;
+ message.saveSettings = (object.saveSettings !== undefined && object.saveSettings !== null)
+ ? SaveSettingsRequest.fromPartial(object.saveSettings)
+ : undefined;
+ message.discardSettings = (object.discardSettings !== undefined && object.discardSettings !== null)
+ ? DiscardSettingsRequest.fromPartial(object.discardSettings)
+ : undefined;
+ message.resetSettings = (object.resetSettings !== undefined && object.resetSettings !== null)
+ ? ResetSettingsRequest.fromPartial(object.resetSettings)
+ : undefined;
+ message.pushBackArray = (object.pushBackArray !== undefined && object.pushBackArray !== null)
+ ? PushBackArrayRequest.fromPartial(object.pushBackArray)
+ : undefined;
+ message.popBackArray = (object.popBackArray !== undefined && object.popBackArray !== null)
+ ? PopBackArrayRequest.fromPartial(object.popBackArray)
+ : undefined;
+ return message;
+ },
+};
+
+function createBaseErrorResponse(): ErrorResponse {
+ return { message: "" };
+}
+
+export const ErrorResponse: MessageFns = {
+ encode(message: ErrorResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.message !== "") {
+ writer.uint32(10).string(message.message);
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): ErrorResponse {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseErrorResponse();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.message = reader.string();
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): ErrorResponse {
+ return ErrorResponse.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): ErrorResponse {
+ const message = createBaseErrorResponse();
+ message.message = object.message ?? "";
+ return message;
+ },
+};
+
+function createBaseStatusResponse(): StatusResponse {
+ return { affectedCount: 0, message: "" };
+}
+
+export const StatusResponse: MessageFns = {
+ encode(message: StatusResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.affectedCount !== 0) {
+ writer.uint32(8).uint32(message.affectedCount);
+ }
+ if (message.message !== "") {
+ writer.uint32(18).string(message.message);
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): StatusResponse {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseStatusResponse();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 8) {
+ break;
+ }
+
+ message.affectedCount = reader.uint32();
+ continue;
+ }
+ case 2: {
+ if (tag !== 18) {
+ break;
+ }
+
+ message.message = reader.string();
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): StatusResponse {
+ return StatusResponse.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): StatusResponse {
+ const message = createBaseStatusResponse();
+ message.affectedCount = object.affectedCount ?? 0;
+ message.message = object.message ?? "";
+ return message;
+ },
+};
+
+function createBaseGetSettingResponse(): GetSettingResponse {
+ return { setting: undefined };
+}
+
+export const GetSettingResponse: MessageFns = {
+ encode(message: GetSettingResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.setting !== undefined) {
+ Setting.encode(message.setting, writer.uint32(10).fork()).join();
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): GetSettingResponse {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseGetSettingResponse();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.setting = Setting.decode(reader, reader.uint32());
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): GetSettingResponse {
+ return GetSettingResponse.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): GetSettingResponse {
+ const message = createBaseGetSettingResponse();
+ message.setting = (object.setting !== undefined && object.setting !== null)
+ ? Setting.fromPartial(object.setting)
+ : undefined;
+ return message;
+ },
+};
+
+function createBaseResponse(): Response {
+ return { error: undefined, status: undefined, getSetting: undefined };
+}
+
+export const Response: MessageFns = {
+ encode(message: Response, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.error !== undefined) {
+ ErrorResponse.encode(message.error, writer.uint32(10).fork()).join();
+ }
+ if (message.status !== undefined) {
+ StatusResponse.encode(message.status, writer.uint32(18).fork()).join();
+ }
+ if (message.getSetting !== undefined) {
+ GetSettingResponse.encode(message.getSetting, writer.uint32(26).fork()).join();
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): Response {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseResponse();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.error = ErrorResponse.decode(reader, reader.uint32());
+ continue;
+ }
+ case 2: {
+ if (tag !== 18) {
+ break;
+ }
+
+ message.status = StatusResponse.decode(reader, reader.uint32());
+ continue;
+ }
+ case 3: {
+ if (tag !== 26) {
+ break;
+ }
+
+ message.getSetting = GetSettingResponse.decode(reader, reader.uint32());
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): Response {
+ return Response.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): Response {
+ const message = createBaseResponse();
+ message.error = (object.error !== undefined && object.error !== null)
+ ? ErrorResponse.fromPartial(object.error)
+ : undefined;
+ message.status = (object.status !== undefined && object.status !== null)
+ ? StatusResponse.fromPartial(object.status)
+ : undefined;
+ message.getSetting = (object.getSetting !== undefined && object.getSetting !== null)
+ ? GetSettingResponse.fromPartial(object.getSetting)
+ : undefined;
+ return message;
+ },
+};
+
+function createBaseSettingNotification(): SettingNotification {
+ return { kind: 0, setting: undefined };
+}
+
+export const SettingNotification: MessageFns = {
+ encode(message: SettingNotification, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.kind !== 0) {
+ writer.uint32(8).int32(message.kind);
+ }
+ if (message.setting !== undefined) {
+ Setting.encode(message.setting, writer.uint32(18).fork()).join();
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): SettingNotification {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseSettingNotification();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 8) {
+ break;
+ }
+
+ message.kind = reader.int32() as any;
+ continue;
+ }
+ case 2: {
+ if (tag !== 18) {
+ break;
+ }
+
+ message.setting = Setting.decode(reader, reader.uint32());
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): SettingNotification {
+ return SettingNotification.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): SettingNotification {
+ const message = createBaseSettingNotification();
+ message.kind = object.kind ?? 0;
+ message.setting = (object.setting !== undefined && object.setting !== null)
+ ? Setting.fromPartial(object.setting)
+ : undefined;
+ return message;
+ },
+};
+
+function createBaseNotification(): Notification {
+ return { setting: undefined };
+}
+
+export const Notification: MessageFns = {
+ encode(message: Notification, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+ if (message.setting !== undefined) {
+ SettingNotification.encode(message.setting, writer.uint32(10).fork()).join();
+ }
+ return writer;
+ },
+
+ decode(input: BinaryReader | Uint8Array, length?: number): Notification {
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
+ const end = length === undefined ? reader.len : reader.pos + length;
+ const message = createBaseNotification();
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: {
+ if (tag !== 10) {
+ break;
+ }
+
+ message.setting = SettingNotification.decode(reader, reader.uint32());
+ continue;
+ }
+ }
+ if ((tag & 7) === 4 || tag === 0) {
+ break;
+ }
+ reader.skip(tag & 7);
+ }
+ return message;
+ },
+
+ create(base?: DeepPartial): Notification {
+ return Notification.fromPartial(base ?? {});
+ },
+ fromPartial(object: DeepPartial): Notification {
+ const message = createBaseNotification();
+ message.setting = (object.setting !== undefined && object.setting !== null)
+ ? SettingNotification.fromPartial(object.setting)
+ : undefined;
+ return message;
+ },
+};
+
+type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
+
+export type DeepPartial = T extends Builtin ? T
+ : T extends globalThis.Array ? globalThis.Array>
+ : T extends ReadonlyArray ? ReadonlyArray>
+ : T extends {} ? { [K in keyof T]?: DeepPartial }
+ : Partial;
+
+export interface MessageFns {
+ encode(message: T, writer?: BinaryWriter): BinaryWriter;
+ decode(input: BinaryReader | Uint8Array, length?: number): T;
+ create(base?: DeepPartial): T;
+ fromPartial(object: DeepPartial): T;
+}