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.
- checksums.yaml +4 -4
- data/bin/rsh +207 -28
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 539ffa4c43d681abff9a260ef5f78e8762906cd5ca566ea0a9e85e39bb74318a
|
4
|
+
data.tar.gz: 80a8ee4bbfb0717c4c2ada364ff65f76364f01dfce3daba83a88f8e41e21eb85
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
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
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
489
|
-
|
490
|
-
|
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
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
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
|
-
|
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.
|
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-
|
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.
|
16
|
-
|
17
|
-
with
|
18
|
-
|
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
|