summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Vazgenovich Shakaryan <dvshakaryan@gmail.com>2026-01-12 23:34:53 -0800
committerDavid Vazgenovich Shakaryan <dvshakaryan@gmail.com>2026-01-12 23:34:53 -0800
commit3c84dd08f36c8368ea7371c1d3f152f1dda6cb71 (patch)
treea6129ca7395f4c6f18f0a64da1f7d7b1fbf60ddc
parent206f73ed62003dba5041348f17433b1b3fe70174 (diff)
downloadmpv-iptv-menu-3c84dd08f36c8368ea7371c1d3f152f1dda6cb71.tar.gz
mpv-iptv-menu-3c84dd08f36c8368ea7371c1d3f152f1dda6cb71.tar.xz
add mouse support
-rw-r--r--catalogue.lua1
-rw-r--r--config.lua4
-rw-r--r--main.lua207
-rw-r--r--osd.lua27
4 files changed, 204 insertions, 35 deletions
diff --git a/catalogue.lua b/catalogue.lua
index d781cf4..1d54a17 100644
--- a/catalogue.lua
+++ b/catalogue.lua
@@ -12,6 +12,7 @@ function catalogue.new()
t:add({
type = 'group',
id = 'root',
+ name = '/',
})
t:add({
type = 'group',
diff --git a/config.lua b/config.lua
index bd0819c..20db1f7 100644
--- a/config.lua
+++ b/config.lua
@@ -9,6 +9,10 @@ config.font_size = 20
config.scroll_margin = 4
+config.click_timeout = 0.5
+config.click_dbl_time = 0.3
+config.click_max_drift = 2
+
config.bg_alpha = '44'
config.colours = {
title = '999999',
diff --git a/main.lua b/main.lua
index 4cc58ea..f57cd85 100644
--- a/main.lua
+++ b/main.lua
@@ -16,6 +16,7 @@ local script_name = mp.get_script_name()
local script_dir = mp.get_script_directory()
local state = _state.new()
+local click_state = {}
local downloader = _downloader.new({limit = 5})
local xc = _xc.new({
@@ -754,9 +755,9 @@ local function open_option_episode_info(opt)
{info = opt.info_data})
end
-local function open_option_info()
+local function open_option_info(opt)
local menu = state:menu()
- local opt = menu.options[menu.cursor]
+ local opt = opt or menu.options[menu.cursor]
if not opt then
return
end
@@ -946,6 +947,139 @@ local function toggle_menu_sort()
update_osd()
end
+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
+
+-- 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 function handle_mouse_drift(_, mpos)
+ local s = click_state
+ if not mpos or not s.ct then
+ return
+ end
+
+ if mouse_has_drifted(mpos.x, mpos.y, s.cx, s.cy) then
+ click_state = {}
+ mp.unobserve_property(handle_mouse_drift)
+ end
+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, mpos, id)
+ mp.unobserve_property(handle_mouse_drift)
+
+ if ev.canceled then
+ click_state = {}
+ return
+ end
+
+ local s = click_state
+ local time = mp.get_time()
+
+ if ev.event == 'down' then
+ s.ct, s.ck = time, ev.key_name
+ s.cx, s.cy, s.ci = mpos.x, mpos.y, id
+ mp.observe_property('mouse-pos', 'native', handle_mouse_drift)
+ return
+ end
+
+ if ev.event ~= 'up' or not s.ct then
+ return
+ end
+
+ if time - s.ct > config.click_timeout or
+ s.ck ~= ev.key_name or s.ci ~= id or
+ mouse_has_drifted(mpos.x, mpos.y, s.cx, s.cy) then
+ click_state = {}
+ return
+ end
+
+ local dbl = s.pt and s.ct - s.pt <= config.click_dbl_time and
+ s.pk == ev.key_name and s.pi == id and
+ not mouse_has_drifted(s.cx, s.cy, s.px, s.py)
+ click_state = dbl and {} or
+ {
+ pt = s.ct, pk = ev.key_name,
+ px = s.cx, py = s.cy, pi = id,
+ }
+ return ev.key_name, dbl
+end
+
+local function process_mouse_click(ev)
+ local mpos = mp.get_property_native('mouse-pos')
+ if not mpos then
+ return
+ end
+
+ local line = osd:get_mouse_line(mpos)
+ if line then
+ line = line - (osd.lines - osd_menu_lines())
+ end
+
+ local key, dbl = handle_mouse_click(ev, mpos, line)
+ return key, dbl, line
+end
+
+local function mouse_click_left(ev)
+ local key, dbl, line = process_mouse_click(ev)
+ if not key or not line or line == 0 then
+ return
+ end
+
+ -- title
+ if line < 0 then
+ if not dbl then
+ return
+ end
+
+ if line == -1 then
+ set_cursor(1)
+ else
+ state.depth = state.depth + line + 1
+ update_osd()
+ end
+ return
+ end
+
+ local menu = state:menu()
+ local pos = menu.view_top + line - 1
+ if dbl then
+ if pos ~= menu.cursor then
+ return
+ end
+
+ select_option()
+ else
+ if pos > #menu.options then
+ return
+ end
+
+ set_cursor(pos)
+ end
+end
+
+local function mouse_click_right(ev)
+ local key, dbl, line = process_mouse_click(ev)
+ if not key or not dbl or not line or line < 1 then
+ return
+ end
+
+ local menu = state:menu()
+ local pos = menu.view_top + line - 1
+ if pos > #menu.options then
+ return
+ end
+
+ open_option_info(menu.options[pos])
+end
+
local function bind_key(key, func, opts)
-- unique name is needed for removal
local i = #key_bindings+1
@@ -964,16 +1098,19 @@ end
function bind_search_keys()
unbind_keys()
- bind_key('ANY_UNICODE', search_input_char, {complex = true})
- bind_key('BS', search_input_bs, {repeatable = true})
- bind_key('DEL', search_input_del, {repeatable = true})
+ local c = {complex = true}
+ local r = {repeatable = true}
+
+ bind_key('ANY_UNICODE', search_input_char, c)
+ bind_key('BS', search_input_bs, r)
+ bind_key('DEL', search_input_del, r)
bind_key('ENTER', end_search)
bind_key('ESC', cancel_search)
bind_key('Ctrl+c', cancel_search)
- bind_key('LEFT', search_cursor_left, {repeatable = true})
- bind_key('RIGHT', search_cursor_right, {repeatable = true})
+ bind_key('LEFT', search_cursor_left, r)
+ bind_key('RIGHT', search_cursor_right, r)
bind_key('Ctrl+a', search_cursor_start)
bind_key('Ctrl+e', search_cursor_end)
end
@@ -981,6 +1118,9 @@ end
function bind_menu_keys()
unbind_keys()
+ local c = {complex = true}
+ local r = {repeatable = true}
+
bind_key('BS', prev_menu)
bind_key('/', start_search)
bind_key('Ctrl+s', toggle_menu_sort)
@@ -992,23 +1132,44 @@ function bind_menu_keys()
bind_key('?', open_option_info)
bind_key('Ctrl+p', goto_playing)
- bind_key('j', cursor_down, {repeatable = true})
- bind_key('k', cursor_up, {repeatable = true})
- bind_key('UP', cursor_up, {repeatable = true})
- bind_key('DOWN', cursor_down, {repeatable = true})
+ bind_key('MBTN_LEFT', mouse_click_left, c)
+ bind_key('MBTN_RIGHT', mouse_click_right, c)
+ bind_key('MBTN_LEFT_DBL')
+ bind_key('MBTN_RIGHT_DBL')
+
+ bind_key('k', cursor_up, r)
+ bind_key('j', cursor_down, r)
+ bind_key('K', cursor_page_up, r)
+ bind_key('J', cursor_page_down, r)
+ bind_key('UP', cursor_up, r)
+ bind_key('DOWN', cursor_down, r)
+ bind_key('Shift+UP', cursor_page_up, r)
+ bind_key('Shift+DOWN', cursor_page_down, r)
+ bind_key('PGUP', cursor_page_up, r)
+ bind_key('PGDWN', cursor_page_down, r)
bind_key('HOME', cursor_start)
bind_key('END', cursor_end)
- bind_key('PGUP', cursor_page_up, {repeatable = true})
- bind_key('PGDWN', cursor_page_down, {repeatable = true})
-
- bind_key('J', move_option_down, {repeatable = true})
- bind_key('K', move_option_up, {repeatable = true})
- bind_key('Shift+UP', move_option_up, {repeatable = true})
- bind_key('Shift+DOWN', move_option_down, {repeatable = true})
- bind_key('Shift+HOME', move_option_start)
- bind_key('Shift+END', move_option_end)
- bind_key('Shift+PGUP', move_option_page_up, {repeatable = true})
- bind_key('Shift+PGDWN', move_option_page_down, {repeatable = true})
+ bind_key('WHEEL_UP', cursor_up, r)
+ bind_key('WHEEL_DOWN', cursor_down, r)
+ bind_key('Shift+WHEEL_UP', cursor_page_up, r)
+ bind_key('Shift+WHEEL_DOWN', cursor_page_down, r)
+
+ bind_key('Alt+k', move_option_up, r)
+ bind_key('Alt+j', move_option_down, r)
+ bind_key('Alt+K', move_option_page_up, r)
+ bind_key('Alt+J', move_option_page_down, r)
+ bind_key('Alt+UP', move_option_up, r)
+ bind_key('Alt+DOWN', move_option_down, r)
+ bind_key('Alt+Shift+UP', move_option_page_up, r)
+ bind_key('Alt+Shift+DOWN', move_option_page_down, r)
+ bind_key('Alt+PGUP', move_option_page_up, r)
+ bind_key('Alt+PGDWN', move_option_page_down, r)
+ bind_key('Alt+HOME', move_option_start)
+ bind_key('Alt+END', move_option_end)
+ bind_key('Alt+WHEEL_UP', move_option_up, r)
+ bind_key('Alt+WHEEL_DOWN', move_option_down, r)
+ bind_key('Alt+Shift+WHEEL_UP', move_option_page_up, r)
+ bind_key('Alt+Shift+WHEEL_DOWN', move_option_page_down, r)
end
-- uses enable-section and disable-section to disable builtin key bindings
@@ -1070,7 +1231,7 @@ end)
load_data()
push_group_menu(catalogue:get('root'))
-osc_visibility = mp.get_property('user-data/osc/visibility', 'auto')
+osc_visibility = mp.get_property_native('user-data/osc/visibility', 'auto')
set_osc_visibility()
-- keys added via bind_key() are unbound when the OSD is closed, but we want
diff --git a/osd.lua b/osd.lua
index 72d56cb..d54362c 100644
--- a/osd.lua
+++ b/osd.lua
@@ -61,12 +61,8 @@ function mt:resize(w, h)
end
function mt:menu_lines(state)
- if state.depth > 1 then
- -- leaves an extra line for padding between titles and options
- return self.lines - state.depth
- else
- return self.lines
- end
+ -- leaves an extra line for padding between titles and options
+ return self.lines - state.depth - 1
end
function mt:menu_title(menu)
@@ -94,7 +90,7 @@ function mt:menu_title(menu)
str = colours.icon_sorted .. '⇅ ' .. col .. str
end
- return col .. ' » ' .. str
+ return col .. '» ' .. str
end
function mt:option_icons(opt, info)
@@ -364,12 +360,10 @@ function mt:redraw(state)
-- navigation which moves the viewport.
local out_max = {}
- if state.depth > 1 then
- for i = 2, state.depth do
- out[#out+1] = self:menu_title(state.menus[i])
- end
- out[#out+1] = ' ' -- space character for correct line height
+ for i = 1, state.depth do
+ out[#out+1] = self:menu_title(state.menus[i])
end
+ out[#out+1] = ' ' -- space character for correct line height
local menu = state:menu()
@@ -469,4 +463,13 @@ function mt:is_hidden()
return self.fg.hidden
end
+function mt:get_mouse_line(mpos)
+ local y = mpos.y / self.scale
+ local line = math.floor((y - self.padding) / config.font_size) + 1
+ if line < 1 or line > self.lines then
+ return
+ end
+ return line
+end
+
return osd