ruby-shell 3.4.2 → 3.4.3

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 (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -0
  3. data/bin/rsh +233 -16
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77bd4af1f0ec04f25fb48fc4bde5b1b18a50b02e046698c9cb660dafa6caa59c
4
- data.tar.gz: 807220a097167fb1338c52ce8868d3fcf0a022d8cec7963cf23654daafd72e8d
3
+ metadata.gz: aa582c1417712c47e836881c478b375ec93f4def3b7f75b4ad9c486c3bfc36d3
4
+ data.tar.gz: e6c40f3e4eb1277bdd72ec20413a3bee631abb102a818e71d7e18716b665ebf0
5
5
  SHA512:
6
- metadata.gz: 6e84bedcc95db51816bcfd6c5ed560914898a3e661767bcd810c6dab22d8aec7fffb8904877a606e3cf9cdc1129307fd50c1db848f429165aa935d8a57f5b46d
7
- data.tar.gz: 0d939b9668cd56581466acf659cc99b75652522ee5f05d5d6fdcf7eb70930da766a818dcbeef6d81597ce24e4dc9f720a6364bc5792ccad05231ba216e9fdcb2
6
+ metadata.gz: 2c9adb770a67ec3c7ac67c42bf7a963b2cedd3c7a4aec8f1d0460210f3fb965286ec528df049426598ca1890df2e00cd174ec96d078e93796acde6ff7ff857e1
7
+ data.tar.gz: ec2dc6f60a5daf6394d3fc57ad0a977b7e1c9e2e0b4e231c8967cc20052d007b68f2c3e85f226885d26da4f706ff86b6976827b1455bf35fcbbfa1b59fed0ac6
data/README.md CHANGED
@@ -359,6 +359,36 @@ Create safety rules to block, confirm, warn, or log specific command patterns:
359
359
 
360
360
  ---
361
361
 
362
+ ## Environment Variables
363
+
364
+ **Note:** rsh uses `:env` commands for environment management, not the standard `export` syntax.
365
+
366
+ ```bash
367
+ # List all environment variables (shows first 20)
368
+ :env
369
+
370
+ # View specific variable
371
+ :env PATH
372
+
373
+ # Set environment variable
374
+ :env set PATH /opt/local/bin:/usr/bin:/bin
375
+
376
+ # Unset environment variable
377
+ :env unset MY_VAR
378
+
379
+ # Export all variables to shell script
380
+ :env export my_env.sh
381
+ ```
382
+
383
+ **Why not `export`?**
384
+ - rsh uses colon commands (`:cmd`) for shell operations
385
+ - Standard `export VAR=value` syntax spawns a subprocess that doesn't affect parent shell
386
+ - Use `:env set VAR value` instead for persistent environment changes
387
+
388
+ **Tip:** Add `:env set` commands to your `~/.rshrc` for variables you need on every startup.
389
+
390
+ ---
391
+
362
392
  ## Plugin System (v3.2.0+)
363
393
 
364
394
  rsh supports a powerful plugin system for extending functionality. Plugins are Ruby classes placed in `~/.rsh/plugins/` that can:
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 = "3.4.2" # Improved defun syntax: :defun lists, :defun -name removes, enhanced :help
11
+ @version = "3.4.3" # Enhanced tab completion: LS_COLORS, visual indicators, smart context, auto-complete ..
12
12
 
13
13
  # MODULES, CLASSES AND EXTENSIONS
14
14
  class String # Add coloring to strings (with escaping for Readline)
@@ -97,6 +97,12 @@ begin # Initialization
97
97
  @c_tabselect = 5 # Color for selected tabcompleted item
98
98
  @c_taboption = 244 # Color for unselected tabcompleted item
99
99
  @c_stamp = 244 # Color for time stamp/command
100
+ # File type colors for tab completion
101
+ @c_dir = 33 # Color for directories (blue)
102
+ @c_exec = 2 # Color for executables (green)
103
+ @c_image = 13 # Color for images (magenta)
104
+ @c_archive = 11 # Color for archives (yellow)
105
+ @c_file = 7 # Color for regular files (white/default)
100
106
  # Prompt
101
107
  @prompt = "rsh > ".c(@c_prompt).b # Very basic prompt if not defined in .rshrc
102
108
  # Hash & array initializations
@@ -153,6 +159,7 @@ begin # Initialization
153
159
  @validation_rules = [] # Custom validation rules
154
160
  @completion_weights = {} # Completion learning weights
155
161
  @completion_learning = true # Enable completion learning (default: on)
162
+ @completion_show_metadata = false # Show file metadata in completions (default: off)
156
163
  @recording = {active: false, name: nil, commands: []} # Command recording state
157
164
  @recordings = {} # Saved recordings
158
165
  @command_cache = {} # Cache for expensive shell command outputs
@@ -599,11 +606,27 @@ def tab(type)
599
606
  if @cmd_completions.key?(last_cmd) && cmd_parts.length == 1
600
607
  type = "cmd_subcommands"
601
608
  @current_cmd = last_cmd
609
+ # If we're completing after a command (not at start of line), show only files
610
+ elsif last_cmd
611
+ type = "files_dirs_only"
602
612
  end
603
613
  end
604
614
  end
605
615
  end
606
616
 
617
+ # Auto-complete . and .. for directory navigation commands
618
+ if type == "dirs_only" && (@tabstr == ".." || @tabstr == ".")
619
+ completed = @tabstr + "/"
620
+ @history[0] = @pretab + completed + @postab
621
+ @pos = @pretab.length + completed.length
622
+ @c_col = @pos0 + @pos
623
+ @c.clear_line
624
+ line_display = cmd_check(@history[0]).to_s
625
+ print @prompt + line_display
626
+ @c.col(@c_col)
627
+ return
628
+ end
629
+
607
630
  while chr != "ENTER"
608
631
  case type
609
632
  when "hist" # Handle history completions ('UP' key)
@@ -663,6 +686,24 @@ def tab(type)
663
686
  fdir = @tabstr + "*"
664
687
  files = Dir.glob(fdir).reject { |f| Dir.exist?(f) }
665
688
  @tabarray = files
689
+ when "files_dirs_only" # Show files and directories, but not commands
690
+ fdir = @tabstr + "*"
691
+ files = Dir.glob(fdir)
692
+ # Only show hidden files if tabstr starts with .
693
+ unless @tabstr.start_with?('.')
694
+ files.reject! { |f| File.basename(f).start_with?('.') }
695
+ end
696
+ files.map! do |e|
697
+ if e =~ /(?<!\\) /
698
+ e = e.sub(/(.*\/|^)(.*)/, '\1\'\2\'') unless e =~ /'/
699
+ end
700
+ Dir.exist?(e) ? e + "/" : e
701
+ end
702
+ # Separate directories and files for better ordering
703
+ dirs = files.select { |f| f.end_with?('/') }
704
+ files_only = files.reject { |f| f.end_with?('/') }
705
+ # Order: directories first, then files
706
+ @tabarray = dirs + files_only
666
707
  when "commands_only" # Only show executable commands
667
708
  ex = @exe.dup
668
709
  ex.prepend(*@nick.keys, *@gnick.keys)
@@ -788,33 +829,91 @@ def tab(type)
788
829
  @newhist0 = @pretab + tabchoice + @postab # Remember now the new value to be given to @history[0]
789
830
  line1 = cmd_check(@pretab).to_s # Syntax highlight before @tabstr
790
831
  line2 = cmd_check(@postab).to_s # Syntax highlight after @tabstr
791
- # Color and underline the current tabchoice on the commandline:
792
- tabline = tabchoice.sub(/(.*)#{@tabstr}(.*)/, '\1'.c(@c_tabselect) + @tabstr.u.c(@c_tabselect) + '\2'.c(@c_tabselect))
832
+ # Color and underline the current tabchoice on the commandline with file type color:
833
+ display_choice = tabchoice.dup
834
+ clean_choice = tabchoice.gsub(/['"]/, '').chomp('/')
835
+ if !tabchoice.end_with?('/') && File.exist?(clean_choice) && File.executable?(clean_choice) && !File.directory?(clean_choice)
836
+ display_choice += '*'
837
+ end
838
+ choice_color = get_file_color(tabchoice)
839
+ # Escape regex special characters in @tabstr for pattern matching
840
+ escaped_tabstr = Regexp.escape(@tabstr)
841
+ tabline = display_choice.sub(/(.*)#{escaped_tabstr}(.*)/, '\1'.c(choice_color) + @tabstr.u.c(choice_color) + '\2'.c(choice_color))
793
842
  print @prompt + line1 + tabline + line2 # Print the commandline
794
843
  @pos = @pretab.length.to_i + tabchoice.length.to_i # Set the position on that commandline
795
844
  @c_col = @pos0 + @pos # The cursor position must include the prompt as well
796
845
  @c.col(@c_col) # Set the cursor position
797
846
  nextline # Then start showing the completion items
798
847
  tabline = @tabarray[i] # Get the next matching tabline
848
+ # Add executable indicator
849
+ display_item = tabline.dup
850
+ clean_item = tabline.gsub(/['"]/, '').chomp('/')
851
+ if !tabline.end_with?('/') && File.exist?(clean_item) && File.executable?(clean_item) && !File.directory?(clean_item)
852
+ display_item += '*'
853
+ end
854
+ # Get file type color, but make selected item bold for distinction
855
+ file_color = get_file_color(tabline)
799
856
  # Can't nest ANSI codes, they must each complete/conclude or they will mess eachother up
800
- if tabline.include?(@tabstr)
801
- tabline1 = tabline.sub(/(.*?)#{@tabstr}.*/, '\1').c(@c_tabselect) # Color the part before the @tabstr
802
- tabline2 = tabline.sub(/.*?#{@tabstr}(.*)/, '\1').c(@c_tabselect) # Color the part after the @tabstr
803
- print " " + tabline1 + @tabstr.c(@c_tabselect).u + tabline2 # Color & underline @tabstr
857
+ escaped_tabstr = Regexp.escape(@tabstr)
858
+ if display_item.include?(@tabstr)
859
+ tabline1 = display_item.sub(/(.*?)#{escaped_tabstr}.*/, '\1').c(file_color).b # Bold + color for selected
860
+ tabline2 = display_item.sub(/.*?#{escaped_tabstr}(.*)/, '\1').c(file_color).b
861
+ print " " + tabline1 + @tabstr.c(file_color).u.b + tabline2 # Bold, color & underline @tabstr
804
862
  else
805
863
  # For fuzzy matches, just show the whole word highlighted
806
- print " " + tabline.c(@c_tabselect)
864
+ print " " + display_item.c(file_color).b
865
+ end
866
+ # Add metadata if enabled
867
+ if @completion_show_metadata && File.exist?(clean_item)
868
+ if File.directory?(clean_item)
869
+ begin
870
+ count = Dir.entries(clean_item).length - 2
871
+ print " [#{count}]".c(244)
872
+ rescue
873
+ end
874
+ else
875
+ size = File.size(clean_item)
876
+ size_str = size < 1024 ? "#{size}B" :
877
+ size < 1024*1024 ? "#{(size/1024.0).round(1)}K" :
878
+ "#{(size/(1024.0*1024)).round(1)}M"
879
+ print " [#{size_str}]".c(244)
880
+ end
807
881
  end
808
882
  else
809
883
  begin
810
884
  tabline = @tabarray[i+x] # Next tabline, and next, etc (usually 4 times here)
811
- if tabline.include?(@tabstr)
812
- tabline1 = tabline.sub(/(.*?)#{@tabstr}.*/, '\1').c(@c_taboption) # Color before @tabstr
813
- tabline2 = tabline.sub(/.*?#{@tabstr}(.*)/, '\1').c(@c_taboption) # Color after @tabstr
814
- print " " + tabline1 + @tabstr.c(@c_taboption).u + tabline2 # Print the whole line
885
+ # Add executable indicator
886
+ display_item = tabline.dup
887
+ clean_item = tabline.gsub(/['"]/, '').chomp('/')
888
+ if !tabline.end_with?('/') && File.exist?(clean_item) && File.executable?(clean_item) && !File.directory?(clean_item)
889
+ display_item += '*'
890
+ end
891
+ # Get file type color for unselected items (no bold)
892
+ file_color = get_file_color(tabline)
893
+ escaped_tabstr = Regexp.escape(@tabstr)
894
+ if display_item.include?(@tabstr)
895
+ tabline1 = display_item.sub(/(.*?)#{escaped_tabstr}.*/, '\1').c(file_color) # Color before @tabstr
896
+ tabline2 = display_item.sub(/.*?#{escaped_tabstr}(.*)/, '\1').c(file_color) # Color after @tabstr
897
+ print " " + tabline1 + @tabstr.c(file_color).u + tabline2 # Print the whole line
815
898
  else
816
899
  # For fuzzy matches, just show the whole word
817
- print " " + tabline.c(@c_taboption)
900
+ print " " + display_item.c(file_color)
901
+ end
902
+ # Add metadata if enabled
903
+ if @completion_show_metadata && File.exist?(clean_item)
904
+ if File.directory?(clean_item)
905
+ begin
906
+ count = Dir.entries(clean_item).length - 2
907
+ print " [#{count}]".c(244)
908
+ rescue
909
+ end
910
+ else
911
+ size = File.size(clean_item)
912
+ size_str = size < 1024 ? "#{size}B" :
913
+ size < 1024*1024 ? "#{(size/1024.0).round(1)}K" :
914
+ "#{(size/(1024.0*1024)).round(1)}M"
915
+ print " [#{size_str}]".c(244)
916
+ end
818
917
  end
819
918
  rescue => e
820
919
  # Log completion errors if debugging enabled
@@ -839,7 +938,7 @@ def tab(type)
839
938
  if @tabstr == ""
840
939
  @history[0] = @pretab + @postab
841
940
  tabend
842
- return
941
+ return
843
942
  end
844
943
  @tabstr.chop!
845
944
  when 'WBACK' # Delete one word to the left (Ctrl-W)
@@ -1032,9 +1131,13 @@ def config(*args) # Configure rsh settings
1032
1131
  @completion_limit = value.to_i
1033
1132
  puts "Completion limit set to #{value}"
1034
1133
  rshrc
1134
+ when 'completion_show_metadata'
1135
+ @completion_show_metadata = %w[on true yes 1].include?(value.to_s.downcase)
1136
+ puts "Completion metadata display #{@completion_show_metadata ? 'enabled' : 'disabled'}"
1137
+ rshrc
1035
1138
  else
1036
1139
  puts "Unknown setting '#{setting}'"
1037
- puts "Available: history_dedup, session_autosave, auto_correct, slow_command_threshold, completion_learning, completion_limit"
1140
+ puts "Available: history_dedup, session_autosave, auto_correct, slow_command_threshold, completion_learning, completion_limit, completion_show_metadata"
1038
1141
  end
1039
1142
  end
1040
1143
  def env(*args) # Environment variable management
@@ -1075,6 +1178,119 @@ def env(*args) # Environment variable management
1075
1178
  end
1076
1179
  end
1077
1180
  end
1181
+ def parse_ls_colors # Parse LS_COLORS into a hash for file type coloring
1182
+ @ls_colors = {}
1183
+
1184
+ # Map ANSI basic color codes (30-37, 90-97) to 256-color equivalents
1185
+ ansi_to_256 = {
1186
+ 30 => 0, # black
1187
+ 31 => 196, # red -> bright red
1188
+ 32 => 2, # green
1189
+ 33 => 11, # yellow -> bright yellow
1190
+ 34 => 33, # blue -> nice blue
1191
+ 35 => 13, # magenta -> nice magenta
1192
+ 36 => 14, # cyan -> bright cyan
1193
+ 37 => 7, # white
1194
+ 90 => 8, # bright black (gray)
1195
+ 91 => 9, # bright red
1196
+ 92 => 10, # bright green
1197
+ 93 => 11, # bright yellow
1198
+ 94 => 12, # bright blue
1199
+ 95 => 13, # bright magenta
1200
+ 96 => 14, # bright cyan
1201
+ 97 => 15 # bright white
1202
+ }
1203
+
1204
+ if ENV['LS_COLORS']
1205
+ ENV['LS_COLORS'].split(':').each do |entry|
1206
+ next if entry.empty?
1207
+ key, value = entry.split('=')
1208
+ next unless key && value
1209
+
1210
+ # LS_COLORS can use multiple formats:
1211
+ # - "38;5;111" or "38;5;111;1" = 256-color format (use color 111 directly)
1212
+ # - "01;34" = ANSI format (bold + basic color 34, needs conversion)
1213
+ # - "34" = simple ANSI basic color (needs conversion)
1214
+
1215
+ if value =~ /38;5;(\d+)/ # 256-color format (check this first!)
1216
+ @ls_colors[key] = $1.to_i
1217
+ elsif value =~ /(\d+);(\d+)/ # ANSI with attributes (e.g., "01;34")
1218
+ ansi_code = $2.to_i
1219
+ @ls_colors[key] = ansi_to_256[ansi_code] || ansi_code
1220
+ elsif value =~ /^(\d+)$/ # Simple ANSI color
1221
+ ansi_code = $1.to_i
1222
+ @ls_colors[key] = ansi_to_256[ansi_code] || ansi_code
1223
+ end
1224
+ end
1225
+ end
1226
+
1227
+ # Always set defaults (even if LS_COLORS isn't available)
1228
+ @ls_colors['di'] ||= 33 # directories = blue
1229
+ @ls_colors['ex'] ||= 2 # executables = green
1230
+ @ls_colors['ln'] ||= 14 # symlinks = cyan
1231
+ @ls_colors['fi'] ||= 7 # regular files = white
1232
+ end
1233
+ def get_file_color(filename) # Get color for a file based on LS_COLORS
1234
+ return 7 unless @ls_colors # Default to white if not initialized
1235
+
1236
+ # Check if it's a directory
1237
+ if filename.end_with?('/')
1238
+ return @ls_colors['di'] || 33
1239
+ end
1240
+
1241
+ # Remove quotes and trailing slash for checking
1242
+ clean_name = filename.gsub(/['"]/, '').chomp('/')
1243
+
1244
+ # Check if file exists and is executable
1245
+ if File.exist?(clean_name) && File.executable?(clean_name) && !File.directory?(clean_name)
1246
+ return @ls_colors['ex'] || 2
1247
+ end
1248
+
1249
+ # Check extension patterns (*.jpg, *.tar, etc.)
1250
+ ext = File.extname(clean_name)
1251
+ if ext && !ext.empty? && @ls_colors["*#{ext}"]
1252
+ return @ls_colors["*#{ext}"]
1253
+ end
1254
+
1255
+ # Default to regular file color (if 0, use 7/white as 0 means "default" in LS_COLORS)
1256
+ file_color = @ls_colors['fi'] || 7
1257
+ file_color = 7 if file_color == 0 # 0 means "reset to default" in LS_COLORS
1258
+ file_color
1259
+ end
1260
+ def format_tab_item(item, show_metadata: false) # Format tab item with color and optional metadata
1261
+ color = get_file_color(item)
1262
+ formatted = item
1263
+
1264
+ # Add executable indicator
1265
+ clean_name = item.gsub(/['"]/, '')
1266
+ if !item.end_with?('/') && File.exist?(clean_name) && File.executable?(clean_name)
1267
+ formatted += '*'
1268
+ end
1269
+
1270
+ # Add metadata if requested
1271
+ if show_metadata && @completion_show_metadata
1272
+ clean_name = clean_name.chomp('/')
1273
+ if File.exist?(clean_name)
1274
+ if File.directory?(clean_name)
1275
+ begin
1276
+ count = Dir.entries(clean_name).length - 2 # Exclude . and ..
1277
+ meta = "[dir, #{count} items]"
1278
+ rescue
1279
+ meta = "[dir]"
1280
+ end
1281
+ else
1282
+ size = File.size(clean_name)
1283
+ size_str = size < 1024 ? "#{size}B" :
1284
+ size < 1024*1024 ? "#{(size/1024.0).round(1)}K" :
1285
+ "#{(size/(1024.0*1024)).round(1)}M"
1286
+ meta = "[#{size_str}]"
1287
+ end
1288
+ formatted = formatted.ljust(30) + " #{meta}".c(244)
1289
+ end
1290
+ end
1291
+
1292
+ formatted.c(color)
1293
+ end
1078
1294
  def cmd_check(str) # Check if each element on the readline matches commands, nicks, paths; color them
1079
1295
  return if str.nil?
1080
1296
 
@@ -2976,11 +3192,12 @@ begin # Load .rshrc and populate @history
2976
3192
  ENV["PATH"] ? ENV["PATH"] += ":" : ENV["PATH"] = ""
2977
3193
  ENV["PATH"] += "/home/#{@user}/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
2978
3194
  if @lscolors and File.exist?(@lscolors)
2979
- ls = File.read(@lscolors)
3195
+ ls = File.read(@lscolors)
2980
3196
  ls.sub!(/export.*/, '')
2981
3197
  ls.sub!(/^LS_COLORS=/, 'ENV["LS_COLORS"]=')
2982
3198
  eval(ls)
2983
3199
  end
3200
+ parse_ls_colors # Parse LS_COLORS for tab completion coloring
2984
3201
  @c = Cursor # Initiate @c as Cursor
2985
3202
  @c.save # Get max row & col
2986
3203
  @c.row(8000)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-shell
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.2
4
+ version: 3.4.3
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-10-23 00:00:00.000000000 Z
11
+ date: 2025-10-25 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