ruby-shell 3.4.2 → 3.4.4

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 +308 -47
  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: 587406cf97c8f8aeba405779d229d073b0c8963b24c224edd1a24d72441411d5
4
+ data.tar.gz: aa4ea0471988e2004e057c575684f6d2a3f2b3875872e49ee769f22045d52bd5
5
5
  SHA512:
6
- metadata.gz: 6e84bedcc95db51816bcfd6c5ed560914898a3e661767bcd810c6dab22d8aec7fffb8904877a606e3cf9cdc1129307fd50c1db848f429165aa935d8a57f5b46d
7
- data.tar.gz: 0d939b9668cd56581466acf659cc99b75652522ee5f05d5d6fdcf7eb70930da766a818dcbeef6d81597ce24e4dc9f720a6364bc5792ccad05231ba216e9fdcb2
6
+ metadata.gz: 8fc3e11246348f5eae8bec22ac1d62d990a5ab2727573202363a9c04d6f6e8448bb265fd776ae664004249ac9133baf12a78042da5045b8fe3589e8484414221
7
+ data.tar.gz: a4305bfba19e86a643440a4d6727e71c3e5d74467fdb9ccf5070345f66a5792256276b299e04714e389d6801e5aadf0253b3da645203bb4135412f5ffa9ec9a2
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.4" # Split .rshrc/.rshstate, performance improvements, symlink colors, auto-migration
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,124 @@ 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
+ # Remove quotes and trailing slash for checking
1237
+ clean_name = filename.gsub(/['"]/, '').chomp('/')
1238
+
1239
+ # Check if it's a symlink (before directory check!)
1240
+ if File.symlink?(clean_name)
1241
+ return @ls_colors['ln'] || 14 # Symlinks get special color
1242
+ end
1243
+
1244
+ # Check if it's a directory
1245
+ if filename.end_with?('/')
1246
+ return @ls_colors['di'] || 33
1247
+ end
1248
+
1249
+ # Check if file exists and is executable
1250
+ if File.exist?(clean_name) && File.executable?(clean_name) && !File.directory?(clean_name)
1251
+ return @ls_colors['ex'] || 2
1252
+ end
1253
+
1254
+ # Check extension patterns (*.jpg, *.tar, etc.)
1255
+ ext = File.extname(clean_name)
1256
+ if ext && !ext.empty? && @ls_colors["*#{ext}"]
1257
+ return @ls_colors["*#{ext}"]
1258
+ end
1259
+
1260
+ # Default to regular file color (if 0, use 7/white as 0 means "default" in LS_COLORS)
1261
+ file_color = @ls_colors['fi'] || 7
1262
+ file_color = 7 if file_color == 0 # 0 means "reset to default" in LS_COLORS
1263
+ file_color
1264
+ end
1265
+ def format_tab_item(item, show_metadata: false) # Format tab item with color and optional metadata
1266
+ color = get_file_color(item)
1267
+ formatted = item
1268
+
1269
+ # Add executable indicator
1270
+ clean_name = item.gsub(/['"]/, '')
1271
+ if !item.end_with?('/') && File.exist?(clean_name) && File.executable?(clean_name)
1272
+ formatted += '*'
1273
+ end
1274
+
1275
+ # Add metadata if requested
1276
+ if show_metadata && @completion_show_metadata
1277
+ clean_name = clean_name.chomp('/')
1278
+ if File.exist?(clean_name)
1279
+ if File.directory?(clean_name)
1280
+ begin
1281
+ count = Dir.entries(clean_name).length - 2 # Exclude . and ..
1282
+ meta = "[dir, #{count} items]"
1283
+ rescue
1284
+ meta = "[dir]"
1285
+ end
1286
+ else
1287
+ size = File.size(clean_name)
1288
+ size_str = size < 1024 ? "#{size}B" :
1289
+ size < 1024*1024 ? "#{(size/1024.0).round(1)}K" :
1290
+ "#{(size/(1024.0*1024)).round(1)}M"
1291
+ meta = "[#{size_str}]"
1292
+ end
1293
+ formatted = formatted.ljust(30) + " #{meta}".c(244)
1294
+ end
1295
+ end
1296
+
1297
+ formatted.c(color)
1298
+ end
1078
1299
  def cmd_check(str) # Check if each element on the readline matches commands, nicks, paths; color them
1079
1300
  return if str.nil?
1080
1301
 
@@ -1122,21 +1343,19 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic
1122
1343
  end
1123
1344
  end
1124
1345
  end
1125
- def rshrc # Write updates to .rshrc
1126
- hist_clean
1346
+ def rshrc # Write user configuration to .rshrc (portable between machines)
1347
+ hist_clean # Clean history before saving
1127
1348
  if File.exist?(Dir.home+'/.rshrc')
1128
1349
  conf = File.read(Dir.home+'/.rshrc')
1129
1350
  else
1130
1351
  conf = ""
1131
1352
  end
1353
+
1354
+ # Only update user-editable items in .rshrc
1132
1355
  conf.sub!(/^@nick.*(\n|$)/, "")
1133
1356
  conf += "@nick = #{@nick}\n"
1134
1357
  conf.sub!(/^@gnick.*(\n|$)/, "")
1135
1358
  conf += "@gnick = #{@gnick}\n"
1136
- conf.sub!(/^@cmd_frequency.*(\n|$)/, "")
1137
- conf += "@cmd_frequency = #{@cmd_frequency}\n"
1138
- conf.sub!(/^@cmd_stats.*(\n|$)/, "")
1139
- conf += "@cmd_stats = #{@cmd_stats}\n" unless @cmd_stats.empty?
1140
1359
  conf.sub!(/^@bookmarks.*(\n|$)/, "")
1141
1360
  conf += "@bookmarks = #{@bookmarks}\n" unless @bookmarks.empty?
1142
1361
  conf.sub!(/^@defuns.*(\n|$)/, "")
@@ -1149,40 +1368,71 @@ def rshrc # Write updates to .rshrc
1149
1368
  conf += "@auto_correct = #{@auto_correct}\n" if @auto_correct
1150
1369
  conf.sub!(/^@slow_command_threshold.*(\n|$)/, "")
1151
1370
  conf += "@slow_command_threshold = #{@slow_command_threshold}\n" if @slow_command_threshold && @slow_command_threshold > 0
1371
+ conf.sub!(/^@completion_learning.*(\n|$)/, "")
1372
+ conf += "@completion_learning = #{@completion_learning}\n" unless @completion_learning
1373
+ conf.sub!(/^@completion_show_metadata.*(\n|$)/, "")
1374
+ conf += "@completion_show_metadata = #{@completion_show_metadata}\n" if @completion_show_metadata
1152
1375
  conf.sub!(/^@plugin_disabled.*(\n|$)/, "")
1153
1376
  conf += "@plugin_disabled = #{@plugin_disabled}\n" unless @plugin_disabled.empty?
1154
1377
  conf.sub!(/^@validation_rules.*(\n|$)/, "")
1155
1378
  conf += "@validation_rules = #{@validation_rules}\n" unless @validation_rules.empty?
1156
- conf.sub!(/^@completion_weights.*(\n|$)/, "")
1157
- conf += "@completion_weights = #{@completion_weights}\n" unless @completion_weights.empty?
1158
- conf.sub!(/^@completion_learning.*(\n|$)/, "")
1159
- conf += "@completion_learning = #{@completion_learning}\n" unless @completion_learning
1160
- conf.sub!(/^@recordings.*(\n|$)/, "")
1161
- conf += "@recordings = #{@recordings}\n" unless @recordings.empty?
1379
+
1380
+ File.write(Dir.home+'/.rshrc', conf)
1381
+ rshstate # Also save runtime state
1382
+ end
1383
+ def rshstate # Write runtime state to .rshstate (auto-managed, machine-specific)
1384
+ state = ""
1385
+
1386
+ # Runtime data that changes frequently
1387
+ state += "@cmd_frequency = #{@cmd_frequency}\n" unless @cmd_frequency.empty?
1388
+ state += "@cmd_stats = #{@cmd_stats}\n" unless @cmd_stats.empty?
1389
+ state += "@completion_weights = #{@completion_weights}\n" unless @completion_weights.empty?
1390
+ state += "@recordings = #{@recordings}\n" unless @recordings.empty?
1391
+
1162
1392
  # Persist executable cache for faster startup
1163
1393
  if @exe && @exe.length > 100
1164
- conf.sub!(/^@exe_cache.*(\n|$)/, "")
1165
- conf += "@exe_cache = #{@exe.inspect}\n"
1166
- conf.sub!(/^@exe_cache_path.*(\n|$)/, "")
1167
- conf += "@exe_cache_path = #{ENV['PATH'].inspect}\n"
1168
- conf.sub!(/^@exe_cache_time.*(\n|$)/, "")
1169
- conf += "@exe_cache_time = #{Time.now.to_i}\n"
1170
- end
1171
- # Only write @cmd_completions if user has customized it
1172
- unless conf =~ /^@cmd_completions\s*=/
1173
- # Don't write default completions to avoid cluttering .rshrc
1174
- end
1175
- conf.sub!(/^@history.*(\n|$)/, "")
1394
+ state += "@exe_cache = #{@exe.inspect}\n"
1395
+ state += "@exe_cache_path = #{ENV['PATH'].inspect}\n"
1396
+ state += "@exe_cache_time = #{Time.now.to_i}\n"
1397
+ end
1398
+
1176
1399
  # Ensure history is properly formatted as valid Ruby array
1177
1400
  begin
1178
1401
  history_str = @history.last(@histsize).inspect
1179
- conf += "@history = #{history_str}\n"
1402
+ state += "@history = #{history_str}\n"
1180
1403
  rescue => e
1181
- conf += "@history = []\n"
1182
- puts "Warning: Error saving history: #{e.message}"
1404
+ state += "@history = []\n"
1405
+ puts "Warning: Error saving history: #{e.message}" if ENV['RSH_DEBUG']
1183
1406
  end
1407
+
1408
+ File.write(Dir.home+'/.rshstate', state)
1409
+ end
1410
+ def migrate_to_split_config # Migrate from old single .rshrc to split .rshrc + .rshstate
1411
+ return if File.exist?(Dir.home+'/.rshstate') # Already migrated
1412
+ return unless File.exist?(Dir.home+'/.rshrc') # Nothing to migrate
1413
+
1414
+ puts "\nMigrating to split configuration (.rshrc + .rshstate)..."
1415
+
1416
+ # Runtime data will already be loaded from .rshrc by load_rshrc_safe
1417
+ # Just need to save it to .rshstate and clean .rshrc
1418
+
1419
+ # Create .rshstate with current runtime data
1420
+ rshstate
1421
+
1422
+ # Clean runtime data from .rshrc
1423
+ conf = File.read(Dir.home+'/.rshrc')
1424
+ conf.sub!(/^@cmd_frequency.*(\n|$)/, "")
1425
+ conf.sub!(/^@cmd_stats.*(\n|$)/, "")
1426
+ conf.sub!(/^@exe_cache.*(\n|$)/, "")
1427
+ conf.sub!(/^@exe_cache_path.*(\n|$)/, "")
1428
+ conf.sub!(/^@exe_cache_time.*(\n|$)/, "")
1429
+ conf.sub!(/^@completion_weights.*(\n|$)/, "")
1430
+ conf.sub!(/^@history =.*(\n|$)/, "")
1184
1431
  File.write(Dir.home+'/.rshrc', conf)
1185
- puts ".rshrc updated"
1432
+
1433
+ puts "Migration complete!"
1434
+ puts " .rshrc -> User config (portable)"
1435
+ puts " .rshstate -> Runtime data (auto-managed)\n"
1186
1436
  end
1187
1437
 
1188
1438
  # RSH FUNCTIONS
@@ -2757,6 +3007,15 @@ def load_rshrc_safe
2757
3007
  end
2758
3008
  end
2759
3009
 
3010
+ # Load runtime state from .rshstate (separate file)
3011
+ if File.exist?(Dir.home+'/.rshstate')
3012
+ begin
3013
+ load(Dir.home+'/.rshstate')
3014
+ rescue => e
3015
+ puts "Warning: Could not load .rshstate: #{e.message}" if ENV['RSH_DEBUG']
3016
+ end
3017
+ end
3018
+
2760
3019
  rescue SyntaxError => e
2761
3020
  puts "\n\033[31mERROR: Syntax error in .rshrc:\033[0m"
2762
3021
  puts e.message
@@ -2953,6 +3212,7 @@ begin # Load .rshrc and populate @history
2953
3212
  end
2954
3213
  firstrun unless File.exist?(Dir.home+'/.rshrc') # Initial loading - to get history
2955
3214
  load_rshrc_safe
3215
+ migrate_to_split_config # Migrate from old format if needed (v3.4.3+)
2956
3216
  # Load login shell files if rsh is running as login shell
2957
3217
  if ENV['LOGIN_SHELL'] or $0 == "-rsh" or ARGV.include?('-l') or ARGV.include?('--login')
2958
3218
  ['/etc/profile', Dir.home+'/.profile', Dir.home+'/.bash_profile', Dir.home+'/.bashrc'].each do |f|
@@ -2976,11 +3236,12 @@ begin # Load .rshrc and populate @history
2976
3236
  ENV["PATH"] ? ENV["PATH"] += ":" : ENV["PATH"] = ""
2977
3237
  ENV["PATH"] += "/home/#{@user}/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
2978
3238
  if @lscolors and File.exist?(@lscolors)
2979
- ls = File.read(@lscolors)
3239
+ ls = File.read(@lscolors)
2980
3240
  ls.sub!(/export.*/, '')
2981
3241
  ls.sub!(/^LS_COLORS=/, 'ENV["LS_COLORS"]=')
2982
3242
  eval(ls)
2983
3243
  end
3244
+ parse_ls_colors # Parse LS_COLORS for tab completion coloring
2984
3245
  @c = Cursor # Initiate @c as Cursor
2985
3246
  @c.save # Get max row & col
2986
3247
  @c.row(8000)
@@ -2995,8 +3256,8 @@ end
2995
3256
  # MAIN PART
2996
3257
  loop do
2997
3258
  begin
2998
- @user = Etc.getpwuid(Process.euid).name # For use in @prompt
2999
- @node = Etc.uname[:nodename] # For use in @prompt
3259
+ @user ||= Etc.getpwuid(Process.euid).name # Cached for performance
3260
+ @node ||= Etc.uname[:nodename] # Cached for performance
3000
3261
  # Only reload .rshrc if directory changed (optimization)
3001
3262
  current_dir = Dir.pwd
3002
3263
  if @last_prompt_dir != current_dir
@@ -3004,7 +3265,7 @@ loop do
3004
3265
  @last_prompt_dir = current_dir
3005
3266
  end
3006
3267
  @prompt.gsub!(/#{Dir.home}/, '~') # Simplify path in prompt
3007
- system("printf \"\033]0;rsh: #{Dir.pwd}\007\"") # Set Window title to path
3268
+ print "\033]0;rsh: #{current_dir}\007" # Set window title (no spawn)
3008
3269
  @history[0] = "" unless @history[0]
3009
3270
  cache_executables # Use cached executable lookup
3010
3271
  # Load plugins on first command (lazy loading)
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.4
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