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
|