summaryrefslogtreecommitdiff
path: root/lua/clever_tee/repeat_resolver.lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua/clever_tee/repeat_resolver.lua')
-rw-r--r--lua/clever_tee/repeat_resolver.lua333
1 files changed, 333 insertions, 0 deletions
diff --git a/lua/clever_tee/repeat_resolver.lua b/lua/clever_tee/repeat_resolver.lua
new file mode 100644
index 0000000..67d6617
--- /dev/null
+++ b/lua/clever_tee/repeat_resolver.lua
@@ -0,0 +1,333 @@
+local domain = require("clever_tee.domain")
+local sequence_state = require("clever_tee.sequence_state")
+local state_transitions = require("clever_tee.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_tee.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.explicit_target(stored_target)
+ if stored_target == nil then
+ return domain.TargetValue.code_fallback(0)
+ end
+ return stored_target
+end
+
+local function build_explicit_request(descriptor, stored_target)
+ if descriptor == nil then
+ return domain.ExplicitRepeatRequest.neutral()
+ end
+ local target = M.explicit_target(stored_target)
+ if target.first_code == 0x80 then
+ return domain.ExplicitRepeatRequest.neutral()
+ end
+ return domain.ExplicitRepeatRequest.new(descriptor, target)
+end
+
+function M.build_same_direction_request(stored_descriptor, stored_target)
+ return build_explicit_request(stored_descriptor, stored_target)
+end
+
+M.explicit_same_direction = M.build_same_direction_request
+M.same_direction_request = 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)
+ if stored_descriptor == nil then
+ return domain.ExplicitRepeatRequest.neutral()
+ end
+ return build_explicit_request(
+ domain.Descriptor.swap(stored_descriptor),
+ stored_target
+ )
+end
+
+M.explicit_opposite_direction = M.build_opposite_direction_request
+M.opposite_direction_request = 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