forked from DFHack/dfhack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhelpdb.lua
More file actions
841 lines (767 loc) · 27.3 KB
/
helpdb.lua
File metadata and controls
841 lines (767 loc) · 27.3 KB
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
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
-- The help text database and query interface.
local _ENV = mkmodule('helpdb')
local argparse = require('argparse')
-- paths
local RENDERED_PATH = 'hack/docs/docs/tools/'
local TAG_DEFINITIONS = 'hack/docs/docs/Tags.txt'
-- used when reading help text embedded in script sources
local SCRIPT_DOC_BEGIN = '[====['
local SCRIPT_DOC_END = ']====]'
local GLOBAL_KEY = 'HELPDB'
-- enums
local ENTRY_TYPES = {
BUILTIN='builtin',
PLUGIN='plugin',
COMMAND='command'
}
local HELP_SOURCES = {
RENDERED='rendered', -- from the installed, rendered help text
PLUGIN='plugin', -- from the plugin source code
SCRIPT='script', -- from the script source code
STUB='stub', -- from a generated stub
}
-- builtin command names, with aliases mapped to their canonical form
local BUILTINS = {
['?']='help',
alias=true,
clear='cls',
cls=true,
['devel/dump-rpc']=true,
die=true,
dir='ls',
disable=true,
enable=true,
fpause=true,
help=true,
hide=true,
keybinding=true,
['kill-lua']=true,
['load']=true,
ls=true,
man='help',
plug=true,
reload=true,
script=true,
['sc-script']=true,
show=true,
tags=true,
['type']=true,
unload=true,
}
---------------------------------------------------------------------------
-- data structures
---------------------------------------------------------------------------
-- help text database, keys are a subset of the entry database
-- entry name -> {
-- help_source (element of HELP_SOURCES),
-- short_help (string),
-- long_help (string),
-- tags (set),
-- source_timestamp (mtime, 0 for non-files),
-- source_path (string, nil for non-files)
-- }
local textdb = {}
-- entry database, points to text in textdb
-- entry name -> {
-- entry_types (set of ENTRY_TYPES),
-- short_help (string, if not nil then overrides short_help in text_entry),
-- text_entry (string)
-- }
--
-- entry_types is a set because plugin commands can also be the plugin names.
local entrydb = {}
-- tag name -> list of entry names
-- Tags defined in the TAG_DEFINITIONS file that have no associated db entries
-- will have an empty list.
local tag_index = {}
---------------------------------------------------------------------------
-- data ingestion
---------------------------------------------------------------------------
local function get_rendered_path(entry_name)
return RENDERED_PATH .. entry_name .. '.txt'
end
local function has_rendered_help(entry_name)
return dfhack.filesystem.mtime(get_rendered_path(entry_name)) ~= -1
end
local DEFAULT_HELP_TEMPLATE = [[
%s
%s
No help available.
]]
local function make_default_entry(entry_name, help_source, kwargs)
local default_long_help = DEFAULT_HELP_TEMPLATE:format(
entry_name, ('='):rep(#entry_name))
return {
help_source=help_source,
short_help='No help available.',
long_help=default_long_help,
tags={},
source_timestamp=kwargs.source_timestamp or 0,
source_path=kwargs.source_path}
end
-- updates the short_text, the long_text, and the tags in the given entry based
-- on the text returned from the iterator.
-- if defined, opts can have the following fields:
-- begin_marker (string that marks the beginning of the help text; all text
-- before this marker is ignored)
-- end_marker (string that marks the end of the help text; text will stop
-- being parsed after this marker is seen)
-- no_header (don't try to find the entity name at the top of the help text)
-- first_line_is_short_help (if set, then read the short help text from the
-- first commented line of the script instead of
-- using the first sentence of the long help text.
-- value is the comment character.)
local function update_entry(entry, iterator, opts)
opts = opts or {}
local lines, tags = {}, ''
local first_line_is_short_help = opts.first_line_is_short_help
local begin_marker_found,header_found = not opts.begin_marker,opts.no_header
local tags_found, short_help_found = false, opts.skip_short_help
local in_tags, in_short_help = false, false
for line in iterator do
line = dfhack.utf2df(line)
if not short_help_found and first_line_is_short_help then
line = line:trim()
local _,_,text = line:find('^'..first_line_is_short_help..'%s*(.*)')
if not text then
-- if no first-line short help found, fall back to getting the
-- first sentence of the help text.
first_line_is_short_help = false
else
if not text:endswith('.') then
text = text .. '.'
end
entry.short_help = text
short_help_found = true
goto continue
end
end
if not begin_marker_found then
local _, endpos = line:find(opts.begin_marker, 1, true)
if endpos == #line then
begin_marker_found = true
end
goto continue
end
if opts.end_marker then
local _, endpos = line:find(opts.end_marker, 1, true)
if endpos == #line then
break
end
end
if not header_found and line:find('%w') then
header_found = true
elseif in_tags then
if #line == 0 then
in_tags = false
else
tags = tags .. line
end
elseif not tags_found and line:find('^[*]*Tags:[*]*') then
_,_,tags = line:trim():find('[*]*Tags:[*]* *(.*)')
in_tags, tags_found = true, true
elseif not short_help_found and
line:find('^%s*%w') and not line:find('^%w+:') then
if in_short_help then
entry.short_help = entry.short_help .. ' ' .. line
else
entry.short_help = line:trim()
end
local sentence_end = entry.short_help:find('.', 1, true)
if sentence_end then
entry.short_help = entry.short_help:sub(1, sentence_end)
short_help_found = true
else
in_short_help = true
end
end
table.insert(lines, line)
::continue::
end
entry.tags = {}
for _,tag in ipairs(tags:split('[ ,|]+')) do
if #tag > 0 and tag_index[tag] then
entry.tags[tag] = true
end
end
if #lines > 0 then
entry.long_help = table.concat(lines, '\n')
end
end
-- create db entry based on parsing sphinx-rendered help text
local function make_rendered_entry(old_entry, entry_name, kwargs)
local source_path = get_rendered_path(entry_name)
local source_timestamp = dfhack.filesystem.mtime(source_path)
if old_entry and old_entry.help_source == HELP_SOURCES.RENDERED and
old_entry.source_timestamp >= source_timestamp then
-- we already have the latest info
return old_entry
end
kwargs.source_path, kwargs.source_timestamp = source_path, source_timestamp
local entry = make_default_entry(entry_name, HELP_SOURCES.RENDERED, kwargs)
local ok, lines = pcall(io.lines, source_path)
if not ok then
return entry
end
update_entry(entry, lines)
return entry
end
-- create db entry based on the help text in the plugin source (used by
-- out-of-tree plugins)
local function make_plugin_entry(old_entry, entry_name, kwargs)
if old_entry and old_entry.source == HELP_SOURCES.PLUGIN then
-- we can't tell when a plugin is reloaded, so we can either choose to
-- always refresh or never refresh. let's go with never for now for
-- performance.
return old_entry
end
local entry = make_default_entry(entry_name, HELP_SOURCES.PLUGIN, kwargs)
local long_help = dfhack.internal.getCommandHelp(entry_name)
if long_help and #long_help:trim() > 0 then
update_entry(entry, long_help:trim():gmatch('[^\n]*'), {no_header=true})
end
return entry
end
-- create db entry based on the help text in the script source (used by
-- out-of-tree scripts)
local function make_script_entry(old_entry, entry_name, kwargs)
local source_path = kwargs.source_path
local source_timestamp = dfhack.filesystem.mtime(source_path)
if old_entry and old_entry.source == HELP_SOURCES.SCRIPT and
old_entry.source_path == source_path and
old_entry.source_timestamp >= source_timestamp then
-- we already have the latest info
return old_entry
end
kwargs.source_timestamp, kwargs.entry_type = source_timestamp
local entry = make_default_entry(entry_name, HELP_SOURCES.SCRIPT, kwargs)
local ok, lines = pcall(io.lines, source_path)
if not ok then
return entry
end
update_entry(entry, lines,
{begin_marker=SCRIPT_DOC_BEGIN,
end_marker=SCRIPT_DOC_END,
first_line_is_short_help='%-%-'})
return entry
end
-- updates the dbs (and associated tag index) with a new entry if the entry_name
-- doesn't already exist in the dbs.
local function update_db(old_db, entry_name, text_entry, help_source, kwargs)
if entrydb[entry_name] then
-- already in db (e.g. from a higher-priority script dir); skip
return
end
entrydb[entry_name] = {
entry_types=kwargs.entry_types,
short_help=kwargs.short_help,
text_entry=text_entry
}
if entry_name ~= text_entry then
return
end
local text_entry, old_entry = nil, old_db[entry_name]
if help_source == HELP_SOURCES.RENDERED then
text_entry = make_rendered_entry(old_entry, entry_name, kwargs)
elseif help_source == HELP_SOURCES.PLUGIN then
text_entry = make_plugin_entry(old_entry, entry_name, kwargs)
elseif help_source == HELP_SOURCES.SCRIPT then
text_entry = make_script_entry(old_entry, entry_name, kwargs)
elseif help_source == HELP_SOURCES.STUB then
text_entry = make_default_entry(entry_name, HELP_SOURCES.STUB, kwargs)
else
error('unhandled help source: ' .. help_source)
end
textdb[entry_name] = text_entry
end
-- add the builtin commands to the db
local function scan_builtins(old_db)
local entry_types = {[ENTRY_TYPES.BUILTIN]=true, [ENTRY_TYPES.COMMAND]=true}
for builtin,canonical in pairs(BUILTINS) do
if canonical == true then canonical = builtin end
update_db(old_db, builtin, canonical,
has_rendered_help(canonical) and
HELP_SOURCES.RENDERED or HELP_SOURCES.STUB,
{entry_types=entry_types})
end
-- easter egg: replace underline for 'die' help with tombstones
textdb.die.long_help = textdb.die.long_help:gsub('=', string.char(239))
end
-- scan for enableable plugins and plugin-provided commands and add their help
-- to the db
local function scan_plugins(old_db)
local plugin_names = dfhack.internal.listPlugins()
for _,plugin in ipairs(plugin_names) do
local commands = dfhack.internal.listCommands(plugin)
local includes_plugin = false
for _,command in ipairs(commands) do
local kwargs = {entry_types={[ENTRY_TYPES.COMMAND]=true}}
if command == plugin then
kwargs.entry_types[ENTRY_TYPES.PLUGIN]=true
includes_plugin = true
end
kwargs.short_help = dfhack.internal.getCommandDescription(command)
update_db(old_db, command, plugin,
has_rendered_help(plugin) and
HELP_SOURCES.RENDERED or HELP_SOURCES.PLUGIN,
kwargs)
end
if not includes_plugin then
update_db(old_db, plugin, plugin,
has_rendered_help(plugin) and
HELP_SOURCES.RENDERED or HELP_SOURCES.STUB,
{entry_types={[ENTRY_TYPES.PLUGIN]=true}})
end
end
end
-- scan for scripts and add their help to the db
local function scan_scripts(old_db)
local entry_types = {[ENTRY_TYPES.COMMAND]=true}
for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do
local files = dfhack.filesystem.listdir_recursive(
script_path, nil, false)
if not files then goto skip_path end
for _,f in ipairs(files) do
if f.isdir or
not f.path:endswith('.lua') or
f.path:startswith('test/') or
f.path:startswith('internal/') then
goto continue
end
local dot_index = f.path:find('%.[^.]*$')
local entry_name = f.path:sub(1, dot_index - 1)
local source_path = script_path .. '/' .. f.path
update_db(old_db, entry_name, entry_name,
has_rendered_help(entry_name) and
HELP_SOURCES.RENDERED or HELP_SOURCES.SCRIPT,
{entry_types=entry_types, source_path=source_path})
::continue::
end
::skip_path::
end
end
-- read tags and descriptions from the TAG_DEFINITIONS file and add them all
-- to tag_index, initizlizing each entry with an empty list.
local function initialize_tags()
local tag, desc, in_desc = nil, nil, false
local ok, lines = pcall(io.lines, TAG_DEFINITIONS)
if not ok then return end
for line in lines do
if in_desc then
line = line:trim()
if #line == 0 then
in_desc = false
goto continue
end
desc = desc .. ' ' .. line
tag_index[tag].description = desc
else
_,_,tag,desc = line:find('^%* (%w+)[^:]*: (.+)')
if not tag then goto continue end
tag_index[tag] = {description=desc}
in_desc = true
end
::continue::
end
end
local function index_tags()
for entry_name,entry in pairs(entrydb) do
for tag in pairs(textdb[entry.text_entry].tags) do
-- ignore unknown tags
if tag_index[tag] then
table.insert(tag_index[tag], entry_name)
end
end
end
end
local needs_refresh = true
-- ensures the db is loaded
local function ensure_db()
if not needs_refresh then return end
needs_refresh = false
local old_db = textdb
textdb, entrydb, tag_index = {}, {}, {}
initialize_tags()
scan_builtins(old_db)
scan_plugins(old_db)
scan_scripts(old_db)
index_tags()
if is_tag('armok') then
dfhack.internal.setArmokTools(get_tag_data('armok'))
end
end
function refresh()
needs_refresh = true
ensure_db()
end
dfhack.onStateChange[GLOBAL_KEY] = function(sc)
if sc ~= SC_WORLD_LOADED then
return
end
-- pick up widgets from active mods
refresh()
end
local function parse_blocks(text)
local blocks = {}
for line in text:gmatch('[^\n]*') do
local _,indent = line:find('^ *')
table.insert(blocks, {line=line:trim(), indent=indent})
end
return blocks
end
local function format_block(line, indent, width)
local wrapped = line:wrap(width - indent)
if indent == 0 then return wrapped end
local padding = (' '):rep(indent)
local indented_lines = {}
for line in wrapped:gmatch('[^\n]*') do
table.insert(indented_lines, padding .. line)
end
return table.concat(indented_lines, '\n')
end
-- wraps the unwrapped source help at the specified width, preserving block
-- indents
local function rewrap(text, width)
local formatted_blocks = {}
for _,block in ipairs(parse_blocks(text)) do
table.insert(formatted_blocks,
format_block(block.line, block.indent, width))
end
return table.concat(formatted_blocks, '\n')
end
---------------------------------------------------------------------------
-- get API
---------------------------------------------------------------------------
-- converts strings into single-element lists containing that string
local function normalize_string_list(l)
if not l or #l == 0 then return nil end
if type(l) == 'string' then
return {l}
end
return l
end
local function has_keys(str, dict)
if not str or #str == 0 then
return false
end
for _,s in ipairs(normalize_string_list(str)) do
if not dict[s] then
return false
end
end
return true
end
-- returns whether the given string (or list of strings) is an entry (are all
-- entries) in the db
function is_entry(str)
ensure_db()
return has_keys(str, entrydb)
end
local function get_db_property(entry_name, property)
ensure_db()
if not entrydb[entry_name] then
error(('helpdb entry not found: "%s"'):format(entry_name))
end
return entrydb[entry_name][property] or
textdb[entrydb[entry_name].text_entry][property]
end
function get_entry_types(entry)
return get_db_property(entry, 'entry_types')
end
-- returns the ~54 char summary blurb associated with the entry
function get_entry_short_help(entry)
return get_db_property(entry, 'short_help')
end
-- returns the full help documentation associated with the entry, optionally
-- wrapped to the specified width (80 if not specified).
function get_entry_long_help(entry, width)
return rewrap(get_db_property(entry, 'long_help'), width or 80)
end
-- returns the set of tags associated with the entry
function get_entry_tags(entry)
return get_db_property(entry, 'tags')
end
-- returns whether the given entry exists and has the specified tag
function has_tag(entry, tag)
return is_entry(entry) and get_entry_tags(entry)[tag]
end
-- returns whether the given string (or list of strings) matches a tag name
function is_tag(str)
ensure_db()
return has_keys(str, tag_index)
end
local function set_to_sorted_list(set)
local list = {}
for item in pairs(set) do
table.insert(list, item)
end
table.sort(list)
return list
end
-- returns the defined tags in alphabetical order
function get_tags()
ensure_db()
return set_to_sorted_list(tag_index)
end
function get_tag_data(tag)
ensure_db()
if not tag_index[tag] then
error(('helpdb tag not found: "%s"'):format(tag))
end
return tag_index[tag]
end
---------------------------------------------------------------------------
-- search API
---------------------------------------------------------------------------
-- returns a list of path elements in reverse order
local function chunk_for_sorting(str)
local parts = str:split('/')
local chunks = {}
for i=1,#parts do
chunks[#parts - i + 1] = parts[i]
end
return chunks
end
-- sorts by last path component, then by parent path components.
-- something comes before nothing.
-- e.g. gui/autofarm comes immediately before autofarm
function sort_by_basename(a, b)
local a = chunk_for_sorting(a)
local b = chunk_for_sorting(b)
local i = 1
while a[i] do
if not b[i] then
return true
end
if a[i] ~= b[i] then
return a[i] < b[i]
end
i = i + 1
end
return false
end
-- returns true if all filter elements are matched (i.e. any of the tags AND
-- any of the strings AND any of the entry_types)
local function matches(entry_name, filter)
if filter.tag then
local matched = false
local tags = get_db_property(entry_name, 'tags')
for _,tag in ipairs(filter.tag) do
if tags[tag] then
matched = true
break
end
end
if not matched then
return false
end
end
if filter.entry_type then
local matched = false
local etypes = get_db_property(entry_name, 'entry_types')
for _,etype in ipairs(filter.entry_type) do
if etypes[etype] then
matched = true
break
end
end
if not matched then
return false
end
end
if filter.str then
local matched = false
for _,str in ipairs(filter.str) do
if entry_name:find(str, 1, true) then
matched = true
break
end
end
if not matched then
return false
end
end
return true
end
local function matches_all(entry_name, filters)
for _,filter in ipairs(filters) do
if not matches(entry_name, filter) then
return false
end
end
return true
end
-- normalizes the lists in the filter and returns nil if no filter elements are
-- populated
local function normalize_filter_map(f)
if not f then return nil end
local filter = {}
filter.str = normalize_string_list(f.str)
filter.tag = normalize_string_list(f.tag)
filter.entry_type = normalize_string_list(f.entry_type)
if not filter.str and not filter.tag and not filter.entry_type then
return nil
end
return filter
end
local function normalize_filter_list(fs)
if not fs then return nil end
local filter_list = {}
for _,f in ipairs(#fs > 0 and fs or {fs}) do
table.insert(filter_list, normalize_filter_map(f))
end
if #filter_list == 0 then return nil end
return filter_list
end
-- returns a list of entry names, alphabetized by their last path component,
-- with populated path components coming before null path components (e.g.
-- autobutcher will immediately follow gui/autobutcher).
-- the optional include and exclude filter params are maps (or lists of maps)
-- with the following elements:
-- str - if a string, filters by the given substring. if a table of strings,
-- includes entry names that match any of the given substrings.
-- tag - if a string, filters by the given tag name. if a table of strings,
-- includes entries that match any of the given tags.
-- entry_type - if a string, matches entries of the given type. if a table of
-- strings, includes entries that match any of the given types. valid
-- types are: "builtin", "plugin", "command". note that many plugin
-- commands have the same name as the plugin, so those entries will
-- match both "plugin" and "command" types.
-- filter elements in a map are ANDed together (e.g. if both str and tag are
-- specified, the match is on any of the str elements AND any of the tag
-- elements). If lists of maps are passed, the maps are ORed (that is, the match
-- succeeds if any of the filters match).
function search_entries(include, exclude)
ensure_db()
include = normalize_filter_list(include)
exclude = normalize_filter_list(exclude)
local entries = {}
for entry in pairs(entrydb) do
if (not include or matches_all(entry, include)) and
(not exclude or not matches_all(entry, exclude)) then
table.insert(entries, entry)
end
end
table.sort(entries, sort_by_basename)
return entries
end
-- returns a list of all commands. used by Core's autocomplete functionality.
function get_commands()
local include = {entry_type=ENTRY_TYPES.COMMAND}
return search_entries(include)
end
function is_builtin(command)
return is_entry(command) and get_entry_types(command)[ENTRY_TYPES.BUILTIN]
end
---------------------------------------------------------------------------
-- print API (outputs to console)
---------------------------------------------------------------------------
-- implements the 'help' builtin command
function help(entry)
ensure_db()
if not entrydb[entry] then
dfhack.printerr(('No help entry found for "%s"'):format(entry))
return
end
print(get_entry_long_help(entry))
end
-- prints col1text (width 21), a one space gap, and col2 (width 58)
-- if col1text is longer than 21 characters, col2text is printed starting on the
-- next line. if col2text is longer than 58 characters, it is wrapped. col2text
-- lines on lines below the col1text output are indented by one space further
-- than the col2text on the first line.
local COL1WIDTH, COL2WIDTH = 20, 58
local function print_columns(col1text, col2text)
col2text = col2text:wrap(COL2WIDTH)
local wrapped_col2 = {}
for line in col2text:gmatch('[^'..NEWLINE..']*') do
table.insert(wrapped_col2, line)
end
local col2_start_line = 1
if #col1text > COL1WIDTH then
print(col1text)
else
print(('%-'..COL1WIDTH..'s %s'):format(col1text, wrapped_col2[1]))
col2_start_line = 2
end
for i=col2_start_line,#wrapped_col2 do
print(('%'..COL1WIDTH..'s %s'):format(' ', wrapped_col2[i]))
end
end
-- prints the requested entries to the console. include and exclude filters are
-- defined as in search_entries() above.
local function list_entries(skip_tags, include, exclude)
local entries = search_entries(include, exclude)
for _,entry in ipairs(entries) do
local short_help = get_entry_short_help(entry)
if not skip_tags then
local tags = set_to_sorted_list(get_entry_tags(entry))
if #tags > 0 then
local taglist = table.concat(tags, ', ')
short_help = short_help .. NEWLINE .. 'tags: ' .. taglist
end
end
print_columns(entry, short_help)
end
if #entries == 0 then
print('No matches.')
end
end
-- wraps the list_entries() API to provide a more convenient interface for Core
-- to implement the 'ls' builtin command.
-- filter_str - if a tag name (or a list of tag names), will filter by that
-- tag/those tags. otherwise, will filter as a substring/list of
-- substrings
-- skip_tags - whether to skip printing tag info
-- show_dev_commands - if true, will include scripts in the modtools/ and
-- devel/ directories. otherwise those scripts will be
-- excluded
-- exclude_strs - comma-separated list of strings. entries are excluded if
-- they match any of the strings.
function ls(filter_str, skip_tags, show_dev_commands, exclude_strs)
local include = {entry_type={ENTRY_TYPES.COMMAND}}
if is_tag(filter_str) then
include.tag = filter_str
else
include.str = filter_str
end
local excludes = {}
if exclude_strs and #exclude_strs > 0 then
table.insert(excludes, {str=argparse.stringList(exclude_strs)})
end
if not show_dev_commands then
local dev_tags = {'dev', 'unavailable'}
if filter_str ~= 'armok' and dfhack.getMortalMode() then
table.insert(dev_tags, 'armok')
end
table.insert(excludes, {tag=dev_tags})
end
list_entries(skip_tags, include, excludes)
end
local function list_tags()
local tags = get_tags()
for _,tag in ipairs(tags) do
print_columns(tag, get_tag_data(tag).description)
end
end
-- implements the 'tags' builtin command
function tags(tag)
if tag and #tag > 0 and not is_tag(tag) then
dfhack.printerr(('unrecognized tag: "%s"'):format(tag))
end
if not is_tag(tag) then
list_tags()
return
end
local skip_tags = true
local include = {entry_type={ENTRY_TYPES.COMMAND}, tag=tag}
local excludes = {tag={}}
if tag ~= 'unavailable' then
table.insert(excludes.tag, 'unavailable')
end
if tag ~= 'armok' and dfhack.getMortalMode() then
table.insert(excludes.tag, 'armok')
end
list_entries(skip_tags, include, excludes)
end
return _ENV