ruby-shell 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +9 -1
- data/bin/rsh +393 -133
- metadata +5 -5
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 5b86a585bea4b0c41a27de5679365d3ddce7b7a33481927cbf0de9bd89dcf27b
         | 
| 4 | 
            +
              data.tar.gz: f346b777aa2863b06ef610a8e9c13325711058452f8986e5d4565aecb8ae21d4
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 181420e5e35ca971f44affd54dbf9cadf1b13d658627d5cf2aa062b1d17678ed8521911b1208a5f3072eaa06a6e8840db1317e5a15d1e1d2de672fbaf66552cb
         | 
| 7 | 
            +
              data.tar.gz: '084bb4b4227110ea0a2230e8455527ed8259fdcf8285ae6fd3dac571315f662c6e0243ee66caad5dc0bacb8157f82c47cf56a1213900ed3fe4f14a15fb4bcefa'
         | 
    
        data/README.md
    CHANGED
    
    | @@ -33,7 +33,15 @@ Or simply `gem install ruby-shell`. | |
| 33 33 | 
             
            * All colors are themeable in .rshrc (see github link for possibilities)
         | 
| 34 34 | 
             
            * Copy current command line to primary selection (paste w/middle button) with `Ctrl-y`
         | 
| 35 35 |  | 
| 36 | 
            -
            ## NEW in v3. | 
| 36 | 
            +
            ## NEW in v3.4.0 - Intelligent Completion Learning ⭐⭐
         | 
| 37 | 
            +
            * **Smart TAB Completion**: Shell learns which completions you use most and ranks them higher
         | 
| 38 | 
            +
            * **Context-Aware Learning**: Separate learning for git, ls, docker, and all other commands
         | 
| 39 | 
            +
            * **Completion Statistics**: `:completion_stats` shows learned patterns with visual charts
         | 
| 40 | 
            +
            * **Manageable**: `:config completion_learning on|off`, `:completion_reset` to clear data
         | 
| 41 | 
            +
            * **Persistent**: Learning data saves to .rshrc, works across sessions
         | 
| 42 | 
            +
            * **Works Everywhere**: Commands, switches, subcommands - all get smarter over time
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            ## v3.3.0 - Quote-less Syntax, Parametrized Nicks & More ⭐⭐⭐
         | 
| 37 45 | 
             
            * **No More Quotes**: Simplified syntax - `:nick la = ls -la` instead of `:nick "la = ls -la"`
         | 
| 38 46 | 
             
            * **Parametrized Nicks**: `:nick gp = git push origin {{branch}}` then use `gp branch=main`
         | 
| 39 47 | 
             
            * **Ctrl-G Multi-line Edit**: Press Ctrl-G to edit command in $EDITOR for complex scripts
         | 
    
        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. | 
| 11 | 
            +
            @version    = "3.4.0" # Completion learning: Shell learns your patterns and boosts frequently-used completions
         | 
| 12 12 |  | 
| 13 13 | 
             
            # MODULES, CLASSES AND EXTENSIONS
         | 
| 14 14 | 
             
            class String # Add coloring to strings (with escaping for Readline)
         | 
| @@ -151,6 +151,10 @@ begin # Initialization | |
| 151 151 | 
             
              @plugin_disabled = []                   # List of disabled plugin names
         | 
| 152 152 | 
             
              @plugin_commands = {}                   # Commands added by plugins
         | 
| 153 153 | 
             
              @validation_rules = []                  # Custom validation rules
         | 
| 154 | 
            +
              @completion_weights = {}                # Completion learning weights
         | 
| 155 | 
            +
              @completion_learning = true             # Enable completion learning (default: on)
         | 
| 156 | 
            +
              @recording = {active: false, name: nil, commands: []} # Command recording state
         | 
| 157 | 
            +
              @recordings = {}                        # Saved recordings
         | 
| 154 158 | 
             
              # Built-in rsh commands are called with : prefix, so no need for separate tracking
         | 
| 155 159 | 
             
              Dir.mkdir(Dir.home + '/.rsh') unless Dir.exist?(Dir.home + '/.rsh')
         | 
| 156 160 | 
             
              Dir.mkdir(@session_dir) unless Dir.exist?(@session_dir)
         | 
| @@ -188,14 +192,18 @@ end | |
| 188 192 | 
             
              * Syntax validation - Pre-execution warnings for dangerous or malformed commands
         | 
| 189 193 | 
             
              * Unified command syntax - :nick, :gnick, :bm all work the same way (list/create/delete)
         | 
| 190 194 |  | 
| 191 | 
            -
              NEW in v3. | 
| 192 | 
            -
              *  | 
| 195 | 
            +
              NEW in v3.4:
         | 
| 196 | 
            +
              * Completion learning - Shell learns which TAB completions you use and ranks them higher
         | 
| 197 | 
            +
              * Context-aware - Separate learning for each command (git, ls, docker, etc.)
         | 
| 198 | 
            +
              * :completion_stats - View learned patterns with visual bar charts
         | 
| 199 | 
            +
              * Persistent - Learning data saves to .rshrc across sessions
         | 
| 200 | 
            +
             | 
| 201 | 
            +
              v3.3 Features:
         | 
| 202 | 
            +
              * Quote-less syntax - No more quotes! Use :nick la = ls -la
         | 
| 193 203 | 
             
              * Parametrized nicks - :nick gp = git push origin {{branch}}, then: gp branch=main
         | 
| 194 204 | 
             
              * Ctrl-G multi-line edit - Press Ctrl-G to edit command in $EDITOR
         | 
| 195 205 | 
             
              * Custom validation - :validate rm -rf / = block prevents dangerous commands
         | 
| 196 206 | 
             
              * Shell script support - for/while/if loops work with full bash syntax
         | 
| 197 | 
            -
              * Simplified - Removed :template, everything now in :nick
         | 
| 198 | 
            -
              * Backward compatible - Old quote syntax still works
         | 
| 199 207 |  | 
| 200 208 | 
             
              v3.2 Features:
         | 
| 201 209 | 
             
              * Plugin system - Extensible architecture for custom commands, completions, and hooks
         | 
| @@ -507,7 +515,7 @@ def getstr # A custom Readline-like function | |
| 507 515 | 
             
                  lift = true
         | 
| 508 516 | 
             
                when 'S-TAB'
         | 
| 509 517 | 
             
                  @ci = nil
         | 
| 510 | 
            -
                   | 
| 518 | 
            +
                  tab("hist")
         | 
| 511 519 | 
             
                  lift = true
         | 
| 512 520 | 
             
                when /^.$/
         | 
| 513 521 | 
             
                  @history[0].insert(@pos,chr)
         | 
| @@ -567,20 +575,29 @@ def tab(type) | |
| 567 575 | 
             
                  last_cmd = nil
         | 
| 568 576 | 
             
                end
         | 
| 569 577 |  | 
| 570 | 
            -
                 | 
| 571 | 
            -
                 | 
| 572 | 
            -
                  type = " | 
| 573 | 
            -
                 | 
| 574 | 
            -
                  type = " | 
| 575 | 
            -
                 | 
| 576 | 
            -
                  type = " | 
| 577 | 
            -
                when "export", "unset"
         | 
| 578 | 
            -
                  type = "env_vars"
         | 
| 578 | 
            +
                # Check for colon command arguments
         | 
| 579 | 
            +
                if @pretab =~ /:record\s*$/
         | 
| 580 | 
            +
                  type = "record_args"
         | 
| 581 | 
            +
                elsif @pretab =~ /:replay\s*$/
         | 
| 582 | 
            +
                  type = "replay_args"
         | 
| 583 | 
            +
                elsif @pretab =~ /:plugins\s*$/
         | 
| 584 | 
            +
                  type = "plugin_args"
         | 
| 579 585 | 
             
                else
         | 
| 580 | 
            -
                   | 
| 581 | 
            -
                   | 
| 582 | 
            -
                    type = " | 
| 583 | 
            -
             | 
| 586 | 
            +
                  case last_cmd
         | 
| 587 | 
            +
                  when "cd", "pushd", "rmdir"
         | 
| 588 | 
            +
                    type = "dirs_only"
         | 
| 589 | 
            +
                  when "vim", "nano", "cat", "less", "more", "head", "tail", "file"
         | 
| 590 | 
            +
                    type = "files_only"
         | 
| 591 | 
            +
                  when "man", "info", "which", "whatis"
         | 
| 592 | 
            +
                    type = "commands_only"
         | 
| 593 | 
            +
                  when "export", "unset"
         | 
| 594 | 
            +
                    type = "env_vars"
         | 
| 595 | 
            +
                  else
         | 
| 596 | 
            +
                    # Check if command has defined completions and we're on the first argument
         | 
| 597 | 
            +
                    if @cmd_completions.key?(last_cmd) && cmd_parts.length == 1
         | 
| 598 | 
            +
                      type = "cmd_subcommands"
         | 
| 599 | 
            +
                      @current_cmd = last_cmd
         | 
| 600 | 
            +
                    end
         | 
| 584 601 | 
             
                  end
         | 
| 585 602 | 
             
                end
         | 
| 586 603 | 
             
              end
         | 
| @@ -591,6 +608,27 @@ def tab(type) | |
| 591 608 | 
             
                  @tabarray = @history.select {|el| el =~ /#{@tabstr}/} # Select history items matching @tabstr
         | 
| 592 609 | 
             
                  @tabarray.shift   # Take away @history[0]
         | 
| 593 610 | 
             
                  return if @tabarray.empty?
         | 
| 611 | 
            +
                when "record_args"
         | 
| 612 | 
            +
                  # Completions for :record command
         | 
| 613 | 
            +
                  @tabarray = %w[start stop status show]
         | 
| 614 | 
            +
                  # Add existing recording names for show
         | 
| 615 | 
            +
                  @tabarray += @recordings.keys.map { |k| "show #{k}" } if @recordings.any?
         | 
| 616 | 
            +
                  # Add delete options
         | 
| 617 | 
            +
                  @tabarray += @recordings.keys.map { |k| "-#{k}" } if @recordings.any?
         | 
| 618 | 
            +
                when "replay_args"
         | 
| 619 | 
            +
                  # Completions for :replay command - just recording names
         | 
| 620 | 
            +
                  @tabarray = @recordings.keys
         | 
| 621 | 
            +
                when "plugin_args"
         | 
| 622 | 
            +
                  # Completions for :plugins command
         | 
| 623 | 
            +
                  @tabarray = %w[reload info]
         | 
| 624 | 
            +
                  # Add plugin names for enable/disable/info
         | 
| 625 | 
            +
                  if @plugins.any?
         | 
| 626 | 
            +
                    @tabarray += @plugins.map { |p| "disable #{p[:name]}" }
         | 
| 627 | 
            +
                    @tabarray += @plugins.map { |p| "info #{p[:name]}" }
         | 
| 628 | 
            +
                  end
         | 
| 629 | 
            +
                  if @plugin_disabled.any?
         | 
| 630 | 
            +
                    @tabarray += @plugin_disabled.map { |p| "enable #{p}" }
         | 
| 631 | 
            +
                  end
         | 
| 594 632 | 
             
                when "switch"
         | 
| 595 633 | 
             
                  cmdswitch = @pretab.split(/[|, ]/).last.to_s.strip
         | 
| 596 634 | 
             
                  @tabarray = get_command_switches(cmdswitch)
         | 
| @@ -642,6 +680,8 @@ def tab(type) | |
| 642 680 | 
             
                    :history :rmhistory :jobs :fg :bg
         | 
| 643 681 | 
             
                    :save_session :load_session :list_sessions :delete_session :rmsession
         | 
| 644 682 | 
             
                    :config :env :theme :plugins :calc :validate
         | 
| 683 | 
            +
                    :completion_stats :completion_reset
         | 
| 684 | 
            +
                    :record :replay
         | 
| 645 685 | 
             
                    :info :version :help
         | 
| 646 686 | 
             
                  ]
         | 
| 647 687 | 
             
                  search_str = @tabstr[1..-1] || ""  # Remove leading :
         | 
| @@ -722,6 +762,19 @@ def tab(type) | |
| 722 762 | 
             
                end
         | 
| 723 763 | 
             
                return if @tabarray.empty?
         | 
| 724 764 | 
             
                @tabarray.delete("")                                      # Don't remember why
         | 
| 765 | 
            +
             | 
| 766 | 
            +
                # Apply completion learning to sort results
         | 
| 767 | 
            +
                if @completion_learning && type != "hist"
         | 
| 768 | 
            +
                  # Determine context for learning
         | 
| 769 | 
            +
                  completion_context = if @pretab && !@pretab.empty?
         | 
| 770 | 
            +
                    # Use the command being completed
         | 
| 771 | 
            +
                    @pretab.strip.split(/[|;&]/).last.strip.split.first || "all"
         | 
| 772 | 
            +
                  else
         | 
| 773 | 
            +
                    "all"
         | 
| 774 | 
            +
                  end
         | 
| 775 | 
            +
                  @tabarray = sort_by_learning(completion_context, @tabarray)
         | 
| 776 | 
            +
                end
         | 
| 777 | 
            +
             | 
| 725 778 | 
             
                @c.clear_screen_down                                      # Here we go
         | 
| 726 779 | 
             
                max_items = @completion_limit || 5
         | 
| 727 780 | 
             
                @tabarray.length.to_i - i < max_items ? l = @tabarray.length.to_i - i : l = max_items
         | 
| @@ -807,6 +860,18 @@ def tab(type) | |
| 807 860 | 
             
              @c.row(@c_row)
         | 
| 808 861 | 
             
              @c.col(@c_col)
         | 
| 809 862 | 
             
              @history[0] = @newhist0
         | 
| 863 | 
            +
             | 
| 864 | 
            +
              # Track completion selection for learning
         | 
| 865 | 
            +
              if @completion_learning && @tabarray && @tabarray[i] && type != "hist"
         | 
| 866 | 
            +
                completion_context = if @pretab && !@pretab.empty?
         | 
| 867 | 
            +
                  @pretab.strip.split(/[|;&]/).last.strip.split.first || "all"
         | 
| 868 | 
            +
                else
         | 
| 869 | 
            +
                  "all"
         | 
| 870 | 
            +
                end
         | 
| 871 | 
            +
                selected = @tabarray[i]
         | 
| 872 | 
            +
                selected = selected.sub(/\s*(-.*?)[,\s].*/, '\1') if type == "switch"
         | 
| 873 | 
            +
                track_completion(completion_context, selected)
         | 
| 874 | 
            +
              end
         | 
| 810 875 | 
             
            end
         | 
| 811 876 | 
             
            def nextline # Handle going to the next line in the terminal
         | 
| 812 877 | 
             
              row, col = @c.pos
         | 
| @@ -928,6 +993,7 @@ def config(*args) # Configure rsh settings | |
| 928 993 | 
             
                puts "    session_autosave:   #{@session_autosave}s #{@session_autosave > 0 ? '(enabled)' : '(disabled)'}"
         | 
| 929 994 | 
             
                puts "    auto_correct:       #{@auto_correct ? 'on' : 'off'}"
         | 
| 930 995 | 
             
                puts "    slow_command_threshold: #{@slow_command_threshold}s #{@slow_command_threshold > 0 ? '(enabled)' : '(disabled)'}"
         | 
| 996 | 
            +
                puts "    completion_learning: #{@completion_learning ? 'on' : 'off'}"
         | 
| 931 997 | 
             
                puts "    completion_limit:   #{@completion_limit}"
         | 
| 932 998 | 
             
                puts "    completion_fuzzy:   #{@completion_fuzzy}"
         | 
| 933 999 | 
             
                puts "    completion_case_sensitive: #{@completion_case_sensitive}"
         | 
| @@ -956,13 +1022,17 @@ def config(*args) # Configure rsh settings | |
| 956 1022 | 
             
                @slow_command_threshold = value.to_i
         | 
| 957 1023 | 
             
                puts "Slow command threshold set to #{value}s #{value.to_i > 0 ? '(enabled)' : '(disabled)'}"
         | 
| 958 1024 | 
             
                rshrc
         | 
| 1025 | 
            +
              when 'completion_learning'
         | 
| 1026 | 
            +
                @completion_learning = %w[on true yes 1].include?(value.to_s.downcase)
         | 
| 1027 | 
            +
                puts "Completion learning #{@completion_learning ? 'enabled' : 'disabled'}"
         | 
| 1028 | 
            +
                rshrc
         | 
| 959 1029 | 
             
              when 'completion_limit'
         | 
| 960 1030 | 
             
                @completion_limit = value.to_i
         | 
| 961 1031 | 
             
                puts "Completion limit set to #{value}"
         | 
| 962 1032 | 
             
                rshrc
         | 
| 963 1033 | 
             
              else
         | 
| 964 1034 | 
             
                puts "Unknown setting '#{setting}'"
         | 
| 965 | 
            -
                puts "Available: history_dedup, session_autosave, auto_correct, slow_command_threshold, completion_limit"
         | 
| 1035 | 
            +
                puts "Available: history_dedup, session_autosave, auto_correct, slow_command_threshold, completion_learning, completion_limit"
         | 
| 966 1036 | 
             
              end
         | 
| 967 1037 | 
             
            end
         | 
| 968 1038 | 
             
            def env(*args) # Environment variable management
         | 
| @@ -1081,6 +1151,12 @@ def rshrc # Write updates to .rshrc | |
| 1081 1151 | 
             
              conf += "@plugin_disabled = #{@plugin_disabled}\n" unless @plugin_disabled.empty?
         | 
| 1082 1152 | 
             
              conf.sub!(/^@validation_rules.*(\n|$)/, "")
         | 
| 1083 1153 | 
             
              conf += "@validation_rules = #{@validation_rules}\n" unless @validation_rules.empty?
         | 
| 1154 | 
            +
              conf.sub!(/^@completion_weights.*(\n|$)/, "")
         | 
| 1155 | 
            +
              conf += "@completion_weights = #{@completion_weights}\n" unless @completion_weights.empty?
         | 
| 1156 | 
            +
              conf.sub!(/^@completion_learning.*(\n|$)/, "")
         | 
| 1157 | 
            +
              conf += "@completion_learning = #{@completion_learning}\n" unless @completion_learning
         | 
| 1158 | 
            +
              conf.sub!(/^@recordings.*(\n|$)/, "")
         | 
| 1159 | 
            +
              conf += "@recordings = #{@recordings}\n" unless @recordings.empty?
         | 
| 1084 1160 | 
             
              # Only write @cmd_completions if user has customized it
         | 
| 1085 1161 | 
             
              unless conf =~ /^@cmd_completions\s*=/
         | 
| 1086 1162 | 
             
                # Don't write default completions to avoid cluttering .rshrc
         | 
| @@ -1101,125 +1177,123 @@ end | |
| 1101 1177 | 
             
            # RSH FUNCTIONS
         | 
| 1102 1178 | 
             
            def help
         | 
| 1103 1179 | 
             
              # Get terminal width
         | 
| 1104 | 
            -
              term_width = @maxcol ||  | 
| 1105 | 
            -
              col_width =  | 
| 1106 | 
            -
             | 
| 1180 | 
            +
              term_width = @maxcol || 120
         | 
| 1181 | 
            +
              col_width = 36  # Width for each of 3 columns (wider)
         | 
| 1182 | 
            +
             | 
| 1107 1183 | 
             
              # Helper function to strip ANSI codes for length calculation
         | 
| 1108 1184 | 
             
              def strip_ansi(str)
         | 
| 1109 1185 | 
             
                str.gsub(/\001?\e\[[0-9;]*m\002?/, '')
         | 
| 1110 1186 | 
             
              end
         | 
| 1111 | 
            -
             | 
| 1112 | 
            -
               | 
| 1113 | 
            -
               | 
| 1114 | 
            -
              
         | 
| 1115 | 
            -
             | 
| 1116 | 
            -
               | 
| 1117 | 
            -
               | 
| 1118 | 
            -
               | 
| 1119 | 
            -
               | 
| 1120 | 
            -
               | 
| 1121 | 
            -
               | 
| 1122 | 
            -
               | 
| 1123 | 
            -
               | 
| 1124 | 
            -
               | 
| 1125 | 
            -
               | 
| 1126 | 
            -
               | 
| 1127 | 
            -
               | 
| 1128 | 
            -
               | 
| 1129 | 
            -
               | 
| 1130 | 
            -
               | 
| 1131 | 
            -
               | 
| 1132 | 
            -
               | 
| 1133 | 
            -
               | 
| 1134 | 
            -
               | 
| 1135 | 
            -
               | 
| 1136 | 
            -
               | 
| 1137 | 
            -
               | 
| 1138 | 
            -
               | 
| 1139 | 
            -
               | 
| 1140 | 
            -
             | 
| 1141 | 
            -
               | 
| 1142 | 
            -
               | 
| 1143 | 
            -
               | 
| 1144 | 
            -
              
         | 
| 1145 | 
            -
               | 
| 1146 | 
            -
               | 
| 1147 | 
            -
               | 
| 1148 | 
            -
               | 
| 1149 | 
            -
               | 
| 1150 | 
            -
               | 
| 1151 | 
            -
               | 
| 1152 | 
            -
               | 
| 1153 | 
            -
               | 
| 1154 | 
            -
               | 
| 1155 | 
            -
               | 
| 1156 | 
            -
               | 
| 1157 | 
            -
               | 
| 1158 | 
            -
               | 
| 1159 | 
            -
               | 
| 1160 | 
            -
               | 
| 1161 | 
            -
               | 
| 1162 | 
            -
               | 
| 1163 | 
            -
               | 
| 1164 | 
            -
               | 
| 1165 | 
            -
               | 
| 1166 | 
            -
               | 
| 1167 | 
            -
               | 
| 1168 | 
            -
               | 
| 1169 | 
            -
               | 
| 1170 | 
            -
             | 
| 1171 | 
            -
               | 
| 1172 | 
            -
               | 
| 1173 | 
            -
               | 
| 1174 | 
            -
               | 
| 1175 | 
            -
               | 
| 1176 | 
            -
               | 
| 1177 | 
            -
               | 
| 1178 | 
            -
               | 
| 1179 | 
            -
               | 
| 1180 | 
            -
               | 
| 1181 | 
            -
               | 
| 1182 | 
            -
               | 
| 1183 | 
            -
               | 
| 1184 | 
            -
               | 
| 1185 | 
            -
               | 
| 1186 | 
            -
               | 
| 1187 | 
            -
               | 
| 1188 | 
            -
               | 
| 1189 | 
            -
               | 
| 1190 | 
            -
               | 
| 1191 | 
            -
               | 
| 1192 | 
            -
               | 
| 1193 | 
            -
               | 
| 1194 | 
            -
               | 
| 1195 | 
            -
               | 
| 1196 | 
            -
               | 
| 1197 | 
            -
               | 
| 1198 | 
            -
             | 
| 1199 | 
            -
              right_col << "EXPANSIONS:".c(@c_prompt).b
         | 
| 1200 | 
            -
              right_col << "~                   Home directory"
         | 
| 1201 | 
            -
              right_col << "$VAR, ${VAR}        Environment var"
         | 
| 1202 | 
            -
              right_col << "$?                  Exit status"
         | 
| 1203 | 
            -
              right_col << "$(cmd), `cmd`       Command subst"
         | 
| 1204 | 
            -
              right_col << "{a,b,c}             Brace expansion"
         | 
| 1205 | 
            -
              right_col << "cmd1 && cmd2        Conditional"
         | 
| 1206 | 
            -
              right_col << "cmd1 || cmd2        Alternative"
         | 
| 1207 | 
            -
              
         | 
| 1187 | 
            +
             | 
| 1188 | 
            +
              col1 = []
         | 
| 1189 | 
            +
              col2 = []
         | 
| 1190 | 
            +
              col3 = []
         | 
| 1191 | 
            +
             | 
| 1192 | 
            +
              # Column 1: Keyboard + Commands + Jobs
         | 
| 1193 | 
            +
              col1 << "KEYBOARD:".c(@c_prompt).b
         | 
| 1194 | 
            +
              col1 << "Ctrl-G    Edit in \$EDITOR"
         | 
| 1195 | 
            +
              col1 << "Ctrl-Y    Copy line"
         | 
| 1196 | 
            +
              col1 << "Ctrl-D    Exit + save"
         | 
| 1197 | 
            +
              col1 << "Ctrl-C    Clear line"
         | 
| 1198 | 
            +
              col1 << "TAB       Complete"
         | 
| 1199 | 
            +
              col1 << "Shift-TAB History search"
         | 
| 1200 | 
            +
              col1 << ""
         | 
| 1201 | 
            +
              col1 << "CORE COMMANDS:".c(@c_prompt).b
         | 
| 1202 | 
            +
              col1 << ":nick a = b      Alias"
         | 
| 1203 | 
            +
              col1 << ":nick gp={{br}}  Parametrized"
         | 
| 1204 | 
            +
              col1 << ":bm name         Bookmark"
         | 
| 1205 | 
            +
              col1 << ":defun f()=x     Function"
         | 
| 1206 | 
            +
              col1 << ":stats           Analytics"
         | 
| 1207 | 
            +
              col1 << ":validate p=a    Safety rules"
         | 
| 1208 | 
            +
              col1 << ":calc expr       Calculator"
         | 
| 1209 | 
            +
              col1 << ":theme name      Color schemes"
         | 
| 1210 | 
            +
              col1 << ":plugins         Extensions"
         | 
| 1211 | 
            +
              col1 << ""
         | 
| 1212 | 
            +
              col1 << "JOBS:".c(@c_prompt).b
         | 
| 1213 | 
            +
              col1 << "cmd &       Background"
         | 
| 1214 | 
            +
              col1 << ":jobs       List jobs"
         | 
| 1215 | 
            +
              col1 << ":fg [id]    Foreground"
         | 
| 1216 | 
            +
             | 
| 1217 | 
            +
              # Column 2: Sessions + Bookmarks + Recording
         | 
| 1218 | 
            +
              col2 << "SESSIONS:".c(@c_prompt).b
         | 
| 1219 | 
            +
              col2 << ":save_session nm   Save state"
         | 
| 1220 | 
            +
              col2 << ":load_session nm   Load state"
         | 
| 1221 | 
            +
              col2 << ":list_sessions     Show all"
         | 
| 1222 | 
            +
              col2 << ":rmsession nm|*    Delete"
         | 
| 1223 | 
            +
              col2 << ""
         | 
| 1224 | 
            +
              col2 << "BOOKMARKS:".c(@c_prompt).b
         | 
| 1225 | 
            +
              col2 << ":bm nm path #tag   Create"
         | 
| 1226 | 
            +
              col2 << "name               Jump to bookmark"
         | 
| 1227 | 
            +
              col2 << ":bm                List all"
         | 
| 1228 | 
            +
              col2 << ":bm --stats        Statistics"
         | 
| 1229 | 
            +
              col2 << ":bm --export f     Export"
         | 
| 1230 | 
            +
              col2 << ""
         | 
| 1231 | 
            +
              col2 << "RECORDING:".c(@c_prompt).b
         | 
| 1232 | 
            +
              col2 << ":record start nm   Start recording"
         | 
| 1233 | 
            +
              col2 << ":record stop       Stop recording"
         | 
| 1234 | 
            +
              col2 << ":record show nm    Show commands"
         | 
| 1235 | 
            +
              col2 << ":record -nm        Delete"
         | 
| 1236 | 
            +
              col2 << ":replay nm         Execute"
         | 
| 1237 | 
            +
              col2 << ":record            List all"
         | 
| 1238 | 
            +
              col2 << ""
         | 
| 1239 | 
            +
              col2 << "FEATURES:".c(@c_prompt).b
         | 
| 1240 | 
            +
              col2 << "gp branch=main     Nick template"
         | 
| 1241 | 
            +
              col2 << "!!                 Repeat last"
         | 
| 1242 | 
            +
              col2 << "!-2                2nd to last"
         | 
| 1243 | 
            +
              col2 << "!5:7               Chain commands"
         | 
| 1244 | 
            +
              col2 << ":stats --graph     Visual charts"
         | 
| 1245 | 
            +
              col2 << ":completion_stats  Learn patterns"
         | 
| 1246 | 
            +
             | 
| 1247 | 
            +
              # Column 3: Config + Integrations + Expansions
         | 
| 1248 | 
            +
              col3 << "CONFIG:".c(@c_prompt).b
         | 
| 1249 | 
            +
              col3 << ":config auto_correct on         Auto-fix"
         | 
| 1250 | 
            +
              col3 << ":config completion_learning on  Learn TAB"
         | 
| 1251 | 
            +
              col3 << ":config slow_command_threshold 5  Slow warn"
         | 
| 1252 | 
            +
              col3 << ":config session_autosave 300    Auto-save"
         | 
| 1253 | 
            +
              col3 << ":config history_dedup smart     Dedup"
         | 
| 1254 | 
            +
              col3 << ""
         | 
| 1255 | 
            +
              col3 << "INTEGRATIONS:".c(@c_prompt).b
         | 
| 1256 | 
            +
              col3 << "r          rtfm file manager"
         | 
| 1257 | 
            +
              col3 << "f          fzf fuzzy finder"
         | 
| 1258 | 
            +
              col3 << "= expr     xrpn calculator"
         | 
| 1259 | 
            +
              col3 << "@ text     AI text response"
         | 
| 1260 | 
            +
              col3 << "@@ cmd     AI command suggest"
         | 
| 1261 | 
            +
              col3 << ""
         | 
| 1262 | 
            +
              col3 << "EXPANSIONS:".c(@c_prompt).b
         | 
| 1263 | 
            +
              col3 << "~            Home directory"
         | 
| 1264 | 
            +
              col3 << "$VAR         Environment var"
         | 
| 1265 | 
            +
              col3 << "$(cmd)       Command subst"
         | 
| 1266 | 
            +
              col3 << "{a,b,c}      Brace expansion"
         | 
| 1267 | 
            +
              col3 << "cmd1 && cmd2 Conditional AND"
         | 
| 1268 | 
            +
              col3 << "for i in...  Bash scripts"
         | 
| 1269 | 
            +
              col3 << ""
         | 
| 1270 | 
            +
              col3 << "MORE:".c(@c_prompt).b
         | 
| 1271 | 
            +
              col3 << ":help        This help"
         | 
| 1272 | 
            +
              col3 << ":info        About rsh"
         | 
| 1273 | 
            +
              col3 << ":version     Version info"
         | 
| 1274 | 
            +
             | 
| 1208 1275 | 
             
              # Pad columns to same length
         | 
| 1209 | 
            -
              max_lines = [ | 
| 1210 | 
            -
               | 
| 1211 | 
            -
               | 
| 1212 | 
            -
              
         | 
| 1213 | 
            -
             | 
| 1276 | 
            +
              max_lines = [col1.length, col2.length, col3.length].max
         | 
| 1277 | 
            +
              col1.fill("", col1.length...max_lines)
         | 
| 1278 | 
            +
              col2.fill("", col2.length...max_lines)
         | 
| 1279 | 
            +
              col3.fill("", col3.length...max_lines)
         | 
| 1280 | 
            +
             | 
| 1281 | 
            +
              # Print in three columns
         | 
| 1214 1282 | 
             
              puts
         | 
| 1215 1283 | 
             
              max_lines.times do |i|
         | 
| 1216 | 
            -
                 | 
| 1217 | 
            -
                 | 
| 1218 | 
            -
                 | 
| 1219 | 
            -
             | 
| 1220 | 
            -
                padding  | 
| 1221 | 
            -
                 | 
| 1222 | 
            -
                 | 
| 1284 | 
            +
                text1 = col1[i].to_s
         | 
| 1285 | 
            +
                text2 = col2[i].to_s
         | 
| 1286 | 
            +
                text3 = col3[i].to_s
         | 
| 1287 | 
            +
             | 
| 1288 | 
            +
                # Calculate padding for each column
         | 
| 1289 | 
            +
                vis1 = strip_ansi(text1).length
         | 
| 1290 | 
            +
                vis2 = strip_ansi(text2).length
         | 
| 1291 | 
            +
                pad1 = col_width - vis1
         | 
| 1292 | 
            +
                pad2 = col_width - vis2
         | 
| 1293 | 
            +
                pad1 = 0 if pad1 < 0
         | 
| 1294 | 
            +
                pad2 = 0 if pad2 < 0
         | 
| 1295 | 
            +
             | 
| 1296 | 
            +
                puts "  #{text1}#{' ' * pad1} #{text2}#{' ' * pad2} #{text3}"
         | 
| 1223 1297 | 
             
              end
         | 
| 1224 1298 | 
             
              puts
         | 
| 1225 1299 | 
             
            end
         | 
| @@ -2150,6 +2224,175 @@ def apply_validation_rules(cmd) # Apply custom validation rules | |
| 2150 2224 |  | 
| 2151 2225 | 
             
              warnings
         | 
| 2152 2226 | 
             
            end
         | 
| 2227 | 
            +
            def track_completion(context, selected) # Track completion selection for learning
         | 
| 2228 | 
            +
              return unless @completion_learning
         | 
| 2229 | 
            +
              return if context.nil? || selected.nil?
         | 
| 2230 | 
            +
             | 
| 2231 | 
            +
              key = "#{context}:#{selected}"
         | 
| 2232 | 
            +
              @completion_weights[key] ||= 0
         | 
| 2233 | 
            +
              @completion_weights[key] += 1
         | 
| 2234 | 
            +
            end
         | 
| 2235 | 
            +
            def sort_by_learning(context, items) # Sort completions by learning weights
         | 
| 2236 | 
            +
              return items unless @completion_learning
         | 
| 2237 | 
            +
              return items if @completion_weights.empty?
         | 
| 2238 | 
            +
             | 
| 2239 | 
            +
              # Score each item
         | 
| 2240 | 
            +
              scored = items.map do |item|
         | 
| 2241 | 
            +
                # Extract just the switch/command part (before space/description)
         | 
| 2242 | 
            +
                item_key = item.split(/\s+/).first
         | 
| 2243 | 
            +
                key = "#{context}:#{item_key}"
         | 
| 2244 | 
            +
                weight = @completion_weights[key] || 0
         | 
| 2245 | 
            +
                {item: item, weight: weight}
         | 
| 2246 | 
            +
              end
         | 
| 2247 | 
            +
             | 
| 2248 | 
            +
              # Sort: highest weight first, then alphabetically
         | 
| 2249 | 
            +
              scored.sort_by { |s| [-s[:weight], s[:item]] }.map { |s| s[:item] }
         | 
| 2250 | 
            +
            end
         | 
| 2251 | 
            +
            def completion_stats # Show completion learning statistics
         | 
| 2252 | 
            +
              if @completion_weights.empty?
         | 
| 2253 | 
            +
                puts "\nNo completion learning data yet"
         | 
| 2254 | 
            +
                puts "Use TAB completion and selections will be learned over time"
         | 
| 2255 | 
            +
                return
         | 
| 2256 | 
            +
              end
         | 
| 2257 | 
            +
             | 
| 2258 | 
            +
              puts "\n  Completion Learning Statistics".c(@c_prompt).b
         | 
| 2259 | 
            +
              puts "  " + "="*50
         | 
| 2260 | 
            +
             | 
| 2261 | 
            +
              # Group by context
         | 
| 2262 | 
            +
              by_context = {}
         | 
| 2263 | 
            +
              @completion_weights.each do |key, weight|
         | 
| 2264 | 
            +
                context, choice = key.split(':', 2)
         | 
| 2265 | 
            +
                by_context[context] ||= []
         | 
| 2266 | 
            +
                by_context[context] << {choice: choice, weight: weight}
         | 
| 2267 | 
            +
              end
         | 
| 2268 | 
            +
             | 
| 2269 | 
            +
              # Show top contexts
         | 
| 2270 | 
            +
              by_context.sort_by { |ctx, items| -items.map { |i| i[:weight] }.sum }.first(10).each do |context, items|
         | 
| 2271 | 
            +
                puts "\n  #{context}:".c(@c_nick)
         | 
| 2272 | 
            +
                items.sort_by { |i| -i[:weight] }.first(5).each do |item|
         | 
| 2273 | 
            +
                  bar = "■" * ([item[:weight] / 2, 20].min)
         | 
| 2274 | 
            +
                  puts "    #{item[:choice].ljust(20)} #{item[:weight].to_s.rjust(3)}x #{bar.c(@c_path)}"
         | 
| 2275 | 
            +
                end
         | 
| 2276 | 
            +
              end
         | 
| 2277 | 
            +
             | 
| 2278 | 
            +
              puts "\n  Total learned patterns: #{@completion_weights.length}"
         | 
| 2279 | 
            +
              puts
         | 
| 2280 | 
            +
            end
         | 
| 2281 | 
            +
            def completion_reset # Reset all completion learning
         | 
| 2282 | 
            +
              @completion_weights = {}
         | 
| 2283 | 
            +
              puts "Completion learning data cleared"
         | 
| 2284 | 
            +
              rshrc
         | 
| 2285 | 
            +
            end
         | 
| 2286 | 
            +
            def record(*args) # Command recording management
         | 
| 2287 | 
            +
              action = args[0]
         | 
| 2288 | 
            +
              name = args[1]
         | 
| 2289 | 
            +
             | 
| 2290 | 
            +
              if action.nil? || action.empty?
         | 
| 2291 | 
            +
                # List recordings
         | 
| 2292 | 
            +
                if @recordings.empty?
         | 
| 2293 | 
            +
                  puts "\nNo recordings. Use: :record start name"
         | 
| 2294 | 
            +
                  return
         | 
| 2295 | 
            +
                end
         | 
| 2296 | 
            +
             | 
| 2297 | 
            +
                puts "\n  Recordings:".c(@c_prompt).b
         | 
| 2298 | 
            +
                @recordings.each do |rec_name, data|
         | 
| 2299 | 
            +
                  created = Time.at(data[:created] || Time.now.to_i).strftime("%Y-%m-%d %H:%M")
         | 
| 2300 | 
            +
                  count = data[:commands]&.length || 0
         | 
| 2301 | 
            +
                  puts "  #{rec_name.ljust(20)} #{count.to_s.rjust(3)} commands  #{created}"
         | 
| 2302 | 
            +
                end
         | 
| 2303 | 
            +
                puts
         | 
| 2304 | 
            +
             | 
| 2305 | 
            +
                # Show if currently recording
         | 
| 2306 | 
            +
                if @recording[:active]
         | 
| 2307 | 
            +
                  puts "  Currently recording: #{@recording[:name]} (#{@recording[:commands].length} commands so far)".c(214)
         | 
| 2308 | 
            +
                end
         | 
| 2309 | 
            +
              elsif action == 'start' && name
         | 
| 2310 | 
            +
                @recording[:active] = true
         | 
| 2311 | 
            +
                @recording[:name] = name
         | 
| 2312 | 
            +
                @recording[:commands] = []
         | 
| 2313 | 
            +
                @recording[:start_time] = Time.now.to_i
         | 
| 2314 | 
            +
                puts "Recording started: #{name}".c(@c_path)
         | 
| 2315 | 
            +
              elsif action == 'stop'
         | 
| 2316 | 
            +
                if @recording[:active]
         | 
| 2317 | 
            +
                  @recordings[@recording[:name]] = {
         | 
| 2318 | 
            +
                    commands: @recording[:commands],
         | 
| 2319 | 
            +
                    created: @recording[:start_time]
         | 
| 2320 | 
            +
                  }
         | 
| 2321 | 
            +
                  puts "Recording stopped: #{@recording[:name]} (#{@recording[:commands].length} commands)".c(@c_path)
         | 
| 2322 | 
            +
                  @recording[:active] = false
         | 
| 2323 | 
            +
                  rshrc
         | 
| 2324 | 
            +
                else
         | 
| 2325 | 
            +
                  puts "No active recording"
         | 
| 2326 | 
            +
                end
         | 
| 2327 | 
            +
              elsif action == 'status'
         | 
| 2328 | 
            +
                if @recording[:active]
         | 
| 2329 | 
            +
                  puts "Recording: #{@recording[:name]} (#{@recording[:commands].length} commands)"
         | 
| 2330 | 
            +
                  @recording[:commands].last(5).each { |cmd| puts "  #{cmd}" }
         | 
| 2331 | 
            +
                else
         | 
| 2332 | 
            +
                  puts "No active recording"
         | 
| 2333 | 
            +
                end
         | 
| 2334 | 
            +
              elsif action == 'show' && name
         | 
| 2335 | 
            +
                # Show recording contents
         | 
| 2336 | 
            +
                unless @recordings[name]
         | 
| 2337 | 
            +
                  puts "Recording '#{name}' not found"
         | 
| 2338 | 
            +
                  return
         | 
| 2339 | 
            +
                end
         | 
| 2340 | 
            +
             | 
| 2341 | 
            +
                recording = @recordings[name]
         | 
| 2342 | 
            +
                created = Time.at(recording[:created] || Time.now.to_i).strftime("%Y-%m-%d %H:%M:%S")
         | 
| 2343 | 
            +
             | 
| 2344 | 
            +
                puts "\n  Recording: #{name}".c(@c_prompt).b
         | 
| 2345 | 
            +
                puts "  Created: #{created}"
         | 
| 2346 | 
            +
                puts "  Commands: #{recording[:commands].length}"
         | 
| 2347 | 
            +
                puts
         | 
| 2348 | 
            +
             | 
| 2349 | 
            +
                recording[:commands].each_with_index do |cmd, i|
         | 
| 2350 | 
            +
                  puts "  #{(i+1).to_s.rjust(3)}. #{cmd}"
         | 
| 2351 | 
            +
                end
         | 
| 2352 | 
            +
                puts
         | 
| 2353 | 
            +
              elsif action =~ /^-(.+)$/
         | 
| 2354 | 
            +
                # Delete recording
         | 
| 2355 | 
            +
                rec_name = $1
         | 
| 2356 | 
            +
                if @recordings.delete(rec_name)
         | 
| 2357 | 
            +
                  puts "Recording '#{rec_name}' deleted"
         | 
| 2358 | 
            +
                  rshrc
         | 
| 2359 | 
            +
                else
         | 
| 2360 | 
            +
                  puts "Recording '#{rec_name}' not found"
         | 
| 2361 | 
            +
                end
         | 
| 2362 | 
            +
              else
         | 
| 2363 | 
            +
                puts "Usage: :record start name|stop|status|show name|-name"
         | 
| 2364 | 
            +
              end
         | 
| 2365 | 
            +
            end
         | 
| 2366 | 
            +
            def replay(*args) # Replay recorded commands
         | 
| 2367 | 
            +
              name = args[0]
         | 
| 2368 | 
            +
             | 
| 2369 | 
            +
              unless name && @recordings[name]
         | 
| 2370 | 
            +
                puts "Recording '#{name}' not found"
         | 
| 2371 | 
            +
                record
         | 
| 2372 | 
            +
                return
         | 
| 2373 | 
            +
              end
         | 
| 2374 | 
            +
             | 
| 2375 | 
            +
              recording = @recordings[name]
         | 
| 2376 | 
            +
              commands = recording[:commands] || []
         | 
| 2377 | 
            +
             | 
| 2378 | 
            +
              puts "Replaying '#{name}' (#{commands.length} commands)...".c(@c_path)
         | 
| 2379 | 
            +
             | 
| 2380 | 
            +
              commands.each_with_index do |cmd, i|
         | 
| 2381 | 
            +
                puts "\n[#{i+1}/#{commands.length}] #{cmd}".c(@c_stamp)
         | 
| 2382 | 
            +
             | 
| 2383 | 
            +
                result = system(cmd)
         | 
| 2384 | 
            +
                exit_code = $?.exitstatus
         | 
| 2385 | 
            +
             | 
| 2386 | 
            +
                unless result
         | 
| 2387 | 
            +
                  puts " Command failed (exit #{exit_code})".c(196)
         | 
| 2388 | 
            +
                  print "Continue? (Y/n): "
         | 
| 2389 | 
            +
                  response = $stdin.gets.chomp
         | 
| 2390 | 
            +
                  break if response.downcase == 'n'
         | 
| 2391 | 
            +
                end
         | 
| 2392 | 
            +
              end
         | 
| 2393 | 
            +
             | 
| 2394 | 
            +
              puts "\nReplay complete".c(@c_path)
         | 
| 2395 | 
            +
            end
         | 
| 2153 2396 | 
             
            def apply_auto_correct(cmd) # Apply auto-correction to command
         | 
| 2154 2397 | 
             
              return cmd unless @auto_correct
         | 
| 2155 2398 | 
             
              return cmd if cmd =~ /^:/  # Don't auto-correct colon commands
         | 
| @@ -2466,6 +2709,10 @@ def load_rshrc_safe | |
| 2466 2709 | 
             
                @plugins = [] unless @plugins.is_a?(Array)
         | 
| 2467 2710 | 
             
                @plugin_commands = {} unless @plugin_commands.is_a?(Hash)
         | 
| 2468 2711 | 
             
                @validation_rules = [] unless @validation_rules.is_a?(Array)
         | 
| 2712 | 
            +
                @completion_weights = {} unless @completion_weights.is_a?(Hash)
         | 
| 2713 | 
            +
                @completion_learning = true if @completion_learning.nil?
         | 
| 2714 | 
            +
                @recording = {active: false, name: nil, commands: []} unless @recording.is_a?(Hash)
         | 
| 2715 | 
            +
                @recordings = {} unless @recordings.is_a?(Hash)
         | 
| 2469 2716 |  | 
| 2470 2717 | 
             
                # Restore defuns from .rshrc
         | 
| 2471 2718 | 
             
                if @defuns && !@defuns.empty?
         | 
| @@ -2608,6 +2855,10 @@ def load_defaults | |
| 2608 2855 | 
             
              @plugins ||= []
         | 
| 2609 2856 | 
             
              @plugin_commands ||= {}
         | 
| 2610 2857 | 
             
              @validation_rules ||= []
         | 
| 2858 | 
            +
              @completion_weights ||= {}
         | 
| 2859 | 
            +
              @completion_learning = true if @completion_learning.nil?
         | 
| 2860 | 
            +
              @recording ||= {active: false, name: nil, commands: []}
         | 
| 2861 | 
            +
              @recordings ||= {}
         | 
| 2611 2862 | 
             
              puts "Loaded with default configuration."
         | 
| 2612 2863 | 
             
            end
         | 
| 2613 2864 |  | 
| @@ -2801,7 +3052,8 @@ loop do | |
| 2801 3052 | 
             
                      # List of all known rsh commands (since respond_to? doesn't work for top-level methods)
         | 
| 2802 3053 | 
             
                      known_commands = %w[nick gnick defun defun? bm bookmark stats calc config env theme plugins
         | 
| 2803 3054 | 
             
                                          save_session load_session list_sessions delete_session rmsession
         | 
| 2804 | 
            -
                                          validate
         | 
| 3055 | 
            +
                                          validate completion_stats completion_reset
         | 
| 3056 | 
            +
                                          record replay
         | 
| 2805 3057 | 
             
                                          history rmhistory jobs fg bg dirs help info version]
         | 
| 2806 3058 |  | 
| 2807 3059 | 
             
                      # Try to call as rsh method
         | 
| @@ -3058,6 +3310,14 @@ loop do | |
| 3058 3310 | 
             
                            puts "⚠ Command took #{'%.1f' % elapsed}s (threshold: #{@slow_command_threshold}s)".c(214)
         | 
| 3059 3311 | 
             
                          end
         | 
| 3060 3312 |  | 
| 3313 | 
            +
                          # Record command if recording is active
         | 
| 3314 | 
            +
                          if @recording[:active] && @last_exit == 0
         | 
| 3315 | 
            +
                            # Don't record :record commands themselves
         | 
| 3316 | 
            +
                            unless @cmd =~ /^:record/
         | 
| 3317 | 
            +
                              @recording[:commands] << @cmd
         | 
| 3318 | 
            +
                            end
         | 
| 3319 | 
            +
                          end
         | 
| 3320 | 
            +
             | 
| 3061 3321 | 
             
                          # Call plugin on_command_after hooks
         | 
| 3062 3322 | 
             
                          call_plugin_hook(:on_command_after, @cmd, @last_exit)
         | 
| 3063 3323 |  | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: ruby-shell
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 3. | 
| 4 | 
            +
              version: 3.4.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Geir Isene
         | 
| @@ -12,10 +12,10 @@ date: 2025-10-23 00:00:00.000000000 Z | |
| 12 12 | 
             
            dependencies: []
         | 
| 13 13 | 
             
            description: 'A shell written in Ruby with extensive tab completions, aliases/nicks,
         | 
| 14 14 | 
             
              history, syntax highlighting, theming, auto-cd, auto-opening files and more. UPDATE
         | 
| 15 | 
            -
              v3. | 
| 16 | 
            -
               | 
| 17 | 
            -
               | 
| 18 | 
            -
               | 
| 15 | 
            +
              v3.4.0: COMPLETION LEARNING - Shell learns which TAB completions you use most and
         | 
| 16 | 
            +
              intelligently ranks them higher. Context-aware learning per command. :completion_stats
         | 
| 17 | 
            +
              shows patterns. Persistent across sessions. Plus all v3.3 features: quote-less syntax,
         | 
| 18 | 
            +
              parametrized nicks, Ctrl-G editing, validation rules, shell scripts!'
         | 
| 19 19 | 
             
            email: g@isene.com
         | 
| 20 20 | 
             
            executables:
         | 
| 21 21 | 
             
            - rsh
         |