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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -1
  3. data/bin/rsh +393 -133
  4. metadata +5 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd3cc49ebe2106b1077e32c07694a7d2cd4a302aba740baa1fd744ab4f097d7e
4
- data.tar.gz: 86a2a648f07605d15850908faf20d92b36a07a501c94d5c88262d9b9aac07b24
3
+ metadata.gz: 5b86a585bea4b0c41a27de5679365d3ddce7b7a33481927cbf0de9bd89dcf27b
4
+ data.tar.gz: f346b777aa2863b06ef610a8e9c13325711058452f8986e5d4565aecb8ae21d4
5
5
  SHA512:
6
- metadata.gz: c9dc294728b34590500a3700f2b7d1c0ca4a42bbce9e4868403f6056cf1fda458ac30a43f6b83013c000ed48d51abe177e0ae9ab2855ee77a85bffc134267ae1
7
- data.tar.gz: a4fbc3bcd9b71a81e89bb91bb4eba80bec7e75ea417cd8170a3150e34de9a7d7a6b63827b00e25a9195dc0198327fb7f25d7284dc7740d4a8b35abdb5e850937
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.3.0 - Quote-less Syntax, Parametrized Nicks & More ⭐⭐⭐
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.3.0" # Quote-less syntax: Simplified colon commands without quotes for cleaner UX
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.3:
192
- * Quote-less syntax - No more quotes! Use :nick la = ls -la instead of :nick "la = ls -la"
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
- tabbing("hist")
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
- case last_cmd
571
- when "cd", "pushd", "rmdir"
572
- type = "dirs_only"
573
- when "vim", "nano", "cat", "less", "more", "head", "tail", "file"
574
- type = "files_only"
575
- when "man", "info", "which", "whatis"
576
- type = "commands_only"
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
- # Check if command has defined completions and we're on the first argument
581
- if @cmd_completions.key?(last_cmd) && cmd_parts.length == 1
582
- type = "cmd_subcommands"
583
- @current_cmd = last_cmd
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 || 80
1105
- col_width = 48 # Fixed width for left column
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
- left_col = []
1113
- right_col = []
1114
-
1115
- # Left column content
1116
- left_col << "KEYBOARD SHORTCUTS:".c(@c_prompt).b
1117
- left_col << "RIGHT/Ctrl-F Accept suggestion"
1118
- left_col << "UP/DOWN Navigate history"
1119
- left_col << "TAB Tab complete"
1120
- left_col << "Shift-TAB Search history"
1121
- left_col << "Ctrl-G Edit in $EDITOR"
1122
- left_col << "Ctrl-Y Copy to clipboard"
1123
- left_col << "Ctrl-D Exit + save .rshrc"
1124
- left_col << "Ctrl-E Exit without save"
1125
- left_col << "Ctrl-L Clear screen"
1126
- left_col << "Ctrl-Z Suspend job"
1127
- left_col << "Ctrl-C Clear line"
1128
- left_col << "Ctrl-K Delete history item"
1129
- left_col << "Ctrl-U Clear line"
1130
- left_col << "Ctrl-W Delete previous word"
1131
- left_col << ""
1132
- left_col << "SPECIAL COMMANDS:".c(@c_prompt).b
1133
- left_col << ":nick ll = ls -l Command alias"
1134
- left_col << ":gnick h = /home General alias"
1135
- left_col << ":nick List nicks"
1136
- left_col << ":gnick List gnicks"
1137
- left_col << ":nick -name Delete nick"
1138
- left_col << ":gnick -name Delete gnick"
1139
- left_col << ":history Show history"
1140
- left_col << ":rmhistory Clear history"
1141
- left_col << ":info About rsh"
1142
- left_col << ":version Version info"
1143
- left_col << ":help This help"
1144
-
1145
- # Right column content
1146
- right_col << "RUBY FUNCTIONS:".c(@c_prompt).b
1147
- right_col << ":defun f(x) = x*2 Define function"
1148
- right_col << ":defun? List functions"
1149
- right_col << ":defun -f Remove function"
1150
- right_col << "Call as: f 5 (returns 10)"
1151
- right_col << ""
1152
- right_col << "JOB CONTROL:".c(@c_prompt).b
1153
- right_col << "command & Background job"
1154
- right_col << ":jobs List jobs"
1155
- right_col << ":fg [id] Foreground job"
1156
- right_col << ":bg [id] Resume in bg"
1157
- right_col << ""
1158
- right_col << "v3.3 NEW FEATURES:".c(@c_prompt).b
1159
- right_col << "No quotes! :nick la = ls -la"
1160
- right_col << ":nick gp={{branch}} Parametrized"
1161
- right_col << ":validate pat = act Safety rules"
1162
- right_col << "Ctrl-G Edit in \$EDITOR"
1163
- right_col << "for i in {1..5} Shell scripts"
1164
- right_col << ""
1165
- right_col << "v3.2 FEATURES:".c(@c_prompt).b
1166
- right_col << ":plugins [cmd] Plugin system"
1167
- right_col << ":stats --graph Visual charts"
1168
- right_col << ":calc 2 + 2 Calculator"
1169
- right_col << "!!, !-2, !5:7 History chain"
1170
- right_col << ""
1171
- right_col << "PLUGIN SYSTEM:".c(@c_prompt).b
1172
- right_col << ":plugins List all"
1173
- right_col << ":plugins reload Reload"
1174
- right_col << ":plugins disable nm Disable"
1175
- right_col << ""
1176
- right_col << "MORE FEATURES:".c(@c_prompt).b
1177
- right_col << ":config auto_correct Auto-fix"
1178
- right_col << ":bm name path #tags Bookmarks"
1179
- right_col << ":save_session nm Sessions"
1180
- right_col << ":theme name Themes"
1181
- right_col << ":env VAR Env vars"
1182
- right_col << ""
1183
- right_col << "INTEGRATIONS:".c(@c_prompt).b
1184
- right_col << "r Launch rtfm"
1185
- right_col << "f Launch fzf"
1186
- right_col << "= <expr> xrpn calculator"
1187
- right_col << ":<ruby code> Execute Ruby"
1188
- right_col << ""
1189
- right_col << "AI FEATURES:".c(@c_prompt).b
1190
- right_col << "@ <question> AI text response"
1191
- right_col << "@@ <request> AI command prompt"
1192
- right_col << ""
1193
- right_col << "SMART COMPLETIONS:".c(@c_prompt).b
1194
- right_col << "git <TAB> Git subcommands"
1195
- right_col << "apt/docker <TAB> Command options"
1196
- right_col << "--format=<TAB> Option values"
1197
- right_col << "Typo suggestions Auto-correct"
1198
- right_col << ""
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 = [left_col.length, right_col.length].max
1210
- left_col.fill("", left_col.length...max_lines)
1211
- right_col.fill("", right_col.length...max_lines)
1212
-
1213
- # Print in two columns
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
- left_text = left_col[i].to_s
1217
- right_text = right_col[i].to_s
1218
- # Calculate padding based on visible characters (without ANSI codes)
1219
- visible_length = strip_ansi(left_text).length
1220
- padding = col_width - visible_length
1221
- padding = 0 if padding < 0
1222
- puts " #{left_text}#{' ' * padding} #{right_text}"
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.3.0
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.3.0: QUOTE-LESS SYNTAX - No quotes needed! Parametrized nicks with {{placeholders}}
16
- - :nick gp=git push {{branch}}, use: gp branch=main. Ctrl-G multi-line editing in
17
- $EDITOR. Custom validation rules. Full bash shell script support. Simpler, cleaner,
18
- more powerful!'
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