cr.rb 3.18.2 → 3.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/bin/cr +725 -716
  3. data/lib/cr.rb +13 -13
  4. metadata +6 -6
data/bin/cr CHANGED
@@ -1,716 +1,725 @@
1
- #!/usr/bin/env ruby
2
- # coding: utf-8
3
- # ------------------------------------------------------
4
- # File : cr.rb
5
- # Authors : ccmywish <ccmywish@qq.com>
6
- # Created on : <2021-07-08>
7
- # Last modified : <2022-04-15>
8
- #
9
- # cr:
10
- #
11
- # This file is used to explain a CRyptic command
12
- # or an acronym's real meaning in computer world or
13
- # other fields.
14
- #
15
- # ------------------------------------------------------
16
-
17
- require 'cr'
18
- require 'tomlrb'
19
- require 'fileutils'
20
-
21
- CRYPTIC_RESOLVER_HOME = File.expand_path("~/.cryptic-resolver")
22
- CRYPTIC_DEFAULT_DICTS = {
23
- computer: "https://github.com/cryptic-resolver/cryptic_computer.git",
24
- common: "https://github.com/cryptic-resolver/cryptic_common.git",
25
- science: "https://github.com/cryptic-resolver/cryptic_science.git",
26
- economy: "https://github.com/cryptic-resolver/cryptic_economy.git",
27
- medicine: "https://github.com/cryptic-resolver/cryptic_medicine.git"
28
- }
29
-
30
-
31
- ####################
32
- # helper: for color
33
- ####################
34
-
35
- def bold(str) "\e[1m#{str}\e[0m" end
36
- def underline(str) "\e[4m#{str}\e[0m" end
37
- def red(str) "\e[31m#{str}\e[0m" end
38
- def green(str) "\e[32m#{str}\e[0m" end
39
- def yellow(str) "\e[33m#{str}\e[0m" end
40
- def blue(str) "\e[34m#{str}\e[0m" end
41
- def purple(str) "\e[35m#{str}\e[0m" end
42
- def cyan(str) "\e[36m#{str}\e[0m" end
43
-
44
-
45
- ####################
46
- # core: logic
47
- ####################
48
-
49
- def is_there_any_dict?
50
- unless Dir.exist? CRYPTIC_RESOLVER_HOME
51
- Dir.mkdir CRYPTIC_RESOLVER_HOME
52
- end
53
-
54
- !Dir.empty? CRYPTIC_RESOLVER_HOME
55
- end
56
-
57
-
58
- def add_default_dicts_if_none_exists
59
- unless is_there_any_dict?
60
- puts "cr: Adding default dictionaries..."
61
-
62
- begin
63
- if RUBY_PLATFORM.include? "mingw"
64
- # Windows doesn't have fork
65
- CRYPTIC_DEFAULT_DICTS.each do |key, dict|
66
- puts "cr: Pulling cryptic_#{key}..."
67
- `git -C #{CRYPTIC_RESOLVER_HOME} clone #{dict} -q`
68
- end
69
- else
70
- # *nix
71
- CRYPTIC_DEFAULT_DICTS.each do |key, dict|
72
- fork do
73
- puts "cr: Pulling cryptic_#{key}..."
74
- `git -C #{CRYPTIC_RESOLVER_HOME} clone #{dict} -q`
75
- end
76
- end
77
- Process.waitall
78
- end
79
-
80
- rescue Interrupt
81
- puts "cr: Cancel add default dicts"
82
- exit 1
83
- end
84
-
85
- puts "cr: Add done"
86
- word_count(p: false)
87
- puts
88
- puts "#{$WordCount} words added"
89
-
90
- # Really added
91
- return true
92
- end
93
- # Not added
94
- return false
95
- end
96
-
97
-
98
- def update_dicts()
99
- return if add_default_dicts_if_none_exists
100
-
101
- word_count(p: false)
102
- old_wc = [$DefaultWordCount, $WordCount-$DefaultWordCount, $WordCount]
103
-
104
- puts "cr: Updating all dictionaries..."
105
-
106
- begin
107
- Dir.chdir CRYPTIC_RESOLVER_HOME do
108
-
109
- if RUBY_PLATFORM.include? "mingw"
110
- # Windows doesn't have fork
111
- Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
112
- puts "cr: Wait to update #{dict}..."
113
- `git -C ./#{dict} pull -q`
114
- end
115
- else
116
- # *nix
117
- Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
118
- fork do
119
- puts "cr: Wait to update #{dict}..."
120
- `git -C ./#{dict} pull -q`
121
- end
122
- end
123
- Process.waitall
124
-
125
- end # end if/else
126
- end
127
-
128
- rescue Interrupt
129
- puts "cr: Cancel update"
130
- exit 1
131
- end
132
-
133
-
134
- puts "cr: Update done"
135
-
136
- # clear
137
- $DefaultWordCount, $WordCount = 0, 0
138
- # recount
139
- word_count(p: false)
140
- new_wc = [$DefaultWordCount, $WordCount-$DefaultWordCount, $WordCount]
141
- diff = []
142
- new_wc.each_with_index do
143
- diff[_2] = _1 - old_wc[_2]
144
- end
145
-
146
- puts
147
- puts "#{diff.[]2} words added: default/#{diff.[]0} user/#{diff.[]1}"
148
-
149
- end
150
-
151
-
152
- def add_dict(repo)
153
- if repo.nil?
154
- puts bold(red("cr: Need an argument!"))
155
- exit -1
156
- end
157
-
158
- # Ensure the cr home dir exists
159
- FileUtils.mkdir_p(CRYPTIC_RESOLVER_HOME)
160
-
161
- begin
162
- puts "cr: Adding new dictionary..."
163
- `git -C #{CRYPTIC_RESOLVER_HOME} clone #{repo} -q`
164
- rescue Interrupt
165
- puts "cr: Cancel add dict"
166
- exit 1
167
- end
168
-
169
- puts "cr: Add new dictionary done"
170
-
171
- # github/com/ccmywish/ruby_knowledge(.git)
172
- dict = repo.split('/')[-1].delete_suffix('.git')
173
- count_dict_words(dict)
174
- puts
175
- puts "#$WordCount words added"
176
-
177
- end
178
-
179
-
180
- def del_dict(repo)
181
- if repo.nil?
182
- puts bold(red("cr: Need an argument!"))
183
- exit -1
184
- end
185
- Dir.chdir CRYPTIC_RESOLVER_HOME do
186
- begin
187
- # Dir.rmdir repo # Can't rm a filled dir
188
- # FileUtils.rmdir repo # Can't rm a filled dir
189
- FileUtils.rm_rf repo
190
- puts "cr: Delete dictionary #{bold(green(repo))} done"
191
- rescue Exception => e
192
- puts bold(red("cr: #{e}"))
193
- list_dictionaries
194
- end
195
- end
196
- end
197
-
198
-
199
- def load_sheet(dict, sheet_name)
200
- file = CRYPTIC_RESOLVER_HOME + "/#{dict}/#{sheet_name}.toml"
201
-
202
- if File.exist? file
203
- return Tomlrb.load_file file # gem 'tomlrb'
204
- # return TOML.load_file file # gem 'toml'
205
- else
206
- nil
207
- end
208
- end
209
-
210
-
211
- #
212
- # Pretty print the info of the given word
213
- #
214
- # A info looks like this
215
- # emacs = {
216
- # name = "Emacs"
217
- # desc = "edit macros"
218
- # more = "a feature-rich editor"
219
- # see = ["Vim"]
220
- # }
221
- #
222
- # @param info [Hash] the information of the given word (mapped to a keyword in TOML)
223
- #
224
- def pp_info(info)
225
- name = info['name'] || red("No name!") # keyword `or` is invalid here in Ruby
226
-
227
- desc = info['desc']
228
- more = info['more']
229
-
230
- if desc
231
- puts "\n #{name}: #{desc}"
232
- print "\n ",more,"\n" if more
233
- else
234
- puts "\n #{name}"
235
- print "\n ",more,"\n" if more
236
- end
237
-
238
- if see_also = info['see']
239
- print "\n", purple("SEE ALSO ")
240
- see_also.each {|x| print underline(x),' '}
241
- puts
242
- end
243
- puts
244
- end
245
-
246
- # Print default cryptic_ dictionaries
247
- def pp_dict(dict)
248
- puts green("From: #{dict}")
249
- end
250
-
251
-
252
- #
253
- # Used for synonym jump
254
- # Because we absolutely jump to a must-have word
255
- # So we can directly lookup to it
256
- #
257
- # Notice that, we must jump to a specific word definition
258
- # So in the toml file, you must specify the precise word.
259
- # If it has multiple meanings, for example
260
- #
261
- # [blah]
262
- # same = "xdg" # this is wrong, because xdg has multiple
263
- # # definitions, and all of them specify a
264
- # # category
265
- #
266
- # [blah]
267
- # same = "XDG downloader <=>xdg.Download" # this is correct
268
- #
269
- # [blah]
270
- # name = "BlaH" # You may want to display a better name first
271
- # same = "XDG downloader <=>xdg.Download" # this is correct
272
- #
273
- #
274
- def pp_same_info(dict, word, cache_content, same_key, own_name)
275
-
276
- # If it's a synonym for anther word,
277
- # we should lookup into this dict again, but maybe with a different file
278
-
279
- # file name
280
- x = word.chr.downcase
281
-
282
- #
283
- # dictionary maintainer must obey the rule for xxx.yyy word:
284
- # xxx should be lower case
285
- # yyy can be any case
286
- #
287
- # Because yyy should clearly explain the category info, IBM is better than ibm
288
- # Implementation should not be too simple if we want to stress the function we
289
- # expect.
290
- #
291
- # 'jump to' will output to user, so this is important not only inside our sheet.
292
- #
293
- # same = "XDM downloader<=>xdm.Download"
294
- #
295
- # We split 'same' key into two parts via spaceship symbol <=>, first part will
296
- # output to user, the second part is for internal jump.
297
- #
298
-
299
- jump_to, same = same_key.split("<=>")
300
- same = jump_to if same.nil?
301
-
302
- unless own_name
303
- own_name = word
304
- end
305
- puts blue(bold(own_name)) + ' redirects to ' + blue(bold(jump_to))
306
-
307
- #
308
- # As '.' is used to delimit a word and a category, what if
309
- # we jump to a dotted word?
310
- #
311
- # [eg]
312
- # same = "e.g." # this must lead to a wrong resolution to
313
- # # word 'e', category 'g'
314
- #
315
- # All you need is to be like this:
316
- #
317
- # [eg]
318
- # same = "'e.g.'" # cr will notice the single quote
319
- #
320
-
321
- if same =~ /^'(.*)'$/
322
- same, category = $1, nil
323
- else
324
- same, category = same.split('.')
325
- end
326
-
327
- if same.chr == x
328
- # No need to load another dictionary if match
329
- sheet_content = cache_content
330
- else
331
- sheet_content = load_sheet(dict, same.chr.downcase)
332
- end
333
-
334
- if category.nil?
335
- info = sheet_content[same]
336
- else
337
- info = sheet_content[same][category]
338
- end
339
-
340
- if info.nil?
341
- puts red("Warn: Synonym jumps to the wrong place `#{same}`,
342
- Please consider fixing this in `#{x}.toml` of the dictionary `#{dict}`")
343
- # exit
344
- return false
345
- # double or more jumps
346
- elsif same_key = info['same']
347
- own_name = info['name']
348
- return pp_same_info(dict, same, cache_content, same_key, own_name)
349
- else
350
- pp_info(info)
351
- return true
352
- end
353
- end
354
-
355
-
356
-
357
- #
358
- # Lookup the given word in a sheet (a toml file) and also print.
359
- # The core idea is that:
360
- #
361
- # 1. if the word is `same` with another synonym, it will directly jump to
362
- # a word in this dictionary, but maybe a different sheet.
363
- #
364
- # 2. load the toml file and check the given word
365
- # 2.1 with no category specifier
366
- # [abcd]
367
- # 2.2 with category specifier
368
- # [abcd.tYPe]
369
- #
370
- def lookup(dict, file, word)
371
- sheet_content = load_sheet(dict, file)
372
- return false if sheet_content.nil?
373
-
374
- info = sheet_content[word]
375
- return false if info.nil?
376
-
377
- # Warn if the info is empty. For example:
378
- # emacs = { }
379
- if info.size == 0
380
- puts red("WARN: Lack of everything of the given word
381
- Please consider fixing this in the dict `#{dict}`")
382
- exit
383
- end
384
-
385
-
386
-
387
- # Word with no category specifier
388
- # We call this meaning as type 1
389
- type_1_exist_flag = false
390
-
391
- # if already jump, don't check the word itself
392
- is_jump = false
393
-
394
- # synonym info print
395
- if same_key = info['same']
396
- own_name = info['name']
397
- pp_dict(dict)
398
- pp_same_info(dict, word, sheet_content, same_key, own_name)
399
- # It's also a type 1
400
- type_1_exist_flag = true
401
- is_jump = true
402
- end
403
-
404
- # normal info print
405
- # To developer:
406
- # The word should at least has one of `desc` and `more`
407
- # But when none exists, this may not be considered wrong,
408
- # Because the type2 make the case too.
409
- #
410
- # So, just ignore it, even if it's really a mistake(insignificant)
411
- # by dictionary maintainers.
412
- #
413
- if !is_jump && (info.has_key?('desc') || info.has_key?('more'))
414
- pp_dict(dict)
415
- pp_info(info)
416
- type_1_exist_flag = true
417
- end
418
-
419
- # Meanings with category specifier
420
- # We call this meaning as type 2
421
- categories = info.keys - ["name", "desc", "more", "same", "see"]
422
-
423
- if !categories.empty?
424
-
425
- if type_1_exist_flag
426
- print blue(bold("OR")),"\n"
427
- else
428
- pp_dict(dict)
429
- end
430
-
431
- categories.each do |meaning|
432
- info0 = sheet_content[word][meaning]
433
- if same_key = info0['same']
434
- own_name = info0['name']
435
- pp_same_info(dict, word, sheet_content, same_key, own_name)
436
- else
437
- pp_info(info0)
438
- end
439
-
440
- # last meaning doesn't show this separate line
441
- print blue(bold("OR")),"\n" unless categories.last == meaning
442
- end
443
- return true
444
- elsif type_1_exist_flag
445
- return true
446
- else
447
- return false
448
- end
449
- end
450
-
451
-
452
- #
453
- # The main procedure of `cr`
454
- # 1. Search the default's first dict first
455
- # 2. Search the rest dictionaries in the cryptic dictionaries default dir
456
- #
457
- # The `search` is done via the `lookup` function. It will print
458
- # the info while finding. If `lookup` always return false then
459
- # means lacking of this word in our dictionaries. So a welcomed
460
- # contribution is printed on the screen.
461
- #
462
- def solve_word(word)
463
-
464
- add_default_dicts_if_none_exists
465
-
466
- word = word.downcase # downcase! would lead to frozen error in Ruby 2.7.2
467
- # The index is the toml file we'll look into
468
- index = word.chr
469
- case index
470
- when '0'..'9'
471
- index = '0123456789'
472
- end
473
-
474
- # Default's first should be 1st to consider
475
- first_dict = "cryptic_" + CRYPTIC_DEFAULT_DICTS.keys[0].to_s # When Ruby3, We can use DICTS.key(0)
476
-
477
- # cache lookup results
478
- results = []
479
- results << lookup(first_dict,index,word)
480
- # return if result == true # We should consider all dicts
481
-
482
- # Then else
483
- rest = Dir.children(CRYPTIC_RESOLVER_HOME)
484
- rest.delete first_dict
485
- rest.each do |dict|
486
- results << lookup(dict,index,word)
487
- # continue if result == false # We should consider all dicts
488
- end
489
-
490
- unless results.include? true
491
- puts <<-NotFound
492
- cr: Not found anything.
493
-
494
- You may use `cr -u` to update all dictionaries.
495
- Or you could contribute to:
496
-
497
- 1. computer: #{CRYPTIC_DEFAULT_DICTS[:computer]}
498
- 2. common: #{CRYPTIC_DEFAULT_DICTS[:common]}
499
- 3. science: #{CRYPTIC_DEFAULT_DICTS[:science]}
500
- 4. economy: #{CRYPTIC_DEFAULT_DICTS[:economy]}
501
- 5. medicine: #{CRYPTIC_DEFAULT_DICTS[:medicine]}
502
-
503
- NotFound
504
-
505
- else
506
- return
507
- end
508
-
509
- end
510
-
511
-
512
-
513
- #
514
- # The search word process is quite like `solve_word``
515
- # Notice:
516
- # We handle two cases
517
- #
518
- # 1. the 'pattern' is the regexp itself
519
- # 2. the 'pattern' is like '/blahblah/'
520
- #
521
- # The second is what Ruby and Perl users like to do, handle it!
522
- #
523
- def search_word(pattern)
524
-
525
- if pattern.nil?
526
- puts bold(red("cr: Need an argument!"))
527
- exit -1
528
- end
529
-
530
- add_default_dicts_if_none_exists
531
-
532
- if pattern =~ /^\/(.*)\/$/
533
- regexp = %r[#$1]
534
- else
535
- regexp = %r[#{pattern}]
536
- end
537
-
538
- found = false
539
-
540
- #
541
- # Try to match every word in all dictionaries
542
- #
543
- Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
544
- sheets = Dir.children(File.join(CRYPTIC_RESOLVER_HOME, dict)).select do
545
- _1.end_with?('.toml')
546
- end
547
-
548
- similar_words_in_a_dict = []
549
-
550
- sheets.each do |sheet|
551
- sheet_content = load_sheet(dict, File.basename(sheet,'.toml'))
552
-
553
- sheet_content.keys.each do
554
- if _1 =~ regexp
555
- found = true
556
- similar_words_in_a_dict << _1
557
- end
558
- end
559
- # end of each sheet in a dict
560
- end
561
-
562
- unless similar_words_in_a_dict.empty?
563
- pp_dict(dict)
564
- require 'ls_table'
565
- LsTable.ls(similar_words_in_a_dict) do |e|
566
- puts blue(e)
567
- end
568
- puts
569
- end
570
- end
571
- if !found
572
- puts red("cr: No words match with #{regexp.inspect}")
573
- puts
574
- end
575
- end
576
-
577
-
578
- def help
579
- word_count(p: false)
580
- user_words = $WordCount - $DefaultWordCount
581
- puts <<-HELP
582
- cr: Cryptic Resolver v#{CR_GEM_VERSION} (#{$WordCount} words: default/#{$DefaultWordCount} user/#{user_words})
583
-
584
- usage:
585
- cr -v => Print version
586
- cr -h => Print this help
587
- cr -c => Print word count
588
- cr -l => List local dictionaries
589
- cr -u => Update all dictionaries
590
- cr -a repo.git => Add a new dictionary
591
- cr -d cryptic_xx => Delete a dictionary
592
- cr -s pattern => Search words matched with pattern
593
- cr emacs => Edit macros: a feature-rich editor
594
-
595
- HELP
596
-
597
- add_default_dicts_if_none_exists
598
-
599
- end
600
-
601
-
602
- def print_version
603
- puts "cr: Cryptic Resolver version #{CR_GEM_VERSION} in Ruby "
604
- end
605
-
606
-
607
- def list_dictionaries
608
- Dir.chdir CRYPTIC_RESOLVER_HOME do
609
- Dir.children(CRYPTIC_RESOLVER_HOME).each_with_index do |dict,i|
610
- puts "#{blue(i+1)}. #{bold(green(dict))}"
611
- end
612
- end
613
- end
614
-
615
-
616
- # All dictionaries word count
617
- $WordCount = 0
618
- # Default dictionaries word count
619
- $DefaultWordCount = 0
620
-
621
- def count_dict_words(dict)
622
-
623
- dict_dir = CRYPTIC_RESOLVER_HOME + "/#{dict}"
624
-
625
- wc = 0
626
-
627
- Dir.children(dict_dir).each do |entry|
628
- next unless entry.end_with?('.toml')
629
- sheet_content = load_sheet(dict, entry.delete_suffix('.toml'))
630
- count = sheet_content.keys.count
631
-
632
- # puts "#{entry}: #{count}"
633
- wc = wc + count
634
- end
635
-
636
- $WordCount += wc
637
-
638
- return wc
639
-
640
- end
641
-
642
-
643
- def word_count(p:)
644
-
645
- # Always check before Dir.children (this method creates dir if not exists)
646
- is_there_any_dict?
647
-
648
- # real dicts in user's directory
649
- locals = []
650
- Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
651
- locals << dict
652
- end
653
-
654
- # pre-defined default
655
- defaults = CRYPTIC_DEFAULT_DICTS.keys.map do |s|
656
- "cryptic_#{s}"
657
- end
658
-
659
- # user may delete some default dicts
660
- defaults &= locals
661
-
662
- unless defaults.empty?
663
- puts(bold(green("Default dict: "))) if p
664
- defaults.each do |s|
665
- wc = count_dict_words(s)
666
- $DefaultWordCount += wc
667
- # With color, ljust not works, so we disable color
668
- puts(" #{s.ljust(17)}: #{wc}") if p
669
- end
670
- end
671
-
672
- users = locals - defaults
673
- user_words = 0
674
- unless users.empty?
675
- wc = 0
676
- puts(bold(blue("\nUser's dict:"))) if p
677
- users.each do |s|
678
- wc = count_dict_words(s)
679
- # no need to add to $WordCount,
680
- # because it's done in `count_dict_words` func
681
- puts(" #{s.ljust(17)}: #{wc}") if p
682
- end
683
-
684
- user_words = $WordCount - $DefaultWordCount
685
- end
686
-
687
- if p
688
- puts
689
- puts "#{$DefaultWordCount.to_s.rjust(4)} words in default dictionaries"
690
- puts "#{user_words.to_s.rjust(4)} words in user's dictionaries"
691
- puts "#{$WordCount.to_s.rjust(4)} words altogether"
692
- end
693
- end
694
-
695
-
696
- ####################
697
- # main: CLI Handling
698
- ####################
699
- arg = ARGV.shift
700
-
701
- case arg
702
- when nil then help
703
- when '-v' then print_version
704
- when '-h' then help
705
- when '-l' then list_dictionaries
706
- when '-c' then word_count(p: true)
707
- when '-u' then update_dicts
708
- when '-s' then search_word ARGV.shift
709
- when '-a' then add_dict ARGV.shift
710
- when '-d' then del_dict ARGV.shift
711
- when '--help'
712
- help
713
- else
714
- solve_word arg
715
- end
716
-
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+ # ------------------------------------------------------
4
+ # File : cr.rb
5
+ # Authors : ccmywish <ccmywish@qq.com>
6
+ # Created on : <2021-07-08>
7
+ # Last modified : <2022-10-20>
8
+ #
9
+ # cr:
10
+ #
11
+ # This file is used to explain a CRyptic command
12
+ # or an acronym's real meaning in computer world or
13
+ # other fields.
14
+ #
15
+ # ------------------------------------------------------
16
+
17
+ require 'cr'
18
+ require 'tomlrb'
19
+ require 'fileutils'
20
+
21
+ CRYPTIC_RESOLVER_HOME = File.expand_path("~/.cryptic-resolver")
22
+ CRYPTIC_DEFAULT_DICTS = {
23
+ common: "https://github.com/cryptic-resolver/cryptic_common.git",
24
+ computer: "https://github.com/cryptic-resolver/cryptic_computer.git",
25
+ windows: "https://github.com/cryptic-resolver/cryptic_windows.git",
26
+ electronics: "https://github.com/cryptic-resolver/cryptic_electronics.git"
27
+ }
28
+
29
+
30
+ ####################
31
+ # helper: for color
32
+ ####################
33
+
34
+ def bold(str) "\e[1m#{str}\e[0m" end
35
+ def underline(str) "\e[4m#{str}\e[0m" end
36
+ def red(str) "\e[31m#{str}\e[0m" end
37
+ def green(str) "\e[32m#{str}\e[0m" end
38
+ def yellow(str) "\e[33m#{str}\e[0m" end
39
+ def blue(str) "\e[34m#{str}\e[0m" end
40
+ def purple(str) "\e[35m#{str}\e[0m" end
41
+ def cyan(str) "\e[36m#{str}\e[0m" end
42
+
43
+
44
+ ####################
45
+ # core: logic
46
+ ####################
47
+
48
+ def is_there_any_dict?
49
+ unless Dir.exist? CRYPTIC_RESOLVER_HOME
50
+ Dir.mkdir CRYPTIC_RESOLVER_HOME
51
+ end
52
+
53
+ !Dir.empty? CRYPTIC_RESOLVER_HOME
54
+ end
55
+
56
+
57
+ def add_default_dicts_if_none_exists
58
+ unless is_there_any_dict?
59
+ puts "cr: Adding default dictionaries..."
60
+
61
+ begin
62
+ if RUBY_PLATFORM.include? "mingw"
63
+ # Windows doesn't have fork
64
+ CRYPTIC_DEFAULT_DICTS.each do |key, dict|
65
+ puts "cr: Pulling cryptic_#{key}..."
66
+ `git -C #{CRYPTIC_RESOLVER_HOME} clone #{dict} -q`
67
+ end
68
+ else
69
+ # *nix
70
+ CRYPTIC_DEFAULT_DICTS.each do |key, dict|
71
+ fork do
72
+ puts "cr: Pulling cryptic_#{key}..."
73
+ `git -C #{CRYPTIC_RESOLVER_HOME} clone #{dict} -q`
74
+ end
75
+ end
76
+ Process.waitall
77
+ end
78
+
79
+ rescue Interrupt
80
+ puts "cr: Cancel add default dicts"
81
+ exit 1
82
+ end
83
+
84
+ puts "cr: Add done"
85
+ word_count(p: false)
86
+ puts
87
+ puts "#{$WordCount} words added"
88
+
89
+ # Really added
90
+ return true
91
+ end
92
+ # Not added
93
+ return false
94
+ end
95
+
96
+
97
+ def update_dicts()
98
+ return if add_default_dicts_if_none_exists
99
+
100
+ word_count(p: false)
101
+ old_wc = [$DefaultWordCount, $WordCount-$DefaultWordCount, $WordCount]
102
+
103
+ puts "cr: Updating all dictionaries..."
104
+
105
+ begin
106
+ Dir.chdir CRYPTIC_RESOLVER_HOME do
107
+
108
+ if RUBY_PLATFORM.include? "mingw"
109
+ # Windows doesn't have fork
110
+ Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
111
+ puts "cr: Wait to update #{dict}..."
112
+ `git -C ./#{dict} pull -q`
113
+ end
114
+ else
115
+ # *nix
116
+ Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
117
+ fork do
118
+ puts "cr: Wait to update #{dict}..."
119
+ `git -C ./#{dict} pull -q`
120
+ end
121
+ end
122
+ Process.waitall
123
+
124
+ end # end if/else
125
+ end
126
+
127
+ rescue Interrupt
128
+ puts "cr: Cancel update"
129
+ exit 1
130
+ end
131
+
132
+
133
+ puts "cr: Update done"
134
+
135
+ # clear
136
+ $DefaultWordCount, $WordCount = 0, 0
137
+ # recount
138
+ word_count(p: false)
139
+ new_wc = [$DefaultWordCount, $WordCount-$DefaultWordCount, $WordCount]
140
+ diff = []
141
+ new_wc.each_with_index do
142
+ diff[_2] = _1 - old_wc[_2]
143
+ end
144
+
145
+ puts
146
+ puts "#{diff.[]2} words added: default/#{diff.[]0} user/#{diff.[]1}"
147
+
148
+ end
149
+
150
+
151
+ def add_dict(repo)
152
+ if repo.nil?
153
+ puts bold(red("cr: Need an argument!"))
154
+ exit -1
155
+ end
156
+
157
+ # Ensure the cr home dir exists
158
+ FileUtils.mkdir_p(CRYPTIC_RESOLVER_HOME)
159
+
160
+ # Simplify adding dictionary
161
+ if !repo.start_with?("https://") and !repo.start_with?("git@")
162
+ repo = "https://github.com/#{repo}.git"
163
+ end
164
+
165
+ begin
166
+ puts "cr: Adding new dictionary..."
167
+ `git -C #{CRYPTIC_RESOLVER_HOME} clone #{repo} -q`
168
+ rescue Interrupt
169
+ puts "cr: Cancel add dict"
170
+ exit 1
171
+ end
172
+
173
+ puts "cr: Add new dictionary done"
174
+
175
+ # github/com/ccmywish/ruby_knowledge(.git)
176
+ dict = repo.split('/')[-1].delete_suffix('.git')
177
+ count_dict_words(dict)
178
+ puts
179
+ puts "#$WordCount words added"
180
+
181
+ end
182
+
183
+
184
+ def del_dict(repo)
185
+ if repo.nil?
186
+ puts bold(red("cr: Need an argument!"))
187
+ exit -1
188
+ end
189
+ Dir.chdir CRYPTIC_RESOLVER_HOME do
190
+ begin
191
+ # Dir.rmdir repo # Can't rm a filled dir
192
+ # FileUtils.rmdir repo # Can't rm a filled dir
193
+ FileUtils.rm_rf repo
194
+ puts "cr: Delete dictionary #{bold(green(repo))} done"
195
+ rescue Exception => e
196
+ puts bold(red("cr: #{e}"))
197
+ list_dictionaries
198
+ end
199
+ end
200
+ end
201
+
202
+
203
+ def load_sheet(dict, sheet_name)
204
+ file = CRYPTIC_RESOLVER_HOME + "/#{dict}/#{sheet_name}.toml"
205
+
206
+ if File.exist? file
207
+ return Tomlrb.load_file file # gem 'tomlrb'
208
+ # return TOML.load_file file # gem 'toml'
209
+ else
210
+ nil
211
+ end
212
+ end
213
+
214
+
215
+ #
216
+ # Pretty print the info of the given word
217
+ #
218
+ # A info looks like this
219
+ # emacs = {
220
+ # name = "Emacs"
221
+ # desc = "edit macros"
222
+ # more = "a feature-rich editor"
223
+ # see = ["Vim"]
224
+ # }
225
+ #
226
+ # @param info [Hash] the information of the given word (mapped to a keyword in TOML)
227
+ #
228
+ def pp_info(info)
229
+ name = info['name'] || red("No name!") # keyword `or` is invalid here in Ruby
230
+
231
+ desc = info['desc']
232
+ more = info['more']
233
+
234
+ if desc
235
+ puts "\n #{name}: #{desc}"
236
+ print "\n ",more,"\n" if more
237
+ else
238
+ puts "\n #{name}"
239
+ print "\n ",more,"\n" if more
240
+ end
241
+
242
+ if see_also = info['see']
243
+ print "\n", purple("SEE ALSO ")
244
+ if see_also.is_a?(Array)
245
+ see_also.each {|x| print underline(x),' '}
246
+ else
247
+ print underline(see_also),' '
248
+ end
249
+ puts
250
+ end
251
+ puts
252
+ end
253
+
254
+ # Print default cryptic_ dictionaries
255
+ def pp_dict(dict)
256
+ puts green("From: #{dict}")
257
+ end
258
+
259
+
260
+ #
261
+ # Used for synonym jump
262
+ # Because we absolutely jump to a must-have word
263
+ # So we can directly lookup to it
264
+ #
265
+ # Notice that, we must jump to a specific word definition
266
+ # So in the toml file, you must specify the precise word.
267
+ # If it has multiple meanings, for example
268
+ #
269
+ # [blah]
270
+ # same = "xdg" # this is wrong, because xdg has multiple
271
+ # # definitions, and all of them specify a
272
+ # # category
273
+ #
274
+ # [blah]
275
+ # same = "XDG downloader <=>xdg.Download" # this is correct
276
+ #
277
+ # [blah]
278
+ # name = "BlaH" # You may want to display a better name first
279
+ # same = "XDG downloader <=>xdg.Download" # this is correct
280
+ #
281
+ #
282
+ def pp_same_info(dict, word, cache_content, same_key, own_name)
283
+
284
+ # If it's a synonym for anther word,
285
+ # we should lookup into this dict again, but maybe with a different file
286
+
287
+ # file name
288
+ x = word.chr.downcase
289
+
290
+ #
291
+ # dictionary maintainer must obey the rule for xxx.yyy word:
292
+ # xxx should be lower case
293
+ # yyy can be any case
294
+ #
295
+ # Because yyy should clearly explain the category info, IBM is better than ibm
296
+ # Implementation should not be too simple if we want to stress the function we
297
+ # expect.
298
+ #
299
+ # 'jump to' will output to user, so this is important not only inside our sheet.
300
+ #
301
+ # same = "XDM downloader<=>xdm.Download"
302
+ #
303
+ # We split 'same' key into two parts via spaceship symbol <=>, first part will
304
+ # output to user, the second part is for internal jump.
305
+ #
306
+
307
+ jump_to, same = same_key.split("<=>")
308
+ same = jump_to if same.nil?
309
+
310
+ unless own_name
311
+ own_name = word
312
+ end
313
+ puts blue(bold(own_name)) + ' redirects to ' + blue(bold(jump_to))
314
+
315
+ #
316
+ # As '.' is used to delimit a word and a category, what if
317
+ # we jump to a dotted word?
318
+ #
319
+ # [eg]
320
+ # same = "e.g." # this must lead to a wrong resolution to
321
+ # # word 'e', category 'g'
322
+ #
323
+ # All you need is to be like this:
324
+ #
325
+ # [eg]
326
+ # same = "'e.g.'" # cr will notice the single quote
327
+ #
328
+
329
+ if same =~ /^'(.*)'$/
330
+ same, category = $1, nil
331
+ else
332
+ same, category = same.split('.')
333
+ end
334
+
335
+ if same.chr == x
336
+ # No need to load another dictionary if match
337
+ sheet_content = cache_content
338
+ else
339
+ sheet_content = load_sheet(dict, same.chr.downcase)
340
+ end
341
+
342
+ if category.nil?
343
+ info = sheet_content[same]
344
+ else
345
+ info = sheet_content[same][category]
346
+ end
347
+
348
+ if info.nil?
349
+ puts red("Warn: Synonym jumps to the wrong place `#{same}`,
350
+ Please consider fixing this in `#{x}.toml` of the dictionary `#{dict}`")
351
+ # exit
352
+ return false
353
+ # double or more jumps
354
+ elsif same_key = info['same']
355
+ own_name = info['name']
356
+ return pp_same_info(dict, same, cache_content, same_key, own_name)
357
+ else
358
+ pp_info(info)
359
+ return true
360
+ end
361
+ end
362
+
363
+
364
+
365
+ #
366
+ # Lookup the given word in a sheet (a toml file) and also print.
367
+ # The core idea is that:
368
+ #
369
+ # 1. if the word is `same` with another synonym, it will directly jump to
370
+ # a word in this dictionary, but maybe a different sheet.
371
+ #
372
+ # 2. load the toml file and check the given word
373
+ # 2.1 with no category specifier
374
+ # [abcd]
375
+ # 2.2 with category specifier
376
+ # [abcd.tYPe]
377
+ #
378
+ def lookup(dict, file, word)
379
+ sheet_content = load_sheet(dict, file)
380
+ return false if sheet_content.nil?
381
+
382
+ info = sheet_content[word]
383
+ return false if info.nil?
384
+
385
+ # Warn if the info is empty. For example:
386
+ # emacs = { }
387
+ if info.size == 0
388
+ puts red("WARN: Lack of everything of the given word
389
+ Please consider fixing this in the dict `#{dict}`")
390
+ exit
391
+ end
392
+
393
+
394
+
395
+ # Word with no category specifier
396
+ # We call this meaning as type 1
397
+ type_1_exist_flag = false
398
+
399
+ # if already jump, don't check the word itself
400
+ is_jump = false
401
+
402
+ # synonym info print
403
+ if same_key = info['same']
404
+ own_name = info['name']
405
+ pp_dict(dict)
406
+ pp_same_info(dict, word, sheet_content, same_key, own_name)
407
+ # It's also a type 1
408
+ type_1_exist_flag = true
409
+ is_jump = true
410
+ end
411
+
412
+ # normal info print
413
+ # To developer:
414
+ # The word should at least has one of `desc` and `more`
415
+ # But when none exists, this may not be considered wrong,
416
+ # Because the type2 make the case too.
417
+ #
418
+ # So, just ignore it, even if it's really a mistake(insignificant)
419
+ # by dictionary maintainers.
420
+ #
421
+ if !is_jump && (info.has_key?('desc') || info.has_key?('more'))
422
+ pp_dict(dict)
423
+ pp_info(info)
424
+ type_1_exist_flag = true
425
+ end
426
+
427
+ # Meanings with category specifier
428
+ # We call this meaning as type 2
429
+ categories = info.keys - ["name", "desc", "more", "same", "see"]
430
+
431
+ if !categories.empty?
432
+
433
+ if type_1_exist_flag
434
+ print blue(bold("OR")),"\n"
435
+ else
436
+ pp_dict(dict)
437
+ end
438
+
439
+ categories.each do |meaning|
440
+ info0 = sheet_content[word][meaning]
441
+ if same_key = info0['same']
442
+ own_name = info0['name']
443
+ pp_same_info(dict, word, sheet_content, same_key, own_name)
444
+ else
445
+ pp_info(info0)
446
+ end
447
+
448
+ # last meaning doesn't show this separate line
449
+ print blue(bold("OR")),"\n" unless categories.last == meaning
450
+ end
451
+ return true
452
+ elsif type_1_exist_flag
453
+ return true
454
+ else
455
+ return false
456
+ end
457
+ end
458
+
459
+
460
+ #
461
+ # The main procedure of `cr`
462
+ # 1. Search the default's first dict first
463
+ # 2. Search the rest dictionaries in the cryptic dictionaries default dir
464
+ #
465
+ # The `search` is done via the `lookup` function. It will print
466
+ # the info while finding. If `lookup` always return false then
467
+ # means lacking of this word in our dictionaries. So a welcomed
468
+ # contribution is printed on the screen.
469
+ #
470
+ def solve_word(word)
471
+
472
+ add_default_dicts_if_none_exists
473
+
474
+ word = word.downcase # downcase! would lead to frozen error in Ruby 2.7.2
475
+ # The index is the toml file we'll look into
476
+ index = word.chr
477
+ case index
478
+ when '0'..'9'
479
+ index = '0123456789'
480
+ end
481
+
482
+ # Default's first should be 1st to consider
483
+ first_dict = "cryptic_" + CRYPTIC_DEFAULT_DICTS.keys[0].to_s # When Ruby3, We can use DICTS.key(0)
484
+
485
+ # cache lookup results
486
+ results = []
487
+ results << lookup(first_dict,index,word)
488
+ # return if result == true # We should consider all dicts
489
+
490
+ # Then else
491
+ rest = Dir.children(CRYPTIC_RESOLVER_HOME)
492
+ rest.delete first_dict
493
+ rest.each do |dict|
494
+ results << lookup(dict,index,word)
495
+ # continue if result == false # We should consider all dicts
496
+ end
497
+
498
+ unless results.include? true
499
+ puts <<-NotFound
500
+ cr: Not found anything.
501
+
502
+ You may
503
+ 1. Use `cr -u` to update all dicts
504
+ 2. Use `cr -a user/repo` to add more available dicts
505
+
506
+ See: https://github.com/cryptic-resolver
507
+
508
+ 3. Contribute to theses dicts
509
+
510
+ NotFound
511
+
512
+ else
513
+ return
514
+ end
515
+
516
+ end
517
+
518
+
519
+
520
+ #
521
+ # The search word process is quite like `solve_word``
522
+ # Notice:
523
+ # We handle two cases
524
+ #
525
+ # 1. the 'pattern' is the regexp itself
526
+ # 2. the 'pattern' is like '/blahblah/'
527
+ #
528
+ # The second is what Ruby and Perl users like to do, handle it!
529
+ #
530
+ def search_word(pattern)
531
+
532
+ if pattern.nil?
533
+ puts bold(red("cr: Need an argument!"))
534
+ exit -1
535
+ end
536
+
537
+ add_default_dicts_if_none_exists
538
+
539
+ if pattern =~ /^\/(.*)\/$/
540
+ regexp = %r[#$1]
541
+ else
542
+ regexp = %r[#{pattern}]
543
+ end
544
+
545
+ found = false
546
+
547
+ #
548
+ # Try to match every word in all dictionaries
549
+ #
550
+ Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
551
+ sheets = Dir.children(File.join(CRYPTIC_RESOLVER_HOME, dict)).select do
552
+ _1.end_with?('.toml')
553
+ end
554
+
555
+ similar_words_in_a_dict = []
556
+
557
+ sheets.each do |sheet|
558
+ sheet_content = load_sheet(dict, File.basename(sheet,'.toml'))
559
+
560
+ sheet_content.keys.each do
561
+ if _1 =~ regexp
562
+ found = true
563
+ similar_words_in_a_dict << _1
564
+ end
565
+ end
566
+ # end of each sheet in a dict
567
+ end
568
+
569
+ unless similar_words_in_a_dict.empty?
570
+ pp_dict(dict)
571
+ require 'ls_table'
572
+ LsTable.ls(similar_words_in_a_dict) do |e|
573
+ puts blue(e)
574
+ end
575
+ puts
576
+ end
577
+ end
578
+ if !found
579
+ puts red("cr: No words match with #{regexp.inspect}")
580
+ puts
581
+ end
582
+ end
583
+
584
+
585
+ def help
586
+ word_count(p: false)
587
+ user_words = $WordCount - $DefaultWordCount
588
+ puts <<-HELP
589
+ cr: Cryptic Resolver v#{CR_GEM_VERSION} (#{$WordCount} words: default/#{$DefaultWordCount} user/#{user_words})
590
+
591
+ usage:
592
+
593
+ cr emacs => Edit macros: a feature-rich editor
594
+ cr -c => Print word count
595
+ cr -l => List local dictionaries
596
+ cr -u => Update all dictionaries
597
+ cr -a repo.git => Add a new dictionary
598
+ cr -a user/repo => Add a new dictionary on Github
599
+ cr -d cryptic_xx => Delete a dictionary
600
+ cr -s pattern => Search words matched with pattern
601
+ cr -v => Print version
602
+ cr -h => Print this help
603
+
604
+ HELP
605
+
606
+ add_default_dicts_if_none_exists
607
+
608
+ end
609
+
610
+
611
+ def print_version
612
+ puts "cr: Cryptic Resolver version #{CR_GEM_VERSION} in Ruby "
613
+ end
614
+
615
+
616
+ def list_dictionaries
617
+ Dir.chdir CRYPTIC_RESOLVER_HOME do
618
+ Dir.children(CRYPTIC_RESOLVER_HOME).each_with_index do |dict,i|
619
+ puts "#{blue(i+1)}. #{bold(green(dict))}"
620
+ end
621
+ end
622
+ end
623
+
624
+
625
+ # All dictionaries word count
626
+ $WordCount = 0
627
+ # Default dictionaries word count
628
+ $DefaultWordCount = 0
629
+
630
+ def count_dict_words(dict)
631
+
632
+ dict_dir = CRYPTIC_RESOLVER_HOME + "/#{dict}"
633
+
634
+ wc = 0
635
+
636
+ Dir.children(dict_dir).each do |entry|
637
+ next unless entry.end_with?('.toml')
638
+ sheet_content = load_sheet(dict, entry.delete_suffix('.toml'))
639
+ count = sheet_content.keys.count
640
+
641
+ # puts "#{entry}: #{count}"
642
+ wc = wc + count
643
+ end
644
+
645
+ $WordCount += wc
646
+
647
+ return wc
648
+
649
+ end
650
+
651
+
652
+ def word_count(p:)
653
+
654
+ # Always check before Dir.children (this method creates dir if not exists)
655
+ is_there_any_dict?
656
+
657
+ # real dicts in user's directory
658
+ locals = []
659
+ Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
660
+ locals << dict
661
+ end
662
+
663
+ # pre-defined default
664
+ defaults = CRYPTIC_DEFAULT_DICTS.keys.map do |s|
665
+ "cryptic_#{s}"
666
+ end
667
+
668
+ # user may delete some default dicts
669
+ defaults &= locals
670
+
671
+ unless defaults.empty?
672
+ puts(bold(green("Default dicts: "))) if p
673
+ defaults.each do |s|
674
+ wc = count_dict_words(s)
675
+ $DefaultWordCount += wc
676
+ # With color, ljust not works, so we disable color
677
+ puts(" #{s.ljust(17)}: #{wc}") if p
678
+ end
679
+ end
680
+
681
+ users = locals - defaults
682
+ user_words = 0
683
+ unless users.empty?
684
+ wc = 0
685
+ puts(bold(blue("\nUser's dict:"))) if p
686
+ users.each do |s|
687
+ wc = count_dict_words(s)
688
+ # no need to add to $WordCount,
689
+ # because it's done in `count_dict_words` func
690
+ puts(" #{s.ljust(17)}: #{wc}") if p
691
+ end
692
+
693
+ user_words = $WordCount - $DefaultWordCount
694
+ end
695
+
696
+ if p
697
+ puts
698
+ puts "#{$DefaultWordCount.to_s.rjust(4)} words in default dictionaries"
699
+ puts "#{user_words.to_s.rjust(4)} words in user's dictionaries"
700
+ puts "#{$WordCount.to_s.rjust(4)} words altogether"
701
+ end
702
+ end
703
+
704
+
705
+ ####################
706
+ # main: CLI Handling
707
+ ####################
708
+ arg = ARGV.shift
709
+
710
+ case arg
711
+ when nil then help
712
+ when '-v' then print_version
713
+ when '-h' then help
714
+ when '-l' then list_dictionaries
715
+ when '-c' then word_count(p: true)
716
+ when '-u' then update_dicts
717
+ when '-s' then search_word ARGV.shift
718
+ when '-a' then add_dict ARGV.shift
719
+ when '-d' then del_dict ARGV.shift
720
+ when '--help'
721
+ help
722
+ else
723
+ solve_word arg
724
+ end
725
+