summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/scripts/texdoc/texdoclib-view.tlu
blob: 15fd5c940b071ef02e20ad01e89347e8e7dbb24b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
-- 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 "<xx>", where xx is the two hex
-- digits for the byte.

-----------------------------   display results   -----------------------------

-- length of the list to be shown
local function count_list_length(doclist, showall)
    local res = 0
    for _, doc in pairs(doclist) do
        if doc:get_quality() == 'good' or
                (showall and doc:get_quality() ~= 'killed') then
            res = res + 1
        end
    end
    return res
end

-- parse user response and view the file
local function view_selected_doc(doclist, num_str, last_i)
    -- 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

-- 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_doclist(name, doclist, showall)
    local max_lines = tonumber(texdoc.config.get_value('max_lines'))
    local interact = texdoc.config.get_value('interact_switch')

    local last_i = 0
    local prompt_more = false

    local len = count_list_length(doclist, showall)
    if interact and len > max_lines then
        local msg = '%d results. Only the top %d are shown below.'
        print(msg:format(len, max_lines))
        prompt_more = true
    elseif len == 0 then -- nothing to show
        return last_i
    end

    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 testing
        if texdoc.config.get_value('machine_switch') == true then
            -- for machine-readable output, list all results in a certain format
            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

            if i >= max_lines and prompt_more then
                io.write('Enter number of file to view, RET to view 1, S to show all results, ' ..
                         'or any other key to exit: ')
                local ans = io.read('*line')
                if ans == 'S' then
                    prompt_more = false  -- and continue this loop
                else
                    view_selected_doc(doclist, ans, last_i)
                    return last_i  -- which should be always positive
                end
            end
        end
    end

    if interact then
        io.write('Enter number of file to view, RET to view 1, anything else to skip: ')
        view_selected_doc(doclist, io.read('*line'), last_i)
    end

    return last_i
end

-- wrapper function for print_doclist(): we could call it twice
local function print_list_menu(name, doclist, showall)
    local last_i = print_doclist(name, doclist, showall)

    if not showall and last_i == 0 then
        err_print('warning', 'No good result found, showing all results.')
        last_i = print_doclist(name, doclist, true)
    end

    if last_i == 0 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
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 '<empty>'
            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_list_menu(name, doclist, mode == 'showall')
    else
        search_online(name, doclist)
    end
end

return M

-- vim: ft=lua: