ruby-shell 2.10.1 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/bin/rsh +207 -28
  3. metadata +7 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee2ef357d1196f43811ad6974313bbdb057f61cf90219146936f70a2ec6f16bb
4
- data.tar.gz: 85540ca12677d88d6b2fb6999f17ce871b7c5142f20a0fa14e03685f82f41853
3
+ metadata.gz: 539ffa4c43d681abff9a260ef5f78e8762906cd5ca566ea0a9e85e39bb74318a
4
+ data.tar.gz: 80a8ee4bbfb0717c4c2ada364ff65f76364f01dfce3daba83a88f8e41e21eb85
5
5
  SHA512:
6
- metadata.gz: f7a35b051b9d768cb2065e6308eecc030f3b153a31881b31e25813a377de06ea98b5e76f55233dbc0ae1ec30920091ee62817352f43fc5645c3a3d69db746c78
7
- data.tar.gz: f8aeb41fa3870b02d96b111126c1770fb74e235a01847947937461c1a697195e3fc6152eb5a45a147c1703278e12a1b8dc5c279e1c1e4b51d81fbeb0672ed27f
6
+ metadata.gz: 7fd78712cdf62fe52827bf2d935c5d7e92d3fecb6a82d564f84b077ef3e9c35101e3e1a9856bfda653f3f8433d74fe539e8cbf172e9e91af7434af2ad429523c
7
+ data.tar.gz: b3075ef2fcb9553e9e8cc071560b0f659c2ad21a30aa8c7e2429c53cd02d458984c5e659865a1fe879a997bda9a431e222e21fb45bf2a7c58d2f543e7784650e
data/bin/rsh CHANGED
@@ -8,7 +8,7 @@
8
8
  # Web_site: http://isene.com/
9
9
  # Github: https://github.com/isene/rsh
10
10
  # License: Public domain
11
- @version = "2.10.1" # Bug fix: Improved && operator logic for proper conditional execution
11
+ @version = "2.11.0" # Enhanced TAB completion with smart context-aware completion, frequency scoring, fuzzy matching, and more
12
12
 
13
13
  # MODULES, CLASSES AND EXTENSIONS
14
14
  class String # Add coloring to strings (with escaping for Readline)
@@ -102,6 +102,9 @@ begin # Initialization
102
102
  @gnick = {} # Initiate generic/global alias/nick hash
103
103
  @history = [] # Initiate history array
104
104
  @exe = []
105
+ @exe_cache_time = 0 # Cache timestamp for executables
106
+ @exe_cache_paths = "" # Cached PATH value
107
+ @cmd_frequency = {} # Track command usage frequency
105
108
  # Paths
106
109
  @user = Etc.getpwuid(Process.euid).name # For use in @prompt
107
110
  @path = ENV["PATH"].split(":") # Get paths
@@ -180,6 +183,12 @@ def firstrun
180
183
  @c_taboption = 244 # Color for unselected tabcompleted item
181
184
  @c_stamp = 244 # Color for time stamp/command
182
185
 
186
+ # TAB COMPLETION SETTINGS
187
+ @completion_limit = 10 # Max completion items to show
188
+ @completion_case_sensitive = false # Case-insensitive completion
189
+ @completion_show_descriptions = false # Show help text inline
190
+ @completion_fuzzy = true # Enable fuzzy matching
191
+
183
192
  @nick = {"ls"=>"ls --color -F"}
184
193
  RSHRC
185
194
  File.write(Dir.home+'/.rshrc', rc)
@@ -279,6 +288,7 @@ def getstr # A custom Readline-like function
279
288
  @c.col(c_col.modulo(@maxcol))
280
289
  end
281
290
  chr = getchr
291
+ puts "DEBUG: Got char: '#{chr}' (length: #{chr.length})" if ENV['RSH_DEBUG']
282
292
  case chr
283
293
  when 'C-G', 'C-C'
284
294
  @history[0] = ""
@@ -401,6 +411,10 @@ def getstr # A custom Readline-like function
401
411
  @pos = 0
402
412
  lift = true
403
413
  when 'TAB' # Tab completion of dirs and files
414
+ puts "\n=== TAB KEY DETECTED ===" if ENV['RSH_DEBUG']
415
+ puts "Current line: '#{@history[0]}'" if ENV['RSH_DEBUG']
416
+ puts "Cursor pos: #{@pos}" if ENV['RSH_DEBUG']
417
+ puts "======================" if ENV['RSH_DEBUG']
404
418
  @ci = nil
405
419
  #@tabsearch =~ /^-/ ? tabbing("switch") : tabbing("all")
406
420
  tab("all")
@@ -426,15 +440,59 @@ def tab(type)
426
440
  i = 0
427
441
  chr = ""
428
442
  @tabarray = []
443
+
429
444
  @pretab = @history[0][0...@pos].to_s # Extract the current line up to cursor
430
445
  @postab = @history[0][@pos..].to_s # Extract the current line from cursor to end
431
446
  @c_row, @c_col = @c.pos # Get cursor position
432
447
  @row0 = @c_row # Save original row
433
- @tabstr = @pretab.split(/[|, ]/).last.to_s # Get the sustring that is being tab completed
434
- @tabstr = "" if @pretab[-1] =~ /[ |]/ # Tab from nothing if tabbing starts with space or pipe
435
- @tabstr = @pretab if type == "hist" # Searching for matches with whole string in history
436
- @pretab = @pretab.delete_suffix(@tabstr)
437
- type = "switch" if @tabstr[0] == "-"
448
+ # Special handling for environment variables first
449
+ if @pretab =~ /\$\w*$/
450
+ @tabstr = @pretab.match(/\$\w*$/)[0]
451
+ @pretab = @pretab.sub(/\$\w*$/, '')
452
+ else
453
+ @tabstr = @pretab.split(/[|;&, ]/).last.to_s # Get the sustring that is being tab completed
454
+ @tabstr = "" if @pretab[-1] =~ /[ |;&]/ # Tab from nothing if tabbing starts with space, pipe, etc.
455
+ @tabstr = @pretab if type == "hist" # Searching for matches with whole string in history
456
+ @pretab = @pretab.delete_suffix(@tabstr)
457
+ end
458
+ type = "switch" if @tabstr && @tabstr[0] == "-"
459
+ type = "env_vars" if @tabstr && @tabstr[0] == "$"
460
+
461
+ # Debug output when RSH_DEBUG is set
462
+ if ENV['RSH_DEBUG']
463
+ puts "\n=== TAB DEBUG ==="
464
+ puts "Input type: #{type}"
465
+ puts "@pretab: '#{@pretab}'"
466
+ puts "@tabstr: '#{@tabstr}'"
467
+ puts "Length: #{@tabstr.length}"
468
+ puts "First char: '#{@tabstr[0] if @tabstr}'"
469
+ puts "=================="
470
+ end
471
+
472
+ # Smart context-aware completion
473
+ unless type == "switch" || type == "hist"
474
+ if @pretab && !@pretab.empty?
475
+ cmd_parts = @pretab.strip.split(/[|;&]/).last.strip.split
476
+ last_cmd = cmd_parts.first if cmd_parts.any?
477
+ else
478
+ cmd_parts = []
479
+ last_cmd = nil
480
+ end
481
+
482
+ case last_cmd
483
+ when "cd", "pushd", "rmdir"
484
+ type = "dirs_only"
485
+ when "vim", "nano", "cat", "less", "more", "head", "tail", "file"
486
+ type = "files_only"
487
+ when "git"
488
+ type = "git_subcommands" if cmd_parts.length == 1
489
+ when "man", "info", "which", "whatis"
490
+ type = "commands_only"
491
+ when "export", "unset"
492
+ type = "env_vars"
493
+ end
494
+ end
495
+
438
496
  while chr != "ENTER"
439
497
  case type
440
498
  when "hist" # Handle history completions ('UP' key)
@@ -448,26 +506,103 @@ def tab(type)
448
506
  hlp = hlp.map{|h| h.sub(/^\s*/, '').sub(/^--/, ' --')}
449
507
  hlp = hlp.reject{|h| /-</ =~ h}
450
508
  @tabarray = hlp
509
+ when "dirs_only" # Only show directories
510
+ fdir = @tabstr + "*"
511
+ dirs = Dir.glob(fdir).select { |d| Dir.exist?(d) }.map { |d| d + "/" }
512
+ @tabarray = dirs
513
+ when "files_only" # Only show files, not directories
514
+ fdir = @tabstr + "*"
515
+ files = Dir.glob(fdir).reject { |f| Dir.exist?(f) }
516
+ @tabarray = files
517
+ when "commands_only" # Only show executable commands
518
+ ex = @exe.dup
519
+ ex.prepend(*@nick.keys, *@gnick.keys)
520
+ regex_flags = @completion_case_sensitive ? 0 : Regexp::IGNORECASE
521
+ @tabarray = ex.select { |s| s =~ Regexp.new(@tabstr, regex_flags) }
522
+ when "git_subcommands" # Git subcommands
523
+ git_cmds = %w[add branch checkout clone commit diff fetch init log merge pull push rebase reset status tag]
524
+ regex_flags = @completion_case_sensitive ? 0 : Regexp::IGNORECASE
525
+ @tabarray = git_cmds.select { |cmd| cmd =~ Regexp.new(@tabstr, regex_flags) }
526
+ when "env_vars" # Environment variables
527
+ env_vars = ENV.keys.map { |k| "$#{k}" }
528
+ regex_flags = @completion_case_sensitive ? 0 : Regexp::IGNORECASE
529
+ @tabarray = env_vars.select { |var| var =~ Regexp.new(@tabstr, regex_flags) }
451
530
  when "all" # Handle all other tab completions
452
531
  ex = []
453
532
  ex += @exe
454
533
  ex.sort!
455
534
  ex.prepend(*@nick.keys) # Add nicks
456
535
  ex.prepend(*@gnick.keys) # Add gnicks
457
- compl = ex.select {|s| s =~ Regexp.new(@tabstr)} # Select only that which matches so far
458
- fdir = @tabstr + "*"
459
- compl.prepend(*Dir.glob(fdir)).map! do |e|
460
- if e =~ /(?<!\\) /
461
- e = e.sub(/(.*\/|^)(.*)/, '\1\'\2\'') unless e =~ /'/
536
+
537
+ # Enhanced matching with case sensitivity and fuzzy support
538
+ regex_flags = @completion_case_sensitive ? 0 : Regexp::IGNORECASE
539
+
540
+ # Try multiple matching strategies
541
+ compl = []
542
+
543
+ # First try exact prefix matching
544
+ compl = ex.select { |s| s =~ Regexp.new("^#{@tabstr}", regex_flags) }
545
+
546
+ # If no results and fuzzy enabled, try fuzzy matching
547
+ if compl.empty? && @completion_fuzzy && @tabstr.length > 1
548
+ # Fuzzy matching: match from start with characters in order
549
+ fuzzy_pattern = "^#{@tabstr.chars.join('.*')}"
550
+ compl = ex.select { |s| s =~ Regexp.new(fuzzy_pattern, regex_flags) }
551
+ end
552
+
553
+ # If still no results, try substring matching
554
+ if compl.empty?
555
+ compl = ex.select { |s| s =~ Regexp.new(@tabstr, regex_flags) }
556
+ end
557
+
558
+ # Sort by frequency (most used first), then alphabetically
559
+ compl.sort! do |a, b|
560
+ freq_a = @cmd_frequency[a] || 0
561
+ freq_b = @cmd_frequency[b] || 0
562
+ if freq_a == freq_b
563
+ a <=> b
564
+ else
565
+ freq_b <=> freq_a
566
+ end
567
+ end
568
+
569
+ # File completion with hidden file handling
570
+ fdir = @tabstr + "*"
571
+ files = Dir.glob(fdir)
572
+
573
+ # Only show hidden files if tabstr starts with .
574
+ unless @tabstr.start_with?('.')
575
+ files.reject! { |f| File.basename(f).start_with?('.') }
576
+ end
577
+
578
+ files.map! do |e|
579
+ if e =~ /(?<!\\) /
580
+ e = e.sub(/(.*\/|^)(.*)/, '\1\'\2\'') unless e =~ /'/
462
581
  end
463
582
  Dir.exist?(e) ? e + "/" : e # Add matching dirs
464
583
  end
465
- @tabarray = compl # Finally put it into @tabarray
584
+
585
+ # Separate directories and files for better ordering
586
+ dirs = files.select { |f| f.end_with?('/') }
587
+ files_only = files.reject { |f| f.end_with?('/') }
588
+
589
+ # Order: directories first, then files, then commands
590
+ @tabarray = dirs + files_only + compl
591
+
592
+ # Debug completion results
593
+ if ENV['RSH_DEBUG']
594
+ puts "=== COMPLETION RESULTS ==="
595
+ puts "Type: #{type}"
596
+ puts "Total matches: #{@tabarray.length}"
597
+ puts "First 5: #{@tabarray.first(5).inspect}"
598
+ puts "=========================="
599
+ end
466
600
  end
467
601
  return if @tabarray.empty?
468
602
  @tabarray.delete("") # Don't remember why
469
603
  @c.clear_screen_down # Here we go
470
- @tabarray.length.to_i - i < 5 ? l = @tabarray.length.to_i - i : l = 5 # Max 5 rows of completion items
604
+ max_items = @completion_limit || 5
605
+ @tabarray.length.to_i - i < max_items ? l = @tabarray.length.to_i - i : l = max_items
471
606
  l.times do |x| # Iterate through
472
607
  if x == 0 # First item goes onto the commandline
473
608
  @c.clear_line # Clear the line
@@ -485,16 +620,28 @@ def tab(type)
485
620
  nextline # Then start showing the completion items
486
621
  tabline = @tabarray[i] # Get the next matching tabline
487
622
  # Can't nest ANSI codes, they must each complete/conclude or they will mess eachother up
488
- tabline1 = tabline.sub(/(.*?)#{@tabstr}.*/, '\1').c(@c_tabselect) # Color the part before the @tabstr
489
- tabline2 = tabline.sub(/.*?#{@tabstr}(.*)/, '\1').c(@c_tabselect) # Color the part after the @tabstr
490
- print " " + tabline1 + @tabstr.c(@c_tabselect).u + tabline2 # Color & underline @tabstr
623
+ if tabline.include?(@tabstr)
624
+ tabline1 = tabline.sub(/(.*?)#{@tabstr}.*/, '\1').c(@c_tabselect) # Color the part before the @tabstr
625
+ tabline2 = tabline.sub(/.*?#{@tabstr}(.*)/, '\1').c(@c_tabselect) # Color the part after the @tabstr
626
+ print " " + tabline1 + @tabstr.c(@c_tabselect).u + tabline2 # Color & underline @tabstr
627
+ else
628
+ # For fuzzy matches, just show the whole word highlighted
629
+ print " " + tabline.c(@c_tabselect)
630
+ end
491
631
  else
492
632
  begin
493
633
  tabline = @tabarray[i+x] # Next tabline, and next, etc (usually 4 times here)
494
- tabline1 = tabline.sub(/(.*?)#{@tabstr}.*/, '\1').c(@c_taboption) # Color before @tabstr
495
- tabline2 = tabline.sub(/.*?#{@tabstr}(.*)/, '\1').c(@c_taboption) # Color after @tabstr
496
- print " " + tabline1 + @tabstr.c(@c_taboption).u + tabline2 # Print the whole line
497
- rescue
634
+ if tabline.include?(@tabstr)
635
+ tabline1 = tabline.sub(/(.*?)#{@tabstr}.*/, '\1').c(@c_taboption) # Color before @tabstr
636
+ tabline2 = tabline.sub(/.*?#{@tabstr}(.*)/, '\1').c(@c_taboption) # Color after @tabstr
637
+ print " " + tabline1 + @tabstr.c(@c_taboption).u + tabline2 # Print the whole line
638
+ else
639
+ # For fuzzy matches, just show the whole word
640
+ print " " + tabline.c(@c_taboption)
641
+ end
642
+ rescue => e
643
+ # Log completion errors if debugging enabled
644
+ File.write("/tmp/rsh_completion.log", "#{Time.now}: Completion error - #{e}\n", mode: 'a') if ENV['RSH_DEBUG']
498
645
  end
499
646
  end
500
647
  nextline # To not run off screen
@@ -605,6 +752,8 @@ def rshrc # Write updates to .rshrc
605
752
  conf += "@nick = #{@nick}\n"
606
753
  conf.sub!(/^@gnick.*\n/, "")
607
754
  conf += "@gnick = #{@gnick}\n"
755
+ conf.sub!(/^@cmd_frequency.*\n/, "")
756
+ conf += "@cmd_frequency = #{@cmd_frequency}\n"
608
757
  conf.sub!(/^@history.*\n/, "")
609
758
  # Ensure history is properly formatted as valid Ruby array
610
759
  begin
@@ -1139,6 +1288,7 @@ def load_rshrc_safe
1139
1288
  @history = [] unless @history.is_a?(Array)
1140
1289
  @nick = {} unless @nick.is_a?(Hash)
1141
1290
  @gnick = {} unless @gnick.is_a?(Hash)
1291
+ @cmd_frequency = {} unless @cmd_frequency.is_a?(Hash)
1142
1292
 
1143
1293
  rescue SyntaxError => e
1144
1294
  puts "\n\033[31mERROR: Syntax error in .rshrc:\033[0m"
@@ -1195,8 +1345,8 @@ def auto_heal_rshrc
1195
1345
  end
1196
1346
 
1197
1347
  # Fix unclosed arrays
1198
- ['@history', '@nick', '@gnick'].each do |var|
1199
- if content =~ /^#{var}\s*=\s*[[{](?!.*[}\]]\s*$)/m
1348
+ ['@history', '@nick', '@gnick', '@cmd_frequency'].each do |var|
1349
+ if content =~ /^#{var}\s*=\s*[\[{](?!.*[}\]]\s*$)/m
1200
1350
  content.sub!(/^(#{var}\s*=\s*)(\[.*?)$/m) { "#{$1}#{$2}]" }
1201
1351
  content.sub!(/^(#{var}\s*=\s*)({.*?)$/m) { "#{$1}#{$2}}" }
1202
1352
  healed = true
@@ -1247,9 +1397,36 @@ def load_defaults
1247
1397
  @history ||= []
1248
1398
  @nick ||= {"ls" => "ls --color -F"}
1249
1399
  @gnick ||= {}
1400
+ @completion_limit ||= 10
1401
+ @completion_case_sensitive ||= false
1402
+ @completion_show_descriptions ||= false
1403
+ @completion_fuzzy ||= true
1404
+ @cmd_frequency ||= {}
1250
1405
  puts "Loaded with default configuration."
1251
1406
  end
1252
1407
 
1408
+ def cache_executables
1409
+ current_path = ENV["PATH"]
1410
+ current_time = Time.now.to_i
1411
+
1412
+ # Only rebuild cache if PATH changed or cache is older than 60 seconds
1413
+ return if @exe_cache_paths == current_path && (current_time - @exe_cache_time) < 60
1414
+
1415
+ @exe = []
1416
+ @path = current_path.split(":")
1417
+ @path.map! {|p| p + "/*"}
1418
+
1419
+ @path.each do |p|
1420
+ Dir.glob(p).each do |c|
1421
+ @exe.append(File.basename(c)) if File.executable?(c) and not Dir.exist?(c)
1422
+ end
1423
+ end
1424
+
1425
+ @exe.uniq!
1426
+ @exe_cache_time = current_time
1427
+ @exe_cache_paths = current_path
1428
+ end
1429
+
1253
1430
  begin # Load .rshrc and populate @history
1254
1431
  trap "SIGINT" do end
1255
1432
  trap "SIGHUP" do
@@ -1309,12 +1486,7 @@ loop do
1309
1486
  @prompt.gsub!(/#{Dir.home}/, '~') # Simplify path in prompt
1310
1487
  system("printf \"\033]0;rsh: #{Dir.pwd}\007\"") # Set Window title to path
1311
1488
  @history[0] = "" unless @history[0]
1312
- @exe = []
1313
- @path.each do |p| # Add all executables in @path
1314
- Dir.glob(p).each do |c|
1315
- @exe.append(File.basename(c)) if File.executable?(c) and not Dir.exist?(c)
1316
- end
1317
- end
1489
+ cache_executables # Use cached executable lookup
1318
1490
  getstr # Main work is here
1319
1491
  @cmd = @history[0]
1320
1492
  @dirs.unshift(Dir.pwd)
@@ -1434,6 +1606,13 @@ loop do
1434
1606
  else
1435
1607
  begin
1436
1608
  pre_cmd
1609
+
1610
+ # Track command frequency for intelligent completion
1611
+ if @cmd && !@cmd.empty?
1612
+ cmd_base = @cmd.split.first
1613
+ @cmd_frequency[cmd_base] = (@cmd_frequency[cmd_base] || 0) + 1 if cmd_base
1614
+ end
1615
+
1437
1616
  # Handle background jobs
1438
1617
  if @cmd.end_with?(' &')
1439
1618
  @cmd = @cmd[0..-3] # Remove the &
metadata CHANGED
@@ -1,21 +1,22 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-shell
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.10.1
4
+ version: 2.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-09-22 00:00:00.000000000 Z
11
+ date: 2025-09-23 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'A shell written in Ruby with extensive tab completions, aliases/nicks,
14
14
  history, syntax highlighting, theming, auto-cd, auto-opening files and more. UPDATE
15
- v2.10.0: Auto-healing for corrupted .rshrc files and robust error handling. v2.9.0:
16
- AI integration! Use @ for AI text responses and @@ for AI command suggestions. Works
17
- with local Ollama or external providers like OpenAI. v2.8.0: Enhanced help system,
18
- :info command, and easier nick management.'
15
+ v2.11.0: Major TAB completion overhaul! Smart context-aware completion (cd shows
16
+ only dirs, vim shows only files), frequency-based command scoring, fuzzy matching
17
+ with fallback, configurable completion options, performance optimizations with executable
18
+ caching, environment variable completion, better error handling, and much more.
19
+ A significant improvement to daily shell usage!'
19
20
  email: g@isene.com
20
21
  executables:
21
22
  - rsh