summaryrefslogtreecommitdiff
path: root/main.lua
diff options
context:
space:
mode:
Diffstat (limited to 'main.lua')
-rw-r--r--main.lua421
1 files changed, 16 insertions, 405 deletions
diff --git a/main.lua b/main.lua
index ea11ebf..f7ceace 100644
--- a/main.lua
+++ b/main.lua
@@ -2,6 +2,7 @@
local cacher = require('cacher')
local config = require('config')
+local input = require('input')
local rt = require('rt')
local util = require('util')
local _catalogue = require('catalogue')
@@ -16,9 +17,7 @@ local mp_utils = require('mp.utils')
local script_name = mp.get_script_name()
local state = _state.new()
-local binding_state = {mappings = {}, active = {}}
local click_state = {}
-local osc_visibility
local downloader = _downloader.new({limit = 5})
local xc = _xc.new({
@@ -46,13 +45,6 @@ xc = cacher.wrap(xc, {
local catalogue = _catalogue.new()
local epg = _epg.new()
-local ctx = {
- binding_state = binding_state,
- catalogue = catalogue,
- epg = epg,
- xc = xc,
-}
-
local osd
local function dl_img(url, path, cb)
downloader:schedule(url, path, function(success, _, path)
@@ -80,410 +72,27 @@ osd = _osd.new({
end
})
-rt.init(state, osd, ctx)
-
-local function mouse_has_drifted(x1, y1, x2, y2)
- return math.abs(x1 - x2) > config.click_max_drift or
- math.abs(y1 - y2) > config.click_max_drift
-end
-
--- when mpv registers a double-click, the second click triggers both
--- double-click and click events. instead of identifying and ignoring this
--- second click event, we can implement double-click detection ourselves for
--- more control and to bypass some mpv inconsistencies.
-local function handle_mouse_click(ev, id)
- if ev.canceled then
- click_state = {}
- return
- end
-
- local mpos = osd.mpos
- if not mpos then
- return
- end
-
- local time = mp.get_time()
- local clk = click_state
-
- if ev.event == 'down' then
- clk.ct, clk.ck = time, ev.key_name
- clk.cx, clk.cy, clk.ci = mpos.x, mpos.y, id
- return
- end
-
- if ev.event ~= 'up' or not clk.ct then
- return
- end
-
- if time - clk.ct > config.click_timeout or
- clk.ck ~= ev.key_name or clk.ci ~= id or
- mouse_has_drifted(mpos.x, mpos.y, clk.cx, clk.cy) then
- click_state = {}
- return
- end
-
- local dbl = clk.pt and clk.ct - clk.pt <= config.click_dbl_time and
- clk.pk == ev.key_name and clk.pi == id and
- not mouse_has_drifted(clk.cx, clk.cy, clk.px, clk.py)
- click_state = dbl and {} or
- {
- pt = clk.ct, pk = ev.key_name,
- px = clk.cx, py = clk.cy, pi = id,
- }
- return ev.key_name, dbl
-end
-
-local function process_mouse_click(ev)
- local ms = osd.mstate
- if not ms then
- return
- end
-
- local area
- local val
- if ms.target == 'scrollbar' then
- area = 'scrollbar'
- val = ms.ratio
- elseif ms.target == 'menu' then
- area = 'menu'
- val = ms.option_line
- end
-
- local key, dbl = handle_mouse_click(ev, area == 'menu' and val or nil)
- return key, dbl, area, val
-end
-
-local function mouse_click_left_menu(dbl, line)
- if line == 0 then
- return
- end
-
- -- title
- if line < 0 then
- if not dbl then
- return
- end
-
- if line == -1 then
- rt.set_cursor(1)
- else
- state.depth = state.depth + line + 1
- osd:dirty()
- end
- return
- end
-
- local menu = state:menu()
- local pos = menu.view_top + line - 1
- if dbl then
- if pos ~= menu.cursor then
- return
- end
-
- rt.select_option()
- else
- if pos > #menu.options then
- return
- end
-
- rt.set_cursor(pos)
- end
-end
-
-local function mouse_click_left_scrollbar(dbl, ratio)
- if not dbl then
- return
- end
-
- -- set_cursor handles out-of-bounds moves (when ratio == 1)
- rt.set_cursor(
- math.floor(ratio * #state:menu().options) + 1,
- {centre = true})
-end
-
-local function mouse_click_left(ev)
- local key, dbl, area, val = process_mouse_click(ev)
- if not key then
- return
- end
-
- if area == 'menu' then
- return mouse_click_left_menu(dbl, val)
- elseif area == 'scrollbar' then
- return mouse_click_left_scrollbar(dbl, val)
- end
-end
-
-local function mouse_click_right_menu(dbl, line)
- if not dbl or line < 1 then
- return
- end
-
- local menu = state:menu()
- local pos = menu.view_top + line - 1
- if pos > #menu.options then
- return
- end
-
- rt.open_option_info(menu.options[pos])
-end
-
-local function mouse_click_right(ev)
- local key, dbl, area, val = process_mouse_click(ev)
- if not key then
- return
- end
-
- if area == 'menu' then
- return mouse_click_right_menu(dbl, val)
- end
-end
-
-binding_state.mappings.MENU = {
- ['BS'] = {rt.prev_menu},
- ['/'] = {rt.start_search},
- ['Ctrl+s'] = {rt.toggle_menu_sort},
- ['Ctrl+R'] = {rt.reload_data},
-
- ['ENTER'] = {rt.select_option},
- ['Ctrl+f'] = {rt.favourite_option},
- ['g'] = {rt.goto_option},
- ['i'] = {rt.open_option_info},
- ['?'] = {rt.open_option_info},
- ['Ctrl+p'] = {rt.goto_playing},
-
- ['MBTN_LEFT'] = {rt.mouse_click_left, 'complex'},
- ['MBTN_RIGHT'] = {rt.mouse_click_right, 'complex'},
-
- ['k'] = {rt.cursor_up, 'repeat'},
- ['j'] = {rt.cursor_down, 'repeat'},
- ['K'] = {rt.cursor_page_up, 'repeat'},
- ['J'] = {rt.cursor_page_down, 'repeat'},
- ['UP'] = {rt.cursor_up, 'repeat'},
- ['DOWN'] = {rt.cursor_down, 'repeat'},
- ['Shift+UP'] = {rt.cursor_page_up, 'repeat'},
- ['Shift+DOWN'] = {rt.cursor_page_down, 'repeat'},
- ['PGUP'] = {rt.cursor_page_up, 'repeat'},
- ['PGDWN'] = {rt.cursor_page_down, 'repeat'},
- ['HOME'] = {rt.cursor_start},
- ['END'] = {rt.cursor_end},
- ['WHEEL_UP'] = {rt.cursor_wheel_up, 'repeat'},
- ['WHEEL_DOWN'] = {rt.cursor_wheel_down, 'repeat'},
- ['Shift+WHEEL_UP'] = {rt.cursor_wheel_page_up, 'repeat'},
- ['Shift+WHEEL_DOWN'] = {rt.cursor_wheel_page_down, 'repeat'},
-
- ['Alt+k'] = {rt.move_option_up, 'repeat'},
- ['Alt+j'] = {rt.move_option_down, 'repeat'},
- ['Alt+K'] = {rt.move_option_page_up, 'repeat'},
- ['Alt+J'] = {rt.move_option_page_down, 'repeat'},
- ['Alt+UP'] = {rt.move_option_up, 'repeat'},
- ['Alt+DOWN'] = {rt.move_option_down, 'repeat'},
- ['Shift+Alt+UP'] = {rt.move_option_page_up, 'repeat'},
- ['Shift+Alt+DOWN'] = {rt.move_option_page_down, 'repeat'},
- ['Alt+PGUP'] = {rt.move_option_page_up, 'repeat'},
- ['Alt+PGDWN'] = {rt.move_option_page_down, 'repeat'},
- ['Alt+HOME'] = {rt.move_option_start},
- ['Alt+END'] = {rt.move_option_end},
- ['Alt+WHEEL_UP'] = {rt.move_option_wheel_up, 'repeat'},
- ['Alt+WHEEL_DOWN'] = {rt.move_option_wheel_down, 'repeat'},
- ['Shift+Alt+WHEEL_UP'] = {rt.move_option_wheel_page_up, 'repeat'},
- ['Shift+Alt+WHEEL_DOWN'] = {rt.move_option_wheel_page_down, 'repeat'},
-}
-
-binding_state.mappings.SEARCH = {
- ['ANY_UNICODE'] = {rt.search_input_char, 'complex'},
- ['BS'] = {rt.search_input_bs, 'repeat'},
- ['DEL'] = {rt.search_input_del, 'repeat'},
-
- ['ENTER'] = {rt.end_search},
- ['ESC'] = {rt.cancel_search},
- ['Ctrl+c'] = {rt.cancel_search},
-
- ['LEFT'] = {rt.search_cursor_left, 'repeat'},
- ['RIGHT'] = {rt.search_cursor_right, 'repeat'},
- ['Ctrl+a'] = {rt.search_cursor_start},
- ['Ctrl+e'] = {rt.search_cursor_end},
+local ctx = {
+ catalogue = catalogue,
+ epg = epg,
+ xc = xc,
}
+rt.init(state, osd, input, ctx)
--- mpv does not process key-binding changes requested by script functions until
--- those functions return. in the meantime, while the function is running, mpv
--- discards any key presses for keys that were not already bound prior to the
--- start of function execution. similarly, any pending key presses (for keys
--- that were bound) are discarded if the binding changes during function
--- execution.
---
--- in other words, we get an inconsistent result where pressing a key while a
--- function is running triggers the bound function upon completion if that
--- binding remains the same, but ignores such key presses when our function
--- changes the corresponding binding or adds a new one. we can avoid this by
--- leaving our keys bound to a common function and building our own logic to
--- route keys per the current state.
-local function handle_key(ev)
- local t = binding_state.active[ev.key_name]
- if not t and ev.key_text then
- t = binding_state.active['ANY_UNICODE']
- end
-
- -- ev.is_mouse is false for some mouse events
- local k = ev.key_name
- if k:find('MOUSE_') or k:find('MBTN_') or k:find('WHEEL_') then
- if not osd.mactive then
- osd:set_mactive(true)
- end
- elseif osd.mactive then
- osd:set_mactive(false)
- click_state = {}
- end
-
- local f = t and t[1]
- if not f then
- goto flush
- end
-
- if t[2] == 'complex' then
- f(ev)
- elseif ev.event == 'down' or
- (ev.event == 'repeat' and t[2] == 'repeat') then
- f()
- end
-
- ::flush::
- osd:flush(state)
-end
-
--- uses enable-section and disable-section to disable builtin key bindings
--- while the OSD is visible. these commands are technically deprecated for
--- non-internal use, but they still work, and there doesn't appear to be
--- another way apart from setting an override for each individual key.
---
--- nonexistent `nodrag' section is enabled to disable VO dragging while the
--- menu is open.
-local function set_key_bindings()
- if osd:is_hidden() then
- if binding_state.bound then
- mp.remove_key_binding('mouse_move')
- mp.remove_key_binding('unmapped')
- mp.command_native({'disable-section', 'nodrag'})
- mp.command_native({
- 'enable-section', 'default',
- 'allow-hide-cursor+allow-vo-dragging'})
- binding_state.bound = false
- end
- return
- end
-
- if not binding_state.bound then
- mp.command_native({'disable-section', 'default'})
- mp.command_native({'enable-section', 'nodrag'})
- mp.add_forced_key_binding('MOUSE_MOVE', 'mouse_move') -- noisy
- mp.add_forced_key_binding(
- 'UNMAPPED', 'unmapped', handle_key, {complex = true})
- binding_state.bound = true
- end
-end
-
-local function set_osc_visibility()
- local v = osd:is_hidden() and osc_visibility or 'never'
- mp.command_native({'script-message', 'osc-visibility', v, ''})
-end
-
-local function toggle_menu()
- osd:toggle_hidden()
-
- if osc_visibility ~= 'never' then
- set_osc_visibility()
- end
-
- set_key_bindings()
-end
-
-local function bind_click(f)
- mp.command_native({'enable-section', 'click-nodrag'})
- mp.add_forced_key_binding(
- 'MBTN_LEFT', 'click', function(ev)
- if process_mouse_click(ev) then
- f()
- end
- end, {complex = true})
- mp.add_forced_key_binding('MBTN_LEFT_DBL', 'click-dbl')
-end
-
-local function unbind_click()
- mp.remove_key_binding('click')
- mp.remove_key_binding('click-dbl')
- mp.command_native({'disable-section', 'click-nodrag'})
-end
-
-local btn_timer; btn_timer = mp.add_periodic_timer(0.1, function()
- if osd.mpos and mp.get_time() - osd.mpos_time > config.btn_timeout then
- osd:show_menu_btn(false)
- btn_timer:kill()
- end
-end, true) -- disabled
mp.observe_property('mouse-pos', 'native', function(_, mpos)
- if not mpos then
- return
- end
-
- local ps = osd.mstate or {}
- local ms = osd:set_mpos(mpos)
- if not ms then
- return
- end
-
- -- mpv normally sends a cancel event when the mouse drifts before a
- -- click is released, but only for a first click and not for the second
- -- click of what mpv internally considers a double-click. this
- -- replicates that drift detection behaviour, allowing for consistency
- -- on double-clicks.
- local clk = click_state
- if clk.ct and mouse_has_drifted(mpos.x, mpos.y, clk.cx, clk.cy) then
- click_state = {}
- end
-
- if not ms.over_btn_area ~= not ps.over_btn_area then
- osd:show_menu_btn(ms.over_btn_area)
- elseif ms.over_btn_area and osd.menu_btn.hidden then
- osd:show_menu_btn(true)
- end
-
- if (ms.target == 'menu_btn') ~= (ps.target == 'menu_btn') then
- if ms.target == 'menu_btn' then
- bind_click(toggle_menu)
- else
- unbind_click()
- end
- end
-
- if ms.over_btn_area and ms.target ~= 'menu_btn' then
- btn_timer:resume()
- else
- btn_timer:kill()
- end
-
- osd:flush(state)
+ input.update_mpos(mpos)
end)
mp.observe_property('user-data/osc/visibility', 'native', function(_, val)
if val and (osd:is_hidden() or val ~= 'never') then
- osc_visibility = val
+ state.saved_osc_visibility = val
end
end)
mp.observe_property('osd-dimensions', 'native', function(_, val)
osd:resize(val.w, val.h)
osd:redraw(state)
-
- -- after a resize, mpv does not update the mouse coordinates until the
- -- mouse is moved. if the mouse was previously over a clickable element
- -- and we do nothing, a click would trigger that element regardless of
- -- mouse position. since we cannot get the new position, we instead
- -- wipe mouse state on resize, clearing it until the next move.
- osd:show_menu_btn(false)
- osd:set_mpos(nil)
- click_state = {}
- unbind_click()
- btn_timer:kill()
+ input.on_resize()
end)
mp.register_event('start-file', function()
@@ -502,12 +111,14 @@ end)
state:push_menu({title = 'mpv-iptv-menu'})
-osc_visibility = mp.get_property_native('user-data/osc/visibility', 'auto')
-set_osc_visibility()
+state.saved_osc_visibility = mp.get_property_native(
+ 'user-data/osc/visibility', 'auto')
+rt.set_osc_visibility()
-mp.add_forced_key_binding('TAB', 'toggle-menu', toggle_menu)
-binding_state.active = binding_state.mappings['MENU']
-set_key_bindings()
+mp.add_forced_key_binding('TAB', 'toggle-menu', rt.toggle_menu)
+input.init(state, osd)
+input.set_key_mapping('MENU')
+input.set_key_bindings()
mp.add_timeout(0, function()
rt.load_data()