// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
pub(all) enum AxisRange {
LowerHalf
UpperHalf
Full
}
///|
pub(all) enum Token {
Uuid(Uuid)
Platform(String)
Name(String)
AxisMapping(Int, AxisOrBtn, AxisRange, AxisRange, Bool)
ButtonMapping(Int, AxisOrBtn, AxisRange)
HatMapping(Int, Int, AxisOrBtn, AxisRange)
}
///|
pub(all) enum ParserErrorKind {
InvalidGuid
InvalidKeyValPair
InvalidValue
EmptyValue
UnknownAxis
UnknownButton
InvalidParserState
UnexpectedEnd
}
///|
suberror ParserError {
Error(ParserErrorKind, Int)
}
///|
pub fn ParserError::kind(self : ParserError) -> ParserErrorKind {
match self {
Error(kind, _) => kind
}
}
///|
pub fn ParserError::position(self : ParserError) -> Int {
match self {
Error(_, pos) => pos
}
}
///|
priv enum State {
Uuid
Name
KeyVal
Invalid
}
///|
priv struct Parser {
data : String
mut pos : Int
mut state : State
}
///|
fn Parser::new(mapping : String) -> Parser {
{ data: mapping, pos: 0, state: Uuid }
}
///|
fn next_comma_or_end(data : String, pos : Int) -> Int {
let n = data.length()
for i in pos.. Int? {
for i in pos.. String {
data.sub(start~, end~).to_owned()
}
///|
fn parse_u16_decimal(s : String, pos : Int) -> Int raise ParserError {
if s.length() == 0 {
raise Error(InvalidValue, pos)
}
let mut acc = 0
for c in s {
if c is ('0'..='9') {
acc = acc * 10 + (c.to_int() - '0'.to_int())
if acc > 65535 {
raise Error(InvalidValue, pos)
}
} else {
raise Error(InvalidValue, pos)
}
}
acc
}
///|
let axes_sdl : Array[String] = [
"a", "b", "back", "c", "dpdown", "dpleft", "dpright", "dpup", "guide", "leftshoulder",
"leftstick", "lefttrigger", "leftx", "lefty", "leftz", "misc1", "paddle1", "paddle2",
"paddle3", "paddle4", "rightshoulder", "rightstick", "righttrigger", "rightx",
"righty", "rightz", "start", "touchpad", "x", "y", "z",
]
///|
let axes : Array[AxisOrBtn] = [
Btn(South),
Btn(East),
Btn(Select),
Btn(C),
Btn(DPadDown),
Btn(DPadLeft),
Btn(DPadRight),
Btn(DPadUp),
Btn(Mode),
Btn(LeftTrigger),
Btn(LeftThumb),
Btn(LeftTrigger2),
Axis(LeftStickX),
Axis(LeftStickY),
Axis(LeftZ),
Btn(Unknown),
Btn(Unknown),
Btn(Unknown),
Btn(Unknown),
Btn(Unknown),
Btn(RightTrigger),
Btn(RightThumb),
Btn(RightTrigger2),
Axis(RightStickX),
Axis(RightStickY),
Axis(RightZ),
Btn(Start),
Btn(Unknown),
Btn(West),
Btn(North),
Btn(Z),
]
///|
fn lookup_axis_or_btn(
key : String,
kind : ParserErrorKind,
pos : Int,
) -> AxisOrBtn raise ParserError {
for i in 0.. Token? raise ParserError {
if self.pos >= self.data.length() {
None
} else {
Some(
match self.state {
Uuid => self.parse_uuid()
Name => self.parse_name()
KeyVal => self.parse_key_val()
Invalid => raise Error(InvalidParserState, self.pos)
},
)
}
}
///|
fn Parser::parse_uuid(self : Parser) -> Token raise ParserError {
let next_comma = next_comma_or_end(self.data, self.pos)
let uuid_field = slice_to_string(self.data, self.pos, next_comma)
if uuid_field == "xinput" {
if next_comma == self.data.length() {
self.state = Invalid
raise Error(UnexpectedEnd, self.pos)
}
self.state = Name
self.pos = next_comma + 1
return Uuid(Uuid::nil())
}
let uuid = Token::Uuid(Uuid::parse(uuid_field)) catch {
_ => {
self.state = Invalid
raise Error(InvalidGuid, self.pos)
}
}
if next_comma == self.data.length() {
self.state = Invalid
raise Error(UnexpectedEnd, self.pos)
}
self.state = Name
self.pos = next_comma + 1
uuid
}
///|
fn Parser::parse_name(self : Parser) -> Token {
let next_comma = next_comma_or_end(self.data, self.pos)
let name = slice_to_string(self.data, self.pos, next_comma)
self.state = KeyVal
self.pos = next_comma + 1
Name(name)
}
///|
fn Parser::parse_key_val(self : Parser) -> Token raise ParserError {
let next_comma = next_comma_or_end(self.data, self.pos)
let pos = self.pos
let _pair = slice_to_string(self.data, self.pos, next_comma)
self.pos = next_comma + 1
let colon = match find_char(self.data, pos, next_comma, ':') {
Some(i) => i
None => raise Error(InvalidKeyValPair, pos)
}
if find_char(self.data, colon + 1, next_comma, ':') is Some(_) {
raise Error(InvalidKeyValPair, pos)
}
let key = slice_to_string(self.data, pos, colon)
let value = slice_to_string(self.data, colon + 1, next_comma)
if value.length() == 0 {
raise Error(EmptyValue, pos)
}
if key == "platform" {
return Platform(value)
}
let (key, output) = match key {
['+', .. rest] => (rest.to_owned(), AxisRange::UpperHalf)
['-', .. rest] => (rest.to_owned(), LowerHalf)
_ => (key, Full)
}
let (from_str, input, inverted, is_axis) = match value {
['+', 'a', .. rest] => {
let mut body = rest.to_owned()
let mut inv = false
if body.length() > 0 &&
body.code_unit_at(body.length() - 1).to_int() == '~'.to_int() {
inv = true
body = body[0:body.length() - 1].to_owned()
}
(body, AxisRange::UpperHalf, inv, true)
}
['-', 'a', .. rest] => {
let mut body = rest.to_owned()
let mut inv = false
if body.length() > 0 &&
body.code_unit_at(body.length() - 1).to_int() == '~'.to_int() {
inv = true
body = body[0:body.length() - 1].to_owned()
}
(body, LowerHalf, inv, true)
}
['a', .. rest] => {
let mut body = rest.to_owned()
let mut inv = false
if body.length() > 0 &&
body.code_unit_at(body.length() - 1).to_int() == '~'.to_int() {
inv = true
body = body[0:body.length() - 1].to_owned()
}
(body, Full, inv, true)
}
['b', .. rest] => (rest.to_owned(), Full, false, false)
['h', .. rest] => {
let body = rest.to_owned()
let dot = match find_char(body, 0, body.length(), '.') {
Some(i) => i
None => raise Error(InvalidValue, pos)
}
let hat = parse_u16_decimal(body[0:dot].to_owned(), pos + 1)
let direction = parse_u16_decimal(
body[dot + 1:body.length()].to_owned(),
pos + dot + 2,
)
let to = lookup_axis_or_btn(key, UnknownButton, pos)
return HatMapping(hat, direction, to, output)
}
_ => raise Error(InvalidValue, pos)
}
let from = parse_u16_decimal(from_str, pos)
if is_axis {
let to = lookup_axis_or_btn(key, UnknownAxis, pos)
AxisMapping(from, to, input, output, inverted)
} else {
let to = lookup_axis_or_btn(key, UnknownButton, pos)
ButtonMapping(from, to, output)
}
}