From ef41e8c9a54119628aa8cffb193e42782e69563f Mon Sep 17 00:00:00 2001 From: David Vazgenovich Shakaryan Date: Thu, 5 Feb 2026 20:50:31 -0800 Subject: support searching currently playing programmes --- osd.lua | 41 +++++++++++++++++++++++------------------ rt.lua | 13 +++++++++++-- state.lua | 52 +++++++++++++++++++++++----------------------------- util.lua | 23 +++++++++++++++++++++++ 4 files changed, 80 insertions(+), 49 deletions(-) diff --git a/osd.lua b/osd.lua index b9be340..2152863 100644 --- a/osd.lua +++ b/osd.lua @@ -244,6 +244,21 @@ function mt:draw_progress_bar(prog, fg, bg) '{\\fsp'..-filled..'}\xe2\x80\x8b{\\fsp0}' end +local function hl_matches(str, matches, col, hl_col) + if not matches then + return col .. asscape(str) + end + + local buf = '' + local n = 0 + for _, match in ipairs(matches) do + buf = buf .. col .. asscape(str:sub(n + 1, match.start - 1)) .. + hl_col .. asscape(str:sub(match.start, match.stop)) + n = match.stop + end + return buf .. col .. asscape(str:sub(n + 1)) +end + function mt:option_text(opt, info) local str = opt.name local col = opt.ended and colour.option_dim or colour.option @@ -255,23 +270,9 @@ function mt:option_text(opt, info) col = colour.group end - if opt.matches then - local buf = '' - local hl_col = info.empty and colour.search_hl_empty or - colour.search_hl - local n = 0 - - for _, match in ipairs(opt.matches) do - buf = buf .. col .. - asscape(str:sub(n + 1, match.start - 1)) .. - hl_col .. - asscape(str:sub(match.start, match.stop)) - n = match.stop - end - str = buf .. col .. asscape(str:sub(n + 1)) - else - str = col .. asscape(str) - end + local hl_col = info.empty and colour.search_hl_empty or + colour.search_hl + str = hl_matches(str, opt.matches, col, hl_col) if opt.type == 'group' and opt.group_type ~= 'series' then str = col .. '[' .. str .. ']' @@ -285,7 +286,11 @@ function mt:option_text(opt, info) local opt_info = opt.info if opt_info and #opt_info > 0 then - str = str .. colour.info .. ' (' .. asscape(opt_info) .. ')' + str = str .. colour.info .. ' (' .. + hl_matches( + opt_info, opt.info_matches, colour.info, + hl_col) .. + ')' end if opt.active_programme then diff --git a/rt.lua b/rt.lua index f68d89f..8ed3dc9 100644 --- a/rt.lua +++ b/rt.lua @@ -621,6 +621,9 @@ function rt.open_option_info(opt) end end +local F_NAME = {name = true} +local F_INFO = {info = true} +local F_BOTH = {name = true, info = true} local function search_text_transform(f, ...) local menu = state:menu() local str, pos = f(menu.search_text, menu.search_cursor, ...) @@ -642,14 +645,20 @@ local function search_text_transform(f, ...) local fm = prefix:find('m') local fs = prefix:find('s') local fg = prefix:find('g') + local fe = prefix:find('e') filter = function(v) - return fc and (v.stream_type == 'live' or - v.stream_type == 'radio_streams') or + local fields + local ch = v.stream_type == 'live' or + v.stream_type == 'radio_streams' + local name = fc and ch or fm and v.stream_type == 'movie' or fs and v.group_type == 'series' or fg and v.type == 'group' and v.group_type ~= 'series' + local info = fe and ch and v.info + return name and (info and F_BOTH or F_NAME) or + info and F_INFO end end diff --git a/state.lua b/state.lua index f7f2234..4db4837 100644 --- a/state.lua +++ b/state.lua @@ -213,6 +213,24 @@ function menu_mt:set_search_text(str, pos, term, filter) self:update_search_matches() end +function menu_mt:_match_fields(v, fields, case_sensitive) + local nm = fields.name and + util.find_matches( + v.name, self.search_term, case_sensitive) + local im = fields.info and + util.find_matches( + v.info, self.search_term, case_sensitive) + + if nm or im then + -- search options may contain dynamic data that is updated on + -- redraw. using a proxy table instead of copying prevents + -- potential updates on every change of search text. + return setmetatable( + {matches = nm, info_matches = im}, {__index = v}) + end +end + +local F_NAME = {name = true} function menu_mt:update_search_matches() if not self.search_filter and #self.search_term == 0 then self.options = self.search_options @@ -224,35 +242,11 @@ function menu_mt:update_search_matches() local options = {} for _, v in ipairs(self.search_options) do - if not self.search_filter or self.search_filter(v) then - local matches - - local name = v.name - if not case_sensitive then - name = name:lower() - end - - local i, j = 0, 0 - while true do - i, j = name:find(self.search_term, j + 1, true) - -- j < i avoids infinite loop on empty term - if not i or j < i then - break - end - matches = matches or {} - matches[#matches+1] = {start = i, stop = j} - end - - if matches then - -- search options may contain dynamic data that - -- is updated on redraw. using a proxy table - -- instead of copying prevents potential - -- updates on every change of search text. - options[#options+1] = setmetatable( - {matches = matches}, {__index = v}) - elseif i then - options[#options+1] = v - end + local fields = not self.search_filter and F_NAME or + self.search_filter(v) + if fields then + options[#options+1] = self.search_term == '' and v or + self:_match_fields(v, fields, case_sensitive) end end diff --git a/util.lua b/util.lua index d678a9e..ca693ba 100644 --- a/util.lua +++ b/util.lua @@ -155,6 +155,29 @@ function util.unflatten_table(src, dst) return dst end +function util.find_matches(str, substr, case_sensitive) + if not str then + return + end + + if not case_sensitive then + str = str:lower() + end + + local matches + local i, j = 0, 0 + while true do + i, j = str:find(substr, j + 1, true) + -- j < i avoids infinite loop on empty substr + if not i or j < i then + break + end + matches = matches or {} + matches[#matches+1] = {start = i, stop = j} + end + return matches or i and {} +end + function util.str_seek_prev_char(str, pos) return util.utf8_seek(str, pos, -1) end -- cgit v1.2.3-70-g09d2