ruby-shell 3.4.3 → 3.4.5
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 +21 -0
- data/bin/rsh +100 -36
- metadata +6 -6
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 1a0c1e45454167bc454071a1419673d2ca195980970208c54b2745870709e024
         | 
| 4 | 
            +
              data.tar.gz: 4981d0f400409d059e703df8d6daa9eab3be19e6a4e0da3f96de7533aac66c6c
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 6632764744a12eb79fda32cf92677e44e0ded8cab07e9b41cad11cfb911df0f678c63d9f8448390c8556fca89d11bb83a37864050f75c716c9589e05c88d4e67
         | 
| 7 | 
            +
              data.tar.gz: fe1dee534ac7e91a7ad925ba6845acc49e9ca897763486973589d5f8997a6b78771ae1155f20ef8bf42d982a3fe4f37255a33a8a1499223cec77fa3b7319b935
         | 
    
        data/README.md
    CHANGED
    
    | @@ -476,6 +476,27 @@ Also, a special variable for better LS_COLOR setup: | |
| 476 476 | 
             
            ```
         | 
| 477 477 | 
             
            Point `@lscolors` to a file that sets your LS_COLORS variable. Use [my extended LS_COLORS setup](https://github.com/isene/LS_COLORS) to make this really fancy.
         | 
| 478 478 |  | 
| 479 | 
            +
            ### Directory Colors in Prompt
         | 
| 480 | 
            +
             | 
| 481 | 
            +
            **rsh is fully LS_COLORS compliant** - both tab completion and prompt paths use LS_COLORS for consistent theming.
         | 
| 482 | 
            +
             | 
| 483 | 
            +
            You can override directory colors in the prompt using pattern matching (like RTFM's @topmatch):
         | 
| 484 | 
            +
            ```ruby
         | 
| 485 | 
            +
            @dir_colors = [
         | 
| 486 | 
            +
              ["PassionFruit", 171],  # Paths containing "PassionFruit" -> magenta
         | 
| 487 | 
            +
              ["Dualog", 72],         # Paths containing "Dualog" -> cyan
         | 
| 488 | 
            +
              ["/G", 172],            # Paths containing "/G" -> orange
         | 
| 489 | 
            +
            ]
         | 
| 490 | 
            +
            ```
         | 
| 491 | 
            +
             | 
| 492 | 
            +
            How it works:
         | 
| 493 | 
            +
            - Array of `[pattern, color]` pairs
         | 
| 494 | 
            +
            - First matching pattern wins (uses Ruby's `include?` method)
         | 
| 495 | 
            +
            - If no pattern matches, uses LS_COLORS 'di' value (your configured directory color)
         | 
| 496 | 
            +
            - Pattern matching is simple substring matching: "/G" matches "/home/user/Main/G/..."
         | 
| 497 | 
            +
             | 
| 498 | 
            +
            This lets you visually distinguish different project directories at a glance in your prompt.
         | 
| 499 | 
            +
             | 
| 479 500 | 
             
            You can add any Ruby code to your .rshrc.
         | 
| 480 501 |  | 
| 481 502 | 
             
            # Enter the world of Ruby
         | 
    
        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.4. | 
| 11 | 
            +
            @version    = "3.4.5" # Dynamic directory colors in prompt via LS_COLORS integration
         | 
| 12 12 |  | 
| 13 13 | 
             
            # MODULES, CLASSES AND EXTENSIONS
         | 
| 14 14 | 
             
            class String # Add coloring to strings (with escaping for Readline)
         | 
| @@ -1233,14 +1233,19 @@ end | |
| 1233 1233 | 
             
            def get_file_color(filename) # Get color for a file based on LS_COLORS
         | 
| 1234 1234 | 
             
              return 7 unless @ls_colors  # Default to white if not initialized
         | 
| 1235 1235 |  | 
| 1236 | 
            +
              # Remove quotes and trailing slash for checking
         | 
| 1237 | 
            +
              clean_name = filename.gsub(/['"]/, '').chomp('/')
         | 
| 1238 | 
            +
             | 
| 1239 | 
            +
              # Check if it's a symlink (before directory check!)
         | 
| 1240 | 
            +
              if File.symlink?(clean_name)
         | 
| 1241 | 
            +
                return @ls_colors['ln'] || 14  # Symlinks get special color
         | 
| 1242 | 
            +
              end
         | 
| 1243 | 
            +
             | 
| 1236 1244 | 
             
              # Check if it's a directory
         | 
| 1237 1245 | 
             
              if filename.end_with?('/')
         | 
| 1238 1246 | 
             
                return @ls_colors['di'] || 33
         | 
| 1239 1247 | 
             
              end
         | 
| 1240 1248 |  | 
| 1241 | 
            -
              # Remove quotes and trailing slash for checking
         | 
| 1242 | 
            -
              clean_name = filename.gsub(/['"]/, '').chomp('/')
         | 
| 1243 | 
            -
             | 
| 1244 1249 | 
             
              # Check if file exists and is executable
         | 
| 1245 1250 | 
             
              if File.exist?(clean_name) && File.executable?(clean_name) && !File.directory?(clean_name)
         | 
| 1246 1251 | 
             
                return @ls_colors['ex'] || 2
         | 
| @@ -1257,6 +1262,18 @@ def get_file_color(filename) # Get color for a file based on LS_COLORS | |
| 1257 1262 | 
             
              file_color = 7 if file_color == 0  # 0 means "reset to default" in LS_COLORS
         | 
| 1258 1263 | 
             
              file_color
         | 
| 1259 1264 | 
             
            end
         | 
| 1265 | 
            +
            def get_dir_color(path) # Get color for directory path in prompt
         | 
| 1266 | 
            +
              # Check pattern matches first (like RTFM's @topmatch)
         | 
| 1267 | 
            +
              # Set @dir_colors in .rshrc as array of [pattern, color] pairs
         | 
| 1268 | 
            +
              # Example: @dir_colors = [["PassionFruit", 171], ["Dualog", 72], ["/G", 172], ["", 33]]
         | 
| 1269 | 
            +
              if @dir_colors && @dir_colors.is_a?(Array)
         | 
| 1270 | 
            +
                match = @dir_colors.find { |pattern, _| pattern.empty? || path.include?(pattern) }
         | 
| 1271 | 
            +
                return match.last if match
         | 
| 1272 | 
            +
              end
         | 
| 1273 | 
            +
             | 
| 1274 | 
            +
              # Fall back to LS_COLORS directory color
         | 
| 1275 | 
            +
              @ls_colors['di'] || 33
         | 
| 1276 | 
            +
            end
         | 
| 1260 1277 | 
             
            def format_tab_item(item, show_metadata: false) # Format tab item with color and optional metadata
         | 
| 1261 1278 | 
             
              color = get_file_color(item)
         | 
| 1262 1279 | 
             
              formatted = item
         | 
| @@ -1330,7 +1347,12 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic | |
| 1330 1347 | 
             
                  # Color bookmarks (after commands and nicks)
         | 
| 1331 1348 | 
             
                  el.c(@c_bookmark)
         | 
| 1332 1349 | 
             
                elsif File.exist?(clean_el)
         | 
| 1333 | 
            -
                   | 
| 1350 | 
            +
                  # Use directory color matching for directories, @c_path for files
         | 
| 1351 | 
            +
                  if File.directory?(clean_el)
         | 
| 1352 | 
            +
                    el.c(get_dir_color(File.expand_path(clean_el)))
         | 
| 1353 | 
            +
                  else
         | 
| 1354 | 
            +
                    el.c(@c_path)
         | 
| 1355 | 
            +
                  end
         | 
| 1334 1356 | 
             
                elsif el[0] == "-"
         | 
| 1335 1357 | 
             
                  el.c(@c_switch)
         | 
| 1336 1358 | 
             
                else
         | 
| @@ -1338,21 +1360,19 @@ def cmd_check(str) # Check if each element on the readline matches commands, nic | |
| 1338 1360 | 
             
                end
         | 
| 1339 1361 | 
             
              end
         | 
| 1340 1362 | 
             
            end
         | 
| 1341 | 
            -
            def rshrc # Write  | 
| 1342 | 
            -
              hist_clean
         | 
| 1363 | 
            +
            def rshrc # Write user configuration to .rshrc (portable between machines)
         | 
| 1364 | 
            +
              hist_clean  # Clean history before saving
         | 
| 1343 1365 | 
             
              if File.exist?(Dir.home+'/.rshrc')
         | 
| 1344 1366 | 
             
                conf = File.read(Dir.home+'/.rshrc')
         | 
| 1345 1367 | 
             
              else
         | 
| 1346 1368 | 
             
                conf = ""
         | 
| 1347 1369 | 
             
              end
         | 
| 1370 | 
            +
             | 
| 1371 | 
            +
              # Only update user-editable items in .rshrc
         | 
| 1348 1372 | 
             
              conf.sub!(/^@nick.*(\n|$)/, "")
         | 
| 1349 1373 | 
             
              conf += "@nick = #{@nick}\n"
         | 
| 1350 1374 | 
             
              conf.sub!(/^@gnick.*(\n|$)/, "")
         | 
| 1351 1375 | 
             
              conf += "@gnick = #{@gnick}\n"
         | 
| 1352 | 
            -
              conf.sub!(/^@cmd_frequency.*(\n|$)/, "")
         | 
| 1353 | 
            -
              conf += "@cmd_frequency = #{@cmd_frequency}\n"
         | 
| 1354 | 
            -
              conf.sub!(/^@cmd_stats.*(\n|$)/, "")
         | 
| 1355 | 
            -
              conf += "@cmd_stats = #{@cmd_stats}\n" unless @cmd_stats.empty?
         | 
| 1356 1376 | 
             
              conf.sub!(/^@bookmarks.*(\n|$)/, "")
         | 
| 1357 1377 | 
             
              conf += "@bookmarks = #{@bookmarks}\n" unless @bookmarks.empty?
         | 
| 1358 1378 | 
             
              conf.sub!(/^@defuns.*(\n|$)/, "")
         | 
| @@ -1365,40 +1385,71 @@ def rshrc # Write updates to .rshrc | |
| 1365 1385 | 
             
              conf += "@auto_correct = #{@auto_correct}\n" if @auto_correct
         | 
| 1366 1386 | 
             
              conf.sub!(/^@slow_command_threshold.*(\n|$)/, "")
         | 
| 1367 1387 | 
             
              conf += "@slow_command_threshold = #{@slow_command_threshold}\n" if @slow_command_threshold && @slow_command_threshold > 0
         | 
| 1388 | 
            +
              conf.sub!(/^@completion_learning.*(\n|$)/, "")
         | 
| 1389 | 
            +
              conf += "@completion_learning = #{@completion_learning}\n" unless @completion_learning
         | 
| 1390 | 
            +
              conf.sub!(/^@completion_show_metadata.*(\n|$)/, "")
         | 
| 1391 | 
            +
              conf += "@completion_show_metadata = #{@completion_show_metadata}\n" if @completion_show_metadata
         | 
| 1368 1392 | 
             
              conf.sub!(/^@plugin_disabled.*(\n|$)/, "")
         | 
| 1369 1393 | 
             
              conf += "@plugin_disabled = #{@plugin_disabled}\n" unless @plugin_disabled.empty?
         | 
| 1370 1394 | 
             
              conf.sub!(/^@validation_rules.*(\n|$)/, "")
         | 
| 1371 1395 | 
             
              conf += "@validation_rules = #{@validation_rules}\n" unless @validation_rules.empty?
         | 
| 1372 | 
            -
             | 
| 1373 | 
            -
              conf | 
| 1374 | 
            -
               | 
| 1375 | 
            -
             | 
| 1376 | 
            -
             | 
| 1377 | 
            -
               | 
| 1396 | 
            +
             | 
| 1397 | 
            +
              File.write(Dir.home+'/.rshrc', conf)
         | 
| 1398 | 
            +
              rshstate  # Also save runtime state
         | 
| 1399 | 
            +
            end
         | 
| 1400 | 
            +
            def rshstate # Write runtime state to .rshstate (auto-managed, machine-specific)
         | 
| 1401 | 
            +
              state = ""
         | 
| 1402 | 
            +
             | 
| 1403 | 
            +
              # Runtime data that changes frequently
         | 
| 1404 | 
            +
              state += "@cmd_frequency = #{@cmd_frequency}\n" unless @cmd_frequency.empty?
         | 
| 1405 | 
            +
              state += "@cmd_stats = #{@cmd_stats}\n" unless @cmd_stats.empty?
         | 
| 1406 | 
            +
              state += "@completion_weights = #{@completion_weights}\n" unless @completion_weights.empty?
         | 
| 1407 | 
            +
              state += "@recordings = #{@recordings}\n" unless @recordings.empty?
         | 
| 1408 | 
            +
             | 
| 1378 1409 | 
             
              # Persist executable cache for faster startup
         | 
| 1379 1410 | 
             
              if @exe && @exe.length > 100
         | 
| 1380 | 
            -
                 | 
| 1381 | 
            -
                 | 
| 1382 | 
            -
                 | 
| 1383 | 
            -
             | 
| 1384 | 
            -
             | 
| 1385 | 
            -
                conf += "@exe_cache_time = #{Time.now.to_i}\n"
         | 
| 1386 | 
            -
              end
         | 
| 1387 | 
            -
              # Only write @cmd_completions if user has customized it
         | 
| 1388 | 
            -
              unless conf =~ /^@cmd_completions\s*=/
         | 
| 1389 | 
            -
                # Don't write default completions to avoid cluttering .rshrc
         | 
| 1390 | 
            -
              end
         | 
| 1391 | 
            -
              conf.sub!(/^@history.*(\n|$)/, "")
         | 
| 1411 | 
            +
                state += "@exe_cache = #{@exe.inspect}\n"
         | 
| 1412 | 
            +
                state += "@exe_cache_path = #{ENV['PATH'].inspect}\n"
         | 
| 1413 | 
            +
                state += "@exe_cache_time = #{Time.now.to_i}\n"
         | 
| 1414 | 
            +
              end
         | 
| 1415 | 
            +
             | 
| 1392 1416 | 
             
              # Ensure history is properly formatted as valid Ruby array
         | 
| 1393 1417 | 
             
              begin
         | 
| 1394 1418 | 
             
                history_str = @history.last(@histsize).inspect
         | 
| 1395 | 
            -
                 | 
| 1419 | 
            +
                state += "@history = #{history_str}\n"
         | 
| 1396 1420 | 
             
              rescue => e
         | 
| 1397 | 
            -
                 | 
| 1398 | 
            -
                puts "Warning: Error saving history: #{e.message}"
         | 
| 1421 | 
            +
                state += "@history = []\n"
         | 
| 1422 | 
            +
                puts "Warning: Error saving history: #{e.message}" if ENV['RSH_DEBUG']
         | 
| 1399 1423 | 
             
              end
         | 
| 1424 | 
            +
             | 
| 1425 | 
            +
              File.write(Dir.home+'/.rshstate', state)
         | 
| 1426 | 
            +
            end
         | 
| 1427 | 
            +
            def migrate_to_split_config # Migrate from old single .rshrc to split .rshrc + .rshstate
         | 
| 1428 | 
            +
              return if File.exist?(Dir.home+'/.rshstate')  # Already migrated
         | 
| 1429 | 
            +
              return unless File.exist?(Dir.home+'/.rshrc')  # Nothing to migrate
         | 
| 1430 | 
            +
             | 
| 1431 | 
            +
              puts "\nMigrating to split configuration (.rshrc + .rshstate)..."
         | 
| 1432 | 
            +
             | 
| 1433 | 
            +
              # Runtime data will already be loaded from .rshrc by load_rshrc_safe
         | 
| 1434 | 
            +
              # Just need to save it to .rshstate and clean .rshrc
         | 
| 1435 | 
            +
             | 
| 1436 | 
            +
              # Create .rshstate with current runtime data
         | 
| 1437 | 
            +
              rshstate
         | 
| 1438 | 
            +
             | 
| 1439 | 
            +
              # Clean runtime data from .rshrc
         | 
| 1440 | 
            +
              conf = File.read(Dir.home+'/.rshrc')
         | 
| 1441 | 
            +
              conf.sub!(/^@cmd_frequency.*(\n|$)/, "")
         | 
| 1442 | 
            +
              conf.sub!(/^@cmd_stats.*(\n|$)/, "")
         | 
| 1443 | 
            +
              conf.sub!(/^@exe_cache.*(\n|$)/, "")
         | 
| 1444 | 
            +
              conf.sub!(/^@exe_cache_path.*(\n|$)/, "")
         | 
| 1445 | 
            +
              conf.sub!(/^@exe_cache_time.*(\n|$)/, "")
         | 
| 1446 | 
            +
              conf.sub!(/^@completion_weights.*(\n|$)/, "")
         | 
| 1447 | 
            +
              conf.sub!(/^@history =.*(\n|$)/, "")
         | 
| 1400 1448 | 
             
              File.write(Dir.home+'/.rshrc', conf)
         | 
| 1401 | 
            -
             | 
| 1449 | 
            +
             | 
| 1450 | 
            +
              puts "Migration complete!"
         | 
| 1451 | 
            +
              puts "  .rshrc     -> User config (portable)"
         | 
| 1452 | 
            +
              puts "  .rshstate  -> Runtime data (auto-managed)\n"
         | 
| 1402 1453 | 
             
            end
         | 
| 1403 1454 |  | 
| 1404 1455 | 
             
            # RSH FUNCTIONS
         | 
| @@ -2973,6 +3024,15 @@ def load_rshrc_safe | |
| 2973 3024 | 
             
                  end
         | 
| 2974 3025 | 
             
                end
         | 
| 2975 3026 |  | 
| 3027 | 
            +
                # Load runtime state from .rshstate (separate file)
         | 
| 3028 | 
            +
                if File.exist?(Dir.home+'/.rshstate')
         | 
| 3029 | 
            +
                  begin
         | 
| 3030 | 
            +
                    load(Dir.home+'/.rshstate')
         | 
| 3031 | 
            +
                  rescue => e
         | 
| 3032 | 
            +
                    puts "Warning: Could not load .rshstate: #{e.message}" if ENV['RSH_DEBUG']
         | 
| 3033 | 
            +
                  end
         | 
| 3034 | 
            +
                end
         | 
| 3035 | 
            +
             | 
| 2976 3036 | 
             
              rescue SyntaxError => e
         | 
| 2977 3037 | 
             
                puts "\n\033[31mERROR: Syntax error in .rshrc:\033[0m"
         | 
| 2978 3038 | 
             
                puts e.message
         | 
| @@ -3168,7 +3228,10 @@ begin # Load .rshrc and populate @history | |
| 3168 3228 | 
             
                exit
         | 
| 3169 3229 | 
             
              end
         | 
| 3170 3230 | 
             
              firstrun unless File.exist?(Dir.home+'/.rshrc') # Initial loading - to get history
         | 
| 3231 | 
            +
              # Initialize @ls_colors with defaults so get_dir_color() works in .rshrc prompt
         | 
| 3232 | 
            +
              @ls_colors = { 'di' => 33, 'ex' => 2, 'ln' => 14, 'fi' => 7 }
         | 
| 3171 3233 | 
             
              load_rshrc_safe
         | 
| 3234 | 
            +
              migrate_to_split_config  # Migrate from old format if needed (v3.4.3+)
         | 
| 3172 3235 | 
             
              # Load login shell files if rsh is running as login shell
         | 
| 3173 3236 | 
             
              if ENV['LOGIN_SHELL'] or $0 == "-rsh" or ARGV.include?('-l') or ARGV.include?('--login')
         | 
| 3174 3237 | 
             
                ['/etc/profile', Dir.home+'/.profile', Dir.home+'/.bash_profile', Dir.home+'/.bashrc'].each do |f|
         | 
| @@ -3191,6 +3254,7 @@ begin # Load .rshrc and populate @history | |
| 3191 3254 | 
             
              ENV["TERM"]  = "rxvt-unicode-256color"
         | 
| 3192 3255 | 
             
              ENV["PATH"]  ? ENV["PATH"] += ":" : ENV["PATH"] = ""
         | 
| 3193 3256 | 
             
              ENV["PATH"] += "/home/#{@user}/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
         | 
| 3257 | 
            +
              # Load actual LS_COLORS from file (now that @lscolors is set from .rshrc)
         | 
| 3194 3258 | 
             
              if @lscolors and File.exist?(@lscolors)
         | 
| 3195 3259 | 
             
                ls = File.read(@lscolors)
         | 
| 3196 3260 | 
             
                ls.sub!(/export.*/, '')
         | 
| @@ -3212,8 +3276,8 @@ end | |
| 3212 3276 | 
             
            # MAIN PART
         | 
| 3213 3277 | 
             
            loop do 
         | 
| 3214 3278 | 
             
              begin
         | 
| 3215 | 
            -
                @user  | 
| 3216 | 
            -
                @node  | 
| 3279 | 
            +
                @user ||= Etc.getpwuid(Process.euid).name # Cached for performance
         | 
| 3280 | 
            +
                @node ||= Etc.uname[:nodename]            # Cached for performance
         | 
| 3217 3281 | 
             
                # Only reload .rshrc if directory changed (optimization)
         | 
| 3218 3282 | 
             
                current_dir = Dir.pwd
         | 
| 3219 3283 | 
             
                if @last_prompt_dir != current_dir
         | 
| @@ -3221,7 +3285,7 @@ loop do | |
| 3221 3285 | 
             
                  @last_prompt_dir = current_dir
         | 
| 3222 3286 | 
             
                end
         | 
| 3223 3287 | 
             
                @prompt.gsub!(/#{Dir.home}/, '~') # Simplify path in prompt
         | 
| 3224 | 
            -
                 | 
| 3288 | 
            +
                print "\033]0;rsh: #{current_dir}\007"   # Set window title (no spawn)
         | 
| 3225 3289 | 
             
                @history[0] = "" unless @history[0]
         | 
| 3226 3290 | 
             
                cache_executables  # Use cached executable lookup
         | 
| 3227 3291 | 
             
                # Load plugins on first command (lazy loading)
         | 
    
        metadata
    CHANGED
    
    | @@ -1,21 +1,21 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: ruby-shell
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 3.4. | 
| 4 | 
            +
              version: 3.4.5
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Geir Isene
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2025-10- | 
| 11 | 
            +
            date: 2025-10-26 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.4. | 
| 16 | 
            -
               | 
| 17 | 
            -
               | 
| 18 | 
            -
               | 
| 15 | 
            +
              v3.4.5: FULL LS_COLORS COMPLIANCE - Prompt and command line now use LS_COLORS with
         | 
| 16 | 
            +
              pattern-based directory coloring. Configure @dir_colors like RTFM''s @topmatch for
         | 
| 17 | 
            +
              visual project distinction. Plus v3.4.0: Completion learning, context-aware ranking,
         | 
| 18 | 
            +
              persistent patterns across sessions!'
         | 
| 19 19 | 
             
            email: g@isene.com
         | 
| 20 20 | 
             
            executables:
         | 
| 21 21 | 
             
            - rsh
         |