local domain = require("clever_f.domain") local sequence_state = require("clever_f.sequence_state") local state_transitions = require("clever_f.state_transitions") local M = {} local RepeatResolver = {} M.RepeatResolver = RepeatResolver M.Decision = { ACQUIRE = "acquire", REPEAT = "repeat", } M.ACQUIRE = M.Decision.ACQUIRE M.REPEAT = M.Decision.REPEAT local resolver_records = setmetatable({}, { __mode = "k" }) local function fail(message, level) error(message, (level or 1) + 1) end local function is_nonnegative_integer(value) return type(value) == "number" and value >= 0 and value < math.huge and value == math.floor(value) end local function require_policy(service) if service ~= nil and (type(service) ~= "table" or type(service.sample_timeouts) ~= "function" or type(service.sample_direction) ~= "function") then fail("RepeatResolver policy must sample timeouts and direction", 3) end return service end local function require_clock(clock) if clock ~= nil and (type(clock) ~= "table" or type(clock.read_time_ms) ~= "function") then fail("RepeatResolver clock must provide read_time_ms", 3) end return clock end local function require_transitions(transitions, state) transitions = transitions or state_transitions.new(state) if type(transitions) ~= "table" or type(transitions.SetRepeatTimestamp) ~= "function" or type(transitions.PublicReset) ~= "function" then fail("RepeatResolver transitions must set repeat time and apply Public Reset", 3) end return transitions end local function finite_time(value) return type(value) == "number" and value == value and value > -math.huge and value < math.huge end function M.truncate_elapsed_ms(elapsed_ms) if not finite_time(elapsed_ms) then fail("elapsed milliseconds must be finite", 2) end local integer_part = math.modf(elapsed_ms) return integer_part end M.truncate_milliseconds = M.truncate_elapsed_ms local resolver_metatable = { __index = RepeatResolver, __newindex = function() fail("RepeatResolver values are immutable", 2) end, __tostring = function() return "repeat-resolver" end, __metatable = "clever_f.repeat_resolver.RepeatResolver", } function RepeatResolver.new(options) if RepeatResolver.is(options) then return options end if options == nil then options = {} elseif sequence_state.is(options) then options = { state = options } elseif type(options) ~= "table" then fail("RepeatResolver options must be a table", 2) end local state = options.state or sequence_state.get() if not sequence_state.is(state) then fail("RepeatResolver state must be the plugin-global SequenceState", 2) end local resolver = setmetatable({}, resolver_metatable) resolver_records[resolver] = { clock = require_clock(options.clock or options.time_provider or options.host), policy = require_policy(options.policy or options.policy_service), state = state, transitions = require_transitions( options.transitions or options.state_transitions, state ), } return resolver end function RepeatResolver.is(value) return type(value) == "table" and resolver_records[value] ~= nil end function RepeatResolver:previous_landing(context) context = domain.ModeContext.from_full_mode(context) return resolver_records[self].state:get_previous_landing(context) end function RepeatResolver:decide(context, current_position, macro_state) current_position = domain.Position.coerce(current_position) local landing = self:previous_landing(context) if landing == nil or not domain.Position.equal(landing, current_position) then return M.Decision.ACQUIRE end if domain.MacroState.new(macro_state).executing then return M.Decision.ACQUIRE end return M.Decision.REPEAT end RepeatResolver.eligibility = RepeatResolver.decide RepeatResolver.resolve_eligibility = RepeatResolver.decide function RepeatResolver:sample_repeat_timeout_ms() local service = resolver_records[self].policy if service == nil then fail("RepeatResolver requires a policy to sample repeat timeout", 2) end local sampled = service:sample_timeouts() local timeout = type(sampled) == "table" and sampled.repeat_timeout_ms or nil if not is_nonnegative_integer(timeout) then fail("repeat_timeout_ms sample must be a nonnegative integer", 2) end return timeout end RepeatResolver.sample_repeat_timeout = RepeatResolver.sample_repeat_timeout_ms function RepeatResolver:evaluate_timeout(current_window) local timeout = self:sample_repeat_timeout_ms() if timeout == 0 then return M.Decision.REPEAT, nil end local clock = resolver_records[self].clock if clock == nil then fail("RepeatResolver requires a clock for positive repeat timeout", 2) end local current_time = clock:read_time_ms() if not finite_time(current_time) then fail("repeat clock must return finite milliseconds", 2) end local record = resolver_records[self] local elapsed_ms = M.truncate_elapsed_ms( current_time - record.state.repeat_timestamp_ms ) record.transitions:SetRepeatTimestamp(current_time) if elapsed_ms <= timeout then return M.Decision.REPEAT, elapsed_ms end local cleanup = record.transitions:PublicReset(current_window) return M.Decision.ACQUIRE, elapsed_ms, cleanup end RepeatResolver.check_timeout = RepeatResolver.evaluate_timeout RepeatResolver.resolve_timeout = RepeatResolver.evaluate_timeout local function sampled_fixed_direction(resolver) local service = resolver_records[resolver].policy if service == nil then fail("RepeatResolver requires a policy to resolve primary direction", 3) end local sampled = service:sample_direction() local fixed if type(sampled) == "table" then fixed = sampled.fix_key_direction end if type(fixed) ~= "boolean" then fail("fix_key_direction sample must be a Boolean", 3) end return fixed end function M.reverse_request(stored_descriptor, pressed_key, fix_key_direction) local stored = domain.Descriptor.from_string(stored_descriptor) local pressed = domain.Descriptor.from_string(pressed_key) if type(fix_key_direction) ~= "boolean" then fail("fix_key_direction must be a Boolean", 2) end local reverse = domain.Descriptor.is_uppercase(pressed) if fix_key_direction and domain.Descriptor.is_uppercase(stored) then reverse = not reverse end return reverse end function M.primary_direction(stored_descriptor, pressed_key, fix_key_direction) local stored = domain.Descriptor.from_string(stored_descriptor) if M.reverse_request(stored, pressed_key, fix_key_direction) then return domain.Descriptor.swap(stored) end return stored end M.resolve_primary_direction = M.primary_direction M.effective_primary_descriptor = M.primary_direction function RepeatResolver:resolve_primary_direction(stored_descriptor, pressed_key) return M.primary_direction( stored_descriptor, pressed_key, sampled_fixed_direction(self) ) end RepeatResolver.primary_direction = RepeatResolver.resolve_primary_direction RepeatResolver.effective_primary_descriptor = RepeatResolver.resolve_primary_direction function M.build_same_direction_request(stored_descriptor, stored_target) return domain.ExplicitRepeatRequest.new(stored_descriptor, stored_target) end M.explicit_same_direction = M.build_same_direction_request function RepeatResolver:same_direction_request(context) context = domain.ModeContext.from_full_mode(context) local state = resolver_records[self].state return M.build_same_direction_request( state:get_previous_descriptor(context), state:get_previous_target(context) ) end RepeatResolver.resolve_explicit_same = RepeatResolver.same_direction_request RepeatResolver.explicit_same = RepeatResolver.same_direction_request function M.build_opposite_direction_request(stored_descriptor, stored_target) return domain.ExplicitRepeatRequest.new( domain.Descriptor.swap(stored_descriptor), stored_target ) end M.explicit_opposite_direction = M.build_opposite_direction_request function RepeatResolver:opposite_direction_request(context) context = domain.ModeContext.from_full_mode(context) local state = resolver_records[self].state return M.build_opposite_direction_request( state:get_previous_descriptor(context), state:get_previous_target(context) ) end RepeatResolver.resolve_explicit_opposite = RepeatResolver.opposite_direction_request RepeatResolver.explicit_opposite = RepeatResolver.opposite_direction_request function M.new(options) return RepeatResolver.new(options) end M.landing = function(context, options) return RepeatResolver.new(options):previous_landing(context) end function M.decide(context, current_position, macro_state, options) return RepeatResolver.new(options):decide( context, current_position, macro_state ) end M.eligibility = M.decide function M.sample_repeat_timeout_ms(options) return RepeatResolver.new(options):sample_repeat_timeout_ms() end function M.evaluate_timeout(options, current_window) return RepeatResolver.new(options):evaluate_timeout(current_window) end setmetatable(M, { __call = function(_, options) return RepeatResolver.new(options) end, }) return M