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