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