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.
- checksums.yaml +4 -4
- data/README.md +30 -0
- data/bin/rsh +308 -47
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 587406cf97c8f8aeba405779d229d073b0c8963b24c224edd1a24d72441411d5
|
|
4
|
+
data.tar.gz: aa4ea0471988e2004e057c575684f6d2a3f2b3875872e49ee769f22045d52bd5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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 " " +
|
|
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
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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 " " +
|
|
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
|
|
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
|
-
|
|
1157
|
-
conf
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
-
|
|
1402
|
+
state += "@history = #{history_str}\n"
|
|
1180
1403
|
rescue => e
|
|
1181
|
-
|
|
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
|
-
|
|
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
|
|
2999
|
-
@node
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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
|