ruby-shell 3.3.0 → 3.4.1
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 +90 -79
- data/bin/rsh +440 -134
- 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: 8c171961f69d09b023cc69bc4cb66b615ec861e940e5369c265ccdf053004764
         | 
| 4 | 
            +
              data.tar.gz: ab185ee8de6daecf91a910f417de02916d259dc315d19c84fe64a2dd4f779123
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 6c8c2343b5cd84bb9508dc6bbe25ce6a7ee7741adc34f77e063628445dc76d28edd5d9774a986066ca46ac551c03dab7290083eaaca59889a525383ca6ff15c0
         | 
| 7 | 
            +
              data.tar.gz: 28c264420c010f966fcfb9cf200ea25b152a3ec2190700132f2a8185a9b34ff1783f786b36afb93e23c8eae3d36a13c68de5ff02d31106d1dd45c31a08cfca2d
         | 
    
        data/README.md
    CHANGED
    
    | @@ -19,85 +19,96 @@ Or simply `gem install ruby-shell`. | |
| 19 19 |  | 
| 20 20 | 
             
            # Features
         | 
| 21 21 |  | 
| 22 | 
            -
            ##  | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
            * ** | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
            * ** | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
            * ** | 
| 52 | 
            -
            * ** | 
| 53 | 
            -
            * ** | 
| 54 | 
            -
            * ** | 
| 55 | 
            -
            * ** | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
            * ** | 
| 59 | 
            -
            * ** | 
| 60 | 
            -
            * ** | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
             | 
| 64 | 
            -
            * ** | 
| 65 | 
            -
            * ** | 
| 66 | 
            -
            * ** | 
| 67 | 
            -
            * ** | 
| 68 | 
            -
             | 
| 69 | 
            -
             | 
| 70 | 
            -
            *  | 
| 71 | 
            -
            *  | 
| 72 | 
            -
            *  | 
| 73 | 
            -
             | 
| 74 | 
            -
             | 
| 75 | 
            -
             | 
| 76 | 
            -
            *  | 
| 77 | 
            -
            *  | 
| 78 | 
            -
            *  | 
| 79 | 
            -
            *  | 
| 80 | 
            -
            *  | 
| 81 | 
            -
             | 
| 82 | 
            -
             | 
| 83 | 
            -
             | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
| 86 | 
            -
             | 
| 87 | 
            -
             | 
| 88 | 
            -
             | 
| 89 | 
            -
             | 
| 90 | 
            -
             | 
| 91 | 
            -
             | 
| 92 | 
            -
             | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 95 | 
            -
             | 
| 96 | 
            -
             | 
| 97 | 
            -
             | 
| 98 | 
            -
             | 
| 99 | 
            -
             | 
| 100 | 
            -
             | 
| 22 | 
            +
            ## Key Features
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            ### Aliases (Nicks)
         | 
| 25 | 
            +
            **rsh uses "nicks" for aliases** - both simple command shortcuts and powerful parametrized templates:
         | 
| 26 | 
            +
             | 
| 27 | 
            +
            ```bash
         | 
| 28 | 
            +
            # Simple aliases
         | 
| 29 | 
            +
            :nick la = ls -la
         | 
| 30 | 
            +
            :nick gs = git status
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            # Parametrized nicks (templates with {{placeholders}})
         | 
| 33 | 
            +
            :nick gp = git push origin {{branch}}
         | 
| 34 | 
            +
            gp branch=main              # Executes: git push origin main
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            :nick deploy = ssh {{user}}@{{host}} 'systemctl restart {{app}}'
         | 
| 37 | 
            +
            deploy user=admin host=prod app=api
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            # List and manage
         | 
| 40 | 
            +
            :nick                       # List all nicks
         | 
| 41 | 
            +
            :nick -la                   # Delete a nick
         | 
| 42 | 
            +
            ```
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            ### Intelligence & Learning
         | 
| 45 | 
            +
            * **Completion Learning**: Shell learns which TAB completions you use and ranks them higher
         | 
| 46 | 
            +
            * **Smart Suggestions**: "Did you mean...?" for typos
         | 
| 47 | 
            +
            * **Auto-correct**: Optional auto-fix with confirmation
         | 
| 48 | 
            +
            * **Command Analytics**: `:stats` shows usage patterns and performance
         | 
| 49 | 
            +
             | 
| 50 | 
            +
            ### Productivity
         | 
| 51 | 
            +
            * **Command Recording**: `:record start name` → run commands → `:record stop` → `:replay name`
         | 
| 52 | 
            +
            * **Sessions**: Save/load entire shell state with bookmarks, history, and functions
         | 
| 53 | 
            +
            * **Bookmarks**: Tag directories and jump instantly
         | 
| 54 | 
            +
            * **Multi-line Editing**: Press Ctrl-G to edit in $EDITOR
         | 
| 55 | 
            +
            * **Shell Scripts**: Full bash support for for/while/if loops
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            ### Extensibility
         | 
| 58 | 
            +
            * **Plugin System**: Add custom commands, completions, and hooks
         | 
| 59 | 
            +
            * **Ruby Functions**: Define callable functions - `:defun hello(name) = puts "Hello, #{name}!"`
         | 
| 60 | 
            +
            * **Validation Rules**: `:validate rm -rf / = block` prevents dangerous commands
         | 
| 61 | 
            +
            * **6 Color Themes**: solarized, dracula, gruvbox, nord, monokai, default
         | 
| 62 | 
            +
             | 
| 63 | 
            +
            ### Integrations
         | 
| 64 | 
            +
            * **AI Support**: @ for questions, @@ for command suggestions (Ollama or OpenAI)
         | 
| 65 | 
            +
            * **RTFM**: Launch file manager with `r`
         | 
| 66 | 
            +
            * **fzf**: Fuzzy finder with `f`
         | 
| 67 | 
            +
            * **XRPN**: Calculator with `= expression`
         | 
| 68 | 
            +
             | 
| 69 | 
            +
            ### Tab Completion
         | 
| 70 | 
            +
            * Smart context-aware completion for git, apt, docker, systemctl, cargo, npm, gem
         | 
| 71 | 
            +
            * Command switches from --help
         | 
| 72 | 
            +
            * Option values (--format=json, --level=debug)
         | 
| 73 | 
            +
            * Learns your patterns and adapts
         | 
| 74 | 
            +
             | 
| 75 | 
            +
            ### Core Shell
         | 
| 76 | 
            +
            * Syntax highlighting for nicks, commands, paths, bookmarks
         | 
| 77 | 
            +
            * History with search, edit, and repeat (!, !!, !-2, !5:7)
         | 
| 78 | 
            +
            * Job control (background jobs, suspend, resume)
         | 
| 79 | 
            +
            * Config file (.rshrc) updates on exit
         | 
| 80 | 
            +
            * All colors themeable
         | 
| 81 | 
            +
             | 
| 82 | 
            +
            ---
         | 
| 83 | 
            +
             | 
| 84 | 
            +
            ## Quick Start
         | 
| 85 | 
            +
             | 
| 86 | 
            +
            ```bash
         | 
| 87 | 
            +
            # Install
         | 
| 88 | 
            +
            gem install ruby-shell
         | 
| 89 | 
            +
             | 
| 90 | 
            +
            # Run
         | 
| 91 | 
            +
            rsh
         | 
| 92 | 
            +
             | 
| 93 | 
            +
            # Create an alias
         | 
| 94 | 
            +
            :nick ll = ls -l
         | 
| 95 | 
            +
            ll
         | 
| 96 | 
            +
             | 
| 97 | 
            +
            # Create parametrized alias
         | 
| 98 | 
            +
            :nick gp = git push origin {{branch}}
         | 
| 99 | 
            +
            gp branch=main
         | 
| 100 | 
            +
             | 
| 101 | 
            +
            # Get help
         | 
| 102 | 
            +
            :help
         | 
| 103 | 
            +
            :info
         | 
| 104 | 
            +
             | 
| 105 | 
            +
            # See version and changelog
         | 
| 106 | 
            +
            :version
         | 
| 107 | 
            +
            ```
         | 
| 108 | 
            +
             | 
| 109 | 
            +
            ---
         | 
| 110 | 
            +
             | 
| 111 | 
            +
            ## Latest Features (v3.4)
         | 
| 101 112 | 
             
            * **Define Ruby functions as shell commands**: `:defun 'weather(*args) = system("curl -s wttr.in/#{args[0] || \"oslo\"}")'`
         | 
| 102 113 | 
             
            * **Call like any shell command**: `weather london`
         | 
| 103 114 | 
             
            * **Full Ruby power**: Access to Ruby stdlib, file operations, JSON parsing, web requests, etc.
         | 
    
        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.1" # Performance optimizations: 50-60% faster startup, optimized .rshrc reload, persistent cache
         | 
| 12 12 |  | 
| 13 13 | 
             
            # MODULES, CLASSES AND EXTENSIONS
         | 
| 14 14 | 
             
            class String # Add coloring to strings (with escaping for Readline)
         | 
| @@ -151,6 +151,12 @@ 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
         | 
| 158 | 
            +
              @command_cache = {}                     # Cache for expensive shell command outputs
         | 
| 159 | 
            +
              @last_prompt_dir = nil                  # Track directory for .rshrc reload optimization
         | 
| 154 160 | 
             
              # Built-in rsh commands are called with : prefix, so no need for separate tracking
         | 
| 155 161 | 
             
              Dir.mkdir(Dir.home + '/.rsh') unless Dir.exist?(Dir.home + '/.rsh')
         | 
| 156 162 | 
             
              Dir.mkdir(@session_dir) unless Dir.exist?(@session_dir)
         | 
| @@ -188,14 +194,18 @@ end | |
| 188 194 | 
             
              * Syntax validation - Pre-execution warnings for dangerous or malformed commands
         | 
| 189 195 | 
             
              * Unified command syntax - :nick, :gnick, :bm all work the same way (list/create/delete)
         | 
| 190 196 |  | 
| 191 | 
            -
              NEW in v3. | 
| 192 | 
            -
              *  | 
| 197 | 
            +
              NEW in v3.4:
         | 
| 198 | 
            +
              * Completion learning - Shell learns which TAB completions you use and ranks them higher
         | 
| 199 | 
            +
              * Context-aware - Separate learning for each command (git, ls, docker, etc.)
         | 
| 200 | 
            +
              * :completion_stats - View learned patterns with visual bar charts
         | 
| 201 | 
            +
              * Persistent - Learning data saves to .rshrc across sessions
         | 
| 202 | 
            +
             | 
| 203 | 
            +
              v3.3 Features:
         | 
| 204 | 
            +
              * Quote-less syntax - No more quotes! Use :nick la = ls -la
         | 
| 193 205 | 
             
              * Parametrized nicks - :nick gp = git push origin {{branch}}, then: gp branch=main
         | 
| 194 206 | 
             
              * Ctrl-G multi-line edit - Press Ctrl-G to edit command in $EDITOR
         | 
| 195 207 | 
             
              * Custom validation - :validate rm -rf / = block prevents dangerous commands
         | 
| 196 208 | 
             
              * 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 209 |  | 
| 200 210 | 
             
              v3.2 Features:
         | 
| 201 211 | 
             
              * Plugin system - Extensible architecture for custom commands, completions, and hooks
         | 
| @@ -507,7 +517,7 @@ def getstr # A custom Readline-like function | |
| 507 517 | 
             
                  lift = true
         | 
| 508 518 | 
             
                when 'S-TAB'
         | 
| 509 519 | 
             
                  @ci = nil
         | 
| 510 | 
            -
                   | 
| 520 | 
            +
                  tab("hist")
         | 
| 511 521 | 
             
                  lift = true
         | 
| 512 522 | 
             
                when /^.$/
         | 
| 513 523 | 
             
                  @history[0].insert(@pos,chr)
         | 
| @@ -567,20 +577,29 @@ def tab(type) | |
| 567 577 | 
             
                  last_cmd = nil
         | 
| 568 578 | 
             
                end
         | 
| 569 579 |  | 
| 570 | 
            -
                 | 
| 571 | 
            -
                 | 
| 572 | 
            -
                  type = " | 
| 573 | 
            -
                 | 
| 574 | 
            -
                  type = " | 
| 575 | 
            -
                 | 
| 576 | 
            -
                  type = " | 
| 577 | 
            -
                when "export", "unset"
         | 
| 578 | 
            -
                  type = "env_vars"
         | 
| 580 | 
            +
                # Check for colon command arguments
         | 
| 581 | 
            +
                if @pretab =~ /:record\s*$/
         | 
| 582 | 
            +
                  type = "record_args"
         | 
| 583 | 
            +
                elsif @pretab =~ /:replay\s*$/
         | 
| 584 | 
            +
                  type = "replay_args"
         | 
| 585 | 
            +
                elsif @pretab =~ /:plugins\s*$/
         | 
| 586 | 
            +
                  type = "plugin_args"
         | 
| 579 587 | 
             
                else
         | 
| 580 | 
            -
                   | 
| 581 | 
            -
                   | 
| 582 | 
            -
                    type = " | 
| 583 | 
            -
             | 
| 588 | 
            +
                  case last_cmd
         | 
| 589 | 
            +
                  when "cd", "pushd", "rmdir"
         | 
| 590 | 
            +
                    type = "dirs_only"
         | 
| 591 | 
            +
                  when "vim", "nano", "cat", "less", "more", "head", "tail", "file"
         | 
| 592 | 
            +
                    type = "files_only"
         | 
| 593 | 
            +
                  when "man", "info", "which", "whatis"
         | 
| 594 | 
            +
                    type = "commands_only"
         | 
| 595 | 
            +
                  when "export", "unset"
         | 
| 596 | 
            +
                    type = "env_vars"
         | 
| 597 | 
            +
                  else
         | 
| 598 | 
            +
                    # Check if command has defined completions and we're on the first argument
         | 
| 599 | 
            +
                    if @cmd_completions.key?(last_cmd) && cmd_parts.length == 1
         | 
| 600 | 
            +
                      type = "cmd_subcommands"
         | 
| 601 | 
            +
                      @current_cmd = last_cmd
         | 
| 602 | 
            +
                    end
         | 
| 584 603 | 
             
                  end
         | 
| 585 604 | 
             
                end
         | 
| 586 605 | 
             
              end
         | 
| @@ -591,6 +610,27 @@ def tab(type) | |
| 591 610 | 
             
                  @tabarray = @history.select {|el| el =~ /#{@tabstr}/} # Select history items matching @tabstr
         | 
| 592 611 | 
             
                  @tabarray.shift   # Take away @history[0]
         | 
| 593 612 | 
             
                  return if @tabarray.empty?
         | 
| 613 | 
            +
                when "record_args"
         | 
| 614 | 
            +
                  # Completions for :record command
         | 
| 615 | 
            +
                  @tabarray = %w[start stop status show]
         | 
| 616 | 
            +
                  # Add existing recording names for show
         | 
| 617 | 
            +
                  @tabarray += @recordings.keys.map { |k| "show #{k}" } if @recordings.any?
         | 
| 618 | 
            +
                  # Add delete options
         | 
| 619 | 
            +
                  @tabarray += @recordings.keys.map { |k| "-#{k}" } if @recordings.any?
         | 
| 620 | 
            +
                when "replay_args"
         | 
| 621 | 
            +
                  # Completions for :replay command - just recording names
         | 
| 622 | 
            +
                  @tabarray = @recordings.keys
         | 
| 623 | 
            +
                when "plugin_args"
         | 
| 624 | 
            +
                  # Completions for :plugins command
         | 
| 625 | 
            +
                  @tabarray = %w[reload info]
         | 
| 626 | 
            +
                  # Add plugin names for enable/disable/info
         | 
| 627 | 
            +
                  if @plugins.any?
         | 
| 628 | 
            +
                    @tabarray += @plugins.map { |p| "disable #{p[:name]}" }
         | 
| 629 | 
            +
                    @tabarray += @plugins.map { |p| "info #{p[:name]}" }
         | 
| 630 | 
            +
                  end
         | 
| 631 | 
            +
                  if @plugin_disabled.any?
         | 
| 632 | 
            +
                    @tabarray += @plugin_disabled.map { |p| "enable #{p}" }
         | 
| 633 | 
            +
                  end
         | 
| 594 634 | 
             
                when "switch"
         | 
| 595 635 | 
             
                  cmdswitch = @pretab.split(/[|, ]/).last.to_s.strip
         | 
| 596 636 | 
             
                  @tabarray = get_command_switches(cmdswitch)
         | 
| @@ -642,6 +682,8 @@ def tab(type) | |
| 642 682 | 
             
                    :history :rmhistory :jobs :fg :bg
         | 
| 643 683 | 
             
                    :save_session :load_session :list_sessions :delete_session :rmsession
         | 
| 644 684 | 
             
                    :config :env :theme :plugins :calc :validate
         | 
| 685 | 
            +
                    :completion_stats :completion_reset
         | 
| 686 | 
            +
                    :record :replay
         | 
| 645 687 | 
             
                    :info :version :help
         | 
| 646 688 | 
             
                  ]
         | 
| 647 689 | 
             
                  search_str = @tabstr[1..-1] || ""  # Remove leading :
         | 
| @@ -722,6 +764,19 @@ def tab(type) | |
| 722 764 | 
             
                end
         | 
| 723 765 | 
             
                return if @tabarray.empty?
         | 
| 724 766 | 
             
                @tabarray.delete("")                                      # Don't remember why
         | 
| 767 | 
            +
             | 
| 768 | 
            +
                # Apply completion learning to sort results
         | 
| 769 | 
            +
                if @completion_learning && type != "hist"
         | 
| 770 | 
            +
                  # Determine context for learning
         | 
| 771 | 
            +
                  completion_context = if @pretab && !@pretab.empty?
         | 
| 772 | 
            +
                    # Use the command being completed
         | 
| 773 | 
            +
                    @pretab.strip.split(/[|;&]/).last.strip.split.first || "all"
         | 
| 774 | 
            +
                  else
         | 
| 775 | 
            +
                    "all"
         | 
| 776 | 
            +
                  end
         | 
| 777 | 
            +
                  @tabarray = sort_by_learning(completion_context, @tabarray)
         | 
| 778 | 
            +
                end
         | 
| 779 | 
            +
             | 
| 725 780 | 
             
                @c.clear_screen_down                                      # Here we go
         | 
| 726 781 | 
             
                max_items = @completion_limit || 5
         | 
| 727 782 | 
             
                @tabarray.length.to_i - i < max_items ? l = @tabarray.length.to_i - i : l = max_items
         | 
| @@ -807,6 +862,18 @@ def tab(type) | |
| 807 862 | 
             
              @c.row(@c_row)
         | 
| 808 863 | 
             
              @c.col(@c_col)
         | 
| 809 864 | 
             
              @history[0] = @newhist0
         | 
| 865 | 
            +
             | 
| 866 | 
            +
              # Track completion selection for learning
         | 
| 867 | 
            +
              if @completion_learning && @tabarray && @tabarray[i] && type != "hist"
         | 
| 868 | 
            +
                completion_context = if @pretab && !@pretab.empty?
         | 
| 869 | 
            +
                  @pretab.strip.split(/[|;&]/).last.strip.split.first || "all"
         | 
| 870 | 
            +
                else
         | 
| 871 | 
            +
                  "all"
         | 
| 872 | 
            +
                end
         | 
| 873 | 
            +
                selected = @tabarray[i]
         | 
| 874 | 
            +
                selected = selected.sub(/\s*(-.*?)[,\s].*/, '\1') if type == "switch"
         | 
| 875 | 
            +
                track_completion(completion_context, selected)
         | 
| 876 | 
            +
              end
         | 
| 810 877 | 
             
            end
         | 
| 811 878 | 
             
            def nextline # Handle going to the next line in the terminal
         | 
| 812 879 | 
             
              row, col = @c.pos
         | 
| @@ -928,6 +995,7 @@ def config(*args) # Configure rsh settings | |
| 928 995 | 
             
                puts "    session_autosave:   #{@session_autosave}s #{@session_autosave > 0 ? '(enabled)' : '(disabled)'}"
         | 
| 929 996 | 
             
                puts "    auto_correct:       #{@auto_correct ? 'on' : 'off'}"
         | 
| 930 997 | 
             
                puts "    slow_command_threshold: #{@slow_command_threshold}s #{@slow_command_threshold > 0 ? '(enabled)' : '(disabled)'}"
         | 
| 998 | 
            +
                puts "    completion_learning: #{@completion_learning ? 'on' : 'off'}"
         | 
| 931 999 | 
             
                puts "    completion_limit:   #{@completion_limit}"
         | 
| 932 1000 | 
             
                puts "    completion_fuzzy:   #{@completion_fuzzy}"
         | 
| 933 1001 | 
             
                puts "    completion_case_sensitive: #{@completion_case_sensitive}"
         | 
| @@ -956,13 +1024,17 @@ def config(*args) # Configure rsh settings | |
| 956 1024 | 
             
                @slow_command_threshold = value.to_i
         | 
| 957 1025 | 
             
                puts "Slow command threshold set to #{value}s #{value.to_i > 0 ? '(enabled)' : '(disabled)'}"
         | 
| 958 1026 | 
             
                rshrc
         | 
| 1027 | 
            +
              when 'completion_learning'
         | 
| 1028 | 
            +
                @completion_learning = %w[on true yes 1].include?(value.to_s.downcase)
         | 
| 1029 | 
            +
                puts "Completion learning #{@completion_learning ? 'enabled' : 'disabled'}"
         | 
| 1030 | 
            +
                rshrc
         | 
| 959 1031 | 
             
              when 'completion_limit'
         | 
| 960 1032 | 
             
                @completion_limit = value.to_i
         | 
| 961 1033 | 
             
                puts "Completion limit set to #{value}"
         | 
| 962 1034 | 
             
                rshrc
         | 
| 963 1035 | 
             
              else
         | 
| 964 1036 | 
             
                puts "Unknown setting '#{setting}'"
         | 
| 965 | 
            -
                puts "Available: history_dedup, session_autosave, auto_correct, slow_command_threshold, completion_limit"
         | 
| 1037 | 
            +
                puts "Available: history_dedup, session_autosave, auto_correct, slow_command_threshold, completion_learning, completion_limit"
         | 
| 966 1038 | 
             
              end
         | 
| 967 1039 | 
             
            end
         | 
| 968 1040 | 
             
            def env(*args) # Environment variable management
         | 
| @@ -1081,6 +1153,21 @@ def rshrc # Write updates to .rshrc | |
| 1081 1153 | 
             
              conf += "@plugin_disabled = #{@plugin_disabled}\n" unless @plugin_disabled.empty?
         | 
| 1082 1154 | 
             
              conf.sub!(/^@validation_rules.*(\n|$)/, "")
         | 
| 1083 1155 | 
             
              conf += "@validation_rules = #{@validation_rules}\n" unless @validation_rules.empty?
         | 
| 1156 | 
            +
              conf.sub!(/^@completion_weights.*(\n|$)/, "")
         | 
| 1157 | 
            +
              conf += "@completion_weights = #{@completion_weights}\n" unless @completion_weights.empty?
         | 
| 1158 | 
            +
              conf.sub!(/^@completion_learning.*(\n|$)/, "")
         | 
| 1159 | 
            +
              conf += "@completion_learning = #{@completion_learning}\n" unless @completion_learning
         | 
| 1160 | 
            +
              conf.sub!(/^@recordings.*(\n|$)/, "")
         | 
| 1161 | 
            +
              conf += "@recordings = #{@recordings}\n" unless @recordings.empty?
         | 
| 1162 | 
            +
              # Persist executable cache for faster startup
         | 
| 1163 | 
            +
              if @exe && @exe.length > 100
         | 
| 1164 | 
            +
                conf.sub!(/^@exe_cache.*(\n|$)/, "")
         | 
| 1165 | 
            +
                conf += "@exe_cache = #{@exe.inspect}\n"
         | 
| 1166 | 
            +
                conf.sub!(/^@exe_cache_path.*(\n|$)/, "")
         | 
| 1167 | 
            +
                conf += "@exe_cache_path = #{ENV['PATH'].inspect}\n"
         | 
| 1168 | 
            +
                conf.sub!(/^@exe_cache_time.*(\n|$)/, "")
         | 
| 1169 | 
            +
                conf += "@exe_cache_time = #{Time.now.to_i}\n"
         | 
| 1170 | 
            +
              end
         | 
| 1084 1171 | 
             
              # Only write @cmd_completions if user has customized it
         | 
| 1085 1172 | 
             
              unless conf =~ /^@cmd_completions\s*=/
         | 
| 1086 1173 | 
             
                # Don't write default completions to avoid cluttering .rshrc
         | 
| @@ -1101,125 +1188,123 @@ end | |
| 1101 1188 | 
             
            # RSH FUNCTIONS
         | 
| 1102 1189 | 
             
            def help
         | 
| 1103 1190 | 
             
              # Get terminal width
         | 
| 1104 | 
            -
              term_width = @maxcol ||  | 
| 1105 | 
            -
              col_width =  | 
| 1106 | 
            -
             | 
| 1191 | 
            +
              term_width = @maxcol || 120
         | 
| 1192 | 
            +
              col_width = 36  # Width for each of 3 columns (wider)
         | 
| 1193 | 
            +
             | 
| 1107 1194 | 
             
              # Helper function to strip ANSI codes for length calculation
         | 
| 1108 1195 | 
             
              def strip_ansi(str)
         | 
| 1109 1196 | 
             
                str.gsub(/\001?\e\[[0-9;]*m\002?/, '')
         | 
| 1110 1197 | 
             
              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 | 
            -
              
         | 
| 1198 | 
            +
             | 
| 1199 | 
            +
              col1 = []
         | 
| 1200 | 
            +
              col2 = []
         | 
| 1201 | 
            +
              col3 = []
         | 
| 1202 | 
            +
             | 
| 1203 | 
            +
              # Column 1: Keyboard + Commands + Jobs
         | 
| 1204 | 
            +
              col1 << "KEYBOARD:".c(@c_prompt).b
         | 
| 1205 | 
            +
              col1 << "Ctrl-G    Edit in \$EDITOR"
         | 
| 1206 | 
            +
              col1 << "Ctrl-Y    Copy line"
         | 
| 1207 | 
            +
              col1 << "Ctrl-D    Exit + save"
         | 
| 1208 | 
            +
              col1 << "Ctrl-C    Clear line"
         | 
| 1209 | 
            +
              col1 << "TAB       Complete"
         | 
| 1210 | 
            +
              col1 << "Shift-TAB History search"
         | 
| 1211 | 
            +
              col1 << ""
         | 
| 1212 | 
            +
              col1 << "CORE COMMANDS:".c(@c_prompt).b
         | 
| 1213 | 
            +
              col1 << ":nick a = b      Alias"
         | 
| 1214 | 
            +
              col1 << ":nick gp={{br}}  Parametrized"
         | 
| 1215 | 
            +
              col1 << ":bm name         Bookmark"
         | 
| 1216 | 
            +
              col1 << ":defun f()=x     Function"
         | 
| 1217 | 
            +
              col1 << ":stats           Analytics"
         | 
| 1218 | 
            +
              col1 << ":validate p=a    Safety rules"
         | 
| 1219 | 
            +
              col1 << ":calc expr       Calculator"
         | 
| 1220 | 
            +
              col1 << ":theme name      Color schemes"
         | 
| 1221 | 
            +
              col1 << ":plugins         Extensions"
         | 
| 1222 | 
            +
              col1 << ""
         | 
| 1223 | 
            +
              col1 << "JOBS:".c(@c_prompt).b
         | 
| 1224 | 
            +
              col1 << "cmd &       Background"
         | 
| 1225 | 
            +
              col1 << ":jobs       List jobs"
         | 
| 1226 | 
            +
              col1 << ":fg [id]    Foreground"
         | 
| 1227 | 
            +
             | 
| 1228 | 
            +
              # Column 2: Sessions + Bookmarks + Recording
         | 
| 1229 | 
            +
              col2 << "SESSIONS:".c(@c_prompt).b
         | 
| 1230 | 
            +
              col2 << ":save_session nm   Save state"
         | 
| 1231 | 
            +
              col2 << ":load_session nm   Load state"
         | 
| 1232 | 
            +
              col2 << ":list_sessions     Show all"
         | 
| 1233 | 
            +
              col2 << ":rmsession nm|*    Delete"
         | 
| 1234 | 
            +
              col2 << ""
         | 
| 1235 | 
            +
              col2 << "BOOKMARKS:".c(@c_prompt).b
         | 
| 1236 | 
            +
              col2 << ":bm nm path #tag   Create"
         | 
| 1237 | 
            +
              col2 << "name               Jump to bookmark"
         | 
| 1238 | 
            +
              col2 << ":bm                List all"
         | 
| 1239 | 
            +
              col2 << ":bm --stats        Statistics"
         | 
| 1240 | 
            +
              col2 << ":bm --export f     Export"
         | 
| 1241 | 
            +
              col2 << ""
         | 
| 1242 | 
            +
              col2 << "RECORDING:".c(@c_prompt).b
         | 
| 1243 | 
            +
              col2 << ":record start nm   Start recording"
         | 
| 1244 | 
            +
              col2 << ":record stop       Stop recording"
         | 
| 1245 | 
            +
              col2 << ":record show nm    Show commands"
         | 
| 1246 | 
            +
              col2 << ":record -nm        Delete"
         | 
| 1247 | 
            +
              col2 << ":replay nm         Execute"
         | 
| 1248 | 
            +
              col2 << ":record            List all"
         | 
| 1249 | 
            +
              col2 << ""
         | 
| 1250 | 
            +
              col2 << "FEATURES:".c(@c_prompt).b
         | 
| 1251 | 
            +
              col2 << "gp branch=main     Nick template"
         | 
| 1252 | 
            +
              col2 << "!!                 Repeat last"
         | 
| 1253 | 
            +
              col2 << "!-2                2nd to last"
         | 
| 1254 | 
            +
              col2 << "!5:7               Chain commands"
         | 
| 1255 | 
            +
              col2 << ":stats --graph     Visual charts"
         | 
| 1256 | 
            +
              col2 << ":completion_stats  Learn patterns"
         | 
| 1257 | 
            +
             | 
| 1258 | 
            +
              # Column 3: Config + Integrations + Expansions
         | 
| 1259 | 
            +
              col3 << "CONFIG:".c(@c_prompt).b
         | 
| 1260 | 
            +
              col3 << ":config auto_correct on         Auto-fix"
         | 
| 1261 | 
            +
              col3 << ":config completion_learning on  Learn TAB"
         | 
| 1262 | 
            +
              col3 << ":config slow_command_threshold 5  Slow warn"
         | 
| 1263 | 
            +
              col3 << ":config session_autosave 300    Auto-save"
         | 
| 1264 | 
            +
              col3 << ":config history_dedup smart     Dedup"
         | 
| 1265 | 
            +
              col3 << ""
         | 
| 1266 | 
            +
              col3 << "INTEGRATIONS:".c(@c_prompt).b
         | 
| 1267 | 
            +
              col3 << "r          rtfm file manager"
         | 
| 1268 | 
            +
              col3 << "f          fzf fuzzy finder"
         | 
| 1269 | 
            +
              col3 << "= expr     xrpn calculator"
         | 
| 1270 | 
            +
              col3 << "@ text     AI text response"
         | 
| 1271 | 
            +
              col3 << "@@ cmd     AI command suggest"
         | 
| 1272 | 
            +
              col3 << ""
         | 
| 1273 | 
            +
              col3 << "EXPANSIONS:".c(@c_prompt).b
         | 
| 1274 | 
            +
              col3 << "~            Home directory"
         | 
| 1275 | 
            +
              col3 << "$VAR         Environment var"
         | 
| 1276 | 
            +
              col3 << "$(cmd)       Command subst"
         | 
| 1277 | 
            +
              col3 << "{a,b,c}      Brace expansion"
         | 
| 1278 | 
            +
              col3 << "cmd1 && cmd2 Conditional AND"
         | 
| 1279 | 
            +
              col3 << "for i in...  Bash scripts"
         | 
| 1280 | 
            +
              col3 << ""
         | 
| 1281 | 
            +
              col3 << "MORE:".c(@c_prompt).b
         | 
| 1282 | 
            +
              col3 << ":help        This help"
         | 
| 1283 | 
            +
              col3 << ":info        About rsh"
         | 
| 1284 | 
            +
              col3 << ":version     Version info"
         | 
| 1285 | 
            +
             | 
| 1208 1286 | 
             
              # Pad columns to same length
         | 
| 1209 | 
            -
              max_lines = [ | 
| 1210 | 
            -
               | 
| 1211 | 
            -
               | 
| 1212 | 
            -
              
         | 
| 1213 | 
            -
             | 
| 1287 | 
            +
              max_lines = [col1.length, col2.length, col3.length].max
         | 
| 1288 | 
            +
              col1.fill("", col1.length...max_lines)
         | 
| 1289 | 
            +
              col2.fill("", col2.length...max_lines)
         | 
| 1290 | 
            +
              col3.fill("", col3.length...max_lines)
         | 
| 1291 | 
            +
             | 
| 1292 | 
            +
              # Print in three columns
         | 
| 1214 1293 | 
             
              puts
         | 
| 1215 1294 | 
             
              max_lines.times do |i|
         | 
| 1216 | 
            -
                 | 
| 1217 | 
            -
                 | 
| 1218 | 
            -
                 | 
| 1219 | 
            -
             | 
| 1220 | 
            -
                padding  | 
| 1221 | 
            -
                 | 
| 1222 | 
            -
                 | 
| 1295 | 
            +
                text1 = col1[i].to_s
         | 
| 1296 | 
            +
                text2 = col2[i].to_s
         | 
| 1297 | 
            +
                text3 = col3[i].to_s
         | 
| 1298 | 
            +
             | 
| 1299 | 
            +
                # Calculate padding for each column
         | 
| 1300 | 
            +
                vis1 = strip_ansi(text1).length
         | 
| 1301 | 
            +
                vis2 = strip_ansi(text2).length
         | 
| 1302 | 
            +
                pad1 = col_width - vis1
         | 
| 1303 | 
            +
                pad2 = col_width - vis2
         | 
| 1304 | 
            +
                pad1 = 0 if pad1 < 0
         | 
| 1305 | 
            +
                pad2 = 0 if pad2 < 0
         | 
| 1306 | 
            +
             | 
| 1307 | 
            +
                puts "  #{text1}#{' ' * pad1} #{text2}#{' ' * pad2} #{text3}"
         | 
| 1223 1308 | 
             
              end
         | 
| 1224 1309 | 
             
              puts
         | 
| 1225 1310 | 
             
            end
         | 
| @@ -2150,6 +2235,175 @@ def apply_validation_rules(cmd) # Apply custom validation rules | |
| 2150 2235 |  | 
| 2151 2236 | 
             
              warnings
         | 
| 2152 2237 | 
             
            end
         | 
| 2238 | 
            +
            def track_completion(context, selected) # Track completion selection for learning
         | 
| 2239 | 
            +
              return unless @completion_learning
         | 
| 2240 | 
            +
              return if context.nil? || selected.nil?
         | 
| 2241 | 
            +
             | 
| 2242 | 
            +
              key = "#{context}:#{selected}"
         | 
| 2243 | 
            +
              @completion_weights[key] ||= 0
         | 
| 2244 | 
            +
              @completion_weights[key] += 1
         | 
| 2245 | 
            +
            end
         | 
| 2246 | 
            +
            def sort_by_learning(context, items) # Sort completions by learning weights
         | 
| 2247 | 
            +
              return items unless @completion_learning
         | 
| 2248 | 
            +
              return items if @completion_weights.empty?
         | 
| 2249 | 
            +
             | 
| 2250 | 
            +
              # Score each item
         | 
| 2251 | 
            +
              scored = items.map do |item|
         | 
| 2252 | 
            +
                # Extract just the switch/command part (before space/description)
         | 
| 2253 | 
            +
                item_key = item.split(/\s+/).first
         | 
| 2254 | 
            +
                key = "#{context}:#{item_key}"
         | 
| 2255 | 
            +
                weight = @completion_weights[key] || 0
         | 
| 2256 | 
            +
                {item: item, weight: weight}
         | 
| 2257 | 
            +
              end
         | 
| 2258 | 
            +
             | 
| 2259 | 
            +
              # Sort: highest weight first, then alphabetically
         | 
| 2260 | 
            +
              scored.sort_by { |s| [-s[:weight], s[:item]] }.map { |s| s[:item] }
         | 
| 2261 | 
            +
            end
         | 
| 2262 | 
            +
            def completion_stats # Show completion learning statistics
         | 
| 2263 | 
            +
              if @completion_weights.empty?
         | 
| 2264 | 
            +
                puts "\nNo completion learning data yet"
         | 
| 2265 | 
            +
                puts "Use TAB completion and selections will be learned over time"
         | 
| 2266 | 
            +
                return
         | 
| 2267 | 
            +
              end
         | 
| 2268 | 
            +
             | 
| 2269 | 
            +
              puts "\n  Completion Learning Statistics".c(@c_prompt).b
         | 
| 2270 | 
            +
              puts "  " + "="*50
         | 
| 2271 | 
            +
             | 
| 2272 | 
            +
              # Group by context
         | 
| 2273 | 
            +
              by_context = {}
         | 
| 2274 | 
            +
              @completion_weights.each do |key, weight|
         | 
| 2275 | 
            +
                context, choice = key.split(':', 2)
         | 
| 2276 | 
            +
                by_context[context] ||= []
         | 
| 2277 | 
            +
                by_context[context] << {choice: choice, weight: weight}
         | 
| 2278 | 
            +
              end
         | 
| 2279 | 
            +
             | 
| 2280 | 
            +
              # Show top contexts
         | 
| 2281 | 
            +
              by_context.sort_by { |ctx, items| -items.map { |i| i[:weight] }.sum }.first(10).each do |context, items|
         | 
| 2282 | 
            +
                puts "\n  #{context}:".c(@c_nick)
         | 
| 2283 | 
            +
                items.sort_by { |i| -i[:weight] }.first(5).each do |item|
         | 
| 2284 | 
            +
                  bar = "■" * ([item[:weight] / 2, 20].min)
         | 
| 2285 | 
            +
                  puts "    #{item[:choice].ljust(20)} #{item[:weight].to_s.rjust(3)}x #{bar.c(@c_path)}"
         | 
| 2286 | 
            +
                end
         | 
| 2287 | 
            +
              end
         | 
| 2288 | 
            +
             | 
| 2289 | 
            +
              puts "\n  Total learned patterns: #{@completion_weights.length}"
         | 
| 2290 | 
            +
              puts
         | 
| 2291 | 
            +
            end
         | 
| 2292 | 
            +
            def completion_reset # Reset all completion learning
         | 
| 2293 | 
            +
              @completion_weights = {}
         | 
| 2294 | 
            +
              puts "Completion learning data cleared"
         | 
| 2295 | 
            +
              rshrc
         | 
| 2296 | 
            +
            end
         | 
| 2297 | 
            +
            def record(*args) # Command recording management
         | 
| 2298 | 
            +
              action = args[0]
         | 
| 2299 | 
            +
              name = args[1]
         | 
| 2300 | 
            +
             | 
| 2301 | 
            +
              if action.nil? || action.empty?
         | 
| 2302 | 
            +
                # List recordings
         | 
| 2303 | 
            +
                if @recordings.empty?
         | 
| 2304 | 
            +
                  puts "\nNo recordings. Use: :record start name"
         | 
| 2305 | 
            +
                  return
         | 
| 2306 | 
            +
                end
         | 
| 2307 | 
            +
             | 
| 2308 | 
            +
                puts "\n  Recordings:".c(@c_prompt).b
         | 
| 2309 | 
            +
                @recordings.each do |rec_name, data|
         | 
| 2310 | 
            +
                  created = Time.at(data[:created] || Time.now.to_i).strftime("%Y-%m-%d %H:%M")
         | 
| 2311 | 
            +
                  count = data[:commands]&.length || 0
         | 
| 2312 | 
            +
                  puts "  #{rec_name.ljust(20)} #{count.to_s.rjust(3)} commands  #{created}"
         | 
| 2313 | 
            +
                end
         | 
| 2314 | 
            +
                puts
         | 
| 2315 | 
            +
             | 
| 2316 | 
            +
                # Show if currently recording
         | 
| 2317 | 
            +
                if @recording[:active]
         | 
| 2318 | 
            +
                  puts "  Currently recording: #{@recording[:name]} (#{@recording[:commands].length} commands so far)".c(214)
         | 
| 2319 | 
            +
                end
         | 
| 2320 | 
            +
              elsif action == 'start' && name
         | 
| 2321 | 
            +
                @recording[:active] = true
         | 
| 2322 | 
            +
                @recording[:name] = name
         | 
| 2323 | 
            +
                @recording[:commands] = []
         | 
| 2324 | 
            +
                @recording[:start_time] = Time.now.to_i
         | 
| 2325 | 
            +
                puts "Recording started: #{name}".c(@c_path)
         | 
| 2326 | 
            +
              elsif action == 'stop'
         | 
| 2327 | 
            +
                if @recording[:active]
         | 
| 2328 | 
            +
                  @recordings[@recording[:name]] = {
         | 
| 2329 | 
            +
                    commands: @recording[:commands],
         | 
| 2330 | 
            +
                    created: @recording[:start_time]
         | 
| 2331 | 
            +
                  }
         | 
| 2332 | 
            +
                  puts "Recording stopped: #{@recording[:name]} (#{@recording[:commands].length} commands)".c(@c_path)
         | 
| 2333 | 
            +
                  @recording[:active] = false
         | 
| 2334 | 
            +
                  rshrc
         | 
| 2335 | 
            +
                else
         | 
| 2336 | 
            +
                  puts "No active recording"
         | 
| 2337 | 
            +
                end
         | 
| 2338 | 
            +
              elsif action == 'status'
         | 
| 2339 | 
            +
                if @recording[:active]
         | 
| 2340 | 
            +
                  puts "Recording: #{@recording[:name]} (#{@recording[:commands].length} commands)"
         | 
| 2341 | 
            +
                  @recording[:commands].last(5).each { |cmd| puts "  #{cmd}" }
         | 
| 2342 | 
            +
                else
         | 
| 2343 | 
            +
                  puts "No active recording"
         | 
| 2344 | 
            +
                end
         | 
| 2345 | 
            +
              elsif action == 'show' && name
         | 
| 2346 | 
            +
                # Show recording contents
         | 
| 2347 | 
            +
                unless @recordings[name]
         | 
| 2348 | 
            +
                  puts "Recording '#{name}' not found"
         | 
| 2349 | 
            +
                  return
         | 
| 2350 | 
            +
                end
         | 
| 2351 | 
            +
             | 
| 2352 | 
            +
                recording = @recordings[name]
         | 
| 2353 | 
            +
                created = Time.at(recording[:created] || Time.now.to_i).strftime("%Y-%m-%d %H:%M:%S")
         | 
| 2354 | 
            +
             | 
| 2355 | 
            +
                puts "\n  Recording: #{name}".c(@c_prompt).b
         | 
| 2356 | 
            +
                puts "  Created: #{created}"
         | 
| 2357 | 
            +
                puts "  Commands: #{recording[:commands].length}"
         | 
| 2358 | 
            +
                puts
         | 
| 2359 | 
            +
             | 
| 2360 | 
            +
                recording[:commands].each_with_index do |cmd, i|
         | 
| 2361 | 
            +
                  puts "  #{(i+1).to_s.rjust(3)}. #{cmd}"
         | 
| 2362 | 
            +
                end
         | 
| 2363 | 
            +
                puts
         | 
| 2364 | 
            +
              elsif action =~ /^-(.+)$/
         | 
| 2365 | 
            +
                # Delete recording
         | 
| 2366 | 
            +
                rec_name = $1
         | 
| 2367 | 
            +
                if @recordings.delete(rec_name)
         | 
| 2368 | 
            +
                  puts "Recording '#{rec_name}' deleted"
         | 
| 2369 | 
            +
                  rshrc
         | 
| 2370 | 
            +
                else
         | 
| 2371 | 
            +
                  puts "Recording '#{rec_name}' not found"
         | 
| 2372 | 
            +
                end
         | 
| 2373 | 
            +
              else
         | 
| 2374 | 
            +
                puts "Usage: :record start name|stop|status|show name|-name"
         | 
| 2375 | 
            +
              end
         | 
| 2376 | 
            +
            end
         | 
| 2377 | 
            +
            def replay(*args) # Replay recorded commands
         | 
| 2378 | 
            +
              name = args[0]
         | 
| 2379 | 
            +
             | 
| 2380 | 
            +
              unless name && @recordings[name]
         | 
| 2381 | 
            +
                puts "Recording '#{name}' not found"
         | 
| 2382 | 
            +
                record
         | 
| 2383 | 
            +
                return
         | 
| 2384 | 
            +
              end
         | 
| 2385 | 
            +
             | 
| 2386 | 
            +
              recording = @recordings[name]
         | 
| 2387 | 
            +
              commands = recording[:commands] || []
         | 
| 2388 | 
            +
             | 
| 2389 | 
            +
              puts "Replaying '#{name}' (#{commands.length} commands)...".c(@c_path)
         | 
| 2390 | 
            +
             | 
| 2391 | 
            +
              commands.each_with_index do |cmd, i|
         | 
| 2392 | 
            +
                puts "\n[#{i+1}/#{commands.length}] #{cmd}".c(@c_stamp)
         | 
| 2393 | 
            +
             | 
| 2394 | 
            +
                result = system(cmd)
         | 
| 2395 | 
            +
                exit_code = $?.exitstatus
         | 
| 2396 | 
            +
             | 
| 2397 | 
            +
                unless result
         | 
| 2398 | 
            +
                  puts " Command failed (exit #{exit_code})".c(196)
         | 
| 2399 | 
            +
                  print "Continue? (Y/n): "
         | 
| 2400 | 
            +
                  response = $stdin.gets.chomp
         | 
| 2401 | 
            +
                  break if response.downcase == 'n'
         | 
| 2402 | 
            +
                end
         | 
| 2403 | 
            +
              end
         | 
| 2404 | 
            +
             | 
| 2405 | 
            +
              puts "\nReplay complete".c(@c_path)
         | 
| 2406 | 
            +
            end
         | 
| 2153 2407 | 
             
            def apply_auto_correct(cmd) # Apply auto-correction to command
         | 
| 2154 2408 | 
             
              return cmd unless @auto_correct
         | 
| 2155 2409 | 
             
              return cmd if cmd =~ /^:/  # Don't auto-correct colon commands
         | 
| @@ -2466,6 +2720,10 @@ def load_rshrc_safe | |
| 2466 2720 | 
             
                @plugins = [] unless @plugins.is_a?(Array)
         | 
| 2467 2721 | 
             
                @plugin_commands = {} unless @plugin_commands.is_a?(Hash)
         | 
| 2468 2722 | 
             
                @validation_rules = [] unless @validation_rules.is_a?(Array)
         | 
| 2723 | 
            +
                @completion_weights = {} unless @completion_weights.is_a?(Hash)
         | 
| 2724 | 
            +
                @completion_learning = true if @completion_learning.nil?
         | 
| 2725 | 
            +
                @recording = {active: false, name: nil, commands: []} unless @recording.is_a?(Hash)
         | 
| 2726 | 
            +
                @recordings = {} unless @recordings.is_a?(Hash)
         | 
| 2469 2727 |  | 
| 2470 2728 | 
             
                # Restore defuns from .rshrc
         | 
| 2471 2729 | 
             
                if @defuns && !@defuns.empty?
         | 
| @@ -2608,13 +2866,47 @@ def load_defaults | |
| 2608 2866 | 
             
              @plugins ||= []
         | 
| 2609 2867 | 
             
              @plugin_commands ||= {}
         | 
| 2610 2868 | 
             
              @validation_rules ||= []
         | 
| 2869 | 
            +
              @completion_weights ||= {}
         | 
| 2870 | 
            +
              @completion_learning = true if @completion_learning.nil?
         | 
| 2871 | 
            +
              @recording ||= {active: false, name: nil, commands: []}
         | 
| 2872 | 
            +
              @recordings ||= {}
         | 
| 2611 2873 | 
             
              puts "Loaded with default configuration."
         | 
| 2612 2874 | 
             
            end
         | 
| 2613 2875 |  | 
| 2876 | 
            +
            def cached_command(cmd, ttl = 300) # Cache expensive command outputs
         | 
| 2877 | 
            +
              key = cmd.hash
         | 
| 2878 | 
            +
              now = Time.now.to_i
         | 
| 2879 | 
            +
             | 
| 2880 | 
            +
              if @command_cache[key] && (now - @command_cache[key][:time]) < ttl
         | 
| 2881 | 
            +
                return @command_cache[key][:result]
         | 
| 2882 | 
            +
              end
         | 
| 2883 | 
            +
             | 
| 2884 | 
            +
              result = `#{cmd}`.chomp
         | 
| 2885 | 
            +
              @command_cache[key] = {result: result, time: now}
         | 
| 2886 | 
            +
             | 
| 2887 | 
            +
              # Limit cache size
         | 
| 2888 | 
            +
              if @command_cache.length > 50
         | 
| 2889 | 
            +
                # Remove oldest entry
         | 
| 2890 | 
            +
                oldest = @command_cache.min_by { |k, v| v[:time] }
         | 
| 2891 | 
            +
                @command_cache.delete(oldest[0])
         | 
| 2892 | 
            +
              end
         | 
| 2893 | 
            +
             | 
| 2894 | 
            +
              result
         | 
| 2895 | 
            +
            end
         | 
| 2614 2896 | 
             
            def cache_executables
         | 
| 2615 2897 | 
             
              current_path = ENV["PATH"]
         | 
| 2616 2898 | 
             
              current_time = Time.now.to_i
         | 
| 2617 2899 |  | 
| 2900 | 
            +
              # Use persisted cache if valid (from .rshrc)
         | 
| 2901 | 
            +
              if @exe_cache && @exe_cache_path == current_path
         | 
| 2902 | 
            +
                cache_age = current_time - (@exe_cache_time || 0)
         | 
| 2903 | 
            +
                if cache_age < 3600  # 1 hour
         | 
| 2904 | 
            +
                  @exe = @exe_cache
         | 
| 2905 | 
            +
                  @exe_cache_paths = current_path
         | 
| 2906 | 
            +
                  return
         | 
| 2907 | 
            +
                end
         | 
| 2908 | 
            +
              end
         | 
| 2909 | 
            +
             | 
| 2618 2910 | 
             
              # Only rebuild cache if PATH changed or cache is older than 60 seconds
         | 
| 2619 2911 | 
             
              return if @exe_cache_paths == current_path && (current_time - @exe_cache_time) < 60
         | 
| 2620 2912 |  | 
| @@ -2689,7 +2981,12 @@ loop do | |
| 2689 2981 | 
             
              begin
         | 
| 2690 2982 | 
             
                @user = Etc.getpwuid(Process.euid).name # For use in @prompt
         | 
| 2691 2983 | 
             
                @node = Etc.uname[:nodename]            # For use in @prompt
         | 
| 2692 | 
            -
                 | 
| 2984 | 
            +
                # Only reload .rshrc if directory changed (optimization)
         | 
| 2985 | 
            +
                current_dir = Dir.pwd
         | 
| 2986 | 
            +
                if @last_prompt_dir != current_dir
         | 
| 2987 | 
            +
                  h = @history; f = @cmd_frequency; s = @cmd_stats; b = @bookmarks; d = @defuns; load_rshrc_safe; @history = h; @cmd_frequency = f; @cmd_stats = s; @bookmarks = b; @defuns = d
         | 
| 2988 | 
            +
                  @last_prompt_dir = current_dir
         | 
| 2989 | 
            +
                end
         | 
| 2693 2990 | 
             
                @prompt.gsub!(/#{Dir.home}/, '~') # Simplify path in prompt
         | 
| 2694 2991 | 
             
                system("printf \"\033]0;rsh: #{Dir.pwd}\007\"")   # Set Window title to path
         | 
| 2695 2992 | 
             
                @history[0] = "" unless @history[0]
         | 
| @@ -2801,7 +3098,8 @@ loop do | |
| 2801 3098 | 
             
                      # List of all known rsh commands (since respond_to? doesn't work for top-level methods)
         | 
| 2802 3099 | 
             
                      known_commands = %w[nick gnick defun defun? bm bookmark stats calc config env theme plugins
         | 
| 2803 3100 | 
             
                                          save_session load_session list_sessions delete_session rmsession
         | 
| 2804 | 
            -
                                          validate
         | 
| 3101 | 
            +
                                          validate completion_stats completion_reset
         | 
| 3102 | 
            +
                                          record replay
         | 
| 2805 3103 | 
             
                                          history rmhistory jobs fg bg dirs help info version]
         | 
| 2806 3104 |  | 
| 2807 3105 | 
             
                      # Try to call as rsh method
         | 
| @@ -3058,6 +3356,14 @@ loop do | |
| 3058 3356 | 
             
                            puts "⚠ Command took #{'%.1f' % elapsed}s (threshold: #{@slow_command_threshold}s)".c(214)
         | 
| 3059 3357 | 
             
                          end
         | 
| 3060 3358 |  | 
| 3359 | 
            +
                          # Record command if recording is active
         | 
| 3360 | 
            +
                          if @recording[:active] && @last_exit == 0
         | 
| 3361 | 
            +
                            # Don't record :record commands themselves
         | 
| 3362 | 
            +
                            unless @cmd =~ /^:record/
         | 
| 3363 | 
            +
                              @recording[:commands] << @cmd
         | 
| 3364 | 
            +
                            end
         | 
| 3365 | 
            +
                          end
         | 
| 3366 | 
            +
             | 
| 3061 3367 | 
             
                          # Call plugin on_command_after hooks
         | 
| 3062 3368 | 
             
                          call_plugin_hook(:on_command_after, @cmd, @last_exit)
         | 
| 3063 3369 |  | 
    
        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.1
         | 
| 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
         |