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 && ( +
+

{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. +

-

- 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; +}