ruby-shell 3.0.0 → 3.2.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/PLUGIN_GUIDE.md +757 -0
- data/README.md +89 -6
- data/bin/rsh +858 -48
- metadata +5 -4
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.2.0" # Plugin system: Extensible architecture with lifecycle hooks, completions, commands, and plugin management
|
|
12
12
|
|
|
13
13
|
# MODULES, CLASSES AND EXTENSIONS
|
|
14
14
|
class String # Add coloring to strings (with escaping for Readline)
|
|
@@ -93,6 +93,7 @@ begin # Initialization
|
|
|
93
93
|
@c_path = 3 # Color for valid path
|
|
94
94
|
@c_switch = 6 # Color for switches/options
|
|
95
95
|
@c_bookmark = 13 # Color for bookmarks
|
|
96
|
+
@c_colon = 4 # Color for colon commands
|
|
96
97
|
@c_tabselect = 5 # Color for selected tabcompleted item
|
|
97
98
|
@c_taboption = 244 # Color for unselected tabcompleted item
|
|
98
99
|
@c_stamp = 244 # Color for time stamp/command
|
|
@@ -138,7 +139,21 @@ begin # Initialization
|
|
|
138
139
|
@bookmarks = {} # Enhanced bookmarks with tags
|
|
139
140
|
@defuns = {} # Store defun definitions for persistence
|
|
140
141
|
@cmd_stats = {} # Command execution statistics
|
|
141
|
-
@
|
|
142
|
+
@session_dir = Dir.home + '/.rsh/sessions' # Sessions directory
|
|
143
|
+
@session_file = @session_dir + '/default.json' # Default session file
|
|
144
|
+
@session_autosave = 0 # Auto-save interval (0 = disabled)
|
|
145
|
+
@session_last_save = Time.now.to_i # Last auto-save timestamp
|
|
146
|
+
@history_dedup = 'smart' # History dedup mode: 'off', 'full', 'smart'
|
|
147
|
+
@auto_correct = false # Auto-correct typos (default: off)
|
|
148
|
+
@slow_command_threshold = 0 # Threshold for slow command alerts (0 = disabled)
|
|
149
|
+
@plugin_dir = Dir.home + '/.rsh/plugins' # Plugins directory
|
|
150
|
+
@plugins = [] # Loaded plugin instances
|
|
151
|
+
@plugin_disabled = [] # List of disabled plugin names
|
|
152
|
+
@plugin_commands = {} # Commands added by plugins
|
|
153
|
+
# Built-in rsh commands are called with : prefix, so no need for separate tracking
|
|
154
|
+
Dir.mkdir(Dir.home + '/.rsh') unless Dir.exist?(Dir.home + '/.rsh')
|
|
155
|
+
Dir.mkdir(@session_dir) unless Dir.exist?(@session_dir)
|
|
156
|
+
Dir.mkdir(@plugin_dir) unless Dir.exist?(@plugin_dir)
|
|
142
157
|
def pre_cmd; end # User-defined function to be run BEFORE command execution
|
|
143
158
|
def post_cmd; end # User-defined function to be run AFTER command execution
|
|
144
159
|
end
|
|
@@ -146,7 +161,7 @@ end
|
|
|
146
161
|
# HELP TEXT
|
|
147
162
|
@info = <<~INFO
|
|
148
163
|
|
|
149
|
-
Hello #{@user}, welcome to rsh v3.
|
|
164
|
+
Hello #{@user}, welcome to rsh v3.2 - the Ruby SHell.
|
|
150
165
|
|
|
151
166
|
rsh does not attempt to compete with the grand old shells like bash and zsh.
|
|
152
167
|
It serves the specific needs and wants of its author. If you like it, then feel free
|
|
@@ -172,6 +187,28 @@ end
|
|
|
172
187
|
* Syntax validation - Pre-execution warnings for dangerous or malformed commands
|
|
173
188
|
* Unified command syntax - :nick, :gnick, :bm all work the same way (list/create/delete)
|
|
174
189
|
|
|
190
|
+
NEW in v3.2:
|
|
191
|
+
* Plugin system - Extensible architecture for custom commands, completions, and hooks
|
|
192
|
+
* Lifecycle hooks - on_startup, on_command_before, on_command_after, on_prompt
|
|
193
|
+
* Plugin management - :plugins list/reload/enable/disable/info
|
|
194
|
+
* Example plugins - git_prompt, command_logger, kubectl_completion included
|
|
195
|
+
* Auto-correct typos - :config "auto_correct" "on" automatically fixes gti → closest match
|
|
196
|
+
* Command timing alerts - :config "slow_command_threshold" "5" warns on slow commands
|
|
197
|
+
* Inline calculator - :calc 2 + 2, :calc "Math.sqrt(16)", full Ruby Math library
|
|
198
|
+
* Enhanced history - !!, !-2, !5:7 for repeat last, nth-to-last, and chaining
|
|
199
|
+
* Stats visualization - :stats --graph for colorful ASCII bar charts
|
|
200
|
+
* See PLUGIN_GUIDE.md for complete plugin development documentation
|
|
201
|
+
|
|
202
|
+
v3.1 Features:
|
|
203
|
+
* Multiple named sessions - :save_session "project" and :load_session "project"
|
|
204
|
+
* Stats export - :stats --csv or :stats --json for data analysis
|
|
205
|
+
* Session auto-save - Set @session_autosave = 300 in .rshrc for 5-min auto-save
|
|
206
|
+
* Bookmark import/export - :bm --export file.json and :bm --import file.json
|
|
207
|
+
* Bookmark statistics - :bm --stats shows usage patterns and tag distribution
|
|
208
|
+
* Color themes - :theme solarized|dracula|gruvbox|nord|monokai
|
|
209
|
+
* Config management - :config shows/sets history_dedup, session_autosave, etc.
|
|
210
|
+
* Environment management - :env for listing/setting/exporting environment variables
|
|
211
|
+
|
|
175
212
|
Config file (.rshrc) updates on exit (Ctrl-d) or not (Ctrl-e).
|
|
176
213
|
All colors are themeable in .rshrc (see github link for possibilities).
|
|
177
214
|
|
|
@@ -206,6 +243,7 @@ def firstrun
|
|
|
206
243
|
@c_path = 208 # Color for valid path
|
|
207
244
|
@c_switch = 148 # Color for switches/options
|
|
208
245
|
@c_bookmark = 13 # Color for bookmarks
|
|
246
|
+
@c_colon = 4 # Color for colon commands
|
|
209
247
|
@c_tabselect = 207 # Color for selected tabcompleted item
|
|
210
248
|
@c_taboption = 244 # Color for unselected tabcompleted item
|
|
211
249
|
@c_stamp = 244 # Color for time stamp/command
|
|
@@ -485,6 +523,7 @@ def tab(type)
|
|
|
485
523
|
type = "switch" if @tabstr && @tabstr[0] == "-" && !@tabstr.include?("=")
|
|
486
524
|
type = "option_value" if @tabstr && @tabstr =~ /^--?[\w-]+=/
|
|
487
525
|
type = "env_vars" if @tabstr && @tabstr[0] == "$"
|
|
526
|
+
type = "colon_commands" if @tabstr && @tabstr[0] == ":"
|
|
488
527
|
|
|
489
528
|
# Debug output when RSH_DEBUG is set
|
|
490
529
|
if ENV['RSH_DEBUG']
|
|
@@ -576,12 +615,25 @@ def tab(type)
|
|
|
576
615
|
env_vars = ENV.keys.map { |k| "$#{k}" }
|
|
577
616
|
regex_flags = @completion_case_sensitive ? 0 : Regexp::IGNORECASE
|
|
578
617
|
@tabarray = env_vars.select { |var| var =~ Regexp.new(@tabstr, regex_flags) }
|
|
618
|
+
when "colon_commands" # Ruby/rsh commands starting with :
|
|
619
|
+
colon_cmds = %w[
|
|
620
|
+
:nick :gnick :bm :bookmark :stats :defun :defun?
|
|
621
|
+
:history :rmhistory :jobs :fg :bg
|
|
622
|
+
:save_session :load_session :list_sessions :delete_session :rmsession
|
|
623
|
+
:config :env :theme :plugins :calc
|
|
624
|
+
:info :version :help
|
|
625
|
+
]
|
|
626
|
+
search_str = @tabstr[1..-1] || "" # Remove leading :
|
|
627
|
+
regex_flags = @completion_case_sensitive ? 0 : Regexp::IGNORECASE
|
|
628
|
+
matches = colon_cmds.select { |cmd| cmd[1..-1] =~ Regexp.new("^#{search_str}", regex_flags) }
|
|
629
|
+
@tabarray = matches
|
|
579
630
|
when "all" # Handle all other tab completions
|
|
580
631
|
ex = []
|
|
581
632
|
ex += @exe
|
|
582
633
|
ex.sort!
|
|
583
634
|
ex.prepend(*@nick.keys) # Add nicks
|
|
584
635
|
ex.prepend(*@gnick.keys) # Add gnicks
|
|
636
|
+
ex.prepend(*@bookmarks.keys) if @bookmarks # Add bookmarks
|
|
585
637
|
|
|
586
638
|
# Enhanced matching with case sensitivity and fuzzy support
|
|
587
639
|
regex_flags = @completion_case_sensitive ? 0 : Regexp::IGNORECASE
|
|
@@ -829,9 +881,106 @@ def suggest_command(cmd) # Smart command suggestions for typos
|
|
|
829
881
|
candidates.first(3)
|
|
830
882
|
end
|
|
831
883
|
def hist_clean # Clean up @history
|
|
832
|
-
@history.uniq!
|
|
833
884
|
@history.compact!
|
|
834
885
|
@history.delete("")
|
|
886
|
+
|
|
887
|
+
# Apply deduplication based on mode
|
|
888
|
+
case @history_dedup
|
|
889
|
+
when 'off'
|
|
890
|
+
# No deduplication
|
|
891
|
+
when 'full', 'smart'
|
|
892
|
+
# Remove duplicates, keeping first (most recent) occurrence
|
|
893
|
+
@history.uniq!
|
|
894
|
+
else
|
|
895
|
+
# Default to smart
|
|
896
|
+
@history.uniq!
|
|
897
|
+
end
|
|
898
|
+
end
|
|
899
|
+
def config(*args) # Configure rsh settings
|
|
900
|
+
setting = args[0]
|
|
901
|
+
value = args[1]
|
|
902
|
+
|
|
903
|
+
if setting.nil?
|
|
904
|
+
# Show current configuration
|
|
905
|
+
puts "\n Current Configuration:".c(@c_prompt).b
|
|
906
|
+
puts " history_dedup: #{@history_dedup}"
|
|
907
|
+
puts " session_autosave: #{@session_autosave}s #{@session_autosave > 0 ? '(enabled)' : '(disabled)'}"
|
|
908
|
+
puts " auto_correct: #{@auto_correct ? 'on' : 'off'}"
|
|
909
|
+
puts " slow_command_threshold: #{@slow_command_threshold}s #{@slow_command_threshold > 0 ? '(enabled)' : '(disabled)'}"
|
|
910
|
+
puts " completion_limit: #{@completion_limit}"
|
|
911
|
+
puts " completion_fuzzy: #{@completion_fuzzy}"
|
|
912
|
+
puts " completion_case_sensitive: #{@completion_case_sensitive}"
|
|
913
|
+
puts
|
|
914
|
+
return
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
case setting
|
|
918
|
+
when 'history_dedup'
|
|
919
|
+
if %w[off full smart].include?(value)
|
|
920
|
+
@history_dedup = value
|
|
921
|
+
puts "History deduplication set to '#{value}'"
|
|
922
|
+
rshrc
|
|
923
|
+
else
|
|
924
|
+
puts "Invalid value. Use: off, full, or smart"
|
|
925
|
+
end
|
|
926
|
+
when 'session_autosave'
|
|
927
|
+
@session_autosave = value.to_i
|
|
928
|
+
puts "Session auto-save set to #{value}s #{value.to_i > 0 ? '(enabled)' : '(disabled)'}"
|
|
929
|
+
rshrc
|
|
930
|
+
when 'auto_correct'
|
|
931
|
+
@auto_correct = %w[on true yes 1].include?(value.to_s.downcase)
|
|
932
|
+
puts "Auto-correct #{@auto_correct ? 'enabled' : 'disabled'}"
|
|
933
|
+
rshrc
|
|
934
|
+
when 'slow_command_threshold'
|
|
935
|
+
@slow_command_threshold = value.to_i
|
|
936
|
+
puts "Slow command threshold set to #{value}s #{value.to_i > 0 ? '(enabled)' : '(disabled)'}"
|
|
937
|
+
rshrc
|
|
938
|
+
when 'completion_limit'
|
|
939
|
+
@completion_limit = value.to_i
|
|
940
|
+
puts "Completion limit set to #{value}"
|
|
941
|
+
rshrc
|
|
942
|
+
else
|
|
943
|
+
puts "Unknown setting '#{setting}'"
|
|
944
|
+
puts "Available: history_dedup, session_autosave, auto_correct, slow_command_threshold, completion_limit"
|
|
945
|
+
end
|
|
946
|
+
end
|
|
947
|
+
def env(*args) # Environment variable management
|
|
948
|
+
arg_str = args.join(' ')
|
|
949
|
+
|
|
950
|
+
if args.empty?
|
|
951
|
+
# List all environment variables
|
|
952
|
+
puts "\n Environment Variables:".c(@c_prompt).b
|
|
953
|
+
ENV.sort.first(20).each do |key, value|
|
|
954
|
+
value_display = value.length > 50 ? value[0..47] + '...' : value
|
|
955
|
+
puts " #{key.c(@c_gnick).ljust(25)} = #{value_display}"
|
|
956
|
+
end
|
|
957
|
+
puts " ... (#{ENV.length} total, showing first 20)"
|
|
958
|
+
puts "\n Use :env \"VARNAME\" to see specific variable"
|
|
959
|
+
puts
|
|
960
|
+
elsif arg_str =~ /^set\s+(\w+)\s+(.+)$/
|
|
961
|
+
# Set environment variable
|
|
962
|
+
var_name, var_value = $1, $2
|
|
963
|
+
ENV[var_name] = var_value
|
|
964
|
+
puts "#{var_name} = #{var_value}"
|
|
965
|
+
elsif arg_str =~ /^unset\s+(\w+)$/
|
|
966
|
+
# Unset environment variable
|
|
967
|
+
var_name = $1
|
|
968
|
+
ENV.delete(var_name)
|
|
969
|
+
puts "#{var_name} unset"
|
|
970
|
+
elsif arg_str =~ /^export\s+(.+)$/
|
|
971
|
+
# Export to shell script
|
|
972
|
+
filename = $1
|
|
973
|
+
File.write(filename, ENV.map { |k,v| "export #{k}=\"#{v}\"" }.join("\n"))
|
|
974
|
+
puts "Environment exported to #{filename}"
|
|
975
|
+
else
|
|
976
|
+
# Show specific variable
|
|
977
|
+
var_name = arg_str.strip
|
|
978
|
+
if ENV[var_name]
|
|
979
|
+
puts "#{var_name} = #{ENV[var_name]}"
|
|
980
|
+
else
|
|
981
|
+
puts "Environment variable '#{var_name}' not set"
|
|
982
|
+
end
|
|
983
|
+
end
|
|
835
984
|
end
|
|
836
985
|
def cmd_check(str) # Check if each element on the readline matches commands, nicks, paths; color them
|
|
837
986
|
return if str.nil?
|
|
@@ -840,7 +989,14 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic
|
|
|
840
989
|
if str =~ /^(@@?)\s+(.*)$/
|
|
841
990
|
prefix = $1
|
|
842
991
|
rest = $2
|
|
843
|
-
return prefix.c(
|
|
992
|
+
return prefix.c(@c_colon) + " " + rest # Color @ or @@ in colon color
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
# Special handling for : commands
|
|
996
|
+
if str =~ /^(:[\w?_]+)/
|
|
997
|
+
colon_cmd = $1
|
|
998
|
+
rest = str.sub(/^:[\w?_]+/, '')
|
|
999
|
+
return colon_cmd.c(@c_colon) + rest # Color colon commands
|
|
844
1000
|
end
|
|
845
1001
|
|
|
846
1002
|
str.gsub(/(?:\S'[^']*'|[^ '])+/) do |el|
|
|
@@ -892,6 +1048,16 @@ def rshrc # Write updates to .rshrc
|
|
|
892
1048
|
conf += "@bookmarks = #{@bookmarks}\n" unless @bookmarks.empty?
|
|
893
1049
|
conf.sub!(/^@defuns.*(\n|$)/, "")
|
|
894
1050
|
conf += "@defuns = #{@defuns}\n" unless @defuns.empty?
|
|
1051
|
+
conf.sub!(/^@history_dedup.*(\n|$)/, "")
|
|
1052
|
+
conf += "@history_dedup = '#{@history_dedup}'\n" if @history_dedup && @history_dedup != 'smart'
|
|
1053
|
+
conf.sub!(/^@session_autosave.*(\n|$)/, "")
|
|
1054
|
+
conf += "@session_autosave = #{@session_autosave}\n" if @session_autosave && @session_autosave > 0
|
|
1055
|
+
conf.sub!(/^@auto_correct.*(\n|$)/, "")
|
|
1056
|
+
conf += "@auto_correct = #{@auto_correct}\n" if @auto_correct
|
|
1057
|
+
conf.sub!(/^@slow_command_threshold.*(\n|$)/, "")
|
|
1058
|
+
conf += "@slow_command_threshold = #{@slow_command_threshold}\n" if @slow_command_threshold && @slow_command_threshold > 0
|
|
1059
|
+
conf.sub!(/^@plugin_disabled.*(\n|$)/, "")
|
|
1060
|
+
conf += "@plugin_disabled = #{@plugin_disabled}\n" unless @plugin_disabled.empty?
|
|
895
1061
|
# Only write @cmd_completions if user has customized it
|
|
896
1062
|
unless conf =~ /^@cmd_completions\s*=/
|
|
897
1063
|
# Don't write default completions to avoid cluttering .rshrc
|
|
@@ -928,6 +1094,7 @@ def help
|
|
|
928
1094
|
left_col << "RIGHT/Ctrl-F Accept suggestion"
|
|
929
1095
|
left_col << "UP/DOWN Navigate history"
|
|
930
1096
|
left_col << "TAB Tab complete"
|
|
1097
|
+
left_col << "Shift-TAB Search history"
|
|
931
1098
|
left_col << "Ctrl-Y Copy to clipboard"
|
|
932
1099
|
left_col << "Ctrl-D Exit + save .rshrc"
|
|
933
1100
|
left_col << "Ctrl-E Exit without save"
|
|
@@ -964,15 +1131,28 @@ def help
|
|
|
964
1131
|
right_col << ":fg [id] Foreground job"
|
|
965
1132
|
right_col << ":bg [id] Resume in bg"
|
|
966
1133
|
right_col << ""
|
|
967
|
-
right_col << "v3.
|
|
968
|
-
right_col << ":
|
|
969
|
-
right_col << ":
|
|
970
|
-
right_col << "
|
|
971
|
-
right_col << ":
|
|
972
|
-
right_col << ":
|
|
973
|
-
right_col << ":
|
|
974
|
-
right_col << "
|
|
975
|
-
right_col << ":
|
|
1134
|
+
right_col << "v3.2 NEW FEATURES:".c(@c_prompt).b
|
|
1135
|
+
right_col << ":plugins [cmd] Plugin system"
|
|
1136
|
+
right_col << ":stats --graph Visual bar charts"
|
|
1137
|
+
right_col << ":calc 2 + 2 Ruby calculator"
|
|
1138
|
+
right_col << "!!, !-2, !5:7 History repeat/chain"
|
|
1139
|
+
right_col << ":config auto_correct Auto-fix (w/confirm)"
|
|
1140
|
+
right_col << ":config slow_cmd... Slow alerts"
|
|
1141
|
+
right_col << ""
|
|
1142
|
+
right_col << "PLUGIN SYSTEM:".c(@c_prompt).b
|
|
1143
|
+
right_col << ":plugins List all"
|
|
1144
|
+
right_col << ":plugins reload Reload"
|
|
1145
|
+
right_col << ":plugins disable nm Disable"
|
|
1146
|
+
right_col << ":plugins info nm Details"
|
|
1147
|
+
right_col << ""
|
|
1148
|
+
right_col << "v3.0/3.1 FEATURES:".c(@c_prompt).b
|
|
1149
|
+
right_col << ":stats --clear Clear stats"
|
|
1150
|
+
right_col << ":stats --csv|--json Export stats"
|
|
1151
|
+
right_col << ":bm \"name\" Bookmarks"
|
|
1152
|
+
right_col << ":save_session [nm] Sessions"
|
|
1153
|
+
right_col << ":theme [name] Themes"
|
|
1154
|
+
right_col << ":config [set val] Config"
|
|
1155
|
+
right_col << ":env [VAR] Env vars"
|
|
976
1156
|
right_col << ""
|
|
977
1157
|
right_col << "INTEGRATIONS:".c(@c_prompt).b
|
|
978
1158
|
right_col << "r Launch rtfm"
|
|
@@ -1176,7 +1356,36 @@ def defun? # Show all user-defined functions
|
|
|
1176
1356
|
end
|
|
1177
1357
|
end
|
|
1178
1358
|
end
|
|
1179
|
-
def stats # Show command execution statistics and analytics
|
|
1359
|
+
def stats(*args) # Show command execution statistics and analytics
|
|
1360
|
+
format = args[0]
|
|
1361
|
+
filename = args[1]
|
|
1362
|
+
|
|
1363
|
+
if format == "--export"
|
|
1364
|
+
# Export to file
|
|
1365
|
+
fname = filename || "rsh_stats.json"
|
|
1366
|
+
export_stats(fname)
|
|
1367
|
+
return
|
|
1368
|
+
elsif format == "--json"
|
|
1369
|
+
fname = filename || "rsh_stats.json"
|
|
1370
|
+
export_stats_json(fname)
|
|
1371
|
+
return
|
|
1372
|
+
elsif format == "--csv"
|
|
1373
|
+
fname = filename || "rsh_stats.csv"
|
|
1374
|
+
export_stats_csv(fname)
|
|
1375
|
+
return
|
|
1376
|
+
elsif format == "--graph"
|
|
1377
|
+
stats_graph
|
|
1378
|
+
return
|
|
1379
|
+
elsif format == "--clear"
|
|
1380
|
+
# Clear all statistics
|
|
1381
|
+
@cmd_frequency = {}
|
|
1382
|
+
@cmd_stats = {}
|
|
1383
|
+
puts "All statistics cleared"
|
|
1384
|
+
rshrc
|
|
1385
|
+
return
|
|
1386
|
+
end
|
|
1387
|
+
|
|
1388
|
+
# Display stats (existing code)
|
|
1180
1389
|
puts "\n Command Execution Statistics".c(@c_prompt).b
|
|
1181
1390
|
puts " " + "="*50
|
|
1182
1391
|
|
|
@@ -1216,11 +1425,115 @@ def stats # Show command execution statistics and analytics
|
|
|
1216
1425
|
puts "\n Last command exit status: #{@last_exit == 0 ? 'Success'.c(@c_path) : "Failed (#{@last_exit})".c(196)}"
|
|
1217
1426
|
puts
|
|
1218
1427
|
end
|
|
1219
|
-
def
|
|
1220
|
-
|
|
1428
|
+
def stats_graph # Visual graph mode for stats
|
|
1429
|
+
puts "\n Command Usage Graph".c(@c_prompt).b
|
|
1430
|
+
puts " " + "="*50
|
|
1431
|
+
|
|
1432
|
+
return puts "No data to display" if @cmd_frequency.nil? || @cmd_frequency.empty?
|
|
1433
|
+
|
|
1434
|
+
sorted = @cmd_frequency.sort_by { |_, count| -count }.first(15)
|
|
1435
|
+
max_count = sorted.first[1]
|
|
1436
|
+
max_width = 40
|
|
1437
|
+
|
|
1438
|
+
puts
|
|
1439
|
+
sorted.each_with_index do |(cmd, count), i|
|
|
1440
|
+
# Calculate bar width (scaled to max_width)
|
|
1441
|
+
bar_width = (count.to_f / max_count * max_width).round
|
|
1442
|
+
bar = "█" * bar_width
|
|
1443
|
+
|
|
1444
|
+
# Color bars by intensity
|
|
1445
|
+
color = case bar_width
|
|
1446
|
+
when 0..10 then 244 # Gray
|
|
1447
|
+
when 11..20 then 3 # Yellow
|
|
1448
|
+
when 21..30 then 214 # Orange
|
|
1449
|
+
else 196 # Red
|
|
1450
|
+
end
|
|
1451
|
+
|
|
1452
|
+
puts " #{cmd.ljust(15)} #{count.to_s.rjust(4)}x #{bar.c(color)}"
|
|
1453
|
+
end
|
|
1454
|
+
|
|
1455
|
+
# Performance graph if data exists
|
|
1456
|
+
if @cmd_stats && !@cmd_stats.empty?
|
|
1457
|
+
puts "\n Command Performance Graph (avg time)".c(@c_prompt).b
|
|
1458
|
+
puts " " + "="*50
|
|
1459
|
+
puts
|
|
1460
|
+
|
|
1461
|
+
slowest = @cmd_stats.sort_by { |_, s| -(s[:avg_time] || 0) }.first(10)
|
|
1462
|
+
max_time = slowest.first[1][:avg_time]
|
|
1463
|
+
|
|
1464
|
+
slowest.each do |cmd, stats|
|
|
1465
|
+
bar_width = (stats[:avg_time] / max_time * max_width).round
|
|
1466
|
+
bar = "█" * bar_width
|
|
1467
|
+
|
|
1468
|
+
color = case bar_width
|
|
1469
|
+
when 0..10 then 2 # Green (fast)
|
|
1470
|
+
when 11..20 then 214 # Orange
|
|
1471
|
+
else 196 # Red (slow)
|
|
1472
|
+
end
|
|
1473
|
+
|
|
1474
|
+
time_str = "#{'%.3f' % stats[:avg_time]}s"
|
|
1475
|
+
puts " #{cmd.ljust(15)} #{time_str.rjust(8)} #{bar.c(color)}"
|
|
1476
|
+
end
|
|
1477
|
+
end
|
|
1478
|
+
|
|
1479
|
+
puts
|
|
1480
|
+
end
|
|
1481
|
+
def export_stats(filename) # Export stats to file (JSON or CSV based on extension)
|
|
1482
|
+
if filename.end_with?('.csv')
|
|
1483
|
+
export_stats_csv(filename)
|
|
1484
|
+
else
|
|
1485
|
+
filename += '.json' unless filename.end_with?('.json')
|
|
1486
|
+
export_stats_json(filename)
|
|
1487
|
+
end
|
|
1488
|
+
end
|
|
1489
|
+
def export_stats_json(filename = 'rsh_stats.json') # Export stats to JSON
|
|
1490
|
+
stats_data = {
|
|
1491
|
+
generated: Time.now.to_i,
|
|
1492
|
+
cmd_frequency: @cmd_frequency,
|
|
1493
|
+
cmd_stats: @cmd_stats,
|
|
1494
|
+
history: {
|
|
1495
|
+
total: @history.length,
|
|
1496
|
+
unique: @history.uniq.length
|
|
1497
|
+
},
|
|
1498
|
+
last_exit: @last_exit
|
|
1499
|
+
}
|
|
1500
|
+
begin
|
|
1501
|
+
require 'json'
|
|
1502
|
+
File.write(filename, JSON.pretty_generate(stats_data))
|
|
1503
|
+
puts "Stats exported to #{filename}"
|
|
1504
|
+
rescue => e
|
|
1505
|
+
puts "Error exporting stats: #{e.message}"
|
|
1506
|
+
end
|
|
1507
|
+
end
|
|
1508
|
+
def export_stats_csv(filename = 'rsh_stats.csv') # Export stats to CSV
|
|
1509
|
+
begin
|
|
1510
|
+
lines = []
|
|
1511
|
+
lines << "command,frequency,count,total_time,avg_time"
|
|
1512
|
+
|
|
1513
|
+
# Merge frequency and performance data
|
|
1514
|
+
all_cmds = (@cmd_frequency.keys + @cmd_stats.keys).uniq
|
|
1515
|
+
all_cmds.sort.each do |cmd|
|
|
1516
|
+
freq = @cmd_frequency[cmd] || 0
|
|
1517
|
+
count = @cmd_stats.dig(cmd, :count) || 0
|
|
1518
|
+
total = @cmd_stats.dig(cmd, :total_time) || 0.0
|
|
1519
|
+
avg = @cmd_stats.dig(cmd, :avg_time) || 0.0
|
|
1520
|
+
lines << "#{cmd},#{freq},#{count},#{'%.3f' % total},#{'%.3f' % avg}"
|
|
1521
|
+
end
|
|
1522
|
+
|
|
1523
|
+
File.write(filename, lines.join("\n"))
|
|
1524
|
+
puts "Stats exported to #{filename}"
|
|
1525
|
+
rescue => e
|
|
1526
|
+
puts "Error exporting stats: #{e.message}"
|
|
1527
|
+
end
|
|
1528
|
+
end
|
|
1529
|
+
def bm(*args) # Enhanced bookmark management with tags
|
|
1530
|
+
# Handle variadic arguments
|
|
1531
|
+
arg_str = args.join(' ')
|
|
1532
|
+
|
|
1533
|
+
if args.empty?
|
|
1221
1534
|
# List all bookmarks
|
|
1222
1535
|
if @bookmarks.empty?
|
|
1223
|
-
puts "No bookmarks defined. Use :
|
|
1536
|
+
puts "No bookmarks defined. Use :bm \"name\" to bookmark current directory"
|
|
1224
1537
|
return
|
|
1225
1538
|
end
|
|
1226
1539
|
puts "\n Bookmarks:".c(@c_prompt).b
|
|
@@ -1230,7 +1543,18 @@ def bm(args = nil) # Enhanced bookmark management with tags
|
|
|
1230
1543
|
puts " #{name.c(@c_nick)} → #{path}#{tags.c(@c_stamp)}"
|
|
1231
1544
|
end
|
|
1232
1545
|
puts
|
|
1233
|
-
elsif args
|
|
1546
|
+
elsif args[0] == '--export'
|
|
1547
|
+
# Export bookmarks to file
|
|
1548
|
+
filename = args[1] || 'bookmarks.json'
|
|
1549
|
+
export_bookmarks(filename)
|
|
1550
|
+
elsif args[0] == '--import'
|
|
1551
|
+
# Import bookmarks from file
|
|
1552
|
+
filename = args[1]
|
|
1553
|
+
import_bookmarks(filename) if filename
|
|
1554
|
+
elsif args[0] == '--stats'
|
|
1555
|
+
# Show bookmark statistics
|
|
1556
|
+
bookmark_stats
|
|
1557
|
+
elsif arg_str =~ /^(\w+)\s+(.+)$/
|
|
1234
1558
|
# Set bookmark with optional tags
|
|
1235
1559
|
name, rest = $1, $2
|
|
1236
1560
|
if rest.include?('#')
|
|
@@ -1244,7 +1568,7 @@ def bm(args = nil) # Enhanced bookmark management with tags
|
|
|
1244
1568
|
end
|
|
1245
1569
|
puts "Bookmark '#{name}' set to #{@bookmarks[name][:path]}"
|
|
1246
1570
|
rshrc
|
|
1247
|
-
elsif
|
|
1571
|
+
elsif arg_str =~ /^-(\w+)$/
|
|
1248
1572
|
# Delete bookmark
|
|
1249
1573
|
name = $1
|
|
1250
1574
|
if @bookmarks.delete(name)
|
|
@@ -1253,7 +1577,7 @@ def bm(args = nil) # Enhanced bookmark management with tags
|
|
|
1253
1577
|
else
|
|
1254
1578
|
puts "Bookmark '#{name}' not found"
|
|
1255
1579
|
end
|
|
1256
|
-
elsif
|
|
1580
|
+
elsif arg_str =~ /^\?(\w*)$/
|
|
1257
1581
|
# Search bookmarks by tag
|
|
1258
1582
|
tag = $1
|
|
1259
1583
|
if tag.empty?
|
|
@@ -1273,17 +1597,82 @@ def bm(args = nil) # Enhanced bookmark management with tags
|
|
|
1273
1597
|
end
|
|
1274
1598
|
else
|
|
1275
1599
|
# Bookmark current directory
|
|
1276
|
-
name =
|
|
1600
|
+
name = arg_str.strip
|
|
1277
1601
|
@bookmarks[name] = {path: Dir.pwd, tags: []}
|
|
1278
1602
|
puts "Bookmark '#{name}' set to #{Dir.pwd}"
|
|
1279
1603
|
rshrc
|
|
1280
1604
|
end
|
|
1281
1605
|
end
|
|
1282
|
-
def bookmark(args
|
|
1283
|
-
bm(args)
|
|
1606
|
+
def bookmark(*args) # Alias for bm
|
|
1607
|
+
bm(*args)
|
|
1608
|
+
end
|
|
1609
|
+
def export_bookmarks(filename = 'bookmarks.json') # Export bookmarks to JSON
|
|
1610
|
+
begin
|
|
1611
|
+
require 'json'
|
|
1612
|
+
File.write(filename, JSON.pretty_generate(@bookmarks))
|
|
1613
|
+
puts "Bookmarks exported to #{filename}"
|
|
1614
|
+
rescue => e
|
|
1615
|
+
puts "Error exporting bookmarks: #{e.message}"
|
|
1616
|
+
end
|
|
1617
|
+
end
|
|
1618
|
+
def import_bookmarks(filename) # Import bookmarks from JSON
|
|
1619
|
+
unless File.exist?(filename)
|
|
1620
|
+
puts "File '#{filename}' not found"
|
|
1621
|
+
return
|
|
1622
|
+
end
|
|
1623
|
+
begin
|
|
1624
|
+
require 'json'
|
|
1625
|
+
imported = JSON.parse(File.read(filename))
|
|
1626
|
+
imported.each do |name, data|
|
|
1627
|
+
# Convert to proper format
|
|
1628
|
+
if data.is_a?(Hash)
|
|
1629
|
+
@bookmarks[name] = {
|
|
1630
|
+
path: data['path'] || data[:path],
|
|
1631
|
+
tags: data['tags'] || data[:tags] || []
|
|
1632
|
+
}.transform_keys(&:to_sym)
|
|
1633
|
+
else
|
|
1634
|
+
@bookmarks[name] = {path: data.to_s, tags: []}
|
|
1635
|
+
end
|
|
1636
|
+
end
|
|
1637
|
+
puts "Imported #{imported.length} bookmarks from #{filename}"
|
|
1638
|
+
rshrc
|
|
1639
|
+
rescue => e
|
|
1640
|
+
puts "Error importing bookmarks: #{e.message}"
|
|
1641
|
+
end
|
|
1642
|
+
end
|
|
1643
|
+
def bookmark_stats # Show bookmark usage statistics
|
|
1644
|
+
if @bookmarks.empty?
|
|
1645
|
+
puts "No bookmarks defined"
|
|
1646
|
+
return
|
|
1647
|
+
end
|
|
1648
|
+
|
|
1649
|
+
puts "\n Bookmark Statistics".c(@c_prompt).b
|
|
1650
|
+
puts " " + "="*50
|
|
1651
|
+
puts "\n Total bookmarks: #{@bookmarks.length}"
|
|
1652
|
+
|
|
1653
|
+
# Count by tags
|
|
1654
|
+
all_tags = @bookmarks.values.flat_map { |d| d.is_a?(Hash) ? (d[:tags] || []) : [] }
|
|
1655
|
+
unless all_tags.empty?
|
|
1656
|
+
puts "\n Tags Distribution:".c(@c_nick)
|
|
1657
|
+
tag_counts = all_tags.group_by(&:itself).transform_values(&:count)
|
|
1658
|
+
tag_counts.sort_by { |_, count| -count }.each do |tag, count|
|
|
1659
|
+
puts " #{tag.ljust(15)} #{count}x"
|
|
1660
|
+
end
|
|
1661
|
+
end
|
|
1662
|
+
|
|
1663
|
+
# Bookmarks by directory depth
|
|
1664
|
+
puts "\n Path Analysis:".c(@c_nick)
|
|
1665
|
+
paths = @bookmarks.values.map { |d| d.is_a?(Hash) ? d[:path] : d }
|
|
1666
|
+
avg_depth = paths.map { |p| p.split('/').length }.sum / paths.length
|
|
1667
|
+
puts " Average path depth: #{avg_depth}"
|
|
1668
|
+
puts
|
|
1284
1669
|
end
|
|
1285
|
-
def save_session # Save current session state
|
|
1670
|
+
def save_session(*args) # Save current session state
|
|
1671
|
+
session_name = args[0] || 'default'
|
|
1672
|
+
session_path = @session_dir + "/#{session_name}.json"
|
|
1673
|
+
|
|
1286
1674
|
session = {
|
|
1675
|
+
name: session_name,
|
|
1287
1676
|
pwd: Dir.pwd,
|
|
1288
1677
|
history: @history.first(50),
|
|
1289
1678
|
bookmarks: @bookmarks,
|
|
@@ -1292,20 +1681,24 @@ def save_session # Save current session state
|
|
|
1292
1681
|
}
|
|
1293
1682
|
begin
|
|
1294
1683
|
require 'json'
|
|
1295
|
-
File.write(
|
|
1296
|
-
puts "Session saved to #{
|
|
1684
|
+
File.write(session_path, JSON.pretty_generate(session))
|
|
1685
|
+
puts "Session '#{session_name}' saved to #{session_path}"
|
|
1297
1686
|
rescue => e
|
|
1298
1687
|
puts "Error saving session: #{e.message}"
|
|
1299
1688
|
end
|
|
1300
1689
|
end
|
|
1301
|
-
def load_session # Restore previous session
|
|
1302
|
-
|
|
1303
|
-
|
|
1690
|
+
def load_session(*args) # Restore previous session
|
|
1691
|
+
session_name = args[0] || 'default'
|
|
1692
|
+
session_path = @session_dir + "/#{session_name}.json"
|
|
1693
|
+
|
|
1694
|
+
unless File.exist?(session_path)
|
|
1695
|
+
puts "Session '#{session_name}' not found"
|
|
1696
|
+
list_sessions
|
|
1304
1697
|
return
|
|
1305
1698
|
end
|
|
1306
1699
|
begin
|
|
1307
1700
|
require 'json'
|
|
1308
|
-
session = JSON.parse(File.read(
|
|
1701
|
+
session = JSON.parse(File.read(session_path), symbolize_names: true)
|
|
1309
1702
|
|
|
1310
1703
|
# Restore state
|
|
1311
1704
|
Dir.chdir(session[:pwd]) if session[:pwd] && Dir.exist?(session[:pwd])
|
|
@@ -1338,12 +1731,273 @@ def load_session # Restore previous session
|
|
|
1338
1731
|
end
|
|
1339
1732
|
|
|
1340
1733
|
saved_time = Time.at(session[:timestamp] || 0).strftime("%Y-%m-%d %H:%M:%S")
|
|
1341
|
-
puts "Session restored from #{saved_time}"
|
|
1734
|
+
puts "Session '#{session_name}' restored from #{saved_time}"
|
|
1342
1735
|
rshrc
|
|
1343
1736
|
rescue => e
|
|
1344
1737
|
puts "Error loading session: #{e.message}"
|
|
1345
1738
|
end
|
|
1346
1739
|
end
|
|
1740
|
+
def list_sessions # List all saved sessions
|
|
1741
|
+
unless Dir.exist?(@session_dir)
|
|
1742
|
+
puts "No sessions directory found"
|
|
1743
|
+
return
|
|
1744
|
+
end
|
|
1745
|
+
|
|
1746
|
+
sessions = Dir.glob(@session_dir + '/*.json').map { |f| File.basename(f, '.json') }
|
|
1747
|
+
|
|
1748
|
+
if sessions.empty?
|
|
1749
|
+
puts "No saved sessions found. Use :save_session \"name\" to create one"
|
|
1750
|
+
return
|
|
1751
|
+
end
|
|
1752
|
+
|
|
1753
|
+
puts "\n Saved Sessions:".c(@c_prompt).b
|
|
1754
|
+
sessions.sort.each do |name|
|
|
1755
|
+
session_path = @session_dir + "/#{name}.json"
|
|
1756
|
+
begin
|
|
1757
|
+
require 'json'
|
|
1758
|
+
session = JSON.parse(File.read(session_path), symbolize_names: true)
|
|
1759
|
+
timestamp = Time.at(session[:timestamp] || 0).strftime("%Y-%m-%d %H:%M")
|
|
1760
|
+
pwd = session[:pwd] || '?'
|
|
1761
|
+
puts " #{name.c(@c_bookmark).ljust(20)} #{timestamp.c(@c_stamp)} #{pwd.c(@c_path)}"
|
|
1762
|
+
rescue => e
|
|
1763
|
+
puts " #{name.c(@c_bookmark).ljust(20)} [corrupted]".c(196)
|
|
1764
|
+
end
|
|
1765
|
+
end
|
|
1766
|
+
puts
|
|
1767
|
+
end
|
|
1768
|
+
def delete_session(*args) # Delete a saved session
|
|
1769
|
+
name = args[0]
|
|
1770
|
+
|
|
1771
|
+
if name == '*'
|
|
1772
|
+
# Delete all sessions except default
|
|
1773
|
+
sessions = Dir.glob(@session_dir + '/*.json').map { |f| File.basename(f, '.json') }
|
|
1774
|
+
sessions.reject! { |s| s == 'default' || s == 'autosave' }
|
|
1775
|
+
|
|
1776
|
+
if sessions.empty?
|
|
1777
|
+
puts "No sessions to delete (keeping default and autosave)"
|
|
1778
|
+
return
|
|
1779
|
+
end
|
|
1780
|
+
|
|
1781
|
+
sessions.each do |session_name|
|
|
1782
|
+
session_path = @session_dir + "/#{session_name}.json"
|
|
1783
|
+
File.delete(session_path)
|
|
1784
|
+
end
|
|
1785
|
+
puts "Deleted #{sessions.length} sessions: #{sessions.join(', ')}"
|
|
1786
|
+
return
|
|
1787
|
+
end
|
|
1788
|
+
|
|
1789
|
+
return puts "Cannot delete default session" if name == 'default'
|
|
1790
|
+
return puts "Cannot delete autosave session (use * to delete all)" if name == 'autosave'
|
|
1791
|
+
|
|
1792
|
+
session_path = @session_dir + "/#{name}.json"
|
|
1793
|
+
unless File.exist?(session_path)
|
|
1794
|
+
puts "Session '#{name}' not found"
|
|
1795
|
+
return
|
|
1796
|
+
end
|
|
1797
|
+
|
|
1798
|
+
File.delete(session_path)
|
|
1799
|
+
puts "Session '#{name}' deleted"
|
|
1800
|
+
end
|
|
1801
|
+
def rmsession(*args) # Alias for delete_session
|
|
1802
|
+
delete_session(*args)
|
|
1803
|
+
end
|
|
1804
|
+
def theme(*args) # Apply color scheme presets
|
|
1805
|
+
name = args[0]
|
|
1806
|
+
|
|
1807
|
+
if name.nil?
|
|
1808
|
+
puts "\n Available themes:".c(@c_prompt).b
|
|
1809
|
+
puts " default, solarized, dracula, gruvbox, nord, monokai"
|
|
1810
|
+
puts "\n Current theme colors:"
|
|
1811
|
+
puts " prompt:#{' '*5}#{@c_prompt} cmd:#{' '*8}#{@c_cmd} nick:#{' '*7}#{@c_nick}"
|
|
1812
|
+
puts " gnick:#{' '*6}#{@c_gnick} path:#{' '*7}#{@c_path} switch:#{' '*5}#{@c_switch}"
|
|
1813
|
+
puts " bookmark:#{' '*3}#{@c_bookmark} colon:#{' '*6}#{@c_colon} tabselect:#{' '*2}#{@c_tabselect}"
|
|
1814
|
+
puts " taboption:#{' '*2}#{@c_taboption} stamp:#{' '*6}#{@c_stamp}"
|
|
1815
|
+
puts
|
|
1816
|
+
return
|
|
1817
|
+
end
|
|
1818
|
+
|
|
1819
|
+
case name.downcase
|
|
1820
|
+
when 'default'
|
|
1821
|
+
@c_prompt, @c_cmd, @c_nick, @c_gnick = 10, 2, 6, 14
|
|
1822
|
+
@c_path, @c_switch, @c_bookmark, @c_colon = 3, 6, 13, 4
|
|
1823
|
+
@c_tabselect, @c_taboption, @c_stamp = 5, 244, 244
|
|
1824
|
+
when 'solarized'
|
|
1825
|
+
@c_prompt, @c_cmd, @c_nick, @c_gnick = 33, 64, 37, 117
|
|
1826
|
+
@c_path, @c_switch, @c_bookmark, @c_colon = 136, 125, 61, 33
|
|
1827
|
+
@c_tabselect, @c_taboption, @c_stamp = 166, 240, 240
|
|
1828
|
+
when 'dracula'
|
|
1829
|
+
@c_prompt, @c_cmd, @c_nick, @c_gnick = 141, 84, 117, 212
|
|
1830
|
+
@c_path, @c_switch, @c_bookmark, @c_colon = 228, 215, 141, 141
|
|
1831
|
+
@c_tabselect, @c_taboption, @c_stamp = 212, 238, 238
|
|
1832
|
+
when 'gruvbox'
|
|
1833
|
+
@c_prompt, @c_cmd, @c_nick, @c_gnick = 214, 142, 109, 175
|
|
1834
|
+
@c_path, @c_switch, @c_bookmark, @c_colon = 208, 142, 167, 214
|
|
1835
|
+
@c_tabselect, @c_taboption, @c_stamp = 208, 243, 243
|
|
1836
|
+
when 'nord'
|
|
1837
|
+
@c_prompt, @c_cmd, @c_nick, @c_gnick = 110, 109, 116, 152
|
|
1838
|
+
@c_path, @c_switch, @c_bookmark, @c_colon = 180, 109, 139, 110
|
|
1839
|
+
@c_tabselect, @c_taboption, @c_stamp = 143, 240, 240
|
|
1840
|
+
when 'monokai'
|
|
1841
|
+
@c_prompt, @c_cmd, @c_nick, @c_gnick = 197, 112, 81, 141
|
|
1842
|
+
@c_path, @c_switch, @c_bookmark, @c_colon = 228, 208, 141, 197
|
|
1843
|
+
@c_tabselect, @c_taboption, @c_stamp = 197, 238, 238
|
|
1844
|
+
else
|
|
1845
|
+
puts "Unknown theme '#{name}'. Available: default, solarized, dracula, gruvbox, nord, monokai"
|
|
1846
|
+
return
|
|
1847
|
+
end
|
|
1848
|
+
|
|
1849
|
+
puts "Theme '#{name}' applied"
|
|
1850
|
+
puts "Add this to .rshrc to make it permanent: :theme \"#{name}\""
|
|
1851
|
+
end
|
|
1852
|
+
def load_plugins # Load all plugins from plugin directory
|
|
1853
|
+
return unless Dir.exist?(@plugin_dir)
|
|
1854
|
+
|
|
1855
|
+
plugin_files = Dir.glob(@plugin_dir + '/*.rb').sort
|
|
1856
|
+
|
|
1857
|
+
plugin_files.each do |plugin_file|
|
|
1858
|
+
plugin_name = File.basename(plugin_file, '.rb')
|
|
1859
|
+
|
|
1860
|
+
# Skip if disabled
|
|
1861
|
+
next if @plugin_disabled.include?(plugin_name)
|
|
1862
|
+
|
|
1863
|
+
begin
|
|
1864
|
+
# Load the plugin file
|
|
1865
|
+
load(plugin_file)
|
|
1866
|
+
|
|
1867
|
+
# Find the plugin class (conventionally PluginNamePlugin)
|
|
1868
|
+
class_name = plugin_name.split('_').map(&:capitalize).join + 'Plugin'
|
|
1869
|
+
|
|
1870
|
+
# Try to instantiate the plugin
|
|
1871
|
+
if Object.const_defined?(class_name)
|
|
1872
|
+
plugin_class = Object.const_get(class_name)
|
|
1873
|
+
rsh_context = {
|
|
1874
|
+
version: @version,
|
|
1875
|
+
history: @history,
|
|
1876
|
+
bookmarks: @bookmarks,
|
|
1877
|
+
nick: @nick,
|
|
1878
|
+
gnick: @gnick,
|
|
1879
|
+
pwd: Dir.pwd,
|
|
1880
|
+
config: method(:config),
|
|
1881
|
+
rsh: self
|
|
1882
|
+
}
|
|
1883
|
+
plugin_instance = plugin_class.new(rsh_context)
|
|
1884
|
+
@plugins << { name: plugin_name, instance: plugin_instance, class: class_name }
|
|
1885
|
+
|
|
1886
|
+
# Load plugin completions
|
|
1887
|
+
if plugin_instance.respond_to?(:add_completions)
|
|
1888
|
+
completions = plugin_instance.add_completions
|
|
1889
|
+
@cmd_completions.merge!(completions) if completions.is_a?(Hash)
|
|
1890
|
+
end
|
|
1891
|
+
|
|
1892
|
+
# Load plugin commands
|
|
1893
|
+
if plugin_instance.respond_to?(:add_commands)
|
|
1894
|
+
commands = plugin_instance.add_commands
|
|
1895
|
+
@plugin_commands.merge!(commands) if commands.is_a?(Hash)
|
|
1896
|
+
end
|
|
1897
|
+
|
|
1898
|
+
puts " Loaded plugin: #{plugin_name} (#{class_name})" if ENV['RSH_DEBUG']
|
|
1899
|
+
end
|
|
1900
|
+
rescue => e
|
|
1901
|
+
puts "Warning: Failed to load plugin '#{plugin_name}': #{e.message}" if ENV['RSH_DEBUG']
|
|
1902
|
+
puts " #{e.backtrace.first}" if ENV['RSH_DEBUG']
|
|
1903
|
+
end
|
|
1904
|
+
end
|
|
1905
|
+
|
|
1906
|
+
# Call on_startup for all plugins
|
|
1907
|
+
@plugins.each do |plugin|
|
|
1908
|
+
begin
|
|
1909
|
+
plugin[:instance].on_startup if plugin[:instance].respond_to?(:on_startup)
|
|
1910
|
+
rescue => e
|
|
1911
|
+
puts "Warning: Plugin '#{plugin[:name]}' on_startup failed: #{e.message}" if ENV['RSH_DEBUG']
|
|
1912
|
+
end
|
|
1913
|
+
end
|
|
1914
|
+
end
|
|
1915
|
+
def call_plugin_hook(hook_name, *args) # Call a lifecycle hook for all plugins
|
|
1916
|
+
results = []
|
|
1917
|
+
@plugins.each do |plugin|
|
|
1918
|
+
begin
|
|
1919
|
+
if plugin[:instance].respond_to?(hook_name)
|
|
1920
|
+
result = plugin[:instance].send(hook_name, *args)
|
|
1921
|
+
results << result unless result.nil?
|
|
1922
|
+
end
|
|
1923
|
+
rescue => e
|
|
1924
|
+
puts "Warning: Plugin '#{plugin[:name]}' hook '#{hook_name}' failed: #{e.message}" if ENV['RSH_DEBUG']
|
|
1925
|
+
end
|
|
1926
|
+
end
|
|
1927
|
+
results
|
|
1928
|
+
end
|
|
1929
|
+
def plugins(*args) # Plugin management command
|
|
1930
|
+
if args.empty?
|
|
1931
|
+
# List all plugins
|
|
1932
|
+
if @plugins.empty?
|
|
1933
|
+
puts "\nNo plugins loaded"
|
|
1934
|
+
puts "Place .rb files in #{@plugin_dir}"
|
|
1935
|
+
return
|
|
1936
|
+
end
|
|
1937
|
+
|
|
1938
|
+
puts "\n Loaded Plugins:".c(@c_prompt).b
|
|
1939
|
+
@plugins.each do |plugin|
|
|
1940
|
+
status = @plugin_disabled.include?(plugin[:name]) ? '[disabled]'.c(196) : '[enabled]'.c(@c_path)
|
|
1941
|
+
puts " #{plugin[:name].ljust(20)} #{status} (#{plugin[:class]})"
|
|
1942
|
+
end
|
|
1943
|
+
|
|
1944
|
+
unless @plugin_disabled.empty?
|
|
1945
|
+
puts "\n Disabled Plugins:".c(@c_stamp)
|
|
1946
|
+
@plugin_disabled.each { |name| puts " #{name}" }
|
|
1947
|
+
end
|
|
1948
|
+
puts
|
|
1949
|
+
elsif args[0] == 'reload'
|
|
1950
|
+
# Reload all plugins
|
|
1951
|
+
@plugins = []
|
|
1952
|
+
@plugin_commands = {}
|
|
1953
|
+
load_plugins
|
|
1954
|
+
puts "Plugins reloaded (#{@plugins.length} loaded)"
|
|
1955
|
+
elsif args[0] == 'enable' && args[1]
|
|
1956
|
+
# Enable a plugin
|
|
1957
|
+
plugin_name = args[1]
|
|
1958
|
+
@plugin_disabled.delete(plugin_name)
|
|
1959
|
+
puts "Plugin '#{plugin_name}' enabled. Use :plugins reload to load it"
|
|
1960
|
+
rshrc
|
|
1961
|
+
elsif args[0] == 'disable' && args[1]
|
|
1962
|
+
# Disable a plugin
|
|
1963
|
+
plugin_name = args[1]
|
|
1964
|
+
unless @plugin_disabled.include?(plugin_name)
|
|
1965
|
+
@plugin_disabled << plugin_name
|
|
1966
|
+
end
|
|
1967
|
+
@plugins.reject! { |p| p[:name] == plugin_name }
|
|
1968
|
+
puts "Plugin '#{plugin_name}' disabled"
|
|
1969
|
+
rshrc
|
|
1970
|
+
elsif args[0] == 'info' && args[1]
|
|
1971
|
+
# Show plugin info
|
|
1972
|
+
plugin_name = args[1]
|
|
1973
|
+
plugin = @plugins.find { |p| p[:name] == plugin_name }
|
|
1974
|
+
if plugin
|
|
1975
|
+
puts "\n Plugin: #{plugin_name}".c(@c_prompt).b
|
|
1976
|
+
puts " Class: #{plugin[:class]}"
|
|
1977
|
+
puts " File: #{@plugin_dir}/#{plugin_name}.rb"
|
|
1978
|
+
|
|
1979
|
+
puts "\n Hooks:".c(@c_nick)
|
|
1980
|
+
%i[on_startup on_command_before on_command_after on_prompt].each do |hook|
|
|
1981
|
+
has_hook = plugin[:instance].respond_to?(hook) ? '✓' : '✗'
|
|
1982
|
+
puts " #{has_hook} #{hook}"
|
|
1983
|
+
end
|
|
1984
|
+
|
|
1985
|
+
puts "\n Extensions:".c(@c_nick)
|
|
1986
|
+
puts " ✓ add_completions" if plugin[:instance].respond_to?(:add_completions)
|
|
1987
|
+
puts " ✓ add_commands" if plugin[:instance].respond_to?(:add_commands)
|
|
1988
|
+
puts
|
|
1989
|
+
else
|
|
1990
|
+
puts "Plugin '#{plugin_name}' not found"
|
|
1991
|
+
end
|
|
1992
|
+
else
|
|
1993
|
+
puts "Usage:"
|
|
1994
|
+
puts " :plugins List all plugins"
|
|
1995
|
+
puts " :plugins reload Reload all plugins"
|
|
1996
|
+
puts " :plugins enable NAME Enable a plugin"
|
|
1997
|
+
puts " :plugins disable NAME Disable a plugin"
|
|
1998
|
+
puts " :plugins info NAME Show plugin details"
|
|
1999
|
+
end
|
|
2000
|
+
end
|
|
1347
2001
|
def validate_command(cmd) # Syntax validation before execution
|
|
1348
2002
|
return nil if cmd.nil? || cmd.empty?
|
|
1349
2003
|
warnings = []
|
|
@@ -1359,19 +2013,75 @@ def validate_command(cmd) # Syntax validation before execution
|
|
|
1359
2013
|
warnings << "WARNING: Force flag without path" if cmd =~ /rm\s+-[rf]+\s*$/
|
|
1360
2014
|
warnings << "WARNING: Sudo with redirection" if cmd =~ /sudo.*>/
|
|
1361
2015
|
|
|
1362
|
-
# Check for common typos in popular commands
|
|
1363
|
-
if cmd =~ /^(\w+)/
|
|
2016
|
+
# Check for common typos in popular commands (skip for : commands)
|
|
2017
|
+
if cmd =~ /^(\w+)/ && cmd !~ /^:/
|
|
1364
2018
|
first_cmd = $1
|
|
1365
|
-
|
|
2019
|
+
# Check if command exists (don't warn for valid commands)
|
|
2020
|
+
is_valid_cmd = @exe.include?(first_cmd) ||
|
|
2021
|
+
@nick.include?(first_cmd) ||
|
|
2022
|
+
first_cmd == "cd" ||
|
|
2023
|
+
(@plugin_commands && @plugin_commands[first_cmd]) ||
|
|
2024
|
+
(@defuns && @defuns.keys.include?(first_cmd))
|
|
2025
|
+
|
|
2026
|
+
unless is_valid_cmd
|
|
1366
2027
|
suggestions = suggest_command(first_cmd)
|
|
1367
2028
|
if suggestions && !suggestions.empty?
|
|
1368
|
-
|
|
2029
|
+
# Auto-correct if enabled and first suggestion has distance ≤ 2
|
|
2030
|
+
if @auto_correct && levenshtein_distance(first_cmd, suggestions[0]) <= 2
|
|
2031
|
+
warnings << "AUTO-CORRECTING: '#{first_cmd}' → '#{suggestions[0]}'"
|
|
2032
|
+
else
|
|
2033
|
+
warnings << "Command '#{first_cmd}' not found. Did you mean: #{suggestions.join(', ')}?"
|
|
2034
|
+
end
|
|
1369
2035
|
end
|
|
1370
2036
|
end
|
|
1371
2037
|
end
|
|
1372
2038
|
|
|
1373
2039
|
warnings.empty? ? nil : warnings
|
|
1374
2040
|
end
|
|
2041
|
+
def calc(*args) # Inline calculator using Ruby's Math library
|
|
2042
|
+
if args.empty?
|
|
2043
|
+
puts "Usage: calc <expression>"
|
|
2044
|
+
puts "Examples:"
|
|
2045
|
+
puts " calc 2 + 2"
|
|
2046
|
+
puts " calc \"Math.sqrt(16)\""
|
|
2047
|
+
puts " calc \"Math::PI * 2\""
|
|
2048
|
+
return
|
|
2049
|
+
end
|
|
2050
|
+
|
|
2051
|
+
expression = args.join(' ')
|
|
2052
|
+
|
|
2053
|
+
begin
|
|
2054
|
+
# Safe evaluation with Math library
|
|
2055
|
+
result = eval(expression, binding, __FILE__, __LINE__)
|
|
2056
|
+
puts result
|
|
2057
|
+
rescue SyntaxError => e
|
|
2058
|
+
puts "Syntax error in expression: #{e.message}"
|
|
2059
|
+
rescue => e
|
|
2060
|
+
puts "Error evaluating expression: #{e.message}"
|
|
2061
|
+
end
|
|
2062
|
+
end
|
|
2063
|
+
def apply_auto_correct(cmd) # Apply auto-correction to command
|
|
2064
|
+
return cmd unless @auto_correct
|
|
2065
|
+
return cmd if cmd =~ /^:/ # Don't auto-correct colon commands
|
|
2066
|
+
return cmd unless cmd =~ /^(\w+)/
|
|
2067
|
+
|
|
2068
|
+
first_cmd = $1
|
|
2069
|
+
|
|
2070
|
+
# Don't auto-correct if command exists
|
|
2071
|
+
return cmd if @exe.include?(first_cmd)
|
|
2072
|
+
return cmd if @nick.include?(first_cmd)
|
|
2073
|
+
return cmd if first_cmd == "cd"
|
|
2074
|
+
return cmd if @plugin_commands && @plugin_commands[first_cmd]
|
|
2075
|
+
return cmd if @defuns && @defuns.keys.include?(first_cmd) # Check user defuns
|
|
2076
|
+
|
|
2077
|
+
suggestions = suggest_command(first_cmd)
|
|
2078
|
+
if suggestions && !suggestions.empty? && levenshtein_distance(first_cmd, suggestions[0]) <= 2
|
|
2079
|
+
# Auto-correct using first (closest) suggestion
|
|
2080
|
+
cmd.sub(/^#{first_cmd}/, suggestions[0])
|
|
2081
|
+
else
|
|
2082
|
+
cmd
|
|
2083
|
+
end
|
|
2084
|
+
end
|
|
1375
2085
|
def execute_conditional(cmd_line)
|
|
1376
2086
|
# Split on && and || while preserving the operators
|
|
1377
2087
|
parts = cmd_line.split(/(\s*&&\s*|\s*\|\|\s*)/)
|
|
@@ -1653,6 +2363,13 @@ def load_rshrc_safe
|
|
|
1653
2363
|
@cmd_stats = {} unless @cmd_stats.is_a?(Hash)
|
|
1654
2364
|
@bookmarks = {} unless @bookmarks.is_a?(Hash)
|
|
1655
2365
|
@defuns = {} unless @defuns.is_a?(Hash)
|
|
2366
|
+
@history_dedup = 'smart' unless @history_dedup.is_a?(String)
|
|
2367
|
+
@session_autosave = 0 unless @session_autosave.is_a?(Integer)
|
|
2368
|
+
@auto_correct = false unless [@auto_correct].any? { |v| v == true || v == false }
|
|
2369
|
+
@slow_command_threshold = 0 unless @slow_command_threshold.is_a?(Integer)
|
|
2370
|
+
@plugin_disabled = [] unless @plugin_disabled.is_a?(Array)
|
|
2371
|
+
@plugins = [] unless @plugins.is_a?(Array)
|
|
2372
|
+
@plugin_commands = {} unless @plugin_commands.is_a?(Hash)
|
|
1656
2373
|
|
|
1657
2374
|
# Restore defuns from .rshrc
|
|
1658
2375
|
if @defuns && !@defuns.empty?
|
|
@@ -1787,6 +2504,13 @@ def load_defaults
|
|
|
1787
2504
|
@defuns ||= {}
|
|
1788
2505
|
@switch_cache ||= {}
|
|
1789
2506
|
@switch_cache_time ||= {}
|
|
2507
|
+
@history_dedup ||= 'smart'
|
|
2508
|
+
@session_autosave ||= 0
|
|
2509
|
+
@auto_correct ||= false
|
|
2510
|
+
@slow_command_threshold ||= 0
|
|
2511
|
+
@plugin_disabled ||= []
|
|
2512
|
+
@plugins ||= []
|
|
2513
|
+
@plugin_commands ||= {}
|
|
1790
2514
|
puts "Loaded with default configuration."
|
|
1791
2515
|
end
|
|
1792
2516
|
|
|
@@ -1860,6 +2584,7 @@ begin # Load .rshrc and populate @history
|
|
|
1860
2584
|
@c.restore # Max row & col gotten, cursor restored
|
|
1861
2585
|
hist_clean # Remove duplicates, etc
|
|
1862
2586
|
@path.map! {|p| p + "/*"} # Set proper format for path search
|
|
2587
|
+
@plugins_loaded = false # Defer plugin loading until first command
|
|
1863
2588
|
end
|
|
1864
2589
|
|
|
1865
2590
|
# MAIN PART
|
|
@@ -1869,17 +2594,58 @@ loop do
|
|
|
1869
2594
|
@node = Etc.uname[:nodename] # For use in @prompt
|
|
1870
2595
|
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 # reload prompt but preserve runtime data
|
|
1871
2596
|
@prompt.gsub!(/#{Dir.home}/, '~') # Simplify path in prompt
|
|
1872
|
-
system("printf \"\033]0;rsh: #{Dir.pwd}\007\"") # Set Window title to path
|
|
2597
|
+
system("printf \"\033]0;rsh: #{Dir.pwd}\007\"") # Set Window title to path
|
|
1873
2598
|
@history[0] = "" unless @history[0]
|
|
1874
2599
|
cache_executables # Use cached executable lookup
|
|
2600
|
+
# Load plugins on first command (lazy loading)
|
|
2601
|
+
unless @plugins_loaded
|
|
2602
|
+
load_plugins
|
|
2603
|
+
@plugins_loaded = true
|
|
2604
|
+
end
|
|
2605
|
+
# Append plugin prompt additions (after plugins loaded)
|
|
2606
|
+
plugin_prompts = call_plugin_hook(:on_prompt)
|
|
2607
|
+
@prompt += plugin_prompts.join if plugin_prompts.any?
|
|
2608
|
+
# Auto-save session if enabled and interval elapsed
|
|
2609
|
+
if @session_autosave && @session_autosave > 0
|
|
2610
|
+
current_time = Time.now.to_i
|
|
2611
|
+
if (current_time - @session_last_save) >= @session_autosave
|
|
2612
|
+
save_session('autosave')
|
|
2613
|
+
@session_last_save = current_time
|
|
2614
|
+
end
|
|
2615
|
+
end
|
|
1875
2616
|
getstr # Main work is here
|
|
1876
2617
|
@cmd = @history[0]
|
|
1877
2618
|
@dirs.unshift(Dir.pwd)
|
|
1878
2619
|
@dirs.pop
|
|
1879
2620
|
hist_clean # Clean up the history
|
|
1880
2621
|
@cmd = "ls" if @cmd == "" # Default to ls when no command is given
|
|
1881
|
-
|
|
1882
|
-
|
|
2622
|
+
# Enhanced history commands
|
|
2623
|
+
if @cmd == '!!'
|
|
2624
|
+
# Repeat last command
|
|
2625
|
+
@cmd = @history[1] if @history.length > 1
|
|
2626
|
+
elsif @cmd =~ /^!-(\d+)$/
|
|
2627
|
+
# Repeat nth to last (e.g., !-2 = 2nd to last)
|
|
2628
|
+
index = $1.to_i
|
|
2629
|
+
@cmd = @history[index] if @history.length > index
|
|
2630
|
+
elsif @cmd =~ /^!(\d+):(\d+)$/
|
|
2631
|
+
# Chain commands (e.g., !5:7 = commands 5, 6, 7)
|
|
2632
|
+
start_idx = $1.to_i + 1
|
|
2633
|
+
end_idx = $2.to_i + 1
|
|
2634
|
+
if start_idx < @history.length && end_idx < @history.length && start_idx <= end_idx
|
|
2635
|
+
commands = @history[start_idx..end_idx].compact.reverse
|
|
2636
|
+
# Filter out colon commands (they don't work in shell chains)
|
|
2637
|
+
commands.reject! { |c| c =~ /^:/ }
|
|
2638
|
+
if commands.empty?
|
|
2639
|
+
puts "Cannot chain colon commands (use shell commands only)"
|
|
2640
|
+
@cmd = "ls" # Default to ls
|
|
2641
|
+
else
|
|
2642
|
+
@cmd = commands.join(' && ')
|
|
2643
|
+
puts " Chaining: #{@cmd}".c(@c_stamp)
|
|
2644
|
+
end
|
|
2645
|
+
end
|
|
2646
|
+
elsif @cmd.match(/^!\d+$/)
|
|
2647
|
+
# Original: !5 = command 5
|
|
2648
|
+
hi = @history[@cmd.sub(/^!(\d+)$/, '\1').to_i+1]
|
|
1883
2649
|
@cmd = hi if hi
|
|
1884
2650
|
end
|
|
1885
2651
|
# Move cursor to end of line and print the full command before clearing
|
|
@@ -1929,18 +2695,27 @@ loop do
|
|
|
1929
2695
|
elsif @cmd == '#' # List previous directories
|
|
1930
2696
|
dirs
|
|
1931
2697
|
else # Execute command
|
|
1932
|
-
# Check if it's a
|
|
2698
|
+
# Check if it's a plugin command FIRST
|
|
1933
2699
|
cmd_parts = @cmd.split(/\s+/)
|
|
1934
|
-
|
|
1935
|
-
if
|
|
2700
|
+
cmd_name = cmd_parts[0]
|
|
2701
|
+
if @plugin_commands && @plugin_commands[cmd_name]
|
|
1936
2702
|
begin
|
|
1937
2703
|
args = cmd_parts[1..]
|
|
1938
|
-
|
|
1939
|
-
result
|
|
2704
|
+
result = @plugin_commands[cmd_name].call(*args)
|
|
2705
|
+
puts result unless result.nil?
|
|
2706
|
+
rescue => e
|
|
2707
|
+
puts "Error in plugin command '#{cmd_name}': #{e}"
|
|
2708
|
+
end
|
|
2709
|
+
# Then check if it's a user-defined Ruby function (before any expansions)
|
|
2710
|
+
elsif self.respond_to?(cmd_name) && singleton_class.instance_methods(false).include?(cmd_name.to_sym)
|
|
2711
|
+
begin
|
|
2712
|
+
args = cmd_parts[1..]
|
|
2713
|
+
puts "DEBUG: Calling #{cmd_name} with args: #{args}" if ENV['RSH_DEBUG']
|
|
2714
|
+
result = self.send(cmd_name, *args)
|
|
1940
2715
|
puts "DEBUG: Result: #{result.inspect}" if ENV['RSH_DEBUG']
|
|
1941
2716
|
puts result unless result.nil?
|
|
1942
2717
|
rescue => e
|
|
1943
|
-
puts "Error calling function '#{
|
|
2718
|
+
puts "Error calling function '#{cmd_name}': #{e}"
|
|
1944
2719
|
end
|
|
1945
2720
|
else
|
|
1946
2721
|
# Handle conditional execution (&& and ||)
|
|
@@ -1976,6 +2751,7 @@ loop do
|
|
|
1976
2751
|
elsif @bookmarks && @bookmarks[@cmd]
|
|
1977
2752
|
bookmark_data = @bookmarks[@cmd]
|
|
1978
2753
|
bm_dir = bookmark_data.is_a?(Hash) ? bookmark_data[:path] : bookmark_data
|
|
2754
|
+
bm_dir = bm_dir.sub(/^~/, Dir.home) # Expand tilde
|
|
1979
2755
|
if Dir.exist?(bm_dir)
|
|
1980
2756
|
Dir.chdir(bm_dir)
|
|
1981
2757
|
puts "Jumped to bookmark '#{@cmd}' → #{bm_dir}".c(@c_path)
|
|
@@ -2001,10 +2777,27 @@ loop do
|
|
|
2001
2777
|
end
|
|
2002
2778
|
else
|
|
2003
2779
|
begin
|
|
2004
|
-
|
|
2780
|
+
pre_cmd
|
|
2781
|
+
|
|
2782
|
+
# Apply auto-correct if enabled (before validation)
|
|
2783
|
+
@cmd = apply_auto_correct(@cmd)
|
|
2784
|
+
|
|
2785
|
+
# Validate command after auto-correction
|
|
2005
2786
|
warnings = validate_command(@cmd)
|
|
2006
2787
|
if warnings && !warnings.empty?
|
|
2007
|
-
|
|
2788
|
+
# Show non-auto-correct warnings
|
|
2789
|
+
warnings.reject { |w| w.start_with?("AUTO-CORRECTING:") }.each { |w| puts "#{w}".c(196) }
|
|
2790
|
+
# Show auto-correct and ask for confirmation
|
|
2791
|
+
auto_correct_warnings = warnings.select { |w| w.start_with?("AUTO-CORRECTING:") }
|
|
2792
|
+
if auto_correct_warnings.any?
|
|
2793
|
+
auto_correct_warnings.each { |w| puts "#{w}".c(214) }
|
|
2794
|
+
print "Accept auto-correction? (Y/n): "
|
|
2795
|
+
response = $stdin.gets.chomp
|
|
2796
|
+
if response.downcase == 'n'
|
|
2797
|
+
puts "Auto-correction cancelled"
|
|
2798
|
+
next
|
|
2799
|
+
end
|
|
2800
|
+
end
|
|
2008
2801
|
# For critical warnings, ask for confirmation
|
|
2009
2802
|
if warnings.any? { |w| w.start_with?("WARNING:") }
|
|
2010
2803
|
print "Continue anyway? (y/N): "
|
|
@@ -2016,7 +2809,16 @@ loop do
|
|
|
2016
2809
|
end
|
|
2017
2810
|
end
|
|
2018
2811
|
|
|
2019
|
-
|
|
2812
|
+
# Call plugin on_command_before hooks
|
|
2813
|
+
plugin_results = call_plugin_hook(:on_command_before, @cmd)
|
|
2814
|
+
# If any plugin returns false, skip command
|
|
2815
|
+
if plugin_results.include?(false)
|
|
2816
|
+
puts "Command blocked by plugin"
|
|
2817
|
+
next
|
|
2818
|
+
end
|
|
2819
|
+
# If any plugin returns a modified command, use it
|
|
2820
|
+
modified_cmd = plugin_results.find { |r| r.is_a?(String) }
|
|
2821
|
+
@cmd = modified_cmd if modified_cmd
|
|
2020
2822
|
|
|
2021
2823
|
# Track command frequency for intelligent completion
|
|
2022
2824
|
cmd_base = @cmd.split.first if @cmd && !@cmd.empty?
|
|
@@ -2057,6 +2859,14 @@ loop do
|
|
|
2057
2859
|
@cmd_stats[cmd_base][:avg_time] = @cmd_stats[cmd_base][:total_time] / @cmd_stats[cmd_base][:count]
|
|
2058
2860
|
end
|
|
2059
2861
|
|
|
2862
|
+
# Slow command alert
|
|
2863
|
+
if @slow_command_threshold > 0 && elapsed > @slow_command_threshold
|
|
2864
|
+
puts "⚠ Command took #{'%.1f' % elapsed}s (threshold: #{@slow_command_threshold}s)".c(214)
|
|
2865
|
+
end
|
|
2866
|
+
|
|
2867
|
+
# Call plugin on_command_after hooks
|
|
2868
|
+
call_plugin_hook(:on_command_after, @cmd, @last_exit)
|
|
2869
|
+
|
|
2060
2870
|
post_cmd
|
|
2061
2871
|
rescue StandardError => err
|
|
2062
2872
|
puts "\nError: #{err}"
|