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
         |