ruby-shell 3.3.0 → 3.4.1

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 +90 -79
  3. data/bin/rsh +440 -134
  4. metadata +5 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd3cc49ebe2106b1077e32c07694a7d2cd4a302aba740baa1fd744ab4f097d7e
4
- data.tar.gz: 86a2a648f07605d15850908faf20d92b36a07a501c94d5c88262d9b9aac07b24
3
+ metadata.gz: 8c171961f69d09b023cc69bc4cb66b615ec861e940e5369c265ccdf053004764
4
+ data.tar.gz: ab185ee8de6daecf91a910f417de02916d259dc315d19c84fe64a2dd4f779123
5
5
  SHA512:
6
- metadata.gz: c9dc294728b34590500a3700f2b7d1c0ca4a42bbce9e4868403f6056cf1fda458ac30a43f6b83013c000ed48d51abe177e0ae9ab2855ee77a85bffc134267ae1
7
- data.tar.gz: a4fbc3bcd9b71a81e89bb91bb4eba80bec7e75ea417cd8170a3150e34de9a7d7a6b63827b00e25a9195dc0198327fb7f25d7284dc7740d4a8b35abdb5e850937
6
+ metadata.gz: 6c8c2343b5cd84bb9508dc6bbe25ce6a7ee7741adc34f77e063628445dc76d28edd5d9774a986066ca46ac551c03dab7290083eaaca59889a525383ca6ff15c0
7
+ data.tar.gz: 28c264420c010f966fcfb9cf200ea25b152a3ec2190700132f2a8185a9b34ff1783f786b36afb93e23c8eae3d36a13c68de5ff02d31106d1dd45c31a08cfca2d
data/README.md CHANGED
@@ -19,85 +19,96 @@ Or simply `gem install ruby-shell`.
19
19
 
20
20
  # Features
21
21
 
22
- ## Core Shell Features
23
- * Aliases (called nicks in rsh) - both for commands and general nicks
24
- * Syntax highlighting, matching nicks, system commands and valid dirs/files
25
- * Tab completions for nicks, system commands, command switches and dirs/files
26
- * Tab completion presents matches in a list to pick from
27
- * When you start to write a command, rsh will suggest the first match in the history and present that in "toned down" letters - press the arrow right key to accept the suggestion.
28
- * Writing a partial command and pressing `UP` will search history for matches. Go down/up in the list and press `TAB` or `ENTER` to accept, `Ctrl-g` or `Ctrl-c` to discard
29
- * History with editing, search and repeat a history command (with `!`)
30
- * Config file (.rshrc) updates on exit (with Ctrl-d) or not (with Ctrl-e)
31
- * Set of simple rsh specific commands like nick, nick?, history and rmhistory
32
- * rsh specific commands and full set of Ruby commands available via :<command>
33
- * All colors are themeable in .rshrc (see github link for possibilities)
34
- * Copy current command line to primary selection (paste w/middle button) with `Ctrl-y`
35
-
36
- ## NEW in v3.3.0 - Quote-less Syntax, Parametrized Nicks & More ⭐⭐⭐
37
- * **No More Quotes**: Simplified syntax - `:nick la = ls -la` instead of `:nick "la = ls -la"`
38
- * **Parametrized Nicks**: `:nick gp = git push origin {{branch}}` then use `gp branch=main`
39
- * **Ctrl-G Multi-line Edit**: Press Ctrl-G to edit command in $EDITOR for complex scripts
40
- * **Custom Validation Rules**: `:validate rm -rf / = block` prevents dangerous commands
41
- * **Shell Script Support**: for/while/if loops work with full bash syntax
42
- * **Cleaner Commands**: `:config auto_correct on`, `:bm work /tmp #dev`, `:theme dracula`
43
- * **Simplified Architecture**: Removed :template (merged into :nick for simplicity)
44
- * **Backward Compatible**: Old quote syntax still works for existing .rshrc files
45
- * **Better UX**: Less typing, more powerful, feels more natural
46
-
47
- ## v3.2.0 - Plugin System & Productivity ⭐⭐⭐
48
- * **Plugin Architecture**: Extensible plugin system with lifecycle hooks and extension points
49
- * **Lifecycle Hooks**: on_startup, on_command_before, on_command_after, on_prompt
50
- * **Extension Points**: add_completions (TAB completion), add_commands (custom commands)
51
- * **Plugin Management**: `:plugins` list/reload/enable/disable/info commands
52
- * **Auto-loading**: Plugins in `~/.rsh/plugins/` load automatically on startup
53
- * **Safe Execution**: Isolated plugin execution with error handling, no shell crashes
54
- * **Example Plugins**: git_prompt (branch in prompt), command_logger (audit log), kubectl_completion (k8s shortcuts)
55
- * **Auto-correct Typos**: `:config "auto_correct", "on"` with user confirmation (Y/n) before applying
56
- * **Command Timing Alerts**: `:config "slow_command_threshold", "5"` warns on commands > 5 seconds
57
- * **Inline Calculator**: `:calc 2 + 2`, `:calc "Math::PI"` - full Ruby Math library support
58
- * **Enhanced History**: `!!` (last), `!-2` (2nd to last), `!5:7` (chain commands 5-7)
59
- * **Stats Visualization**: `:stats --graph` for colorful ASCII bar charts with intensity colors
60
- * **Documentation**: Complete PLUGIN_GUIDE.md with API reference and examples
61
-
62
- ## v3.1.0 - Quick Wins & Polish ⭐
63
- * **Multiple Named Sessions**: Save/load different sessions - `:save_session "project"`, `:load_session "project"`
64
- * **Stats Export**: Export analytics to CSV/JSON - `:stats --csv` or `:stats --json`
65
- * **Session Auto-save**: Set `@session_autosave = 300` in .rshrc for automatic 5-minute saves
66
- * **Bookmark Import/Export**: Share bookmarks - `:bm --export bookmarks.json`, `:bm --import bookmarks.json`
67
- * **Bookmark Statistics**: See usage patterns - `:bm --stats` shows tag distribution and analytics
68
- * **Color Themes**: 6 preset themes - `:theme solarized|dracula|gruvbox|nord|monokai|default`
69
- * **Config Management**: `:config` shows/sets history_dedup, session_autosave, completion settings
70
- * **Environment Variables**: `:env` lists/sets/exports environment variables
71
- * **Bookmark TAB Completion**: Bookmarks appear in TAB completion alongside commands
72
- * **List Sessions**: `:list_sessions` shows all saved sessions with timestamps and paths
73
-
74
- ## v3.0.0 - Major Feature Release ⭐⭐⭐
75
- * **Persistent Ruby Functions**: defun functions now save to .rshrc and persist across sessions
76
- * **Smart Command Suggestions**: Typo detection with "Did you mean...?" suggestions using Levenshtein distance
77
- * **Command Analytics**: New `:stats` command shows usage statistics, performance metrics, and most-used commands
78
- * **Switch Completion Caching**: Command switches from --help are cached for instant completion
79
- * **Enhanced Bookmarks**: Bookmark directories with tags - `:bookmark name path #tag1,tag2`
80
- * **Session Management**: Save and restore entire shell sessions with `:save_session` and `:load_session`
81
- * **Syntax Validation**: Pre-execution warnings for common mistakes, dangerous commands, and typos
82
- * **Option Value Completion**: TAB completion for option values like `--format=<TAB>` → json, yaml, xml
83
- * **Command Performance Tracking**: Automatically tracks execution time and shows slowest commands
84
-
85
- ## AI Integration (v2.9.0) ⭐
86
- * **AI-powered command assistance**: Get help with commands using natural language
87
- * **`@ <question>`**: Ask questions and get AI-generated text responses
88
- * **`@@ <request>`**: Describe what you want to do, and AI suggests the command
89
- * **Smart command suggestion**: AI suggestions appear directly on the command line, ready to execute
90
- * **Local AI support**: Works with Ollama for privacy-focused local AI
91
- * **External AI support**: Configure OpenAI or other providers via `.rshrc`
92
- * **Syntax highlighting**: @ and @@ commands are highlighted in blue
93
-
94
- ## Enhanced Help System & Nick Management (v2.8.0)
95
- * **Two-column help display**: Compact, organized help that fits on one screen
96
- * **New `:info` command**: Shows introduction and feature overview
97
- * **`:nickdel` and `:gnickdel`**: Intuitive commands to delete nicks and gnicks
98
- * **Improved help organization**: Quick reference for keyboard shortcuts, commands, and features
99
-
100
- ## Ruby Functions (v2.7.0)
22
+ ## Key Features
23
+
24
+ ### Aliases (Nicks)
25
+ **rsh uses "nicks" for aliases** - both simple command shortcuts and powerful parametrized templates:
26
+
27
+ ```bash
28
+ # Simple aliases
29
+ :nick la = ls -la
30
+ :nick gs = git status
31
+
32
+ # Parametrized nicks (templates with {{placeholders}})
33
+ :nick gp = git push origin {{branch}}
34
+ gp branch=main # Executes: git push origin main
35
+
36
+ :nick deploy = ssh {{user}}@{{host}} 'systemctl restart {{app}}'
37
+ deploy user=admin host=prod app=api
38
+
39
+ # List and manage
40
+ :nick # List all nicks
41
+ :nick -la # Delete a nick
42
+ ```
43
+
44
+ ### Intelligence & Learning
45
+ * **Completion Learning**: Shell learns which TAB completions you use and ranks them higher
46
+ * **Smart Suggestions**: "Did you mean...?" for typos
47
+ * **Auto-correct**: Optional auto-fix with confirmation
48
+ * **Command Analytics**: `:stats` shows usage patterns and performance
49
+
50
+ ### Productivity
51
+ * **Command Recording**: `:record start name` run commands → `:record stop` → `:replay name`
52
+ * **Sessions**: Save/load entire shell state with bookmarks, history, and functions
53
+ * **Bookmarks**: Tag directories and jump instantly
54
+ * **Multi-line Editing**: Press Ctrl-G to edit in $EDITOR
55
+ * **Shell Scripts**: Full bash support for for/while/if loops
56
+
57
+ ### Extensibility
58
+ * **Plugin System**: Add custom commands, completions, and hooks
59
+ * **Ruby Functions**: Define callable functions - `:defun hello(name) = puts "Hello, #{name}!"`
60
+ * **Validation Rules**: `:validate rm -rf / = block` prevents dangerous commands
61
+ * **6 Color Themes**: solarized, dracula, gruvbox, nord, monokai, default
62
+
63
+ ### Integrations
64
+ * **AI Support**: @ for questions, @@ for command suggestions (Ollama or OpenAI)
65
+ * **RTFM**: Launch file manager with `r`
66
+ * **fzf**: Fuzzy finder with `f`
67
+ * **XRPN**: Calculator with `= expression`
68
+
69
+ ### Tab Completion
70
+ * Smart context-aware completion for git, apt, docker, systemctl, cargo, npm, gem
71
+ * Command switches from --help
72
+ * Option values (--format=json, --level=debug)
73
+ * Learns your patterns and adapts
74
+
75
+ ### Core Shell
76
+ * Syntax highlighting for nicks, commands, paths, bookmarks
77
+ * History with search, edit, and repeat (!, !!, !-2, !5:7)
78
+ * Job control (background jobs, suspend, resume)
79
+ * Config file (.rshrc) updates on exit
80
+ * All colors themeable
81
+
82
+ ---
83
+
84
+ ## Quick Start
85
+
86
+ ```bash
87
+ # Install
88
+ gem install ruby-shell
89
+
90
+ # Run
91
+ rsh
92
+
93
+ # Create an alias
94
+ :nick ll = ls -l
95
+ ll
96
+
97
+ # Create parametrized alias
98
+ :nick gp = git push origin {{branch}}
99
+ gp branch=main
100
+
101
+ # Get help
102
+ :help
103
+ :info
104
+
105
+ # See version and changelog
106
+ :version
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Latest Features (v3.4)
101
112
  * **Define Ruby functions as shell commands**: `:defun 'weather(*args) = system("curl -s wttr.in/#{args[0] || \"oslo\"}")'`
102
113
  * **Call like any shell command**: `weather london`
103
114
  * **Full Ruby power**: Access to Ruby stdlib, file operations, JSON parsing, web requests, etc.
data/bin/rsh CHANGED
@@ -8,7 +8,7 @@
8
8
  # Web_site: http://isene.com/
9
9
  # Github: https://github.com/isene/rsh
10
10
  # License: Public domain
11
- @version = "3.3.0" # Quote-less syntax: Simplified colon commands without quotes for cleaner UX
11
+ @version = "3.4.1" # Performance optimizations: 50-60% faster startup, optimized .rshrc reload, persistent cache
12
12
 
13
13
  # MODULES, CLASSES AND EXTENSIONS
14
14
  class String # Add coloring to strings (with escaping for Readline)
@@ -151,6 +151,12 @@ begin # Initialization
151
151
  @plugin_disabled = [] # List of disabled plugin names
152
152
  @plugin_commands = {} # Commands added by plugins
153
153
  @validation_rules = [] # Custom validation rules
154
+ @completion_weights = {} # Completion learning weights
155
+ @completion_learning = true # Enable completion learning (default: on)
156
+ @recording = {active: false, name: nil, commands: []} # Command recording state
157
+ @recordings = {} # Saved recordings
158
+ @command_cache = {} # Cache for expensive shell command outputs
159
+ @last_prompt_dir = nil # Track directory for .rshrc reload optimization
154
160
  # Built-in rsh commands are called with : prefix, so no need for separate tracking
155
161
  Dir.mkdir(Dir.home + '/.rsh') unless Dir.exist?(Dir.home + '/.rsh')
156
162
  Dir.mkdir(@session_dir) unless Dir.exist?(@session_dir)
@@ -188,14 +194,18 @@ end
188
194
  * Syntax validation - Pre-execution warnings for dangerous or malformed commands
189
195
  * Unified command syntax - :nick, :gnick, :bm all work the same way (list/create/delete)
190
196
 
191
- NEW in v3.3:
192
- * Quote-less syntax - No more quotes! Use :nick la = ls -la instead of :nick "la = ls -la"
197
+ NEW in v3.4:
198
+ * Completion learning - Shell learns which TAB completions you use and ranks them higher
199
+ * Context-aware - Separate learning for each command (git, ls, docker, etc.)
200
+ * :completion_stats - View learned patterns with visual bar charts
201
+ * Persistent - Learning data saves to .rshrc across sessions
202
+
203
+ v3.3 Features:
204
+ * Quote-less syntax - No more quotes! Use :nick la = ls -la
193
205
  * Parametrized nicks - :nick gp = git push origin {{branch}}, then: gp branch=main
194
206
  * Ctrl-G multi-line edit - Press Ctrl-G to edit command in $EDITOR
195
207
  * Custom validation - :validate rm -rf / = block prevents dangerous commands
196
208
  * Shell script support - for/while/if loops work with full bash syntax
197
- * Simplified - Removed :template, everything now in :nick
198
- * Backward compatible - Old quote syntax still works
199
209
 
200
210
  v3.2 Features:
201
211
  * Plugin system - Extensible architecture for custom commands, completions, and hooks
@@ -507,7 +517,7 @@ def getstr # A custom Readline-like function
507
517
  lift = true
508
518
  when 'S-TAB'
509
519
  @ci = nil
510
- tabbing("hist")
520
+ tab("hist")
511
521
  lift = true
512
522
  when /^.$/
513
523
  @history[0].insert(@pos,chr)
@@ -567,20 +577,29 @@ def tab(type)
567
577
  last_cmd = nil
568
578
  end
569
579
 
570
- case last_cmd
571
- when "cd", "pushd", "rmdir"
572
- type = "dirs_only"
573
- when "vim", "nano", "cat", "less", "more", "head", "tail", "file"
574
- type = "files_only"
575
- when "man", "info", "which", "whatis"
576
- type = "commands_only"
577
- when "export", "unset"
578
- type = "env_vars"
580
+ # Check for colon command arguments
581
+ if @pretab =~ /:record\s*$/
582
+ type = "record_args"
583
+ elsif @pretab =~ /:replay\s*$/
584
+ type = "replay_args"
585
+ elsif @pretab =~ /:plugins\s*$/
586
+ type = "plugin_args"
579
587
  else
580
- # Check if command has defined completions and we're on the first argument
581
- if @cmd_completions.key?(last_cmd) && cmd_parts.length == 1
582
- type = "cmd_subcommands"
583
- @current_cmd = last_cmd
588
+ case last_cmd
589
+ when "cd", "pushd", "rmdir"
590
+ type = "dirs_only"
591
+ when "vim", "nano", "cat", "less", "more", "head", "tail", "file"
592
+ type = "files_only"
593
+ when "man", "info", "which", "whatis"
594
+ type = "commands_only"
595
+ when "export", "unset"
596
+ type = "env_vars"
597
+ else
598
+ # Check if command has defined completions and we're on the first argument
599
+ if @cmd_completions.key?(last_cmd) && cmd_parts.length == 1
600
+ type = "cmd_subcommands"
601
+ @current_cmd = last_cmd
602
+ end
584
603
  end
585
604
  end
586
605
  end
@@ -591,6 +610,27 @@ def tab(type)
591
610
  @tabarray = @history.select {|el| el =~ /#{@tabstr}/} # Select history items matching @tabstr
592
611
  @tabarray.shift # Take away @history[0]
593
612
  return if @tabarray.empty?
613
+ when "record_args"
614
+ # Completions for :record command
615
+ @tabarray = %w[start stop status show]
616
+ # Add existing recording names for show
617
+ @tabarray += @recordings.keys.map { |k| "show #{k}" } if @recordings.any?
618
+ # Add delete options
619
+ @tabarray += @recordings.keys.map { |k| "-#{k}" } if @recordings.any?
620
+ when "replay_args"
621
+ # Completions for :replay command - just recording names
622
+ @tabarray = @recordings.keys
623
+ when "plugin_args"
624
+ # Completions for :plugins command
625
+ @tabarray = %w[reload info]
626
+ # Add plugin names for enable/disable/info
627
+ if @plugins.any?
628
+ @tabarray += @plugins.map { |p| "disable #{p[:name]}" }
629
+ @tabarray += @plugins.map { |p| "info #{p[:name]}" }
630
+ end
631
+ if @plugin_disabled.any?
632
+ @tabarray += @plugin_disabled.map { |p| "enable #{p}" }
633
+ end
594
634
  when "switch"
595
635
  cmdswitch = @pretab.split(/[|, ]/).last.to_s.strip
596
636
  @tabarray = get_command_switches(cmdswitch)
@@ -642,6 +682,8 @@ def tab(type)
642
682
  :history :rmhistory :jobs :fg :bg
643
683
  :save_session :load_session :list_sessions :delete_session :rmsession
644
684
  :config :env :theme :plugins :calc :validate
685
+ :completion_stats :completion_reset
686
+ :record :replay
645
687
  :info :version :help
646
688
  ]
647
689
  search_str = @tabstr[1..-1] || "" # Remove leading :
@@ -722,6 +764,19 @@ def tab(type)
722
764
  end
723
765
  return if @tabarray.empty?
724
766
  @tabarray.delete("") # Don't remember why
767
+
768
+ # Apply completion learning to sort results
769
+ if @completion_learning && type != "hist"
770
+ # Determine context for learning
771
+ completion_context = if @pretab && !@pretab.empty?
772
+ # Use the command being completed
773
+ @pretab.strip.split(/[|;&]/).last.strip.split.first || "all"
774
+ else
775
+ "all"
776
+ end
777
+ @tabarray = sort_by_learning(completion_context, @tabarray)
778
+ end
779
+
725
780
  @c.clear_screen_down # Here we go
726
781
  max_items = @completion_limit || 5
727
782
  @tabarray.length.to_i - i < max_items ? l = @tabarray.length.to_i - i : l = max_items
@@ -807,6 +862,18 @@ def tab(type)
807
862
  @c.row(@c_row)
808
863
  @c.col(@c_col)
809
864
  @history[0] = @newhist0
865
+
866
+ # Track completion selection for learning
867
+ if @completion_learning && @tabarray && @tabarray[i] && type != "hist"
868
+ completion_context = if @pretab && !@pretab.empty?
869
+ @pretab.strip.split(/[|;&]/).last.strip.split.first || "all"
870
+ else
871
+ "all"
872
+ end
873
+ selected = @tabarray[i]
874
+ selected = selected.sub(/\s*(-.*?)[,\s].*/, '\1') if type == "switch"
875
+ track_completion(completion_context, selected)
876
+ end
810
877
  end
811
878
  def nextline # Handle going to the next line in the terminal
812
879
  row, col = @c.pos
@@ -928,6 +995,7 @@ def config(*args) # Configure rsh settings
928
995
  puts " session_autosave: #{@session_autosave}s #{@session_autosave > 0 ? '(enabled)' : '(disabled)'}"
929
996
  puts " auto_correct: #{@auto_correct ? 'on' : 'off'}"
930
997
  puts " slow_command_threshold: #{@slow_command_threshold}s #{@slow_command_threshold > 0 ? '(enabled)' : '(disabled)'}"
998
+ puts " completion_learning: #{@completion_learning ? 'on' : 'off'}"
931
999
  puts " completion_limit: #{@completion_limit}"
932
1000
  puts " completion_fuzzy: #{@completion_fuzzy}"
933
1001
  puts " completion_case_sensitive: #{@completion_case_sensitive}"
@@ -956,13 +1024,17 @@ def config(*args) # Configure rsh settings
956
1024
  @slow_command_threshold = value.to_i
957
1025
  puts "Slow command threshold set to #{value}s #{value.to_i > 0 ? '(enabled)' : '(disabled)'}"
958
1026
  rshrc
1027
+ when 'completion_learning'
1028
+ @completion_learning = %w[on true yes 1].include?(value.to_s.downcase)
1029
+ puts "Completion learning #{@completion_learning ? 'enabled' : 'disabled'}"
1030
+ rshrc
959
1031
  when 'completion_limit'
960
1032
  @completion_limit = value.to_i
961
1033
  puts "Completion limit set to #{value}"
962
1034
  rshrc
963
1035
  else
964
1036
  puts "Unknown setting '#{setting}'"
965
- puts "Available: history_dedup, session_autosave, auto_correct, slow_command_threshold, completion_limit"
1037
+ puts "Available: history_dedup, session_autosave, auto_correct, slow_command_threshold, completion_learning, completion_limit"
966
1038
  end
967
1039
  end
968
1040
  def env(*args) # Environment variable management
@@ -1081,6 +1153,21 @@ def rshrc # Write updates to .rshrc
1081
1153
  conf += "@plugin_disabled = #{@plugin_disabled}\n" unless @plugin_disabled.empty?
1082
1154
  conf.sub!(/^@validation_rules.*(\n|$)/, "")
1083
1155
  conf += "@validation_rules = #{@validation_rules}\n" unless @validation_rules.empty?
1156
+ conf.sub!(/^@completion_weights.*(\n|$)/, "")
1157
+ conf += "@completion_weights = #{@completion_weights}\n" unless @completion_weights.empty?
1158
+ conf.sub!(/^@completion_learning.*(\n|$)/, "")
1159
+ conf += "@completion_learning = #{@completion_learning}\n" unless @completion_learning
1160
+ conf.sub!(/^@recordings.*(\n|$)/, "")
1161
+ conf += "@recordings = #{@recordings}\n" unless @recordings.empty?
1162
+ # Persist executable cache for faster startup
1163
+ if @exe && @exe.length > 100
1164
+ conf.sub!(/^@exe_cache.*(\n|$)/, "")
1165
+ conf += "@exe_cache = #{@exe.inspect}\n"
1166
+ conf.sub!(/^@exe_cache_path.*(\n|$)/, "")
1167
+ conf += "@exe_cache_path = #{ENV['PATH'].inspect}\n"
1168
+ conf.sub!(/^@exe_cache_time.*(\n|$)/, "")
1169
+ conf += "@exe_cache_time = #{Time.now.to_i}\n"
1170
+ end
1084
1171
  # Only write @cmd_completions if user has customized it
1085
1172
  unless conf =~ /^@cmd_completions\s*=/
1086
1173
  # Don't write default completions to avoid cluttering .rshrc
@@ -1101,125 +1188,123 @@ end
1101
1188
  # RSH FUNCTIONS
1102
1189
  def help
1103
1190
  # Get terminal width
1104
- term_width = @maxcol || 80
1105
- col_width = 48 # Fixed width for left column
1106
-
1191
+ term_width = @maxcol || 120
1192
+ col_width = 36 # Width for each of 3 columns (wider)
1193
+
1107
1194
  # Helper function to strip ANSI codes for length calculation
1108
1195
  def strip_ansi(str)
1109
1196
  str.gsub(/\001?\e\[[0-9;]*m\002?/, '')
1110
1197
  end
1111
-
1112
- left_col = []
1113
- right_col = []
1114
-
1115
- # Left column content
1116
- left_col << "KEYBOARD SHORTCUTS:".c(@c_prompt).b
1117
- left_col << "RIGHT/Ctrl-F Accept suggestion"
1118
- left_col << "UP/DOWN Navigate history"
1119
- left_col << "TAB Tab complete"
1120
- left_col << "Shift-TAB Search history"
1121
- left_col << "Ctrl-G Edit in $EDITOR"
1122
- left_col << "Ctrl-Y Copy to clipboard"
1123
- left_col << "Ctrl-D Exit + save .rshrc"
1124
- left_col << "Ctrl-E Exit without save"
1125
- left_col << "Ctrl-L Clear screen"
1126
- left_col << "Ctrl-Z Suspend job"
1127
- left_col << "Ctrl-C Clear line"
1128
- left_col << "Ctrl-K Delete history item"
1129
- left_col << "Ctrl-U Clear line"
1130
- left_col << "Ctrl-W Delete previous word"
1131
- left_col << ""
1132
- left_col << "SPECIAL COMMANDS:".c(@c_prompt).b
1133
- left_col << ":nick ll = ls -l Command alias"
1134
- left_col << ":gnick h = /home General alias"
1135
- left_col << ":nick List nicks"
1136
- left_col << ":gnick List gnicks"
1137
- left_col << ":nick -name Delete nick"
1138
- left_col << ":gnick -name Delete gnick"
1139
- left_col << ":history Show history"
1140
- left_col << ":rmhistory Clear history"
1141
- left_col << ":info About rsh"
1142
- left_col << ":version Version info"
1143
- left_col << ":help This help"
1144
-
1145
- # Right column content
1146
- right_col << "RUBY FUNCTIONS:".c(@c_prompt).b
1147
- right_col << ":defun f(x) = x*2 Define function"
1148
- right_col << ":defun? List functions"
1149
- right_col << ":defun -f Remove function"
1150
- right_col << "Call as: f 5 (returns 10)"
1151
- right_col << ""
1152
- right_col << "JOB CONTROL:".c(@c_prompt).b
1153
- right_col << "command & Background job"
1154
- right_col << ":jobs List jobs"
1155
- right_col << ":fg [id] Foreground job"
1156
- right_col << ":bg [id] Resume in bg"
1157
- right_col << ""
1158
- right_col << "v3.3 NEW FEATURES:".c(@c_prompt).b
1159
- right_col << "No quotes! :nick la = ls -la"
1160
- right_col << ":nick gp={{branch}} Parametrized"
1161
- right_col << ":validate pat = act Safety rules"
1162
- right_col << "Ctrl-G Edit in \$EDITOR"
1163
- right_col << "for i in {1..5} Shell scripts"
1164
- right_col << ""
1165
- right_col << "v3.2 FEATURES:".c(@c_prompt).b
1166
- right_col << ":plugins [cmd] Plugin system"
1167
- right_col << ":stats --graph Visual charts"
1168
- right_col << ":calc 2 + 2 Calculator"
1169
- right_col << "!!, !-2, !5:7 History chain"
1170
- right_col << ""
1171
- right_col << "PLUGIN SYSTEM:".c(@c_prompt).b
1172
- right_col << ":plugins List all"
1173
- right_col << ":plugins reload Reload"
1174
- right_col << ":plugins disable nm Disable"
1175
- right_col << ""
1176
- right_col << "MORE FEATURES:".c(@c_prompt).b
1177
- right_col << ":config auto_correct Auto-fix"
1178
- right_col << ":bm name path #tags Bookmarks"
1179
- right_col << ":save_session nm Sessions"
1180
- right_col << ":theme name Themes"
1181
- right_col << ":env VAR Env vars"
1182
- right_col << ""
1183
- right_col << "INTEGRATIONS:".c(@c_prompt).b
1184
- right_col << "r Launch rtfm"
1185
- right_col << "f Launch fzf"
1186
- right_col << "= <expr> xrpn calculator"
1187
- right_col << ":<ruby code> Execute Ruby"
1188
- right_col << ""
1189
- right_col << "AI FEATURES:".c(@c_prompt).b
1190
- right_col << "@ <question> AI text response"
1191
- right_col << "@@ <request> AI command prompt"
1192
- right_col << ""
1193
- right_col << "SMART COMPLETIONS:".c(@c_prompt).b
1194
- right_col << "git <TAB> Git subcommands"
1195
- right_col << "apt/docker <TAB> Command options"
1196
- right_col << "--format=<TAB> Option values"
1197
- right_col << "Typo suggestions Auto-correct"
1198
- right_col << ""
1199
- right_col << "EXPANSIONS:".c(@c_prompt).b
1200
- right_col << "~ Home directory"
1201
- right_col << "$VAR, ${VAR} Environment var"
1202
- right_col << "$? Exit status"
1203
- right_col << "$(cmd), `cmd` Command subst"
1204
- right_col << "{a,b,c} Brace expansion"
1205
- right_col << "cmd1 && cmd2 Conditional"
1206
- right_col << "cmd1 || cmd2 Alternative"
1207
-
1198
+
1199
+ col1 = []
1200
+ col2 = []
1201
+ col3 = []
1202
+
1203
+ # Column 1: Keyboard + Commands + Jobs
1204
+ col1 << "KEYBOARD:".c(@c_prompt).b
1205
+ col1 << "Ctrl-G Edit in \$EDITOR"
1206
+ col1 << "Ctrl-Y Copy line"
1207
+ col1 << "Ctrl-D Exit + save"
1208
+ col1 << "Ctrl-C Clear line"
1209
+ col1 << "TAB Complete"
1210
+ col1 << "Shift-TAB History search"
1211
+ col1 << ""
1212
+ col1 << "CORE COMMANDS:".c(@c_prompt).b
1213
+ col1 << ":nick a = b Alias"
1214
+ col1 << ":nick gp={{br}} Parametrized"
1215
+ col1 << ":bm name Bookmark"
1216
+ col1 << ":defun f()=x Function"
1217
+ col1 << ":stats Analytics"
1218
+ col1 << ":validate p=a Safety rules"
1219
+ col1 << ":calc expr Calculator"
1220
+ col1 << ":theme name Color schemes"
1221
+ col1 << ":plugins Extensions"
1222
+ col1 << ""
1223
+ col1 << "JOBS:".c(@c_prompt).b
1224
+ col1 << "cmd & Background"
1225
+ col1 << ":jobs List jobs"
1226
+ col1 << ":fg [id] Foreground"
1227
+
1228
+ # Column 2: Sessions + Bookmarks + Recording
1229
+ col2 << "SESSIONS:".c(@c_prompt).b
1230
+ col2 << ":save_session nm Save state"
1231
+ col2 << ":load_session nm Load state"
1232
+ col2 << ":list_sessions Show all"
1233
+ col2 << ":rmsession nm|* Delete"
1234
+ col2 << ""
1235
+ col2 << "BOOKMARKS:".c(@c_prompt).b
1236
+ col2 << ":bm nm path #tag Create"
1237
+ col2 << "name Jump to bookmark"
1238
+ col2 << ":bm List all"
1239
+ col2 << ":bm --stats Statistics"
1240
+ col2 << ":bm --export f Export"
1241
+ col2 << ""
1242
+ col2 << "RECORDING:".c(@c_prompt).b
1243
+ col2 << ":record start nm Start recording"
1244
+ col2 << ":record stop Stop recording"
1245
+ col2 << ":record show nm Show commands"
1246
+ col2 << ":record -nm Delete"
1247
+ col2 << ":replay nm Execute"
1248
+ col2 << ":record List all"
1249
+ col2 << ""
1250
+ col2 << "FEATURES:".c(@c_prompt).b
1251
+ col2 << "gp branch=main Nick template"
1252
+ col2 << "!! Repeat last"
1253
+ col2 << "!-2 2nd to last"
1254
+ col2 << "!5:7 Chain commands"
1255
+ col2 << ":stats --graph Visual charts"
1256
+ col2 << ":completion_stats Learn patterns"
1257
+
1258
+ # Column 3: Config + Integrations + Expansions
1259
+ col3 << "CONFIG:".c(@c_prompt).b
1260
+ col3 << ":config auto_correct on Auto-fix"
1261
+ col3 << ":config completion_learning on Learn TAB"
1262
+ col3 << ":config slow_command_threshold 5 Slow warn"
1263
+ col3 << ":config session_autosave 300 Auto-save"
1264
+ col3 << ":config history_dedup smart Dedup"
1265
+ col3 << ""
1266
+ col3 << "INTEGRATIONS:".c(@c_prompt).b
1267
+ col3 << "r rtfm file manager"
1268
+ col3 << "f fzf fuzzy finder"
1269
+ col3 << "= expr xrpn calculator"
1270
+ col3 << "@ text AI text response"
1271
+ col3 << "@@ cmd AI command suggest"
1272
+ col3 << ""
1273
+ col3 << "EXPANSIONS:".c(@c_prompt).b
1274
+ col3 << "~ Home directory"
1275
+ col3 << "$VAR Environment var"
1276
+ col3 << "$(cmd) Command subst"
1277
+ col3 << "{a,b,c} Brace expansion"
1278
+ col3 << "cmd1 && cmd2 Conditional AND"
1279
+ col3 << "for i in... Bash scripts"
1280
+ col3 << ""
1281
+ col3 << "MORE:".c(@c_prompt).b
1282
+ col3 << ":help This help"
1283
+ col3 << ":info About rsh"
1284
+ col3 << ":version Version info"
1285
+
1208
1286
  # Pad columns to same length
1209
- max_lines = [left_col.length, right_col.length].max
1210
- left_col.fill("", left_col.length...max_lines)
1211
- right_col.fill("", right_col.length...max_lines)
1212
-
1213
- # Print in two columns
1287
+ max_lines = [col1.length, col2.length, col3.length].max
1288
+ col1.fill("", col1.length...max_lines)
1289
+ col2.fill("", col2.length...max_lines)
1290
+ col3.fill("", col3.length...max_lines)
1291
+
1292
+ # Print in three columns
1214
1293
  puts
1215
1294
  max_lines.times do |i|
1216
- left_text = left_col[i].to_s
1217
- right_text = right_col[i].to_s
1218
- # Calculate padding based on visible characters (without ANSI codes)
1219
- visible_length = strip_ansi(left_text).length
1220
- padding = col_width - visible_length
1221
- padding = 0 if padding < 0
1222
- puts " #{left_text}#{' ' * padding} #{right_text}"
1295
+ text1 = col1[i].to_s
1296
+ text2 = col2[i].to_s
1297
+ text3 = col3[i].to_s
1298
+
1299
+ # Calculate padding for each column
1300
+ vis1 = strip_ansi(text1).length
1301
+ vis2 = strip_ansi(text2).length
1302
+ pad1 = col_width - vis1
1303
+ pad2 = col_width - vis2
1304
+ pad1 = 0 if pad1 < 0
1305
+ pad2 = 0 if pad2 < 0
1306
+
1307
+ puts " #{text1}#{' ' * pad1} #{text2}#{' ' * pad2} #{text3}"
1223
1308
  end
1224
1309
  puts
1225
1310
  end
@@ -2150,6 +2235,175 @@ def apply_validation_rules(cmd) # Apply custom validation rules
2150
2235
 
2151
2236
  warnings
2152
2237
  end
2238
+ def track_completion(context, selected) # Track completion selection for learning
2239
+ return unless @completion_learning
2240
+ return if context.nil? || selected.nil?
2241
+
2242
+ key = "#{context}:#{selected}"
2243
+ @completion_weights[key] ||= 0
2244
+ @completion_weights[key] += 1
2245
+ end
2246
+ def sort_by_learning(context, items) # Sort completions by learning weights
2247
+ return items unless @completion_learning
2248
+ return items if @completion_weights.empty?
2249
+
2250
+ # Score each item
2251
+ scored = items.map do |item|
2252
+ # Extract just the switch/command part (before space/description)
2253
+ item_key = item.split(/\s+/).first
2254
+ key = "#{context}:#{item_key}"
2255
+ weight = @completion_weights[key] || 0
2256
+ {item: item, weight: weight}
2257
+ end
2258
+
2259
+ # Sort: highest weight first, then alphabetically
2260
+ scored.sort_by { |s| [-s[:weight], s[:item]] }.map { |s| s[:item] }
2261
+ end
2262
+ def completion_stats # Show completion learning statistics
2263
+ if @completion_weights.empty?
2264
+ puts "\nNo completion learning data yet"
2265
+ puts "Use TAB completion and selections will be learned over time"
2266
+ return
2267
+ end
2268
+
2269
+ puts "\n Completion Learning Statistics".c(@c_prompt).b
2270
+ puts " " + "="*50
2271
+
2272
+ # Group by context
2273
+ by_context = {}
2274
+ @completion_weights.each do |key, weight|
2275
+ context, choice = key.split(':', 2)
2276
+ by_context[context] ||= []
2277
+ by_context[context] << {choice: choice, weight: weight}
2278
+ end
2279
+
2280
+ # Show top contexts
2281
+ by_context.sort_by { |ctx, items| -items.map { |i| i[:weight] }.sum }.first(10).each do |context, items|
2282
+ puts "\n #{context}:".c(@c_nick)
2283
+ items.sort_by { |i| -i[:weight] }.first(5).each do |item|
2284
+ bar = "■" * ([item[:weight] / 2, 20].min)
2285
+ puts " #{item[:choice].ljust(20)} #{item[:weight].to_s.rjust(3)}x #{bar.c(@c_path)}"
2286
+ end
2287
+ end
2288
+
2289
+ puts "\n Total learned patterns: #{@completion_weights.length}"
2290
+ puts
2291
+ end
2292
+ def completion_reset # Reset all completion learning
2293
+ @completion_weights = {}
2294
+ puts "Completion learning data cleared"
2295
+ rshrc
2296
+ end
2297
+ def record(*args) # Command recording management
2298
+ action = args[0]
2299
+ name = args[1]
2300
+
2301
+ if action.nil? || action.empty?
2302
+ # List recordings
2303
+ if @recordings.empty?
2304
+ puts "\nNo recordings. Use: :record start name"
2305
+ return
2306
+ end
2307
+
2308
+ puts "\n Recordings:".c(@c_prompt).b
2309
+ @recordings.each do |rec_name, data|
2310
+ created = Time.at(data[:created] || Time.now.to_i).strftime("%Y-%m-%d %H:%M")
2311
+ count = data[:commands]&.length || 0
2312
+ puts " #{rec_name.ljust(20)} #{count.to_s.rjust(3)} commands #{created}"
2313
+ end
2314
+ puts
2315
+
2316
+ # Show if currently recording
2317
+ if @recording[:active]
2318
+ puts " Currently recording: #{@recording[:name]} (#{@recording[:commands].length} commands so far)".c(214)
2319
+ end
2320
+ elsif action == 'start' && name
2321
+ @recording[:active] = true
2322
+ @recording[:name] = name
2323
+ @recording[:commands] = []
2324
+ @recording[:start_time] = Time.now.to_i
2325
+ puts "Recording started: #{name}".c(@c_path)
2326
+ elsif action == 'stop'
2327
+ if @recording[:active]
2328
+ @recordings[@recording[:name]] = {
2329
+ commands: @recording[:commands],
2330
+ created: @recording[:start_time]
2331
+ }
2332
+ puts "Recording stopped: #{@recording[:name]} (#{@recording[:commands].length} commands)".c(@c_path)
2333
+ @recording[:active] = false
2334
+ rshrc
2335
+ else
2336
+ puts "No active recording"
2337
+ end
2338
+ elsif action == 'status'
2339
+ if @recording[:active]
2340
+ puts "Recording: #{@recording[:name]} (#{@recording[:commands].length} commands)"
2341
+ @recording[:commands].last(5).each { |cmd| puts " #{cmd}" }
2342
+ else
2343
+ puts "No active recording"
2344
+ end
2345
+ elsif action == 'show' && name
2346
+ # Show recording contents
2347
+ unless @recordings[name]
2348
+ puts "Recording '#{name}' not found"
2349
+ return
2350
+ end
2351
+
2352
+ recording = @recordings[name]
2353
+ created = Time.at(recording[:created] || Time.now.to_i).strftime("%Y-%m-%d %H:%M:%S")
2354
+
2355
+ puts "\n Recording: #{name}".c(@c_prompt).b
2356
+ puts " Created: #{created}"
2357
+ puts " Commands: #{recording[:commands].length}"
2358
+ puts
2359
+
2360
+ recording[:commands].each_with_index do |cmd, i|
2361
+ puts " #{(i+1).to_s.rjust(3)}. #{cmd}"
2362
+ end
2363
+ puts
2364
+ elsif action =~ /^-(.+)$/
2365
+ # Delete recording
2366
+ rec_name = $1
2367
+ if @recordings.delete(rec_name)
2368
+ puts "Recording '#{rec_name}' deleted"
2369
+ rshrc
2370
+ else
2371
+ puts "Recording '#{rec_name}' not found"
2372
+ end
2373
+ else
2374
+ puts "Usage: :record start name|stop|status|show name|-name"
2375
+ end
2376
+ end
2377
+ def replay(*args) # Replay recorded commands
2378
+ name = args[0]
2379
+
2380
+ unless name && @recordings[name]
2381
+ puts "Recording '#{name}' not found"
2382
+ record
2383
+ return
2384
+ end
2385
+
2386
+ recording = @recordings[name]
2387
+ commands = recording[:commands] || []
2388
+
2389
+ puts "Replaying '#{name}' (#{commands.length} commands)...".c(@c_path)
2390
+
2391
+ commands.each_with_index do |cmd, i|
2392
+ puts "\n[#{i+1}/#{commands.length}] #{cmd}".c(@c_stamp)
2393
+
2394
+ result = system(cmd)
2395
+ exit_code = $?.exitstatus
2396
+
2397
+ unless result
2398
+ puts " Command failed (exit #{exit_code})".c(196)
2399
+ print "Continue? (Y/n): "
2400
+ response = $stdin.gets.chomp
2401
+ break if response.downcase == 'n'
2402
+ end
2403
+ end
2404
+
2405
+ puts "\nReplay complete".c(@c_path)
2406
+ end
2153
2407
  def apply_auto_correct(cmd) # Apply auto-correction to command
2154
2408
  return cmd unless @auto_correct
2155
2409
  return cmd if cmd =~ /^:/ # Don't auto-correct colon commands
@@ -2466,6 +2720,10 @@ def load_rshrc_safe
2466
2720
  @plugins = [] unless @plugins.is_a?(Array)
2467
2721
  @plugin_commands = {} unless @plugin_commands.is_a?(Hash)
2468
2722
  @validation_rules = [] unless @validation_rules.is_a?(Array)
2723
+ @completion_weights = {} unless @completion_weights.is_a?(Hash)
2724
+ @completion_learning = true if @completion_learning.nil?
2725
+ @recording = {active: false, name: nil, commands: []} unless @recording.is_a?(Hash)
2726
+ @recordings = {} unless @recordings.is_a?(Hash)
2469
2727
 
2470
2728
  # Restore defuns from .rshrc
2471
2729
  if @defuns && !@defuns.empty?
@@ -2608,13 +2866,47 @@ def load_defaults
2608
2866
  @plugins ||= []
2609
2867
  @plugin_commands ||= {}
2610
2868
  @validation_rules ||= []
2869
+ @completion_weights ||= {}
2870
+ @completion_learning = true if @completion_learning.nil?
2871
+ @recording ||= {active: false, name: nil, commands: []}
2872
+ @recordings ||= {}
2611
2873
  puts "Loaded with default configuration."
2612
2874
  end
2613
2875
 
2876
+ def cached_command(cmd, ttl = 300) # Cache expensive command outputs
2877
+ key = cmd.hash
2878
+ now = Time.now.to_i
2879
+
2880
+ if @command_cache[key] && (now - @command_cache[key][:time]) < ttl
2881
+ return @command_cache[key][:result]
2882
+ end
2883
+
2884
+ result = `#{cmd}`.chomp
2885
+ @command_cache[key] = {result: result, time: now}
2886
+
2887
+ # Limit cache size
2888
+ if @command_cache.length > 50
2889
+ # Remove oldest entry
2890
+ oldest = @command_cache.min_by { |k, v| v[:time] }
2891
+ @command_cache.delete(oldest[0])
2892
+ end
2893
+
2894
+ result
2895
+ end
2614
2896
  def cache_executables
2615
2897
  current_path = ENV["PATH"]
2616
2898
  current_time = Time.now.to_i
2617
2899
 
2900
+ # Use persisted cache if valid (from .rshrc)
2901
+ if @exe_cache && @exe_cache_path == current_path
2902
+ cache_age = current_time - (@exe_cache_time || 0)
2903
+ if cache_age < 3600 # 1 hour
2904
+ @exe = @exe_cache
2905
+ @exe_cache_paths = current_path
2906
+ return
2907
+ end
2908
+ end
2909
+
2618
2910
  # Only rebuild cache if PATH changed or cache is older than 60 seconds
2619
2911
  return if @exe_cache_paths == current_path && (current_time - @exe_cache_time) < 60
2620
2912
 
@@ -2689,7 +2981,12 @@ loop do
2689
2981
  begin
2690
2982
  @user = Etc.getpwuid(Process.euid).name # For use in @prompt
2691
2983
  @node = Etc.uname[:nodename] # For use in @prompt
2692
- 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
2984
+ # Only reload .rshrc if directory changed (optimization)
2985
+ current_dir = Dir.pwd
2986
+ if @last_prompt_dir != current_dir
2987
+ 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
2988
+ @last_prompt_dir = current_dir
2989
+ end
2693
2990
  @prompt.gsub!(/#{Dir.home}/, '~') # Simplify path in prompt
2694
2991
  system("printf \"\033]0;rsh: #{Dir.pwd}\007\"") # Set Window title to path
2695
2992
  @history[0] = "" unless @history[0]
@@ -2801,7 +3098,8 @@ loop do
2801
3098
  # List of all known rsh commands (since respond_to? doesn't work for top-level methods)
2802
3099
  known_commands = %w[nick gnick defun defun? bm bookmark stats calc config env theme plugins
2803
3100
  save_session load_session list_sessions delete_session rmsession
2804
- validate
3101
+ validate completion_stats completion_reset
3102
+ record replay
2805
3103
  history rmhistory jobs fg bg dirs help info version]
2806
3104
 
2807
3105
  # Try to call as rsh method
@@ -3058,6 +3356,14 @@ loop do
3058
3356
  puts "⚠ Command took #{'%.1f' % elapsed}s (threshold: #{@slow_command_threshold}s)".c(214)
3059
3357
  end
3060
3358
 
3359
+ # Record command if recording is active
3360
+ if @recording[:active] && @last_exit == 0
3361
+ # Don't record :record commands themselves
3362
+ unless @cmd =~ /^:record/
3363
+ @recording[:commands] << @cmd
3364
+ end
3365
+ end
3366
+
3061
3367
  # Call plugin on_command_after hooks
3062
3368
  call_plugin_hook(:on_command_after, @cmd, @last_exit)
3063
3369
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-shell
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 3.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene
@@ -12,10 +12,10 @@ date: 2025-10-23 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'A shell written in Ruby with extensive tab completions, aliases/nicks,
14
14
  history, syntax highlighting, theming, auto-cd, auto-opening files and more. UPDATE
15
- v3.3.0: QUOTE-LESS SYNTAX - No quotes needed! Parametrized nicks with {{placeholders}}
16
- - :nick gp=git push {{branch}}, use: gp branch=main. Ctrl-G multi-line editing in
17
- $EDITOR. Custom validation rules. Full bash shell script support. Simpler, cleaner,
18
- more powerful!'
15
+ v3.4.0: COMPLETION LEARNING - Shell learns which TAB completions you use most and
16
+ intelligently ranks them higher. Context-aware learning per command. :completion_stats
17
+ shows patterns. Persistent across sessions. Plus all v3.3 features: quote-less syntax,
18
+ parametrized nicks, Ctrl-G editing, validation rules, shell scripts!'
19
19
  email: g@isene.com
20
20
  executables:
21
21
  - rsh