-- texdoclib-view.tlu: view a document and/or display the list of results in texdoc -- -- The TeX Live Team, GPLv3, see texdoclib.tlu for details -- dependencies local texdoc = { const = require('texdoclib-const'), util = require('texdoclib-util'), config = require('texdoclib-config'), search = require('texdoclib-search'), } -- shortcuts local M = {} local C = texdoc.const local err_print = texdoc.util.err_print local dbg_print = texdoc.util.dbg_print ----------------------------- view a document ----------------------------- -- view a document -- see texdoclib-search.tlu for the structure of the argument local function view_doc(docfile) M.view_file(docfile.realpath) end -- view a file, if possible local function try_viewing(view_command, viewer_replacement) if string.match (view_command, C.place_holder) then view_command = string.gsub( view_command, C.place_holder, viewer_replacement) else view_command = view_command .. ' ' .. viewer_replacement end err_print('info', 'View command: ' .. view_command) -- See long comment below this function for the LC_CTYPE story. -- We only want to reset the environment if we have the value -- to reset it to. In older versions of luatex, status.lc_* will be nil. local env_lc_ctype = status.lc_ctype local luatex_lc_ctype = os.setlocale(nil, 'ctype') if (env_lc_ctype) then err_print('info', 'Setting environment LC_CTYPE to: ' .. env_lc_ctype) os.setenv('LC_CTYPE', env_lc_ctype) end -- the big casino: run the external command. if os.execute(view_command) > 0 then err_print('error', 'Failed to execute: ' .. view_command) -- try to catch problems with missing DISPLAY on Unix if os.type == 'unix' and not (os.name == 'macosx') and os.getenv('DISPLAY') == nil then err_print('error', 'Maybe your viewer failed because DISPLAY is not set.') end os.exit(C.exit_error) end -- reset back to "C" (should always be C and always happen, but in case...) if (luatex_lc_ctype) then os.setenv('LC_CTYPE', luatex_lc_ctype) end end -- get viewer and viewer_replacement before calling try_viewing -- returns false of failure, true on success viewer_replacement is either: -- -- 1. the filename, quoted with "" -- 2. the filename, quoted with "" and followed by some rm commands -- -- The second case happens when the doc is zipped. In the case, this function -- unzips it in a tmpdir so that the viewer command can use the unzipped file. function M.view_file(filename) local viewer, viewer_replacement -- check if the file is zipped local nozipname, zipext = texdoc.util.parse_zip(filename) -- determine viewer_replacement if zipext then local unzip_cmd = texdoc.config.get_value('unzip_' .. zipext) if not unzip_cmd then err_print('error', 'No unzip command for ".%s" files.', zipext) os.exit(C.exit_error) end local tmpdir = os.tmpdir('/tmp/texdoc.XXXXXX') if not tmpdir then err_print('error', 'Failed to create tempdir to unzip.') os.exit(C.exit_error) end local basename = string.match(nozipname, '.*/(.*)$') or nozipname local tmpfile = '"' .. tmpdir .. '/' .. basename .. '"' local unzip = unzip_cmd .. ' "' .. filename .. '">' .. tmpfile dbg_print('view', 'Unzip command: ' .. unzip) if not os.execute(unzip) then err_print('error', 'Failed to unzip ' .. filename) os.remove(tmpfile) os.remove(tmpdir) os.exit(C.exit_error) end -- it is necessary to sleep a few secounds. Otherwise, the temporary -- file could be removed before opening it. viewer_replacement = tmpfile .. '; sleep 2; ' .. texdoc.config.get_value('rm_file') .. ' ' ..tmpfile .. '; ' .. texdoc.config.get_value('rm_dir') .. ' ' .. tmpdir filename = nozipname else viewer_replacement = '"' .. texdoc.util.w32_path(filename) .. '"' end -- files without extension are assumed to be text local viewext = (filename:match('.*%.([^/]*)$') or 'txt'):lower() if filename:match('^http') then viewext = 'html' end -- special case : sty files use txt viewer -- FIXME: hardcoding such cases this way is not very clean if viewext == 'sty' then viewext = 'txt' end if viewext == 'texlive' then viewext = 'txt' end if viewext == 'htm' then viewext = 'html' end -- get a viewer from built-in defaults if not already set if not texdoc.config.get_value('viewer_' .. viewext) then texdoc.config.get_default_viewers() end -- still no viewers? use txt as a fallback if not texdoc.config.get_value('viewer_' .. viewext) then err_print('warning', 'No viewer defined for ".%s" files, using "viewer_txt" instead.', viewext) viewext = 'txt' end -- finally, check validity of the viewer viewer = texdoc.config.get_value('viewer_' .. viewext) assert(viewer, 'Internal error: no viewer found.') dbg_print('view', 'Using "viewer_%s" to open the file.', viewext ) return try_viewing(viewer, viewer_replacement) end -- Explanation of locale madness: -- LuaTeX resets LC_CTYPE, LC_COLLATE, LC_NUMERIC to "C". That is good for -- inside luatex, but when we run an external program, if the user's -- environment is something else, we want to switch back to it. As of -- TL 2017 LuaTeX, we can inspect the user's locale with status.lc_ctype, etc. -- -- For texdoc purposes, what matters is LC_CTYPE (so we don't bother with -- the others). For example, with the less pager, when LC_CTYPE=C, -- non-ASCII bytes are displayed as "", where xx is the two hex -- digits for the byte. ----------------------------- display results ----------------------------- -- print a list of docfile objects (see texdoclib-search.tlu) as a menu -- if showall is false, stop as soon as a bad result is encountered local function print_menu(name, doclist, showall) local max_lines = tonumber(texdoc.config.get_value('max_lines')) if texdoc.config.get_value('interact_switch') and doclist[max_lines + 1] then -- there may be too many lines, count them local n = 0 for _, doc in pairs(doclist) do if doc:get_quality() == 'good' or (showall and doc:get_quality() ~= 'killed') then n = n + 1 end end if n > max_lines then io.write(n, ' results. Display them all? (y/N) ') local ans = io.read('*line') if not ((ans == 'y') or (ans == 'Y') -- io.read had a bug wrt windows eol on some versions of texlua or (ans == '\ry') or (ans == '\rY')) then return end end end local last_i while true do for i, doc in ipairs(doclist) do if doc:get_quality() == 'killed' then break end if doc:get_quality() ~= 'good' and not showall then break end last_i = i -- save for test below if texdoc.config.get_value('machine_switch') == true then print(name, doc.score, texdoc.util.w32_path(doc.realpath), doc.lang or '', doc.details or '') else print(string.format('%2d %s', i, texdoc.util.w32_path(doc.realpath))) if doc.details or doc.lang then local line = ' = ' if doc.lang then line = line .. '[' .. doc.lang .. '] ' end if doc.details then line = line .. doc.details end print(line) end end end if last_i or showall then break else err_print('warning', 'No good result found, showing all results.') showall = true end end if not last_i then local msg = string.gsub(C.notfound_msg, C.notfound_msg_ph, name) io.stderr:write(msg .. '\n') -- get rid of gsub's 2nd value os.exit(C.exit_notfound) end if texdoc.config.get_value('interact_switch') then io.write('Enter number of file to view, RET to view 1, anything else to skip: ') local num_str = io.read('*line') -- That returns the empty string on an empty line, nil on EOF. -- We only want to default to viewing 1 on an empty line. -- Use Lua's faked ternary operator for fun and brevity: num = (num_str == '' and 1 or tonumber(num_str)) if num and doclist[num] and num <= last_i then view_doc(doclist[num]) end end end local function search_online(name, doclist) if not texdoc.config.get_value('interact_switch') or texdoc.config.get_value('machine_switch') then view_doc(doclist[1]) return end local function write(msg) io.stderr:write(msg .. '') -- cast to string end -- Fuzzy heuristic to see if the user has _any_ local documentation local kpathsea_docs = texdoc.search.get_doclist("kpathsea") local docs_installed = kpathsea_docs[1] and kpathsea_docs[1].basename == "kpathsea.pdf" and not os.getenv("TEXDOC_NO_LOCAL_DOCS") if docs_installed and doclist[1] then write(C.badmatch_msg:gsub(C.badmatch_msg_ph, name)) for i=1,3 do local doc = (doclist[i] and doclist[i].normname) or '' write(' ' .. i .. ' ' .. doc .. '\n') end write('\n') elseif docs_installed then write(C.nomatch_msg:gsub(C.nomatch_msg_ph, name)) else write(C.nolocaldocs_msg) end local url = texdoc.config.get_value('online_url') :gsub(C.online_baseurl_ph, name) write(C.online_msg:gsub(C.online_msg_url, url) :gsub(C.online_msg_ph, name)) if docs_installed and doclist[1] then write(C.badmatch_prompt) else write(C.nolocaldocs_prompt) end local ans = io.read('*line') if ans:match('^\r?[yY]') then M.view_file(url) elseif tonumber(ans) then view_doc(doclist[tonumber(ans)]) end end ----------------------- deliver results based on mode --------------------- function M.deliver_results(name, doclist, many) -- ensure that results were found or apologize if (not doclist[1] or doclist[1]:get_quality() == 'killed') and not texdoc.config.get_value('interact_switch') then local msg = string.gsub(C.notfound_msg, C.notfound_msg_ph, name) io.stderr:write(msg .. '\n') -- get rid of gsub's 2nd value os.exit(C.exit_notfound) end -- shall we show all of them or only the "good" ones? local mode = texdoc.config.get_value('mode') -- view result or show menu based on mode and number of results if (mode == 'view' and doclist[1] and doclist[1]:get_quality() == 'good') or (mode == 'mixed' and (not doclist[2] or doclist[2]:get_quality() ~= 'good') ) then view_doc(doclist[1]) elseif mode ~= 'view' then if many and not texdoc.config.get_value('machine_switch') then print('*** Results for: ' .. name .. ' ***') end print_menu(name, doclist, mode == 'showall') else search_online(name, doclist) end end return M -- vim: ft=lua: