ruby-shell 2.12.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.
- checksums.yaml +4 -4
- data/README.md +47 -9
- data/bin/rsh +880 -70
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 318b51625674f856dfa297d3b8fc8a3bf0e2fd2bb87e4f95d4184bae6e94be72
|
|
4
|
+
data.tar.gz: 03cfd00609fc1f2a011458d3a75837adfe2ac3975c38718b8fcca9998ffa22b6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 982ec206a79d08fed5b95324b356edd4069c158bef62b52cf0205906bef39f38c8e44031821cf7c4f7eec8f9162df61e186589dee240a6074ae8dc592f090393
|
|
7
|
+
data.tar.gz: b0d025d2b2a0df814b3a9239f2151bcecc66a35ca71f0f020aa360ee2b0081cecbe963e6a2124fa35993f3595652e4797760219e88a423390f6b6825e77d8207
|
data/README.md
CHANGED
|
@@ -33,7 +33,30 @@ 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
|
|
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 ⭐⭐⭐
|
|
49
|
+
* **Persistent Ruby Functions**: defun functions now save to .rshrc and persist across sessions
|
|
50
|
+
* **Smart Command Suggestions**: Typo detection with "Did you mean...?" suggestions using Levenshtein distance
|
|
51
|
+
* **Command Analytics**: New `:stats` command shows usage statistics, performance metrics, and most-used commands
|
|
52
|
+
* **Switch Completion Caching**: Command switches from --help are cached for instant completion
|
|
53
|
+
* **Enhanced Bookmarks**: Bookmark directories with tags - `:bookmark name path #tag1,tag2`
|
|
54
|
+
* **Session Management**: Save and restore entire shell sessions with `:save_session` and `:load_session`
|
|
55
|
+
* **Syntax Validation**: Pre-execution warnings for common mistakes, dangerous commands, and typos
|
|
56
|
+
* **Option Value Completion**: TAB completion for option values like `--format=<TAB>` → json, yaml, xml
|
|
57
|
+
* **Command Performance Tracking**: Automatically tracks execution time and shows slowest commands
|
|
58
|
+
|
|
59
|
+
## AI Integration (v2.9.0) ⭐
|
|
37
60
|
* **AI-powered command assistance**: Get help with commands using natural language
|
|
38
61
|
* **`@ <question>`**: Ask questions and get AI-generated text responses
|
|
39
62
|
* **`@@ <request>`**: Describe what you want to do, and AI suggests the command
|
|
@@ -73,13 +96,18 @@ Special functions/integrations:
|
|
|
73
96
|
Special commands:
|
|
74
97
|
* `:nick 'll = ls -l'` to make a command alias (ll) point to a command (ls -l)
|
|
75
98
|
* `:gnick 'h = /home/me'` to make a general alias (h) point to something (/home/me)
|
|
76
|
-
* `:
|
|
77
|
-
* `:
|
|
78
|
-
* `:nick?` will list all command nicks and general nicks (you can edit your nicks in .rshrc)
|
|
99
|
+
* `:nick` lists all command nicks, `:gnick` lists general nicks (NEW in v3.0)
|
|
100
|
+
* `:nick '-name'` delete a command nick, `:gnick '-name'` delete a general nick (NEW in v3.0)
|
|
79
101
|
* `:history` will list the command history, while `:rmhistory` will delete the history
|
|
80
102
|
* `:jobs` will list background jobs, `:fg [job_id]` brings jobs to foreground, `:bg [job_id]` resumes stopped jobs
|
|
81
|
-
* `:defun 'func(args) = code'` defines Ruby functions callable as shell commands
|
|
103
|
+
* `:defun 'func(args) = code'` defines Ruby functions callable as shell commands (now persistent!)
|
|
82
104
|
* `:defun?` lists all user-defined functions, `:defun '-func'` removes functions
|
|
105
|
+
* `:stats` shows command execution statistics and analytics (NEW in v3.0)
|
|
106
|
+
* `:bm "name"` or `:bookmark "name"` bookmark current directory, `:bm "name path #tags"` with tags (NEW in v3.0)
|
|
107
|
+
* `:bm` lists all bookmarks, just type bookmark name to jump (e.g., `work`) (NEW in v3.0)
|
|
108
|
+
* `:bm "-name"` delete bookmark, `:bm "?tag"` search by tag (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)
|
|
83
111
|
* `:info` shows introduction and feature overview
|
|
84
112
|
* `:version` Shows the rsh version number and the last published gem file version
|
|
85
113
|
* `:help` will display a compact command reference in two columns
|
|
@@ -109,12 +137,19 @@ Add to your `.rshrc`:
|
|
|
109
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`.
|
|
110
138
|
|
|
111
139
|
## Nicks
|
|
112
|
-
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
|
|
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.
|
|
113
141
|
|
|
114
142
|
## Tab completion
|
|
115
|
-
You can tab complete almost anything. Hitting `TAB` will try to complete in this priority: nicks, gnicks, commands, dirs/files.
|
|
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.)
|
|
116
149
|
|
|
117
|
-
|
|
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.
|
|
118
153
|
|
|
119
154
|
## Open files
|
|
120
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`.
|
|
@@ -185,7 +220,7 @@ Enter the command `f` to launch the fuzzy finder - select the directory/file you
|
|
|
185
220
|
If you start a line with "=", the rest of the line will be interpreted as an XRPN program. This gives you the full power of XRPN right at your fingertips. You can do simple stuff like this: `=13,23,*,x^2` and the answer to `(13 * 23)^2` will be given (89401) in the format that you have set in your `.xrpn/conf`. Or you can do more elaborate stuff like `=fix 6,5,sto c,time,'Time now is: ',atime,aview,pse,fix 0,lbl a,rcl c,prx,dse c,gto a`. Go crazy. Use single-quotes for any Alpha entry.
|
|
186
221
|
|
|
187
222
|
## Syntax highlighting
|
|
188
|
-
rsh will highlight nicks, gnicks, commands and dirs/files as they are written on the command line.
|
|
223
|
+
rsh will highlight nicks, gnicks, bookmarks, commands, switches and dirs/files as they are written on the command line. Each element type has its own color (customizable in .rshrc).
|
|
189
224
|
|
|
190
225
|
## Theming
|
|
191
226
|
In the supplied `.rshrc`, you will find a set of colors that you can change:
|
|
@@ -197,6 +232,9 @@ Variable | Description
|
|
|
197
232
|
`@c_nick` | Color for matching nick
|
|
198
233
|
`@c_gnick` | Color for matching gnick
|
|
199
234
|
`@c_path` | Color for valid path
|
|
235
|
+
`@c_switch` | Color for command switches/options
|
|
236
|
+
`@c_bookmark` | Color for bookmarks (NEW in v3.0)
|
|
237
|
+
`@c_colon` | Color for colon commands (NEW in v3.1)
|
|
200
238
|
`@c_tabselect` | Color for selected tabcompleted item
|
|
201
239
|
`@c_taboption` | Color for unselected tabcompleted item
|
|
202
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 = "
|
|
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)
|
|
@@ -92,6 +92,8 @@ begin # Initialization
|
|
|
92
92
|
@c_gnick = 14 # Color for matching gnick
|
|
93
93
|
@c_path = 3 # Color for valid path
|
|
94
94
|
@c_switch = 6 # Color for switches/options
|
|
95
|
+
@c_bookmark = 13 # Color for bookmarks
|
|
96
|
+
@c_colon = 4 # Color for colon commands
|
|
95
97
|
@c_tabselect = 5 # Color for selected tabcompleted item
|
|
96
98
|
@c_taboption = 244 # Color for unselected tabcompleted item
|
|
97
99
|
@c_stamp = 244 # Color for time stamp/command
|
|
@@ -131,6 +133,19 @@ begin # Initialization
|
|
|
131
133
|
"gem" => %w[install uninstall update list search build push],
|
|
132
134
|
"bundle" => %w[install update exec check config]
|
|
133
135
|
}
|
|
136
|
+
# New v3.0 features initialization
|
|
137
|
+
@switch_cache = {} # Cache for command switches from --help
|
|
138
|
+
@switch_cache_time = {} # Timestamp for cache expiry
|
|
139
|
+
@bookmarks = {} # Enhanced bookmarks with tags
|
|
140
|
+
@defuns = {} # Store defun definitions for persistence
|
|
141
|
+
@cmd_stats = {} # Command execution statistics
|
|
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)
|
|
134
149
|
def pre_cmd; end # User-defined function to be run BEFORE command execution
|
|
135
150
|
def post_cmd; end # User-defined function to be run AFTER command execution
|
|
136
151
|
end
|
|
@@ -138,32 +153,49 @@ end
|
|
|
138
153
|
# HELP TEXT
|
|
139
154
|
@info = <<~INFO
|
|
140
155
|
|
|
141
|
-
Hello #{@user}, welcome to rsh - the Ruby SHell.
|
|
142
|
-
|
|
143
|
-
rsh does not attempt to compete with the grand old shells like bash and zsh.
|
|
144
|
-
It serves the specific needs and wants of its author. If you like it, then feel free
|
|
156
|
+
Hello #{@user}, welcome to rsh v3.1 - the Ruby SHell.
|
|
157
|
+
|
|
158
|
+
rsh does not attempt to compete with the grand old shells like bash and zsh.
|
|
159
|
+
It serves the specific needs and wants of its author. If you like it, then feel free
|
|
145
160
|
to ask for more or different features here: https://github.com/isene/rsh.
|
|
146
|
-
|
|
147
|
-
Features:
|
|
161
|
+
|
|
162
|
+
Core Features:
|
|
148
163
|
* Aliases (called nicks in rsh) - both for commands and general nicks
|
|
149
|
-
* Syntax highlighting
|
|
164
|
+
* Syntax highlighting for nicks, bookmarks, commands, switches and valid dirs/files
|
|
150
165
|
* Tab completions for nicks, system commands, command switches and dirs/files
|
|
151
166
|
* Smart context-aware tab completion for git, apt, docker, systemctl, cargo, npm, gem, bundle
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
|
|
155
|
-
*
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
* History with editing, search and repeat (use `!` or UP arrow)
|
|
168
|
+
* Auto-suggestions from history (press RIGHT arrow to accept)
|
|
169
|
+
* Ruby functions callable as shell commands (persistent across sessions)
|
|
170
|
+
* AI integration: Use @ for text responses and @@ for command suggestions
|
|
171
|
+
|
|
172
|
+
NEW in v3.0:
|
|
173
|
+
* Command analytics - :stats shows usage patterns and performance metrics
|
|
174
|
+
* Enhanced bookmarks with tags - :bm "name path #tag1,tag2" then just type name to jump
|
|
175
|
+
* Session management - :save_session and :load_session preserve your entire shell state
|
|
176
|
+
* Smart typo detection - "Did you mean...?" suggestions for misspelled commands
|
|
177
|
+
* Switch caching - Faster TAB completion for command options
|
|
178
|
+
* Option value completion - TAB complete values like --format=json
|
|
179
|
+
* Syntax validation - Pre-execution warnings for dangerous or malformed commands
|
|
180
|
+
* Unified command syntax - :nick, :gnick, :bm all work the same way (list/create/delete)
|
|
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
|
+
|
|
194
|
+
Config file (.rshrc) updates on exit (Ctrl-d) or not (Ctrl-e).
|
|
195
|
+
All colors are themeable in .rshrc (see github link for possibilities).
|
|
196
|
+
|
|
197
|
+
Use `:help` for complete command reference.
|
|
198
|
+
|
|
167
199
|
INFO
|
|
168
200
|
|
|
169
201
|
@help = <<~HELP
|
|
@@ -192,6 +224,8 @@ def firstrun
|
|
|
192
224
|
@c_gnick = 87 # Color for matching gnick
|
|
193
225
|
@c_path = 208 # Color for valid path
|
|
194
226
|
@c_switch = 148 # Color for switches/options
|
|
227
|
+
@c_bookmark = 13 # Color for bookmarks
|
|
228
|
+
@c_colon = 4 # Color for colon commands
|
|
195
229
|
@c_tabselect = 207 # Color for selected tabcompleted item
|
|
196
230
|
@c_taboption = 244 # Color for unselected tabcompleted item
|
|
197
231
|
@c_stamp = 244 # Color for time stamp/command
|
|
@@ -468,8 +502,10 @@ def tab(type)
|
|
|
468
502
|
@tabstr = @pretab if type == "hist" # Searching for matches with whole string in history
|
|
469
503
|
@pretab = @pretab.delete_suffix(@tabstr)
|
|
470
504
|
end
|
|
471
|
-
type = "switch" if @tabstr && @tabstr[0] == "-"
|
|
505
|
+
type = "switch" if @tabstr && @tabstr[0] == "-" && !@tabstr.include?("=")
|
|
506
|
+
type = "option_value" if @tabstr && @tabstr =~ /^--?[\w-]+=/
|
|
472
507
|
type = "env_vars" if @tabstr && @tabstr[0] == "$"
|
|
508
|
+
type = "colon_commands" if @tabstr && @tabstr[0] == ":"
|
|
473
509
|
|
|
474
510
|
# Debug output when RSH_DEBUG is set
|
|
475
511
|
if ENV['RSH_DEBUG']
|
|
@@ -517,12 +553,29 @@ def tab(type)
|
|
|
517
553
|
@tabarray.shift # Take away @history[0]
|
|
518
554
|
return if @tabarray.empty?
|
|
519
555
|
when "switch"
|
|
520
|
-
cmdswitch = @pretab.split(/[|, ]/).last.to_s
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
556
|
+
cmdswitch = @pretab.split(/[|, ]/).last.to_s.strip
|
|
557
|
+
@tabarray = get_command_switches(cmdswitch)
|
|
558
|
+
when "option_value" # Completion for option values like --format=<value>
|
|
559
|
+
if @tabstr =~ /^--?[\w-]+=(.*)/
|
|
560
|
+
value_prefix = $1
|
|
561
|
+
option = @tabstr.sub(/=.*/, '')
|
|
562
|
+
# Define common option value completions
|
|
563
|
+
value_completions = {
|
|
564
|
+
/format/ => %w[json yaml xml csv plain],
|
|
565
|
+
/output/ => %w[json yaml xml text html],
|
|
566
|
+
/level|log-level/ => %w[debug info warn error fatal],
|
|
567
|
+
/color/ => %w[auto always never],
|
|
568
|
+
/type/ => %w[file dir link all]
|
|
569
|
+
}
|
|
570
|
+
matches = []
|
|
571
|
+
value_completions.each do |pattern, values|
|
|
572
|
+
if option =~ pattern
|
|
573
|
+
matches = values.select { |v| v.start_with?(value_prefix) }
|
|
574
|
+
break unless matches.empty?
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
@tabarray = matches.map { |v| "#{option}=#{v}" }
|
|
578
|
+
end
|
|
526
579
|
when "dirs_only" # Only show directories
|
|
527
580
|
fdir = @tabstr + "*"
|
|
528
581
|
dirs = Dir.glob(fdir).select { |d| Dir.exist?(d) }.map { |d| d + "/" }
|
|
@@ -544,12 +597,25 @@ def tab(type)
|
|
|
544
597
|
env_vars = ENV.keys.map { |k| "$#{k}" }
|
|
545
598
|
regex_flags = @completion_case_sensitive ? 0 : Regexp::IGNORECASE
|
|
546
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
|
|
547
612
|
when "all" # Handle all other tab completions
|
|
548
613
|
ex = []
|
|
549
614
|
ex += @exe
|
|
550
615
|
ex.sort!
|
|
551
616
|
ex.prepend(*@nick.keys) # Add nicks
|
|
552
617
|
ex.prepend(*@gnick.keys) # Add gnicks
|
|
618
|
+
ex.prepend(*@bookmarks.keys) if @bookmarks # Add bookmarks
|
|
553
619
|
|
|
554
620
|
# Enhanced matching with case sensitivity and fuzzy support
|
|
555
621
|
regex_flags = @completion_case_sensitive ? 0 : Regexp::IGNORECASE
|
|
@@ -717,10 +783,176 @@ def tabend
|
|
|
717
783
|
@c_col = @pos0 + @pos
|
|
718
784
|
@c.col(@c_col)
|
|
719
785
|
end
|
|
786
|
+
def get_command_switches(command) # Helper function to extract switches from --help
|
|
787
|
+
# Check cache first (cache expires after 1 hour)
|
|
788
|
+
cache_key = command.to_s.strip
|
|
789
|
+
return [] if cache_key.empty?
|
|
790
|
+
|
|
791
|
+
current_time = Time.now.to_i
|
|
792
|
+
if @switch_cache[cache_key] && @switch_cache_time[cache_key] && (current_time - @switch_cache_time[cache_key]) < 3600
|
|
793
|
+
return @switch_cache[cache_key]
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
# Parse --help output
|
|
797
|
+
hlp = `#{command} --help 2>/dev/null`
|
|
798
|
+
# Try -h if --help didn't work
|
|
799
|
+
hlp = `#{command} -h 2>/dev/null` if hlp.empty?
|
|
800
|
+
return [] if hlp.empty?
|
|
801
|
+
|
|
802
|
+
switches = []
|
|
803
|
+
|
|
804
|
+
# Method 1: Lines starting with switches (traditional format)
|
|
805
|
+
switches = hlp.split("\n").grep(/^\s*-{1,2}[^-]/)
|
|
806
|
+
switches.map! { |h| h.sub(/^\s*/, '').sub(/^--/, ' --') }
|
|
807
|
+
switches.reject! { |h| /-</ =~ h }
|
|
808
|
+
|
|
809
|
+
# Method 2: Extract switches from usage line (git-style format)
|
|
810
|
+
if switches.empty?
|
|
811
|
+
usage_lines = hlp.split("\n").select { |l| l =~ /usage:|Usage:/ }
|
|
812
|
+
usage_lines.each do |line|
|
|
813
|
+
# Extract all switches from the usage line
|
|
814
|
+
line.scan(/(-[a-zA-Z]|--[a-z-]+)/).each do |match|
|
|
815
|
+
switch = match[0]
|
|
816
|
+
switches << (switch.start_with?('--') ? " #{switch}" : switch)
|
|
817
|
+
end
|
|
818
|
+
end
|
|
819
|
+
switches.uniq!
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
# Cache the result (even if empty, to avoid repeated failures)
|
|
823
|
+
@switch_cache[cache_key] = switches
|
|
824
|
+
@switch_cache_time[cache_key] = current_time
|
|
825
|
+
|
|
826
|
+
switches
|
|
827
|
+
end
|
|
828
|
+
def levenshtein_distance(s, t) # Calculate edit distance for smart suggestions
|
|
829
|
+
m = s.length
|
|
830
|
+
n = t.length
|
|
831
|
+
return m if n == 0
|
|
832
|
+
return n if m == 0
|
|
833
|
+
d = Array.new(m+1) {Array.new(n+1)}
|
|
834
|
+
(0..m).each {|i| d[i][0] = i}
|
|
835
|
+
(0..n).each {|j| d[0][j] = j}
|
|
836
|
+
(1..n).each do |j|
|
|
837
|
+
(1..m).each do |i|
|
|
838
|
+
d[i][j] = if s[i-1] == t[j-1]
|
|
839
|
+
d[i-1][j-1]
|
|
840
|
+
else
|
|
841
|
+
[d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+1].min
|
|
842
|
+
end
|
|
843
|
+
end
|
|
844
|
+
end
|
|
845
|
+
d[m][n]
|
|
846
|
+
end
|
|
847
|
+
def suggest_command(cmd) # Smart command suggestions for typos
|
|
848
|
+
return nil if cmd.nil? || cmd.empty?
|
|
849
|
+
return nil if @exe.include?(cmd) || @nick.include?(cmd)
|
|
850
|
+
|
|
851
|
+
# Find commands with small edit distance
|
|
852
|
+
candidates = (@exe + @nick.keys).select do |c|
|
|
853
|
+
next false if c.length < 2
|
|
854
|
+
dist = levenshtein_distance(cmd, c)
|
|
855
|
+
max_dist = [cmd.length / 3, 2].max
|
|
856
|
+
dist <= max_dist && dist > 0
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
return nil if candidates.empty?
|
|
860
|
+
|
|
861
|
+
# Sort by distance
|
|
862
|
+
candidates.sort_by! { |c| levenshtein_distance(cmd, c) }
|
|
863
|
+
candidates.first(3)
|
|
864
|
+
end
|
|
720
865
|
def hist_clean # Clean up @history
|
|
721
|
-
@history.uniq!
|
|
722
866
|
@history.compact!
|
|
723
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
|
|
724
956
|
end
|
|
725
957
|
def cmd_check(str) # Check if each element on the readline matches commands, nicks, paths; color them
|
|
726
958
|
return if str.nil?
|
|
@@ -729,11 +961,19 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic
|
|
|
729
961
|
if str =~ /^(@@?)\s+(.*)$/
|
|
730
962
|
prefix = $1
|
|
731
963
|
rest = $2
|
|
732
|
-
return prefix.c(
|
|
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
|
|
733
972
|
end
|
|
734
973
|
|
|
735
974
|
str.gsub(/(?:\S'[^']*'|[^ '])+/) do |el|
|
|
736
975
|
clean_el = el.gsub("'", "")
|
|
976
|
+
# Priority: commands > nicks > bookmarks > paths
|
|
737
977
|
if @exe.include?(el)
|
|
738
978
|
el.c(@c_cmd)
|
|
739
979
|
elsif el == "cd"
|
|
@@ -741,8 +981,6 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic
|
|
|
741
981
|
elsif clean_el =~ /^\.\/(.+)/ && File.exist?(clean_el) && File.executable?(clean_el)
|
|
742
982
|
# Color local executables starting with ./
|
|
743
983
|
el.c(@c_cmd)
|
|
744
|
-
elsif File.exist?(clean_el)
|
|
745
|
-
el.c(@c_path)
|
|
746
984
|
elsif @nick.include?(el)
|
|
747
985
|
el.c(@c_nick)
|
|
748
986
|
elsif el == "r" or el == "f"
|
|
@@ -751,6 +989,11 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic
|
|
|
751
989
|
el.c(@c_gnick)
|
|
752
990
|
elsif self.respond_to?(el) && singleton_class.instance_methods(false).include?(el.to_sym)
|
|
753
991
|
el.c(@c_nick).b # Ruby functions in bold nick color
|
|
992
|
+
elsif @bookmarks && @bookmarks.include?(el)
|
|
993
|
+
# Color bookmarks (after commands and nicks)
|
|
994
|
+
el.c(@c_bookmark)
|
|
995
|
+
elsif File.exist?(clean_el)
|
|
996
|
+
el.c(@c_path)
|
|
754
997
|
elsif el[0] == "-"
|
|
755
998
|
el.c(@c_switch)
|
|
756
999
|
else
|
|
@@ -771,6 +1014,16 @@ def rshrc # Write updates to .rshrc
|
|
|
771
1014
|
conf += "@gnick = #{@gnick}\n"
|
|
772
1015
|
conf.sub!(/^@cmd_frequency.*(\n|$)/, "")
|
|
773
1016
|
conf += "@cmd_frequency = #{@cmd_frequency}\n"
|
|
1017
|
+
conf.sub!(/^@cmd_stats.*(\n|$)/, "")
|
|
1018
|
+
conf += "@cmd_stats = #{@cmd_stats}\n" unless @cmd_stats.empty?
|
|
1019
|
+
conf.sub!(/^@bookmarks.*(\n|$)/, "")
|
|
1020
|
+
conf += "@bookmarks = #{@bookmarks}\n" unless @bookmarks.empty?
|
|
1021
|
+
conf.sub!(/^@defuns.*(\n|$)/, "")
|
|
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
|
|
774
1027
|
# Only write @cmd_completions if user has customized it
|
|
775
1028
|
unless conf =~ /^@cmd_completions\s*=/
|
|
776
1029
|
# Don't write default completions to avoid cluttering .rshrc
|
|
@@ -807,6 +1060,7 @@ def help
|
|
|
807
1060
|
left_col << "RIGHT/Ctrl-F Accept suggestion"
|
|
808
1061
|
left_col << "UP/DOWN Navigate history"
|
|
809
1062
|
left_col << "TAB Tab complete"
|
|
1063
|
+
left_col << "Shift-TAB Search history"
|
|
810
1064
|
left_col << "Ctrl-Y Copy to clipboard"
|
|
811
1065
|
left_col << "Ctrl-D Exit + save .rshrc"
|
|
812
1066
|
left_col << "Ctrl-E Exit without save"
|
|
@@ -820,9 +1074,10 @@ def help
|
|
|
820
1074
|
left_col << "SPECIAL COMMANDS:".c(@c_prompt).b
|
|
821
1075
|
left_col << ":nick 'll = ls -l' Command alias"
|
|
822
1076
|
left_col << ":gnick 'h = /home' General alias"
|
|
823
|
-
left_col << ":
|
|
824
|
-
left_col << ":
|
|
825
|
-
left_col << ":nick
|
|
1077
|
+
left_col << ":nick List nicks"
|
|
1078
|
+
left_col << ":gnick List gnicks"
|
|
1079
|
+
left_col << ":nick '-name' Delete nick"
|
|
1080
|
+
left_col << ":gnick '-name' Delete gnick"
|
|
826
1081
|
left_col << ":history Show history"
|
|
827
1082
|
left_col << ":rmhistory Clear history"
|
|
828
1083
|
left_col << ":info About rsh"
|
|
@@ -842,6 +1097,21 @@ def help
|
|
|
842
1097
|
right_col << ":fg [id] Foreground job"
|
|
843
1098
|
right_col << ":bg [id] Resume in bg"
|
|
844
1099
|
right_col << ""
|
|
1100
|
+
right_col << "v3.0/3.1 FEATURES:".c(@c_prompt).b
|
|
1101
|
+
right_col << ":stats [--csv|--json] Analytics"
|
|
1102
|
+
right_col << ":bm \"name\" Create bookmark"
|
|
1103
|
+
right_col << "name Jump to bookmark"
|
|
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"
|
|
1114
|
+
right_col << ""
|
|
845
1115
|
right_col << "INTEGRATIONS:".c(@c_prompt).b
|
|
846
1116
|
right_col << "r Launch rtfm"
|
|
847
1117
|
right_col << "f Launch fzf"
|
|
@@ -855,8 +1125,8 @@ def help
|
|
|
855
1125
|
right_col << "SMART COMPLETIONS:".c(@c_prompt).b
|
|
856
1126
|
right_col << "git <TAB> Git subcommands"
|
|
857
1127
|
right_col << "apt/docker <TAB> Command options"
|
|
858
|
-
right_col << "
|
|
859
|
-
right_col << "
|
|
1128
|
+
right_col << "--format=<TAB> Option values"
|
|
1129
|
+
right_col << "Typo suggestions Auto-correct"
|
|
860
1130
|
right_col << ""
|
|
861
1131
|
right_col << "EXPANSIONS:".c(@c_prompt).b
|
|
862
1132
|
right_col << "~ Home directory"
|
|
@@ -899,43 +1169,47 @@ def rmhistory # Delete history
|
|
|
899
1169
|
@history = []
|
|
900
1170
|
puts "History deleted."
|
|
901
1171
|
end
|
|
902
|
-
def nick(nick_str) # Define a new nick like this: `:nick "ls = ls --color"`
|
|
903
|
-
if nick_str.
|
|
1172
|
+
def nick(nick_str = nil) # Define a new nick like this: `:nick "ls = ls --color"`
|
|
1173
|
+
if nick_str.nil? || nick_str.empty?
|
|
1174
|
+
# List all nicks
|
|
1175
|
+
puts "\n Command nicks:".c(@c_nick).b
|
|
1176
|
+
if @nick.empty?
|
|
1177
|
+
puts " (none defined)"
|
|
1178
|
+
else
|
|
1179
|
+
@nick.sort.each {|key, value| puts " #{key.c(@c_nick)} = #{value}"}
|
|
1180
|
+
end
|
|
1181
|
+
puts
|
|
1182
|
+
elsif nick_str.match(/^\s*-/)
|
|
904
1183
|
source = nick_str.sub(/^\s*-/, '')
|
|
905
1184
|
@nick.delete(source)
|
|
1185
|
+
rshrc
|
|
906
1186
|
else
|
|
907
1187
|
source = nick_str.sub(/ =.*/, '')
|
|
908
1188
|
target = nick_str.sub(/.*= /, '')
|
|
909
1189
|
@nick[source] = target
|
|
1190
|
+
rshrc
|
|
910
1191
|
end
|
|
911
|
-
rshrc
|
|
912
1192
|
end
|
|
913
|
-
def gnick(nick_str) # Define a generic/global nick to match not only commands (format like nick)
|
|
914
|
-
if nick_str.
|
|
1193
|
+
def gnick(nick_str = nil) # Define a generic/global nick to match not only commands (format like nick)
|
|
1194
|
+
if nick_str.nil? || nick_str.empty?
|
|
1195
|
+
# List all gnicks
|
|
1196
|
+
puts "\n General nicks:".c(@c_gnick).b
|
|
1197
|
+
if @gnick.empty?
|
|
1198
|
+
puts " (none defined)"
|
|
1199
|
+
else
|
|
1200
|
+
@gnick.sort.each {|key, value| puts " #{key.c(@c_gnick)} = #{value}"}
|
|
1201
|
+
end
|
|
1202
|
+
puts
|
|
1203
|
+
elsif nick_str.match(/^\s*-/)
|
|
915
1204
|
source = nick_str.sub(/^\s*-/, '')
|
|
916
1205
|
@gnick.delete(source)
|
|
1206
|
+
rshrc
|
|
917
1207
|
else
|
|
918
1208
|
source = nick_str.sub(/ =.*/, '')
|
|
919
1209
|
target = nick_str.sub(/.*= /, '')
|
|
920
1210
|
@gnick[source] = target
|
|
1211
|
+
rshrc
|
|
921
1212
|
end
|
|
922
|
-
rshrc
|
|
923
|
-
end
|
|
924
|
-
def nick? # Show nicks
|
|
925
|
-
puts " Command nicks:".c(@c_nick)
|
|
926
|
-
@nick.sort.each {|key, value| puts " #{key} = #{value}"}
|
|
927
|
-
puts " General nicks:".c(@c_gnick)
|
|
928
|
-
@gnick.sort.each {|key, value| puts " #{key} = #{value}"}
|
|
929
|
-
end
|
|
930
|
-
def nickdel(nick_name) # Delete a command nick
|
|
931
|
-
@nick.delete(nick_name)
|
|
932
|
-
rshrc
|
|
933
|
-
puts "Nick '#{nick_name}' deleted"
|
|
934
|
-
end
|
|
935
|
-
def gnickdel(nick_name) # Delete a general/global nick
|
|
936
|
-
@gnick.delete(nick_name)
|
|
937
|
-
rshrc
|
|
938
|
-
puts "General nick '#{nick_name}' deleted"
|
|
939
1213
|
end
|
|
940
1214
|
def dirs
|
|
941
1215
|
puts "Past direactories:"
|
|
@@ -994,6 +1268,7 @@ def defun(func_def) # Define a Ruby function like: `:defun "myls(*args) = Dir.g
|
|
|
994
1268
|
func_name = func_def.sub(/^\s*-/, '')
|
|
995
1269
|
if self.respond_to?(func_name)
|
|
996
1270
|
singleton_class.remove_method(func_name.to_sym)
|
|
1271
|
+
@defuns.delete(func_name)
|
|
997
1272
|
puts "Function '#{func_name}' removed"
|
|
998
1273
|
else
|
|
999
1274
|
puts "Function '#{func_name}' not found"
|
|
@@ -1005,11 +1280,12 @@ def defun(func_def) # Define a Ruby function like: `:defun "myls(*args) = Dir.g
|
|
|
1005
1280
|
func_name = $1
|
|
1006
1281
|
func_params = $2
|
|
1007
1282
|
func_body = $3
|
|
1008
|
-
|
|
1283
|
+
|
|
1009
1284
|
begin
|
|
1010
1285
|
eval_code = "def #{func_name}(#{func_params}); #{func_body}; end"
|
|
1011
1286
|
puts " DEBUG: Evaluating: #{eval_code}" if ENV['RSH_DEBUG']
|
|
1012
1287
|
singleton_class.class_eval(eval_code)
|
|
1288
|
+
@defuns[func_name] = func_def # Store for persistence
|
|
1013
1289
|
puts "Function '#{func_name}' defined"
|
|
1014
1290
|
puts " DEBUG: Method created? #{respond_to?(func_name)}" if ENV['RSH_DEBUG']
|
|
1015
1291
|
rescue SyntaxError => e
|
|
@@ -1038,6 +1314,464 @@ def defun? # Show all user-defined functions
|
|
|
1038
1314
|
end
|
|
1039
1315
|
end
|
|
1040
1316
|
end
|
|
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)
|
|
1337
|
+
puts "\n Command Execution Statistics".c(@c_prompt).b
|
|
1338
|
+
puts " " + "="*50
|
|
1339
|
+
|
|
1340
|
+
# Most used commands
|
|
1341
|
+
if @cmd_frequency && !@cmd_frequency.empty?
|
|
1342
|
+
puts "\n Top 10 Most Used Commands:".c(@c_nick)
|
|
1343
|
+
sorted = @cmd_frequency.sort_by { |_, count| -count }.first(10)
|
|
1344
|
+
sorted.each_with_index do |(cmd, count), i|
|
|
1345
|
+
bar = "■" * ([count / 5, 20].min)
|
|
1346
|
+
puts " #{(i+1).to_s.rjust(2)}. #{cmd.ljust(20)} #{count.to_s.rjust(5)}x #{bar.c(@c_path)}"
|
|
1347
|
+
end
|
|
1348
|
+
end
|
|
1349
|
+
|
|
1350
|
+
# Command statistics from @cmd_stats
|
|
1351
|
+
if @cmd_stats && !@cmd_stats.empty?
|
|
1352
|
+
total_time = @cmd_stats.values.map { |s| s[:total_time] || 0 }.sum
|
|
1353
|
+
total_cmds = @cmd_stats.values.map { |s| s[:count] || 0 }.sum
|
|
1354
|
+
|
|
1355
|
+
puts "\n Performance Statistics:".c(@c_nick)
|
|
1356
|
+
puts " Total commands executed: #{total_cmds}"
|
|
1357
|
+
puts " Total execution time: #{'%.2f' % total_time}s"
|
|
1358
|
+
puts " Average time per command: #{'%.2f' % (total_time / total_cmds)}s" if total_cmds > 0
|
|
1359
|
+
|
|
1360
|
+
puts "\n Slowest Commands:".c(@c_nick)
|
|
1361
|
+
slowest = @cmd_stats.sort_by { |_, s| -(s[:avg_time] || 0) }.first(5)
|
|
1362
|
+
slowest.each_with_index do |(cmd, stats), i|
|
|
1363
|
+
puts " #{(i+1).to_s.rjust(2)}. #{cmd.ljust(20)} avg: #{'%.3f' % (stats[:avg_time] || 0)}s"
|
|
1364
|
+
end
|
|
1365
|
+
end
|
|
1366
|
+
|
|
1367
|
+
# History statistics
|
|
1368
|
+
puts "\n History Statistics:".c(@c_nick)
|
|
1369
|
+
puts " Total history entries: #{@history.length}"
|
|
1370
|
+
puts " Unique commands: #{@history.uniq.length}"
|
|
1371
|
+
|
|
1372
|
+
# Success/failure tracking
|
|
1373
|
+
puts "\n Last command exit status: #{@last_exit == 0 ? 'Success'.c(@c_path) : "Failed (#{@last_exit})".c(196)}"
|
|
1374
|
+
puts
|
|
1375
|
+
end
|
|
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?
|
|
1429
|
+
# List all bookmarks
|
|
1430
|
+
if @bookmarks.empty?
|
|
1431
|
+
puts "No bookmarks defined. Use :bm \"name\" to bookmark current directory"
|
|
1432
|
+
return
|
|
1433
|
+
end
|
|
1434
|
+
puts "\n Bookmarks:".c(@c_prompt).b
|
|
1435
|
+
@bookmarks.each do |name, data|
|
|
1436
|
+
path = data.is_a?(Hash) ? data[:path] : data
|
|
1437
|
+
tags = data.is_a?(Hash) && data[:tags] ? " [#{data[:tags].join(', ')}]" : ""
|
|
1438
|
+
puts " #{name.c(@c_nick)} → #{path}#{tags.c(@c_stamp)}"
|
|
1439
|
+
end
|
|
1440
|
+
puts
|
|
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+(.+)$/
|
|
1453
|
+
# Set bookmark with optional tags
|
|
1454
|
+
name, rest = $1, $2
|
|
1455
|
+
if rest.include?('#')
|
|
1456
|
+
path_part, tag_part = rest.split('#', 2)
|
|
1457
|
+
path = path_part.strip
|
|
1458
|
+
path = Dir.pwd if path.empty?
|
|
1459
|
+
tags = tag_part.split(',').map(&:strip)
|
|
1460
|
+
@bookmarks[name] = {path: path, tags: tags}
|
|
1461
|
+
else
|
|
1462
|
+
@bookmarks[name] = {path: rest.strip, tags: []}
|
|
1463
|
+
end
|
|
1464
|
+
puts "Bookmark '#{name}' set to #{@bookmarks[name][:path]}"
|
|
1465
|
+
rshrc
|
|
1466
|
+
elsif arg_str =~ /^-(\w+)$/
|
|
1467
|
+
# Delete bookmark
|
|
1468
|
+
name = $1
|
|
1469
|
+
if @bookmarks.delete(name)
|
|
1470
|
+
puts "Bookmark '#{name}' deleted"
|
|
1471
|
+
rshrc
|
|
1472
|
+
else
|
|
1473
|
+
puts "Bookmark '#{name}' not found"
|
|
1474
|
+
end
|
|
1475
|
+
elsif arg_str =~ /^\?(\w*)$/
|
|
1476
|
+
# Search bookmarks by tag
|
|
1477
|
+
tag = $1
|
|
1478
|
+
if tag.empty?
|
|
1479
|
+
puts "Available tags:"
|
|
1480
|
+
all_tags = @bookmarks.values.flat_map { |d| d.is_a?(Hash) ? d[:tags] : [] }.uniq.sort
|
|
1481
|
+
puts " " + all_tags.join(", ")
|
|
1482
|
+
else
|
|
1483
|
+
matches = @bookmarks.select do |_, data|
|
|
1484
|
+
data.is_a?(Hash) && data[:tags] && data[:tags].include?(tag)
|
|
1485
|
+
end
|
|
1486
|
+
if matches.empty?
|
|
1487
|
+
puts "No bookmarks with tag '#{tag}'"
|
|
1488
|
+
else
|
|
1489
|
+
puts "Bookmarks with tag '#{tag}':"
|
|
1490
|
+
matches.each { |name, data| puts " #{name} → #{data[:path]}" }
|
|
1491
|
+
end
|
|
1492
|
+
end
|
|
1493
|
+
else
|
|
1494
|
+
# Bookmark current directory
|
|
1495
|
+
name = arg_str.strip
|
|
1496
|
+
@bookmarks[name] = {path: Dir.pwd, tags: []}
|
|
1497
|
+
puts "Bookmark '#{name}' set to #{Dir.pwd}"
|
|
1498
|
+
rshrc
|
|
1499
|
+
end
|
|
1500
|
+
end
|
|
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
|
|
1564
|
+
end
|
|
1565
|
+
def save_session(*args) # Save current session state
|
|
1566
|
+
session_name = args[0] || 'default'
|
|
1567
|
+
session_path = @session_dir + "/#{session_name}.json"
|
|
1568
|
+
|
|
1569
|
+
session = {
|
|
1570
|
+
name: session_name,
|
|
1571
|
+
pwd: Dir.pwd,
|
|
1572
|
+
history: @history.first(50),
|
|
1573
|
+
bookmarks: @bookmarks,
|
|
1574
|
+
defuns: @defuns,
|
|
1575
|
+
timestamp: Time.now.to_i
|
|
1576
|
+
}
|
|
1577
|
+
begin
|
|
1578
|
+
require 'json'
|
|
1579
|
+
File.write(session_path, JSON.pretty_generate(session))
|
|
1580
|
+
puts "Session '#{session_name}' saved to #{session_path}"
|
|
1581
|
+
rescue => e
|
|
1582
|
+
puts "Error saving session: #{e.message}"
|
|
1583
|
+
end
|
|
1584
|
+
end
|
|
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
|
|
1592
|
+
return
|
|
1593
|
+
end
|
|
1594
|
+
begin
|
|
1595
|
+
require 'json'
|
|
1596
|
+
session = JSON.parse(File.read(session_path), symbolize_names: true)
|
|
1597
|
+
|
|
1598
|
+
# Restore state
|
|
1599
|
+
Dir.chdir(session[:pwd]) if session[:pwd] && Dir.exist?(session[:pwd])
|
|
1600
|
+
|
|
1601
|
+
# Merge history (prepend saved history)
|
|
1602
|
+
if session[:history]
|
|
1603
|
+
@history = (session[:history] + @history).uniq.first(@histsize)
|
|
1604
|
+
end
|
|
1605
|
+
|
|
1606
|
+
# Restore bookmarks
|
|
1607
|
+
if session[:bookmarks]
|
|
1608
|
+
session[:bookmarks].each do |name, data|
|
|
1609
|
+
bookmark_data = data.is_a?(Hash) ? data.transform_keys(&:to_sym) : data
|
|
1610
|
+
@bookmarks[name.to_s] = bookmark_data
|
|
1611
|
+
end
|
|
1612
|
+
end
|
|
1613
|
+
|
|
1614
|
+
# Restore defuns
|
|
1615
|
+
if session[:defuns]
|
|
1616
|
+
session[:defuns].each do |name, func_def|
|
|
1617
|
+
next unless func_def.is_a?(String)
|
|
1618
|
+
@defuns[name.to_s] = func_def
|
|
1619
|
+
# Re-evaluate the function
|
|
1620
|
+
if func_def =~ /^(\w+)\s*\(([^)]*)\)\s*=\s*(.+)$/
|
|
1621
|
+
func_name, func_params, func_body = $1, $2, $3
|
|
1622
|
+
eval_code = "def #{func_name}(#{func_params}); #{func_body}; end"
|
|
1623
|
+
singleton_class.class_eval(eval_code) rescue nil
|
|
1624
|
+
end
|
|
1625
|
+
end
|
|
1626
|
+
end
|
|
1627
|
+
|
|
1628
|
+
saved_time = Time.at(session[:timestamp] || 0).strftime("%Y-%m-%d %H:%M:%S")
|
|
1629
|
+
puts "Session '#{session_name}' restored from #{saved_time}"
|
|
1630
|
+
rshrc
|
|
1631
|
+
rescue => e
|
|
1632
|
+
puts "Error loading session: #{e.message}"
|
|
1633
|
+
end
|
|
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
|
|
1747
|
+
def validate_command(cmd) # Syntax validation before execution
|
|
1748
|
+
return nil if cmd.nil? || cmd.empty?
|
|
1749
|
+
warnings = []
|
|
1750
|
+
|
|
1751
|
+
# Check for common mistakes
|
|
1752
|
+
warnings << "Unmatched quotes" if cmd.count("'").odd? || cmd.count('"').odd?
|
|
1753
|
+
warnings << "Unmatched parentheses" if cmd.count("(") != cmd.count(")")
|
|
1754
|
+
warnings << "Unmatched brackets" if cmd.count("[") != cmd.count("]")
|
|
1755
|
+
warnings << "Unmatched braces" if cmd.count("{") != cmd.count("}")
|
|
1756
|
+
|
|
1757
|
+
# Check for potentially dangerous patterns
|
|
1758
|
+
warnings << "WARNING: Recursive rm detected" if cmd =~ /rm\s+.*-r.*\//
|
|
1759
|
+
warnings << "WARNING: Force flag without path" if cmd =~ /rm\s+-[rf]+\s*$/
|
|
1760
|
+
warnings << "WARNING: Sudo with redirection" if cmd =~ /sudo.*>/
|
|
1761
|
+
|
|
1762
|
+
# Check for common typos in popular commands
|
|
1763
|
+
if cmd =~ /^(\w+)/
|
|
1764
|
+
first_cmd = $1
|
|
1765
|
+
unless @exe.include?(first_cmd) || @nick.include?(first_cmd) || first_cmd == "cd"
|
|
1766
|
+
suggestions = suggest_command(first_cmd)
|
|
1767
|
+
if suggestions && !suggestions.empty?
|
|
1768
|
+
warnings << "Command '#{first_cmd}' not found. Did you mean: #{suggestions.join(', ')}?"
|
|
1769
|
+
end
|
|
1770
|
+
end
|
|
1771
|
+
end
|
|
1772
|
+
|
|
1773
|
+
warnings.empty? ? nil : warnings
|
|
1774
|
+
end
|
|
1041
1775
|
def execute_conditional(cmd_line)
|
|
1042
1776
|
# Split on && and || while preserving the operators
|
|
1043
1777
|
parts = cmd_line.split(/(\s*&&\s*|\s*\|\|\s*)/)
|
|
@@ -1316,6 +2050,27 @@ def load_rshrc_safe
|
|
|
1316
2050
|
@nick = {} unless @nick.is_a?(Hash)
|
|
1317
2051
|
@gnick = {} unless @gnick.is_a?(Hash)
|
|
1318
2052
|
@cmd_frequency = {} unless @cmd_frequency.is_a?(Hash)
|
|
2053
|
+
@cmd_stats = {} unless @cmd_stats.is_a?(Hash)
|
|
2054
|
+
@bookmarks = {} unless @bookmarks.is_a?(Hash)
|
|
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)
|
|
2058
|
+
|
|
2059
|
+
# Restore defuns from .rshrc
|
|
2060
|
+
if @defuns && !@defuns.empty?
|
|
2061
|
+
@defuns.each do |name, func_def|
|
|
2062
|
+
next unless func_def.is_a?(String)
|
|
2063
|
+
if func_def =~ /^(\w+)\s*\(([^)]*)\)\s*=\s*(.+)$/
|
|
2064
|
+
func_name, func_params, func_body = $1, $2, $3
|
|
2065
|
+
begin
|
|
2066
|
+
eval_code = "def #{func_name}(#{func_params}); #{func_body}; end"
|
|
2067
|
+
singleton_class.class_eval(eval_code)
|
|
2068
|
+
rescue => e
|
|
2069
|
+
puts "Warning: Could not load defun '#{name}': #{e.message}" if ENV['RSH_DEBUG']
|
|
2070
|
+
end
|
|
2071
|
+
end
|
|
2072
|
+
end
|
|
2073
|
+
end
|
|
1319
2074
|
|
|
1320
2075
|
rescue SyntaxError => e
|
|
1321
2076
|
puts "\n\033[31mERROR: Syntax error in .rshrc:\033[0m"
|
|
@@ -1429,6 +2184,13 @@ def load_defaults
|
|
|
1429
2184
|
@completion_show_descriptions ||= false
|
|
1430
2185
|
@completion_fuzzy ||= true
|
|
1431
2186
|
@cmd_frequency ||= {}
|
|
2187
|
+
@cmd_stats ||= {}
|
|
2188
|
+
@bookmarks ||= {}
|
|
2189
|
+
@defuns ||= {}
|
|
2190
|
+
@switch_cache ||= {}
|
|
2191
|
+
@switch_cache_time ||= {}
|
|
2192
|
+
@history_dedup ||= 'smart'
|
|
2193
|
+
@session_autosave ||= 0
|
|
1432
2194
|
puts "Loaded with default configuration."
|
|
1433
2195
|
end
|
|
1434
2196
|
|
|
@@ -1509,11 +2271,19 @@ loop do
|
|
|
1509
2271
|
begin
|
|
1510
2272
|
@user = Etc.getpwuid(Process.euid).name # For use in @prompt
|
|
1511
2273
|
@node = Etc.uname[:nodename] # For use in @prompt
|
|
1512
|
-
h = @history; load_rshrc_safe; @history = h # reload prompt but
|
|
2274
|
+
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
|
|
1513
2275
|
@prompt.gsub!(/#{Dir.home}/, '~') # Simplify path in prompt
|
|
1514
2276
|
system("printf \"\033]0;rsh: #{Dir.pwd}\007\"") # Set Window title to path
|
|
1515
2277
|
@history[0] = "" unless @history[0]
|
|
1516
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
|
|
1517
2287
|
getstr # Main work is here
|
|
1518
2288
|
@cmd = @history[0]
|
|
1519
2289
|
@dirs.unshift(Dir.pwd)
|
|
@@ -1609,11 +2379,23 @@ loop do
|
|
|
1609
2379
|
@cmd = Dir.home if @cmd == "~"
|
|
1610
2380
|
@cmd = @dirs[1] if @cmd == "-"
|
|
1611
2381
|
@cmd = @dirs[@cmd.to_i] if @cmd =~ /^\d$/
|
|
1612
|
-
# Check if it's a directory to change to
|
|
2382
|
+
# Check if it's a directory to change to first
|
|
1613
2383
|
dir = @cmd.strip.sub(/~/, Dir.home)
|
|
1614
2384
|
if Dir.exist?(dir)
|
|
1615
|
-
Dir.chdir(dir)
|
|
2385
|
+
Dir.chdir(dir)
|
|
1616
2386
|
system("git status .") if Dir.exist?(".git")
|
|
2387
|
+
# Then check if it's a bookmark (commands and nicks already handled above)
|
|
2388
|
+
elsif @bookmarks && @bookmarks[@cmd]
|
|
2389
|
+
bookmark_data = @bookmarks[@cmd]
|
|
2390
|
+
bm_dir = bookmark_data.is_a?(Hash) ? bookmark_data[:path] : bookmark_data
|
|
2391
|
+
bm_dir = bm_dir.sub(/^~/, Dir.home) # Expand tilde
|
|
2392
|
+
if Dir.exist?(bm_dir)
|
|
2393
|
+
Dir.chdir(bm_dir)
|
|
2394
|
+
puts "Jumped to bookmark '#{@cmd}' → #{bm_dir}".c(@c_path)
|
|
2395
|
+
system("git status .") if Dir.exist?(".git")
|
|
2396
|
+
else
|
|
2397
|
+
puts "Bookmark '#{@cmd}' points to non-existent directory: #{bm_dir}".c(196)
|
|
2398
|
+
end
|
|
1617
2399
|
else
|
|
1618
2400
|
puts "#{Time.now.strftime("%H:%M:%S")}: #{@cmd}".c(@c_stamp)
|
|
1619
2401
|
if @cmd == "f" # fzf integration (https://github.com/junegunn/fzf)
|
|
@@ -1630,16 +2412,34 @@ loop do
|
|
|
1630
2412
|
Thread.new { system("xdg-open #{@cmd} 2>/dev/null") }
|
|
1631
2413
|
end
|
|
1632
2414
|
end
|
|
1633
|
-
else
|
|
2415
|
+
else
|
|
1634
2416
|
begin
|
|
2417
|
+
# Validate command before execution
|
|
2418
|
+
warnings = validate_command(@cmd)
|
|
2419
|
+
if warnings && !warnings.empty?
|
|
2420
|
+
warnings.each { |w| puts "#{w}".c(196) }
|
|
2421
|
+
# For critical warnings, ask for confirmation
|
|
2422
|
+
if warnings.any? { |w| w.start_with?("WARNING:") }
|
|
2423
|
+
print "Continue anyway? (y/N): "
|
|
2424
|
+
response = $stdin.gets.chomp
|
|
2425
|
+
unless response.downcase == 'y'
|
|
2426
|
+
puts "Command cancelled"
|
|
2427
|
+
next
|
|
2428
|
+
end
|
|
2429
|
+
end
|
|
2430
|
+
end
|
|
2431
|
+
|
|
1635
2432
|
pre_cmd
|
|
1636
2433
|
|
|
1637
2434
|
# Track command frequency for intelligent completion
|
|
1638
|
-
if @cmd && !@cmd.empty?
|
|
1639
|
-
|
|
1640
|
-
@cmd_frequency[cmd_base] = (@cmd_frequency[cmd_base] || 0) + 1
|
|
2435
|
+
cmd_base = @cmd.split.first if @cmd && !@cmd.empty?
|
|
2436
|
+
if cmd_base
|
|
2437
|
+
@cmd_frequency[cmd_base] = (@cmd_frequency[cmd_base] || 0) + 1
|
|
1641
2438
|
end
|
|
1642
2439
|
|
|
2440
|
+
# Start timing
|
|
2441
|
+
start_time = Time.now
|
|
2442
|
+
|
|
1643
2443
|
# Handle background jobs
|
|
1644
2444
|
if @cmd.end_with?(' &')
|
|
1645
2445
|
@cmd = @cmd[0..-3] # Remove the &
|
|
@@ -1653,13 +2453,23 @@ loop do
|
|
|
1653
2453
|
@jobs[@job_id] = {pid: pid, cmd: @cmd, status: :running}
|
|
1654
2454
|
puts "[#{@job_id}] #{pid} #{@cmd}"
|
|
1655
2455
|
else
|
|
1656
|
-
# Better handling of pipes and redirections
|
|
2456
|
+
# Better handling of pipes and redirections
|
|
1657
2457
|
@current_pid = spawn(@cmd)
|
|
1658
2458
|
Process.wait(@current_pid)
|
|
1659
2459
|
@last_exit = $?.exitstatus
|
|
1660
2460
|
@current_pid = nil
|
|
1661
2461
|
puts " Command failed: #{@cmd} (exit #{@last_exit})" unless @last_exit == 0
|
|
1662
2462
|
end
|
|
2463
|
+
|
|
2464
|
+
# Track execution time
|
|
2465
|
+
elapsed = Time.now - start_time
|
|
2466
|
+
if cmd_base && elapsed > 0.01 # Only track if > 10ms
|
|
2467
|
+
@cmd_stats[cmd_base] ||= {count: 0, total_time: 0.0, avg_time: 0.0}
|
|
2468
|
+
@cmd_stats[cmd_base][:count] += 1
|
|
2469
|
+
@cmd_stats[cmd_base][:total_time] += elapsed
|
|
2470
|
+
@cmd_stats[cmd_base][:avg_time] = @cmd_stats[cmd_base][:total_time] / @cmd_stats[cmd_base][:count]
|
|
2471
|
+
end
|
|
2472
|
+
|
|
1663
2473
|
post_cmd
|
|
1664
2474
|
rescue StandardError => err
|
|
1665
2475
|
puts "\nError: #{err}"
|
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:
|
|
4
|
+
version: 3.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Geir Isene
|
|
@@ -12,8 +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
|
-
|
|
16
|
-
|
|
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!'
|
|
17
18
|
email: g@isene.com
|
|
18
19
|
executables:
|
|
19
20
|
- rsh
|