ruby-shell 3.0.0 → 3.1.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 +26 -6
  3. data/bin/rsh +443 -30
  4. metadata +4 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 121b91f0e19b4e13cd6cacf00094552851ea44bf376c4e1f0e6554c83ee99a34
4
- data.tar.gz: e63b51b9f7742b87b4bbc96ee41da6ba12ef93df0631b4ac564548b87f6d0a33
3
+ metadata.gz: 318b51625674f856dfa297d3b8fc8a3bf0e2fd2bb87e4f95d4184bae6e94be72
4
+ data.tar.gz: 03cfd00609fc1f2a011458d3a75837adfe2ac3975c38718b8fcca9998ffa22b6
5
5
  SHA512:
6
- metadata.gz: bc4fb4e0cc354ff353532a01f334ba85e23725cc5757328118ea85c0b25f46c814f12e9e63dfcf2ca83ef48f3c015696ffd9b8c6661b94398295a67c0be285dc
7
- data.tar.gz: 1790d5590bcdec77d461dd491fc34fb915324e78eea7d385bbdd35e2f71f86eaa9ab3995fcbd3855c640dad029530baa7615994318fb9a42718aa7246ea938a9
6
+ metadata.gz: 982ec206a79d08fed5b95324b356edd4069c158bef62b52cf0205906bef39f38c8e44031821cf7c4f7eec8f9162df61e186589dee240a6074ae8dc592f090393
7
+ data.tar.gz: b0d025d2b2a0df814b3a9239f2151bcecc66a35ca71f0f020aa360ee2b0081cecbe963e6a2124fa35993f3595652e4797760219e88a423390f6b6825e77d8207
data/README.md CHANGED
@@ -33,7 +33,19 @@ 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.0.0 - Major Feature Release ⭐⭐⭐
36
+ ## NEW in v3.1.0 - Quick Wins & Polish ⭐
37
+ * **Multiple Named Sessions**: Save/load different sessions - `:save_session "project"`, `:load_session "project"`
38
+ * **Stats Export**: Export analytics to CSV/JSON - `:stats --csv` or `:stats --json`
39
+ * **Session Auto-save**: Set `@session_autosave = 300` in .rshrc for automatic 5-minute saves
40
+ * **Bookmark Import/Export**: Share bookmarks - `:bm --export bookmarks.json`, `:bm --import bookmarks.json`
41
+ * **Bookmark Statistics**: See usage patterns - `:bm --stats` shows tag distribution and analytics
42
+ * **Color Themes**: 6 preset themes - `:theme solarized|dracula|gruvbox|nord|monokai|default`
43
+ * **Config Management**: `:config` shows/sets history_dedup, session_autosave, completion settings
44
+ * **Environment Variables**: `:env` lists/sets/exports environment variables
45
+ * **Bookmark TAB Completion**: Bookmarks appear in TAB completion alongside commands
46
+ * **List Sessions**: `:list_sessions` shows all saved sessions with timestamps and paths
47
+
48
+ ## v3.0.0 - Major Feature Release ⭐⭐⭐
37
49
  * **Persistent Ruby Functions**: defun functions now save to .rshrc and persist across sessions
38
50
  * **Smart Command Suggestions**: Typo detection with "Did you mean...?" suggestions using Levenshtein distance
39
51
  * **Command Analytics**: New `:stats` command shows usage statistics, performance metrics, and most-used commands
@@ -94,8 +106,8 @@ Special commands:
94
106
  * `:bm "name"` or `:bookmark "name"` bookmark current directory, `:bm "name path #tags"` with tags (NEW in v3.0)
95
107
  * `:bm` lists all bookmarks, just type bookmark name to jump (e.g., `work`) (NEW in v3.0)
96
108
  * `:bm "-name"` delete bookmark, `:bm "?tag"` search by tag (NEW in v3.0)
97
- * `:save_session` saves current shell session (pwd, history, bookmarks, defuns) (NEW in v3.0)
98
- * `:load_session` restores previously saved session (NEW in v3.0)
109
+ * `:save_session "name"` saves named session, `:load_session "name"` loads session (NEW in v3.0)
110
+ * `:list_sessions` shows all saved sessions, `:rmsession "name"` or `:rmsession "*"` deletes (NEW in v3.1)
99
111
  * `:info` shows introduction and feature overview
100
112
  * `:version` Shows the rsh version number and the last published gem file version
101
113
  * `:help` will display a compact command reference in two columns
@@ -125,12 +137,19 @@ Add to your `.rshrc`:
125
137
  While you `cd` around to different directories, you can see the last 10 directories visited via the command `:dirs` or the convenient shortcut `#`. Entering the number in the list (like `6` and ENTER) will jump you to that directory. Entering `-` will jump you back to the previous dir (equivalent of `1`. Entering `~` will get you to your home dir. If you want to bookmark a special directory, you can do that via a general nick like this: `:gnick "x = /path/to/a/dir/"` - this would bookmark the directory to the single letter `x`.
126
138
 
127
139
  ## Nicks
128
- Add command nicks (aliases) with `:nick "some_nick = some_command"`, e.g. `:nick "ls = ls --color"`. Add general nicks that will substitute anything on a command line (not just commands) like this `:gnick "some_gnick = some_command"`, e.g. `:gnick "x = /home/user/somewhere"`. List (g)nicks with `:nick?`. Remove a nick with `:nick "-some_command"`, e.g. `:nick "-ls"` to remove an `ls` nick. Same for gnicks.
140
+ Add command nicks (aliases) with `:nick "some_nick = some_command"`, e.g. `:nick "ls = ls --color"`. Add general nicks that will substitute anything on a command line (not just commands) like this `:gnick "some_gnick = some_command"`, e.g. `:gnick "x = /home/user/somewhere"`. List nicks with `:nick`, list gnicks with `:gnick`. Remove a nick with `:nick "-some_command"`, e.g. `:nick "-ls"` to remove an `ls` nick. Same for gnicks.
129
141
 
130
142
  ## Tab completion
131
- You can tab complete almost anything. Hitting `TAB` will try to complete in this priority: nicks, gnicks, commands, dirs/files. Hitting `TAB`after a `-` will list the command switches for the preceding command with a short explanation (from the command's --help), like this `ls -`(`TAB`) will list all the switches/options for the `ls` command. You can add to (or subtract from) the search criteria while selecting possible matches - hit any letter to specify the search, while backspace removes a letter from the search criteria.
143
+ You can tab complete almost anything. Hitting `TAB` will try to complete in this priority: nicks, gnicks, commands, dirs/files. Special completions:
144
+ - `ls -<TAB>` lists command switches from --help with descriptions
145
+ - `:st<TAB>` completes colon commands (:stats, etc.)
146
+ - `$HO<TAB>` completes environment variables ($HOME, etc.)
147
+ - `git <TAB>` shows git subcommands (add, commit, push, etc.)
148
+ - `--format=<TAB>` completes option values (json, yaml, xml, etc.)
132
149
 
133
- Hitting Shift-TAB will do a similar search through the command history - but with a general match of the search criteria (not only matching at the start).
150
+ You can add to (or subtract from) the search criteria while selecting matches - hit any letter to refine the search, backspace removes a letter from the criteria.
151
+
152
+ Hitting Shift-TAB will search through the command history with fuzzy matching.
134
153
 
135
154
  ## Open files
136
155
  If you press `ENTER` after writing or tab-completing to a file, rsh will try to open the file in the user's EDITOR of choice (if it is a valid text file) or use `xdg-open` to open the file using the correct program. If you, for some reason want to use `run-mailcap` instead of `xdg-open` as the file opener, simply add `@runmailcap = true` to your `.rshrc`.
@@ -215,6 +234,7 @@ Variable | Description
215
234
  `@c_path` | Color for valid path
216
235
  `@c_switch` | Color for command switches/options
217
236
  `@c_bookmark` | Color for bookmarks (NEW in v3.0)
237
+ `@c_colon` | Color for colon commands (NEW in v3.1)
218
238
  `@c_tabselect` | Color for selected tabcompleted item
219
239
  `@c_taboption` | Color for unselected tabcompleted item
220
240
  `@c_stamp` | Color for time stamp/command
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.0.0" # Major release: Persistent defuns, switch caching, smart suggestions, analytics, tooltips, bookmarks, validation, sessions
11
+ @version = "3.1.0" # Quick wins: Multiple sessions, stats export, bookmark features, themes, config, env 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,13 @@ 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
- @session_file = Dir.home + '/.rsh_session' # Session save file
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
+ Dir.mkdir(Dir.home + '/.rsh') unless Dir.exist?(Dir.home + '/.rsh')
148
+ Dir.mkdir(@session_dir) unless Dir.exist?(@session_dir)
142
149
  def pre_cmd; end # User-defined function to be run BEFORE command execution
143
150
  def post_cmd; end # User-defined function to be run AFTER command execution
144
151
  end
@@ -146,7 +153,7 @@ end
146
153
  # HELP TEXT
147
154
  @info = <<~INFO
148
155
 
149
- Hello #{@user}, welcome to rsh v3.0 - the Ruby SHell.
156
+ Hello #{@user}, welcome to rsh v3.1 - the Ruby SHell.
150
157
 
151
158
  rsh does not attempt to compete with the grand old shells like bash and zsh.
152
159
  It serves the specific needs and wants of its author. If you like it, then feel free
@@ -172,6 +179,18 @@ end
172
179
  * Syntax validation - Pre-execution warnings for dangerous or malformed commands
173
180
  * Unified command syntax - :nick, :gnick, :bm all work the same way (list/create/delete)
174
181
 
182
+ NEW in v3.1:
183
+ * Multiple named sessions - :save_session "project" and :load_session "project"
184
+ * Stats export - :stats --csv or :stats --json for data analysis
185
+ * Session auto-save - Set @session_autosave = 300 in .rshrc for 5-min auto-save
186
+ * Bookmark import/export - :bm --export file.json and :bm --import file.json
187
+ * Bookmark statistics - :bm --stats shows usage patterns and tag distribution
188
+ * Color themes - :theme solarized|dracula|gruvbox|nord|monokai
189
+ * Config management - :config shows/sets history_dedup, session_autosave, etc.
190
+ * Environment management - :env for listing/setting/exporting environment variables
191
+ * Bookmark TAB completion - Bookmarks appear in TAB completion list
192
+ * List sessions - :list_sessions shows all saved sessions with timestamps
193
+
175
194
  Config file (.rshrc) updates on exit (Ctrl-d) or not (Ctrl-e).
176
195
  All colors are themeable in .rshrc (see github link for possibilities).
177
196
 
@@ -206,6 +225,7 @@ def firstrun
206
225
  @c_path = 208 # Color for valid path
207
226
  @c_switch = 148 # Color for switches/options
208
227
  @c_bookmark = 13 # Color for bookmarks
228
+ @c_colon = 4 # Color for colon commands
209
229
  @c_tabselect = 207 # Color for selected tabcompleted item
210
230
  @c_taboption = 244 # Color for unselected tabcompleted item
211
231
  @c_stamp = 244 # Color for time stamp/command
@@ -485,6 +505,7 @@ def tab(type)
485
505
  type = "switch" if @tabstr && @tabstr[0] == "-" && !@tabstr.include?("=")
486
506
  type = "option_value" if @tabstr && @tabstr =~ /^--?[\w-]+=/
487
507
  type = "env_vars" if @tabstr && @tabstr[0] == "$"
508
+ type = "colon_commands" if @tabstr && @tabstr[0] == ":"
488
509
 
489
510
  # Debug output when RSH_DEBUG is set
490
511
  if ENV['RSH_DEBUG']
@@ -576,12 +597,25 @@ def tab(type)
576
597
  env_vars = ENV.keys.map { |k| "$#{k}" }
577
598
  regex_flags = @completion_case_sensitive ? 0 : Regexp::IGNORECASE
578
599
  @tabarray = env_vars.select { |var| var =~ Regexp.new(@tabstr, regex_flags) }
600
+ when "colon_commands" # Ruby/rsh commands starting with :
601
+ colon_cmds = %w[
602
+ :nick :gnick :bm :bookmark :stats :defun :defun?
603
+ :history :rmhistory :jobs :fg :bg
604
+ :save_session :load_session :list_sessions :delete_session :rmsession
605
+ :config :env :theme
606
+ :info :version :help
607
+ ]
608
+ search_str = @tabstr[1..-1] || "" # Remove leading :
609
+ regex_flags = @completion_case_sensitive ? 0 : Regexp::IGNORECASE
610
+ matches = colon_cmds.select { |cmd| cmd[1..-1] =~ Regexp.new("^#{search_str}", regex_flags) }
611
+ @tabarray = matches
579
612
  when "all" # Handle all other tab completions
580
613
  ex = []
581
614
  ex += @exe
582
615
  ex.sort!
583
616
  ex.prepend(*@nick.keys) # Add nicks
584
617
  ex.prepend(*@gnick.keys) # Add gnicks
618
+ ex.prepend(*@bookmarks.keys) if @bookmarks # Add bookmarks
585
619
 
586
620
  # Enhanced matching with case sensitivity and fuzzy support
587
621
  regex_flags = @completion_case_sensitive ? 0 : Regexp::IGNORECASE
@@ -829,9 +863,96 @@ def suggest_command(cmd) # Smart command suggestions for typos
829
863
  candidates.first(3)
830
864
  end
831
865
  def hist_clean # Clean up @history
832
- @history.uniq!
833
866
  @history.compact!
834
867
  @history.delete("")
868
+
869
+ # Apply deduplication based on mode
870
+ case @history_dedup
871
+ when 'off'
872
+ # No deduplication
873
+ when 'full', 'smart'
874
+ # Remove duplicates, keeping first (most recent) occurrence
875
+ @history.uniq!
876
+ else
877
+ # Default to smart
878
+ @history.uniq!
879
+ end
880
+ end
881
+ def config(*args) # Configure rsh settings
882
+ setting = args[0]
883
+ value = args[1]
884
+
885
+ if setting.nil?
886
+ # Show current configuration
887
+ puts "\n Current Configuration:".c(@c_prompt).b
888
+ puts " history_dedup: #{@history_dedup}"
889
+ puts " session_autosave: #{@session_autosave}s #{@session_autosave > 0 ? '(enabled)' : '(disabled)'}"
890
+ puts " completion_limit: #{@completion_limit}"
891
+ puts " completion_fuzzy: #{@completion_fuzzy}"
892
+ puts " completion_case_sensitive: #{@completion_case_sensitive}"
893
+ puts
894
+ return
895
+ end
896
+
897
+ case setting
898
+ when 'history_dedup'
899
+ if %w[off full smart].include?(value)
900
+ @history_dedup = value
901
+ puts "History deduplication set to '#{value}'"
902
+ rshrc
903
+ else
904
+ puts "Invalid value. Use: off, full, or smart"
905
+ end
906
+ when 'session_autosave'
907
+ @session_autosave = value.to_i
908
+ puts "Session auto-save set to #{value}s #{value.to_i > 0 ? '(enabled)' : '(disabled)'}"
909
+ rshrc
910
+ when 'completion_limit'
911
+ @completion_limit = value.to_i
912
+ puts "Completion limit set to #{value}"
913
+ rshrc
914
+ else
915
+ puts "Unknown setting '#{setting}'"
916
+ puts "Available: history_dedup, session_autosave, completion_limit"
917
+ end
918
+ end
919
+ def env(*args) # Environment variable management
920
+ arg_str = args.join(' ')
921
+
922
+ if args.empty?
923
+ # List all environment variables
924
+ puts "\n Environment Variables:".c(@c_prompt).b
925
+ ENV.sort.first(20).each do |key, value|
926
+ value_display = value.length > 50 ? value[0..47] + '...' : value
927
+ puts " #{key.c(@c_gnick).ljust(25)} = #{value_display}"
928
+ end
929
+ puts " ... (#{ENV.length} total, showing first 20)"
930
+ puts "\n Use :env \"VARNAME\" to see specific variable"
931
+ puts
932
+ elsif arg_str =~ /^set\s+(\w+)\s+(.+)$/
933
+ # Set environment variable
934
+ var_name, var_value = $1, $2
935
+ ENV[var_name] = var_value
936
+ puts "#{var_name} = #{var_value}"
937
+ elsif arg_str =~ /^unset\s+(\w+)$/
938
+ # Unset environment variable
939
+ var_name = $1
940
+ ENV.delete(var_name)
941
+ puts "#{var_name} unset"
942
+ elsif arg_str =~ /^export\s+(.+)$/
943
+ # Export to shell script
944
+ filename = $1
945
+ File.write(filename, ENV.map { |k,v| "export #{k}=\"#{v}\"" }.join("\n"))
946
+ puts "Environment exported to #{filename}"
947
+ else
948
+ # Show specific variable
949
+ var_name = arg_str.strip
950
+ if ENV[var_name]
951
+ puts "#{var_name} = #{ENV[var_name]}"
952
+ else
953
+ puts "Environment variable '#{var_name}' not set"
954
+ end
955
+ end
835
956
  end
836
957
  def cmd_check(str) # Check if each element on the readline matches commands, nicks, paths; color them
837
958
  return if str.nil?
@@ -840,7 +961,14 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic
840
961
  if str =~ /^(@@?)\s+(.*)$/
841
962
  prefix = $1
842
963
  rest = $2
843
- return prefix.c(4) + " " + rest # Color @ or @@ in blue (4), rest uncolored
964
+ return prefix.c(@c_colon) + " " + rest # Color @ or @@ in colon color
965
+ end
966
+
967
+ # Special handling for : commands
968
+ if str =~ /^(:[\w?_]+)/
969
+ colon_cmd = $1
970
+ rest = str.sub(/^:[\w?_]+/, '')
971
+ return colon_cmd.c(@c_colon) + rest # Color colon commands
844
972
  end
845
973
 
846
974
  str.gsub(/(?:\S'[^']*'|[^ '])+/) do |el|
@@ -892,6 +1020,10 @@ def rshrc # Write updates to .rshrc
892
1020
  conf += "@bookmarks = #{@bookmarks}\n" unless @bookmarks.empty?
893
1021
  conf.sub!(/^@defuns.*(\n|$)/, "")
894
1022
  conf += "@defuns = #{@defuns}\n" unless @defuns.empty?
1023
+ conf.sub!(/^@history_dedup.*(\n|$)/, "")
1024
+ conf += "@history_dedup = '#{@history_dedup}'\n" if @history_dedup && @history_dedup != 'smart'
1025
+ conf.sub!(/^@session_autosave.*(\n|$)/, "")
1026
+ conf += "@session_autosave = #{@session_autosave}\n" if @session_autosave && @session_autosave > 0
895
1027
  # Only write @cmd_completions if user has customized it
896
1028
  unless conf =~ /^@cmd_completions\s*=/
897
1029
  # Don't write default completions to avoid cluttering .rshrc
@@ -928,6 +1060,7 @@ def help
928
1060
  left_col << "RIGHT/Ctrl-F Accept suggestion"
929
1061
  left_col << "UP/DOWN Navigate history"
930
1062
  left_col << "TAB Tab complete"
1063
+ left_col << "Shift-TAB Search history"
931
1064
  left_col << "Ctrl-Y Copy to clipboard"
932
1065
  left_col << "Ctrl-D Exit + save .rshrc"
933
1066
  left_col << "Ctrl-E Exit without save"
@@ -964,15 +1097,20 @@ def help
964
1097
  right_col << ":fg [id] Foreground job"
965
1098
  right_col << ":bg [id] Resume in bg"
966
1099
  right_col << ""
967
- right_col << "v3.0 NEW FEATURES:".c(@c_prompt).b
968
- right_col << ":stats Command analytics"
1100
+ right_col << "v3.0/3.1 FEATURES:".c(@c_prompt).b
1101
+ right_col << ":stats [--csv|--json] Analytics"
969
1102
  right_col << ":bm \"name\" Create bookmark"
970
1103
  right_col << "name Jump to bookmark"
971
- right_col << ":bm List bookmarks"
972
- right_col << ":bm \"-name\" Delete bookmark"
973
- right_col << ":bm \"?tag\" Search by tag"
974
- right_col << ":save_session Save session"
975
- right_col << ":load_session Restore session"
1104
+ right_col << ":bm --stats Bookmark stats"
1105
+ right_col << ":bm --export file Export bookmarks"
1106
+ right_col << ":bm --import file Import bookmarks"
1107
+ right_col << ":save_session [nm] Save session"
1108
+ right_col << ":load_session [nm] Load session"
1109
+ right_col << ":list_sessions List all sessions"
1110
+ right_col << ":rmsession name|* Delete session(s)"
1111
+ right_col << ":theme [name] Color presets"
1112
+ right_col << ":config [set val] Settings"
1113
+ right_col << ":env [VARNAME] Env management"
976
1114
  right_col << ""
977
1115
  right_col << "INTEGRATIONS:".c(@c_prompt).b
978
1116
  right_col << "r Launch rtfm"
@@ -1176,7 +1314,26 @@ def defun? # Show all user-defined functions
1176
1314
  end
1177
1315
  end
1178
1316
  end
1179
- def stats # Show command execution statistics and analytics
1317
+ def stats(*args) # Show command execution statistics and analytics
1318
+ format = args[0]
1319
+ filename = args[1]
1320
+
1321
+ if format == "--export"
1322
+ # Export to file
1323
+ fname = filename || "rsh_stats.json"
1324
+ export_stats(fname)
1325
+ return
1326
+ elsif format == "--json"
1327
+ fname = filename || "rsh_stats.json"
1328
+ export_stats_json(fname)
1329
+ return
1330
+ elsif format == "--csv"
1331
+ fname = filename || "rsh_stats.csv"
1332
+ export_stats_csv(fname)
1333
+ return
1334
+ end
1335
+
1336
+ # Display stats (existing code)
1180
1337
  puts "\n Command Execution Statistics".c(@c_prompt).b
1181
1338
  puts " " + "="*50
1182
1339
 
@@ -1216,11 +1373,62 @@ def stats # Show command execution statistics and analytics
1216
1373
  puts "\n Last command exit status: #{@last_exit == 0 ? 'Success'.c(@c_path) : "Failed (#{@last_exit})".c(196)}"
1217
1374
  puts
1218
1375
  end
1219
- def bm(args = nil) # Enhanced bookmark management with tags
1220
- if args.nil? || args.empty?
1376
+ def export_stats(filename) # Export stats to file (JSON or CSV based on extension)
1377
+ if filename.end_with?('.csv')
1378
+ export_stats_csv(filename)
1379
+ else
1380
+ filename += '.json' unless filename.end_with?('.json')
1381
+ export_stats_json(filename)
1382
+ end
1383
+ end
1384
+ def export_stats_json(filename = 'rsh_stats.json') # Export stats to JSON
1385
+ stats_data = {
1386
+ generated: Time.now.to_i,
1387
+ cmd_frequency: @cmd_frequency,
1388
+ cmd_stats: @cmd_stats,
1389
+ history: {
1390
+ total: @history.length,
1391
+ unique: @history.uniq.length
1392
+ },
1393
+ last_exit: @last_exit
1394
+ }
1395
+ begin
1396
+ require 'json'
1397
+ File.write(filename, JSON.pretty_generate(stats_data))
1398
+ puts "Stats exported to #{filename}"
1399
+ rescue => e
1400
+ puts "Error exporting stats: #{e.message}"
1401
+ end
1402
+ end
1403
+ def export_stats_csv(filename = 'rsh_stats.csv') # Export stats to CSV
1404
+ begin
1405
+ lines = []
1406
+ lines << "command,frequency,count,total_time,avg_time"
1407
+
1408
+ # Merge frequency and performance data
1409
+ all_cmds = (@cmd_frequency.keys + @cmd_stats.keys).uniq
1410
+ all_cmds.sort.each do |cmd|
1411
+ freq = @cmd_frequency[cmd] || 0
1412
+ count = @cmd_stats.dig(cmd, :count) || 0
1413
+ total = @cmd_stats.dig(cmd, :total_time) || 0.0
1414
+ avg = @cmd_stats.dig(cmd, :avg_time) || 0.0
1415
+ lines << "#{cmd},#{freq},#{count},#{'%.3f' % total},#{'%.3f' % avg}"
1416
+ end
1417
+
1418
+ File.write(filename, lines.join("\n"))
1419
+ puts "Stats exported to #{filename}"
1420
+ rescue => e
1421
+ puts "Error exporting stats: #{e.message}"
1422
+ end
1423
+ end
1424
+ def bm(*args) # Enhanced bookmark management with tags
1425
+ # Handle variadic arguments
1426
+ arg_str = args.join(' ')
1427
+
1428
+ if args.empty?
1221
1429
  # List all bookmarks
1222
1430
  if @bookmarks.empty?
1223
- puts "No bookmarks defined. Use :bookmark <name> to bookmark current directory"
1431
+ puts "No bookmarks defined. Use :bm \"name\" to bookmark current directory"
1224
1432
  return
1225
1433
  end
1226
1434
  puts "\n Bookmarks:".c(@c_prompt).b
@@ -1230,7 +1438,18 @@ def bm(args = nil) # Enhanced bookmark management with tags
1230
1438
  puts " #{name.c(@c_nick)} → #{path}#{tags.c(@c_stamp)}"
1231
1439
  end
1232
1440
  puts
1233
- elsif args =~ /^(\w+)\s+(.+)$/
1441
+ elsif args[0] == '--export'
1442
+ # Export bookmarks to file
1443
+ filename = args[1] || 'bookmarks.json'
1444
+ export_bookmarks(filename)
1445
+ elsif args[0] == '--import'
1446
+ # Import bookmarks from file
1447
+ filename = args[1]
1448
+ import_bookmarks(filename) if filename
1449
+ elsif args[0] == '--stats'
1450
+ # Show bookmark statistics
1451
+ bookmark_stats
1452
+ elsif arg_str =~ /^(\w+)\s+(.+)$/
1234
1453
  # Set bookmark with optional tags
1235
1454
  name, rest = $1, $2
1236
1455
  if rest.include?('#')
@@ -1244,7 +1463,7 @@ def bm(args = nil) # Enhanced bookmark management with tags
1244
1463
  end
1245
1464
  puts "Bookmark '#{name}' set to #{@bookmarks[name][:path]}"
1246
1465
  rshrc
1247
- elsif args =~ /^-(\w+)$/
1466
+ elsif arg_str =~ /^-(\w+)$/
1248
1467
  # Delete bookmark
1249
1468
  name = $1
1250
1469
  if @bookmarks.delete(name)
@@ -1253,7 +1472,7 @@ def bm(args = nil) # Enhanced bookmark management with tags
1253
1472
  else
1254
1473
  puts "Bookmark '#{name}' not found"
1255
1474
  end
1256
- elsif args =~ /^\?(\w*)$/
1475
+ elsif arg_str =~ /^\?(\w*)$/
1257
1476
  # Search bookmarks by tag
1258
1477
  tag = $1
1259
1478
  if tag.empty?
@@ -1273,17 +1492,82 @@ def bm(args = nil) # Enhanced bookmark management with tags
1273
1492
  end
1274
1493
  else
1275
1494
  # Bookmark current directory
1276
- name = args.strip
1495
+ name = arg_str.strip
1277
1496
  @bookmarks[name] = {path: Dir.pwd, tags: []}
1278
1497
  puts "Bookmark '#{name}' set to #{Dir.pwd}"
1279
1498
  rshrc
1280
1499
  end
1281
1500
  end
1282
- def bookmark(args = nil) # Alias for bm
1283
- bm(args)
1501
+ def bookmark(*args) # Alias for bm
1502
+ bm(*args)
1503
+ end
1504
+ def export_bookmarks(filename = 'bookmarks.json') # Export bookmarks to JSON
1505
+ begin
1506
+ require 'json'
1507
+ File.write(filename, JSON.pretty_generate(@bookmarks))
1508
+ puts "Bookmarks exported to #{filename}"
1509
+ rescue => e
1510
+ puts "Error exporting bookmarks: #{e.message}"
1511
+ end
1512
+ end
1513
+ def import_bookmarks(filename) # Import bookmarks from JSON
1514
+ unless File.exist?(filename)
1515
+ puts "File '#{filename}' not found"
1516
+ return
1517
+ end
1518
+ begin
1519
+ require 'json'
1520
+ imported = JSON.parse(File.read(filename))
1521
+ imported.each do |name, data|
1522
+ # Convert to proper format
1523
+ if data.is_a?(Hash)
1524
+ @bookmarks[name] = {
1525
+ path: data['path'] || data[:path],
1526
+ tags: data['tags'] || data[:tags] || []
1527
+ }.transform_keys(&:to_sym)
1528
+ else
1529
+ @bookmarks[name] = {path: data.to_s, tags: []}
1530
+ end
1531
+ end
1532
+ puts "Imported #{imported.length} bookmarks from #{filename}"
1533
+ rshrc
1534
+ rescue => e
1535
+ puts "Error importing bookmarks: #{e.message}"
1536
+ end
1537
+ end
1538
+ def bookmark_stats # Show bookmark usage statistics
1539
+ if @bookmarks.empty?
1540
+ puts "No bookmarks defined"
1541
+ return
1542
+ end
1543
+
1544
+ puts "\n Bookmark Statistics".c(@c_prompt).b
1545
+ puts " " + "="*50
1546
+ puts "\n Total bookmarks: #{@bookmarks.length}"
1547
+
1548
+ # Count by tags
1549
+ all_tags = @bookmarks.values.flat_map { |d| d.is_a?(Hash) ? (d[:tags] || []) : [] }
1550
+ unless all_tags.empty?
1551
+ puts "\n Tags Distribution:".c(@c_nick)
1552
+ tag_counts = all_tags.group_by(&:itself).transform_values(&:count)
1553
+ tag_counts.sort_by { |_, count| -count }.each do |tag, count|
1554
+ puts " #{tag.ljust(15)} #{count}x"
1555
+ end
1556
+ end
1557
+
1558
+ # Bookmarks by directory depth
1559
+ puts "\n Path Analysis:".c(@c_nick)
1560
+ paths = @bookmarks.values.map { |d| d.is_a?(Hash) ? d[:path] : d }
1561
+ avg_depth = paths.map { |p| p.split('/').length }.sum / paths.length
1562
+ puts " Average path depth: #{avg_depth}"
1563
+ puts
1284
1564
  end
1285
- def save_session # Save current session state
1565
+ def save_session(*args) # Save current session state
1566
+ session_name = args[0] || 'default'
1567
+ session_path = @session_dir + "/#{session_name}.json"
1568
+
1286
1569
  session = {
1570
+ name: session_name,
1287
1571
  pwd: Dir.pwd,
1288
1572
  history: @history.first(50),
1289
1573
  bookmarks: @bookmarks,
@@ -1292,20 +1576,24 @@ def save_session # Save current session state
1292
1576
  }
1293
1577
  begin
1294
1578
  require 'json'
1295
- File.write(@session_file, JSON.pretty_generate(session))
1296
- puts "Session saved to #{@session_file}"
1579
+ File.write(session_path, JSON.pretty_generate(session))
1580
+ puts "Session '#{session_name}' saved to #{session_path}"
1297
1581
  rescue => e
1298
1582
  puts "Error saving session: #{e.message}"
1299
1583
  end
1300
1584
  end
1301
- def load_session # Restore previous session
1302
- unless File.exist?(@session_file)
1303
- puts "No saved session found"
1585
+ def load_session(*args) # Restore previous session
1586
+ session_name = args[0] || 'default'
1587
+ session_path = @session_dir + "/#{session_name}.json"
1588
+
1589
+ unless File.exist?(session_path)
1590
+ puts "Session '#{session_name}' not found"
1591
+ list_sessions
1304
1592
  return
1305
1593
  end
1306
1594
  begin
1307
1595
  require 'json'
1308
- session = JSON.parse(File.read(@session_file), symbolize_names: true)
1596
+ session = JSON.parse(File.read(session_path), symbolize_names: true)
1309
1597
 
1310
1598
  # Restore state
1311
1599
  Dir.chdir(session[:pwd]) if session[:pwd] && Dir.exist?(session[:pwd])
@@ -1338,12 +1626,124 @@ def load_session # Restore previous session
1338
1626
  end
1339
1627
 
1340
1628
  saved_time = Time.at(session[:timestamp] || 0).strftime("%Y-%m-%d %H:%M:%S")
1341
- puts "Session restored from #{saved_time}"
1629
+ puts "Session '#{session_name}' restored from #{saved_time}"
1342
1630
  rshrc
1343
1631
  rescue => e
1344
1632
  puts "Error loading session: #{e.message}"
1345
1633
  end
1346
1634
  end
1635
+ def list_sessions # List all saved sessions
1636
+ unless Dir.exist?(@session_dir)
1637
+ puts "No sessions directory found"
1638
+ return
1639
+ end
1640
+
1641
+ sessions = Dir.glob(@session_dir + '/*.json').map { |f| File.basename(f, '.json') }
1642
+
1643
+ if sessions.empty?
1644
+ puts "No saved sessions found. Use :save_session \"name\" to create one"
1645
+ return
1646
+ end
1647
+
1648
+ puts "\n Saved Sessions:".c(@c_prompt).b
1649
+ sessions.sort.each do |name|
1650
+ session_path = @session_dir + "/#{name}.json"
1651
+ begin
1652
+ require 'json'
1653
+ session = JSON.parse(File.read(session_path), symbolize_names: true)
1654
+ timestamp = Time.at(session[:timestamp] || 0).strftime("%Y-%m-%d %H:%M")
1655
+ pwd = session[:pwd] || '?'
1656
+ puts " #{name.c(@c_bookmark).ljust(20)} #{timestamp.c(@c_stamp)} #{pwd.c(@c_path)}"
1657
+ rescue => e
1658
+ puts " #{name.c(@c_bookmark).ljust(20)} [corrupted]".c(196)
1659
+ end
1660
+ end
1661
+ puts
1662
+ end
1663
+ def delete_session(*args) # Delete a saved session
1664
+ name = args[0]
1665
+
1666
+ if name == '*'
1667
+ # Delete all sessions except default
1668
+ sessions = Dir.glob(@session_dir + '/*.json').map { |f| File.basename(f, '.json') }
1669
+ sessions.reject! { |s| s == 'default' || s == 'autosave' }
1670
+
1671
+ if sessions.empty?
1672
+ puts "No sessions to delete (keeping default and autosave)"
1673
+ return
1674
+ end
1675
+
1676
+ sessions.each do |session_name|
1677
+ session_path = @session_dir + "/#{session_name}.json"
1678
+ File.delete(session_path)
1679
+ end
1680
+ puts "Deleted #{sessions.length} sessions: #{sessions.join(', ')}"
1681
+ return
1682
+ end
1683
+
1684
+ return puts "Cannot delete default session" if name == 'default'
1685
+ return puts "Cannot delete autosave session (use * to delete all)" if name == 'autosave'
1686
+
1687
+ session_path = @session_dir + "/#{name}.json"
1688
+ unless File.exist?(session_path)
1689
+ puts "Session '#{name}' not found"
1690
+ return
1691
+ end
1692
+
1693
+ File.delete(session_path)
1694
+ puts "Session '#{name}' deleted"
1695
+ end
1696
+ def rmsession(*args) # Alias for delete_session
1697
+ delete_session(*args)
1698
+ end
1699
+ def theme(*args) # Apply color scheme presets
1700
+ name = args[0]
1701
+
1702
+ if name.nil?
1703
+ puts "\n Available themes:".c(@c_prompt).b
1704
+ puts " default, solarized, dracula, gruvbox, nord, monokai"
1705
+ puts "\n Current theme colors:"
1706
+ puts " prompt:#{' '*5}#{@c_prompt} cmd:#{' '*8}#{@c_cmd} nick:#{' '*7}#{@c_nick}"
1707
+ puts " gnick:#{' '*6}#{@c_gnick} path:#{' '*7}#{@c_path} switch:#{' '*5}#{@c_switch}"
1708
+ puts " bookmark:#{' '*3}#{@c_bookmark} colon:#{' '*6}#{@c_colon} tabselect:#{' '*2}#{@c_tabselect}"
1709
+ puts " taboption:#{' '*2}#{@c_taboption} stamp:#{' '*6}#{@c_stamp}"
1710
+ puts
1711
+ return
1712
+ end
1713
+
1714
+ case name.downcase
1715
+ when 'default'
1716
+ @c_prompt, @c_cmd, @c_nick, @c_gnick = 10, 2, 6, 14
1717
+ @c_path, @c_switch, @c_bookmark, @c_colon = 3, 6, 13, 4
1718
+ @c_tabselect, @c_taboption, @c_stamp = 5, 244, 244
1719
+ when 'solarized'
1720
+ @c_prompt, @c_cmd, @c_nick, @c_gnick = 33, 64, 37, 117
1721
+ @c_path, @c_switch, @c_bookmark, @c_colon = 136, 125, 61, 33
1722
+ @c_tabselect, @c_taboption, @c_stamp = 166, 240, 240
1723
+ when 'dracula'
1724
+ @c_prompt, @c_cmd, @c_nick, @c_gnick = 141, 84, 117, 212
1725
+ @c_path, @c_switch, @c_bookmark, @c_colon = 228, 215, 141, 141
1726
+ @c_tabselect, @c_taboption, @c_stamp = 212, 238, 238
1727
+ when 'gruvbox'
1728
+ @c_prompt, @c_cmd, @c_nick, @c_gnick = 214, 142, 109, 175
1729
+ @c_path, @c_switch, @c_bookmark, @c_colon = 208, 142, 167, 214
1730
+ @c_tabselect, @c_taboption, @c_stamp = 208, 243, 243
1731
+ when 'nord'
1732
+ @c_prompt, @c_cmd, @c_nick, @c_gnick = 110, 109, 116, 152
1733
+ @c_path, @c_switch, @c_bookmark, @c_colon = 180, 109, 139, 110
1734
+ @c_tabselect, @c_taboption, @c_stamp = 143, 240, 240
1735
+ when 'monokai'
1736
+ @c_prompt, @c_cmd, @c_nick, @c_gnick = 197, 112, 81, 141
1737
+ @c_path, @c_switch, @c_bookmark, @c_colon = 228, 208, 141, 197
1738
+ @c_tabselect, @c_taboption, @c_stamp = 197, 238, 238
1739
+ else
1740
+ puts "Unknown theme '#{name}'. Available: default, solarized, dracula, gruvbox, nord, monokai"
1741
+ return
1742
+ end
1743
+
1744
+ puts "Theme '#{name}' applied"
1745
+ puts "Add this to .rshrc to make it permanent: :theme \"#{name}\""
1746
+ end
1347
1747
  def validate_command(cmd) # Syntax validation before execution
1348
1748
  return nil if cmd.nil? || cmd.empty?
1349
1749
  warnings = []
@@ -1653,6 +2053,8 @@ def load_rshrc_safe
1653
2053
  @cmd_stats = {} unless @cmd_stats.is_a?(Hash)
1654
2054
  @bookmarks = {} unless @bookmarks.is_a?(Hash)
1655
2055
  @defuns = {} unless @defuns.is_a?(Hash)
2056
+ @history_dedup = 'smart' unless @history_dedup.is_a?(String)
2057
+ @session_autosave = 0 unless @session_autosave.is_a?(Integer)
1656
2058
 
1657
2059
  # Restore defuns from .rshrc
1658
2060
  if @defuns && !@defuns.empty?
@@ -1787,6 +2189,8 @@ def load_defaults
1787
2189
  @defuns ||= {}
1788
2190
  @switch_cache ||= {}
1789
2191
  @switch_cache_time ||= {}
2192
+ @history_dedup ||= 'smart'
2193
+ @session_autosave ||= 0
1790
2194
  puts "Loaded with default configuration."
1791
2195
  end
1792
2196
 
@@ -1872,6 +2276,14 @@ loop do
1872
2276
  system("printf \"\033]0;rsh: #{Dir.pwd}\007\"") # Set Window title to path
1873
2277
  @history[0] = "" unless @history[0]
1874
2278
  cache_executables # Use cached executable lookup
2279
+ # Auto-save session if enabled and interval elapsed
2280
+ if @session_autosave && @session_autosave > 0
2281
+ current_time = Time.now.to_i
2282
+ if (current_time - @session_last_save) >= @session_autosave
2283
+ save_session('autosave')
2284
+ @session_last_save = current_time
2285
+ end
2286
+ end
1875
2287
  getstr # Main work is here
1876
2288
  @cmd = @history[0]
1877
2289
  @dirs.unshift(Dir.pwd)
@@ -1976,6 +2388,7 @@ loop do
1976
2388
  elsif @bookmarks && @bookmarks[@cmd]
1977
2389
  bookmark_data = @bookmarks[@cmd]
1978
2390
  bm_dir = bookmark_data.is_a?(Hash) ? bookmark_data[:path] : bookmark_data
2391
+ bm_dir = bm_dir.sub(/^~/, Dir.home) # Expand tilde
1979
2392
  if Dir.exist?(bm_dir)
1980
2393
  Dir.chdir(bm_dir)
1981
2394
  puts "Jumped to bookmark '#{@cmd}' → #{bm_dir}".c(@c_path)
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.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene
@@ -12,9 +12,9 @@ date: 2025-10-22 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.0.0: MAJOR RELEASE - Persistent defuns, smart command suggestions with typo detection,
16
- command analytics with :stats, switch caching, enhanced bookmarks with tags, session
17
- save/restore, syntax validation, option value completion, and performance tracking.'
15
+ v3.1.0: Multiple named sessions, stats export (CSV/JSON), session auto-save, bookmark
16
+ import/export, bookmark statistics, 6 color themes, config management, environment
17
+ variable tools, bookmark TAB completion, and much more!'
18
18
  email: g@isene.com
19
19
  executables:
20
20
  - rsh