nirish

config files for niri window manager
Log | Files | Refs | README | LICENSE

scanning.lua (7149B)


      1 local mp = require 'mp'
      2 local msg = require 'mp.msg'
      3 local utils = require 'mp.utils'
      4 
      5 local g = require 'modules.globals'
      6 local fb_utils = require 'modules.utils'
      7 local cursor = require 'modules.navigation.cursor'
      8 local ass = require 'modules.ass'
      9 
     10 local parse_state_API = require 'modules.apis.parse-state'
     11 
     12 ---@class scanning
     13 local scanning = {}
     14 
     15 ---@enum NavigationType
     16 local NavType = {
     17     DOWN = 1,
     18     UP = -1,
     19     REDIRECT = 2,
     20     GOTO = 3,
     21     RESCAN = 4,
     22 }
     23 
     24 scanning.NavType = NavType
     25 
     26 ---@param directory_stack? boolean
     27 local function clear_non_adjacent_state(directory_stack)
     28     g.state.directory_label = nil
     29     if directory_stack then
     30         g.directory_stack.stack = {g.state.directory}
     31         g.directory_stack.position = 1
     32     end
     33 end
     34 
     35 ---parses the given directory or defers to the next parser if nil is returned
     36 ---@async
     37 ---@param directory string
     38 ---@param index number
     39 ---@return List?
     40 ---@return Opts?
     41 function scanning.choose_and_parse(directory, index)
     42     msg.debug(("finding parser for %q"):format(directory))
     43     ---@type Parser, List?, Opts?
     44     local parser, list, opts
     45     local parse_state = g.parse_states[coroutine.running() or ""]
     46     while list == nil and not parse_state.already_deferred and index <= #g.parsers do
     47         parser = g.parsers[index]
     48         if parser:can_parse(directory, parse_state) then
     49             msg.debug("attempting parser:", parser:get_id())
     50             list, opts = parser:parse(directory, parse_state)
     51         end
     52         index = index + 1
     53     end
     54     if not list then return nil, {} end
     55 
     56     msg.debug("list returned from:", parser:get_id())
     57     opts = opts or {}
     58     if list then opts.id = opts.id or parser:get_id() end
     59     return list, opts
     60 end
     61 
     62 ---Sets up the parse_state table and runs the parse operation.
     63 ---@async
     64 ---@param directory string
     65 ---@param parse_state_template ParseStateTemplate
     66 ---@return List|nil
     67 ---@return Opts
     68 local function run_parse(directory, parse_state_template)
     69     msg.verbose(("scanning files in %q"):format(directory))
     70 
     71     ---@type ParseStateFields
     72     local parse_state = {
     73         source = parse_state_template.source,
     74         directory = directory,
     75         properties = parse_state_template.properties or {}
     76     }
     77 
     78     local co = coroutine.running()
     79     g.parse_states[co] = fb_utils.set_prototype(parse_state, parse_state_API) --[[@as ParseState]]
     80 
     81     local list, opts = scanning.choose_and_parse(directory, 1)
     82 
     83     if list == nil then return msg.debug("no successful parsers found"), {} end
     84     opts = opts or {}
     85     opts.parser = g.parsers[opts.id]
     86 
     87     if not opts.filtered then fb_utils.filter(list) end
     88     if not opts.sorted then fb_utils.sort(list) end
     89     return list, opts
     90 end
     91 
     92 ---Returns the contents of the given directory using the given parse state.
     93 ---If a coroutine has already been used for a parse then create a new coroutine so that
     94 ---the every parse operation has a unique thread ID.
     95 ---@async
     96 ---@param directory string
     97 ---@param parse_state ParseStateTemplate
     98 ---@return List|nil
     99 ---@return Opts
    100 function scanning.scan_directory(directory, parse_state)
    101     local co = fb_utils.coroutine.assert("scan_directory must be executed from within a coroutine - aborting scan "..utils.to_string(parse_state))
    102     if not g.parse_states[co] then return run_parse(directory, parse_state) end
    103 
    104     --if this coroutine is already is use by another parse operation then we create a new
    105     --one and hand execution over to that
    106     ---@async
    107     local new_co = coroutine.create(function()
    108         fb_utils.coroutine.resume_err(co, run_parse(directory, parse_state))
    109     end)
    110 
    111     --queue the new coroutine on the mpv event queue
    112     mp.add_timeout(0, function()
    113         local success, err = coroutine.resume(new_co)
    114         if not success then
    115             fb_utils.traceback(err, new_co)
    116             fb_utils.coroutine.resume_err(co)
    117         end
    118     end)
    119     return g.parse_states[co]:yield()
    120 end
    121 
    122 ---Sends update requests to the different parsers.
    123 ---@async
    124 ---@param moving_adjacent? number|boolean
    125 ---@param parse_properties? ParseProperties
    126 local function update_list(moving_adjacent, parse_properties)
    127     msg.verbose('opening directory: ' .. g.state.directory)
    128 
    129     g.state.selected = 1
    130     g.state.selection = {}
    131 
    132     local directory = g.state.directory
    133     local list, opts = scanning.scan_directory(g.state.directory, { source = "browser", properties = parse_properties })
    134 
    135     --if the running coroutine isn't the one stored in the state variable, then the user
    136     --changed directories while the coroutine was paused, and this operation should be aborted
    137     if coroutine.running() ~= g.state.co then
    138         msg.verbose(g.ABORT_ERROR.msg)
    139         msg.debug("expected:", g.state.directory, "received:", directory)
    140         return
    141     end
    142 
    143     --apply fallbacks if the scan failed
    144     if not list then
    145         msg.warn("could not read directory", g.state.directory)
    146         list, opts = {}, {}
    147         opts.empty_text = g.style.warning..'Error: could not parse directory'
    148     end
    149 
    150     g.state.list = list
    151     g.state.parser = opts.parser
    152 
    153     --setting custom options from parsers
    154     g.state.directory_label = opts.directory_label
    155     g.state.empty_text = opts.empty_text or g.state.empty_text
    156 
    157     --we assume that directory is only changed when redirecting to a different location
    158     --therefore we need to change the `moving_adjacent` flag and clear some state values
    159     if opts.directory then
    160         g.state.directory = opts.directory
    161         moving_adjacent = false
    162         clear_non_adjacent_state(true)
    163     end
    164 
    165     if opts.selected_index then
    166         g.state.selected = opts.selected_index or g.state.selected
    167         if g.state.selected > #g.state.list then g.state.selected = #g.state.list
    168         elseif g.state.selected < 1 then g.state.selected = 1 end
    169     end
    170 
    171     if moving_adjacent then cursor.select_prev_directory()
    172     else cursor.select_playing_item() end
    173     g.state.prev_directory = g.state.directory
    174 end
    175 
    176 ---rescans the folder and updates the list.
    177 ---@param nav_type? NavigationType
    178 ---@param cb? function
    179 ---@param parse_properties? ParseProperties
    180 ---@return thread # The coroutine for the triggered parse operation. May be aborted early if directory is in the cache.
    181 function scanning.rescan(nav_type, cb, parse_properties)
    182     if nav_type == nil then nav_type = NavType.RESCAN end
    183 
    184     --we can only make assumptions about the directory label when moving from adjacent directories
    185     if nav_type == NavType.GOTO or nav_type == NavType.REDIRECT then
    186         clear_non_adjacent_state(nav_type == NavType.GOTO)
    187     end
    188 
    189     g.state.empty_text = "~"
    190     g.state.list = {}
    191     cursor.disable_select_mode()
    192     ass.update_ass()
    193 
    194     --the directory is always handled within a coroutine to allow addons to
    195     --pause execution for asynchronous operations
    196     ---@async
    197     local co = fb_utils.coroutine.queue(function()
    198         update_list(nav_type, parse_properties)
    199         if g.state.empty_text == "~" then g.state.empty_text = "empty directory" end
    200 
    201         ass.update_ass()
    202         if cb then fb_utils.coroutine.run(cb) end
    203     end)
    204 
    205     g.state.co = co
    206     return co
    207 end
    208 
    209 
    210 return scanning