srsh 0.7.1 → 0.8.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.
data/bin/srsh DELETED
@@ -1,1491 +0,0 @@
1
- #!/usr/bin/env ruby
2
- require 'shellwords'
3
- require 'socket'
4
- require 'time'
5
- require 'etc'
6
- require 'rbconfig'
7
- require 'io/console'
8
-
9
- # ---------------- Version ----------------
10
- SRSH_VERSION = "0.7.1"
11
-
12
- $0 = "srsh-#{SRSH_VERSION}"
13
- ENV['SHELL'] = "srsh-#{SRSH_VERSION}"
14
- print "\033]0;srsh-#{SRSH_VERSION}\007"
15
-
16
- Dir.chdir(ENV['HOME']) if ENV['HOME']
17
-
18
- # ---------------- Globals ----------------
19
- $child_pids = []
20
- $aliases = {}
21
- $last_render_rows = 0
22
-
23
- $last_status = 0
24
- $rsh_functions = {}
25
- $rsh_positional = {}
26
- $rsh_script_mode = false
27
-
28
- Signal.trap("INT", "IGNORE")
29
-
30
- # Control-flow exceptions for the scripting engine
31
- class RshBreak < StandardError; end
32
- class RshContinue < StandardError; end
33
- class RshReturn < StandardError; end
34
-
35
- # ---------------- History ----------------
36
- HISTORY_FILE = File.join(Dir.home, ".srsh_history")
37
- HISTORY = if File.exist?(HISTORY_FILE)
38
- File.readlines(HISTORY_FILE, chomp: true)
39
- else
40
- []
41
- end
42
-
43
- at_exit do
44
- begin
45
- File.open(HISTORY_FILE, "w") do |f|
46
- HISTORY.each { |line| f.puts line }
47
- end
48
- rescue
49
- end
50
- end
51
-
52
- # ---------------- RC file (create if missing) ----------------
53
- RC_FILE = File.join(Dir.home, ".srshrc")
54
- begin
55
- unless File.exist?(RC_FILE)
56
- File.write(RC_FILE, <<~RC)
57
- # ~/.srshrc — srsh configuration
58
- # This file was created automatically by srsh v#{SRSH_VERSION}.
59
- # You can keep personal notes or planned settings here.
60
- # (Currently not sourced by srsh runtime.)
61
- RC
62
- end
63
- rescue
64
- end
65
-
66
- # ---------------- Utilities ----------------
67
- def color(text, code)
68
- "\e[#{code}m#{text}\e[0m"
69
- end
70
-
71
- def random_color
72
- [31, 32, 33, 34, 35, 36, 37].sample
73
- end
74
-
75
- def rainbow_codes
76
- [31, 33, 32, 36, 34, 35, 91, 93, 92, 96, 94, 95]
77
- end
78
-
79
- def human_bytes(bytes)
80
- units = ['B', 'KB', 'MB', 'GB', 'TB']
81
- size = bytes.to_f
82
- unit = units.shift
83
- while size > 1024 && !units.empty?
84
- size /= 1024
85
- unit = units.shift
86
- end
87
- "#{format('%.2f', size)} #{unit}"
88
- end
89
-
90
- def nice_bar(p, w = 30, code = 32)
91
- p = [[p, 0.0].max, 1.0].min
92
- f = (p * w).round
93
- b = "█" * f + "░" * (w - f)
94
- pct = (p * 100).to_i
95
- "#{color("[#{b}]", code)} #{color(sprintf("%3d%%", pct), 37)}"
96
- end
97
-
98
- def terminal_width
99
- IO.console.winsize[1]
100
- rescue
101
- 80
102
- end
103
-
104
- def strip_ansi(str)
105
- str.to_s.gsub(/\e\[[0-9;]*m/, '')
106
- end
107
-
108
- # Simple $(...) command substitution (no nesting)
109
- def expand_command_substitutions(str)
110
- return "" if str.nil?
111
- s = str.to_s.dup
112
-
113
- s.gsub(/\$\(([^()]*)\)/) do
114
- inner = $1.to_s.strip
115
- next "" if inner.empty?
116
- begin
117
- out = `#{inner} 2>/dev/null`
118
- out.to_s.strip
119
- rescue
120
- ""
121
- end
122
- end
123
- end
124
-
125
- # variable expansion: $VAR, $1, $2, $0, $?
126
- def expand_vars(str)
127
- return "" if str.nil?
128
-
129
- # First handle $(...) substitution
130
- s = expand_command_substitutions(str.to_s)
131
-
132
- # $VARNAME from ENV
133
- s = s.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)/) do
134
- ENV[$1] || ""
135
- end
136
-
137
- # $1, $2, $0 from positional table
138
- s = s.gsub(/\$(\d+)/) do
139
- idx = $1.to_i
140
- ($rsh_positional && $rsh_positional[idx]) || ""
141
- end
142
-
143
- # $? -> last exit status
144
- s = s.gsub(/\$\?/) { $last_status.to_s }
145
-
146
- s
147
- end
148
-
149
- def parse_redirection(cmd)
150
- stdin_file = nil
151
- stdout_file = nil
152
- append = false
153
-
154
- if cmd =~ /(.*)>>\s*(\S+)/
155
- cmd = $1.strip
156
- stdout_file = $2.strip
157
- append = true
158
- elsif cmd =~ /(.*)>\s*(\S+)/
159
- cmd = $1.strip
160
- stdout_file = $2.strip
161
- end
162
-
163
- if cmd =~ /(.*)<\s*(\S+)/
164
- cmd = $1.strip
165
- stdin_file = $2.strip
166
- end
167
-
168
- [cmd, stdin_file, stdout_file, append]
169
- end
170
-
171
- # ---------------- Aliases ----------------
172
- def expand_aliases(cmd, seen = [])
173
- return cmd if cmd.nil? || cmd.strip.empty?
174
- first_word, rest = cmd.strip.split(' ', 2)
175
- return cmd if seen.include?(first_word)
176
- seen << first_word
177
-
178
- if $aliases.key?(first_word)
179
- replacement = $aliases[first_word]
180
- expanded = expand_aliases(replacement, seen)
181
- rest ? "#{expanded} #{rest}" : expanded
182
- else
183
- cmd
184
- end
185
- end
186
-
187
- # ---------------- System Info ----------------
188
- def current_time
189
- Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")
190
- end
191
-
192
- def detect_distro
193
- if File.exist?('/etc/os-release')
194
- line = File.read('/etc/os-release').lines.find { |l|
195
- l.start_with?('PRETTY_NAME="') || l.start_with?('PRETTY_NAME=')
196
- }
197
- return line.split('=').last.strip.delete('"') if line
198
- end
199
- "#{RbConfig::CONFIG['host_os']}"
200
- end
201
-
202
- def os_type
203
- host = RbConfig::CONFIG['host_os'].to_s
204
- case host
205
- when /linux/i
206
- :linux
207
- when /darwin/i
208
- :mac
209
- when /bsd/i
210
- :bsd
211
- else
212
- :other
213
- end
214
- end
215
-
216
- # ---------------- Quotes ----------------
217
- QUOTES = [
218
- "Keep calm and code on.",
219
- "Did you try turning it off and on again?",
220
- "There’s no place like 127.0.0.1.",
221
- "To iterate is human, to recurse divine.",
222
- "sudo rm -rf / – Just kidding, don’t do that!",
223
- "The shell is mightier than the sword.",
224
- "A journey of a thousand commits begins with a single push.",
225
- "In case of fire: git commit, git push, leave building.",
226
- "Debugging is like being the detective in a crime movie where you are also the murderer.",
227
- "Unix is user-friendly. It's just selective about who its friends are.",
228
- "Old sysadmins never die, they just become daemons.",
229
- "Listen you flatpaker! – Totally Terry Davis",
230
- "How is #{detect_distro}? 🤔",
231
- "Life is short, but your command history is eternal.",
232
- "If at first you don’t succeed, git commit and push anyway.",
233
- "rm -rf: the ultimate trust exercise.",
234
- "Coding is like magic, but with more coffee.",
235
- "There’s no bug, only undocumented features.",
236
- "Keep your friends close and your aliases closer.",
237
- "Why wait for the future when you can Ctrl+Z it?",
238
- "A watched process never completes.",
239
- "When in doubt, make it a function.",
240
- "Some call it procrastination, we call it debugging curiosity.",
241
- "Life is like a terminal; some commands just don’t execute.",
242
- "Good code is like a good joke; it needs no explanation.",
243
- "sudo: because sometimes responsibility is overrated.",
244
- "Pipes make the world go round.",
245
- "In bash we trust, in Ruby we wonder.",
246
- "A system without errors is like a day without coffee.",
247
- "Keep your loops tight and your sleeps short.",
248
- "Stack traces are just life giving you directions.",
249
- "Your mom called, she wants her semicolons back."
250
- ]
251
-
252
- $current_quote = QUOTES.sample
253
-
254
- def dynamic_quote
255
- chars = $current_quote.chars
256
- rainbow = rainbow_codes.cycle
257
- chars.map { |c| color(c, rainbow.next) }.join
258
- end
259
-
260
- # ---------------- CPU / RAM / Storage ----------------
261
- def read_cpu_times
262
- return [] unless File.exist?('/proc/stat')
263
- cpu_line = File.readlines('/proc/stat').find { |line| line.start_with?('cpu ') }
264
- return [] unless cpu_line
265
- cpu_line.split[1..-1].map(&:to_i)
266
- end
267
-
268
- def calculate_cpu_usage(prev, current)
269
- return 0.0 if prev.empty? || current.empty?
270
- prev_idle = prev[3] + (prev[4] || 0)
271
- idle = current[3] + (current[4] || 0)
272
- prev_non_idle = prev[0] + prev[1] + prev[2] +
273
- (prev[5] || 0) + (prev[6] || 0) + (prev[7] || 0)
274
- non_idle = current[0] + current[1] + current[2] +
275
- (current[5] || 0) + (current[6] || 0) + (current[7] || 0)
276
- prev_total = prev_idle + prev_non_idle
277
- total = idle + non_idle
278
- totald = total - prev_total
279
- idled = idle - prev_idle
280
- return 0.0 if totald <= 0
281
- ((totald - idled).to_f / totald) * 100
282
- end
283
-
284
- def cpu_cores_and_freq
285
- return [0, []] unless File.exist?('/proc/cpuinfo')
286
- cores = 0
287
- freqs = []
288
- File.foreach('/proc/cpuinfo') do |line|
289
- cores += 1 if line =~ /^processor\s*:\s*\d+/
290
- if line =~ /^cpu MHz\s*:\s*([\d.]+)/
291
- freqs << $1.to_f
292
- end
293
- end
294
- [cores, freqs.first(cores)]
295
- end
296
-
297
- def cpu_info
298
- usage = 0.0
299
- cores = 0
300
- freq_display = "N/A"
301
-
302
- case os_type
303
- when :linux
304
- prev = read_cpu_times
305
- sleep 0.05
306
- current = read_cpu_times
307
- usage = calculate_cpu_usage(prev, current).round(1)
308
- cores, freqs = cpu_cores_and_freq
309
- freq_display = freqs.empty? ? "N/A" : freqs.map { |f| "#{f.round(0)}MHz" }.join(', ')
310
- else
311
- cores = begin
312
- `sysctl -n hw.ncpu 2>/dev/null`.to_i
313
- rescue
314
- 0
315
- end
316
-
317
- raw_freq_hz = begin
318
- `sysctl -n hw.cpufrequency 2>/dev/null`.to_i
319
- rescue
320
- 0
321
- end
322
-
323
- freq_display =
324
- if raw_freq_hz > 0
325
- mhz = (raw_freq_hz.to_f / 1_000_000.0).round(0)
326
- "#{mhz.to_i}MHz"
327
- else
328
- "N/A"
329
- end
330
-
331
- usage = begin
332
- ps_output = `ps -A -o %cpu 2>/dev/null`
333
- lines = ps_output.lines
334
- values = lines[1..-1] || []
335
- sum = values.map { |l| l.to_f }.inject(0.0, :+)
336
- if cores > 0
337
- (sum / cores).round(1)
338
- else
339
- sum.round(1)
340
- end
341
- rescue
342
- 0.0
343
- end
344
- end
345
-
346
- "#{color("CPU Usage:",36)} #{color("#{usage}%",33)} | " \
347
- "#{color("Cores:",36)} #{color(cores.to_s,32)} | " \
348
- "#{color("Freqs:",36)} #{color(freq_display,35)}"
349
- end
350
-
351
- def ram_info
352
- case os_type
353
- when :linux
354
- if File.exist?('/proc/meminfo')
355
- meminfo = {}
356
- File.read('/proc/meminfo').each_line do |line|
357
- key, val = line.split(':')
358
- meminfo[key.strip] = val.strip.split.first.to_i * 1024 if key && val
359
- end
360
- total = meminfo['MemTotal'] || 0
361
- free = (meminfo['MemFree'] || 0) + (meminfo['Buffers'] || 0) + (meminfo['Cached'] || 0)
362
- used = total - free
363
- "#{color("RAM Usage:",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
364
- else
365
- "#{color("RAM Usage:",36)} Info not available"
366
- end
367
- else
368
- begin
369
- if os_type == :mac
370
- total = `sysctl -n hw.memsize 2>/dev/null`.to_i
371
- return "#{color("RAM Usage:",36)} Info not available" if total <= 0
372
-
373
- vm = `vm_stat 2>/dev/null`
374
- page_size = vm[/page size of (\d+) bytes/, 1].to_i
375
- page_size = 4096 if page_size <= 0
376
-
377
- stats = {}
378
- vm.each_line do |line|
379
- if line =~ /^(.+):\s+(\d+)\./
380
- stats[$1] = $2.to_i
381
- end
382
- end
383
-
384
- used_pages = 0
385
- %w[Pages active Pages wired down Pages occupied by compressor].each do |k|
386
- used_pages += stats[k].to_i
387
- end
388
- used = used_pages * page_size
389
-
390
- "#{color("RAM Usage:",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
391
- else
392
- total = `sysctl -n hw.physmem 2>/dev/null`.to_i
393
- total = `sysctl -n hw.realmem 2>/dev/null`.to_i if total <= 0
394
- return "#{color("RAM Usage:",36)} Info not available" if total <= 0
395
- "#{color("RAM Usage:",36)} #{color("Unknown",33)} / #{color(human_bytes(total),32)}"
396
- end
397
- rescue
398
- "#{color("RAM Usage:",36)} Info not available"
399
- end
400
- end
401
- end
402
-
403
- def storage_info
404
- begin
405
- require 'sys/filesystem'
406
- stat = Sys::Filesystem.stat(Dir.pwd)
407
- total = stat.bytes_total
408
- free = stat.bytes_available
409
- used = total - free
410
- "#{color("Storage Usage (#{Dir.pwd}):",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
411
- rescue LoadError
412
- "#{color("Install 'sys-filesystem' gem for storage info:",31)} #{color('gem install sys-filesystem',33)}"
413
- rescue
414
- "#{color("Storage Usage:",36)} Info not available"
415
- end
416
- end
417
-
418
- # ---------------- Builtin helpers ----------------
419
- def builtin_help
420
- puts color('=' * 60, "1;35")
421
- puts color("srsh #{SRSH_VERSION} - Builtin Commands", "1;33")
422
- puts color(sprintf("%-15s%-45s", "Command", "Description"), "1;36")
423
- puts color('-' * 60, "1;34")
424
- puts color(sprintf("%-15s", "cd"), "1;36") + "Change directory"
425
- puts color(sprintf("%-15s", "pwd"), "1;36") + "Print working directory"
426
- puts color(sprintf("%-15s", "exit / quit"), "1;36") + "Exit the shell"
427
- puts color(sprintf("%-15s", "alias"), "1;36") + "Create or list aliases"
428
- puts color(sprintf("%-15s", "unalias"), "1;36") + "Remove alias"
429
- puts color(sprintf("%-15s", "jobs"), "1;36") + "Show background jobs (tracked pids)"
430
- puts color(sprintf("%-15s", "systemfetch"), "1;36") + "Display system information"
431
- puts color(sprintf("%-15s", "hist"), "1;36") + "Show shell history"
432
- puts color(sprintf("%-15s", "clearhist"), "1;36") + "Clear saved history (memory + file)"
433
- puts color(sprintf("%-15s", "put"), "1;36") + "Print text (like echo)"
434
- puts color(sprintf("%-15s", "set"), "1;36") + "Set or list variables"
435
- puts color(sprintf("%-15s", "unset"), "1;36") + "Unset a variable"
436
- puts color(sprintf("%-15s", "read"), "1;36") + "Read a line into a variable"
437
- puts color(sprintf("%-15s", "sleep"), "1;36") + "Sleep for N seconds"
438
- puts color(sprintf("%-15s", "true / false"), "1;36")+ "Always succeed / fail"
439
- puts color(sprintf("%-15s", "source / ."), "1;36") + "Run another rsh script"
440
- puts color(sprintf("%-15s", "break / continue"), "1;36") + "Loop control (in scripts)"
441
- puts color(sprintf("%-15s", "return"), "1;36") + "Return from a function"
442
- puts color(sprintf("%-15s", "help"), "1;36") + "Show this help message"
443
- puts color('=' * 60, "1;35")
444
- end
445
-
446
- def builtin_systemfetch
447
- user = ENV['USER'] || Etc.getlogin || Etc.getpwuid.name rescue ENV['USER'] || Etc.getlogin
448
- host = Socket.gethostname
449
- os = detect_distro
450
- ruby_ver = RUBY_VERSION
451
-
452
- cpu_percent = begin
453
- case os_type
454
- when :linux
455
- prev = read_cpu_times
456
- sleep 0.05
457
- cur = read_cpu_times
458
- calculate_cpu_usage(prev, cur).round(1)
459
- else
460
- cores = `sysctl -n hw.ncpu 2>/dev/null`.to_i rescue 0
461
- ps_output = `ps -A -o %cpu 2>/dev/null`
462
- lines = ps_output.lines
463
- values = lines[1..-1] || []
464
- sum = values.map { |l| l.to_f }.inject(0.0, :+)
465
- if cores > 0
466
- (sum / cores).round(1)
467
- else
468
- sum.round(1)
469
- end
470
- end
471
- rescue
472
- 0.0
473
- end
474
-
475
- mem_percent = begin
476
- case os_type
477
- when :linux
478
- if File.exist?('/proc/meminfo')
479
- meminfo = {}
480
- File.read('/proc/meminfo').each_line do |line|
481
- k, v = line.split(':')
482
- meminfo[k.strip] = v.strip.split.first.to_i * 1024 if k && v
483
- end
484
- total = meminfo['MemTotal'] || 1
485
- free = (meminfo['MemAvailable'] || meminfo['MemFree'] || 0)
486
- used = total - free
487
- (used.to_f / total.to_f * 100).round(1)
488
- else
489
- 0.0
490
- end
491
- when :mac
492
- total = `sysctl -n hw.memsize 2>/dev/null`.to_i
493
- if total <= 0
494
- 0.0
495
- else
496
- vm = `vm_stat 2>/dev/null`
497
- page_size = vm[/page size of (\d+) bytes/, 1].to_i
498
- page_size = 4096 if page_size <= 0
499
-
500
- stats = {}
501
- vm.each_line do |line|
502
- if line =~ /^(.+):\s+(\d+)\./
503
- stats[$1] = $2.to_i
504
- end
505
- end
506
-
507
- used_pages = 0
508
- %w[Pages active Pages wired down Pages occupied by compressor].each do |k|
509
- used_pages += stats[k].to_i
510
- end
511
- used = used_pages * page_size
512
- ((used.to_f / total.to_f) * 100).round(1)
513
- end
514
- else
515
- 0.0
516
- end
517
- rescue
518
- 0.0
519
- end
520
-
521
- puts color('=' * 60, "1;35")
522
- puts color("srsh System Information", "1;33")
523
- puts color("User: ", "1;36") + color("#{user}@#{host}", "0;37")
524
- puts color("OS: ", "1;36") + color(os, "0;37")
525
- puts color("Shell: ", "1;36") + color("srsh v#{SRSH_VERSION}", "0;37")
526
- puts color("Ruby: ", "1;36") + color(ruby_ver, "0;37")
527
- puts color("CPU Usage: ", "1;36") + nice_bar(cpu_percent / 100.0, 30, 32)
528
- puts color("RAM Usage: ", "1;36") + nice_bar(mem_percent / 100.0, 30, 35)
529
- puts color('=' * 60, "1;35")
530
- end
531
-
532
- def builtin_jobs
533
- if $child_pids.empty?
534
- puts color("No tracked child jobs.", 36)
535
- return
536
- end
537
- $child_pids.each do |pid|
538
- status = begin
539
- Process.kill(0, pid)
540
- 'running'
541
- rescue Errno::ESRCH
542
- 'done'
543
- rescue Errno::EPERM
544
- 'running'
545
- end
546
- puts "[#{pid}] #{status}"
547
- end
548
- end
549
-
550
- def builtin_hist
551
- HISTORY.each_with_index do |h, i|
552
- printf "%5d %s\n", i + 1, h
553
- end
554
- end
555
-
556
- def builtin_clearhist
557
- HISTORY.clear
558
- if File.exist?(HISTORY_FILE)
559
- begin
560
- File.delete(HISTORY_FILE)
561
- rescue
562
- end
563
- end
564
- puts color("History cleared (memory + file).", 32)
565
- end
566
-
567
- # -------- Pretty column printer for colored text (used by ls) --------
568
- def print_columns_colored(labels)
569
- return if labels.nil? || labels.empty?
570
-
571
- width = terminal_width
572
- visible_lengths = labels.map { |s| strip_ansi(s).length }
573
- max_len = visible_lengths.max || 0
574
- col_width = [max_len + 2, 4].max
575
- cols = [width / col_width, 1].max
576
- rows = (labels.length.to_f / cols).ceil
577
-
578
- rows.times do |r|
579
- line = ""
580
- cols.times do |c|
581
- idx = c * rows + r
582
- break if idx >= labels.length
583
- label = labels[idx]
584
- visible = strip_ansi(label).length
585
- padding = col_width - visible
586
- line << label << (" " * padding)
587
- end
588
- STDOUT.print("\r")
589
- STDOUT.print(line.rstrip)
590
- STDOUT.print("\n")
591
- end
592
- end
593
-
594
- def builtin_ls(path = ".")
595
- begin
596
- entries = Dir.children(path).sort
597
- rescue => e
598
- puts color("ls: #{e.message}", 31)
599
- return
600
- end
601
-
602
- labels = entries.map do |name|
603
- full = File.join(path, name)
604
- begin
605
- if File.directory?(full)
606
- color("#{name}/", 36)
607
- elsif File.executable?(full)
608
- color("#{name}*", 32)
609
- else
610
- color(name, 37)
611
- end
612
- rescue
613
- name
614
- end
615
- end
616
-
617
- print_columns_colored(labels)
618
- end
619
-
620
- # ---------------- rsh scripting helpers ----------------
621
-
622
- # Evaluate rsh condition expressions, Ruby-style with $VARS
623
- def eval_rsh_expr(expr)
624
- return false if expr.nil? || expr.strip.empty?
625
- s = expr.to_s
626
-
627
- s = s.gsub(/\$([A-Za-z_][A-Za-z0-9_]*)/) do
628
- (ENV[$1] || "").inspect
629
- end
630
-
631
- s = s.gsub(/\$(\d+)/) do
632
- idx = $1.to_i
633
- val = ($rsh_positional && $rsh_positional[idx]) || ""
634
- val.inspect
635
- end
636
-
637
- begin
638
- !!eval(s)
639
- rescue
640
- false
641
- end
642
- end
643
-
644
- def rsh_find_if_bounds(lines, start_idx)
645
- depth = 1
646
- else_idx = nil
647
- i = start_idx + 1
648
- while i < lines.length
649
- line = strip_rsh_comment(lines[i].to_s).strip
650
- if line.start_with?("if ")
651
- depth += 1
652
- elsif line.start_with?("while ")
653
- depth += 1
654
- elsif line.start_with?("fn ")
655
- depth += 1
656
- elsif line == "end"
657
- depth -= 1
658
- return [else_idx, i] if depth == 0
659
- elsif line == "else" && depth == 1
660
- else_idx = i
661
- end
662
- i += 1
663
- end
664
- raise "Unmatched 'if' in rsh script"
665
- end
666
-
667
- def rsh_find_block_end(lines, start_idx)
668
- depth = 1
669
- i = start_idx + 1
670
- while i < lines.length
671
- line = strip_rsh_comment(lines[i].to_s).strip
672
- if line.start_with?("if ") || line.start_with?("while ") || line.start_with?("fn ")
673
- depth += 1
674
- elsif line == "end"
675
- depth -= 1
676
- return i if depth == 0
677
- end
678
- i += 1
679
- end
680
- raise "Unmatched block in rsh script"
681
- end
682
-
683
- def strip_rsh_comment(line)
684
- in_single = false
685
- in_double = false
686
- escaped = false
687
- i = 0
688
-
689
- while i < line.length
690
- ch = line[i]
691
- if escaped
692
- escaped = false
693
- elsif ch == '\\'
694
- escaped = true
695
- elsif ch == "'" && !in_double
696
- in_single = !in_single
697
- elsif ch == '"' && !in_single
698
- in_double = !in_double
699
- elsif ch == '#' && !in_single && !in_double
700
- return line[0...i]
701
- end
702
- i += 1
703
- end
704
-
705
- line
706
- end
707
-
708
- def run_rsh_block(lines, start_idx, end_idx)
709
- i = start_idx
710
- while i < end_idx
711
- raw = lines[i]
712
- i += 1
713
- next if raw.nil?
714
-
715
- line = strip_rsh_comment(raw).strip
716
- next if line.empty?
717
-
718
- if line.start_with?("if ")
719
- cond_expr = line[3..-1].strip
720
- else_idx, end_idx_2 = rsh_find_if_bounds(lines, i - 1)
721
- if eval_rsh_expr(cond_expr)
722
- body_end = else_idx || end_idx_2
723
- run_rsh_block(lines, i, body_end)
724
- elsif else_idx
725
- run_rsh_block(lines, else_idx + 1, end_idx_2)
726
- end
727
- i = end_idx_2 + 1
728
- next
729
-
730
- elsif line.start_with?("while ")
731
- cond_expr = line[6..-1].strip
732
- block_end = rsh_find_block_end(lines, i - 1)
733
- while eval_rsh_expr(cond_expr)
734
- begin
735
- run_rsh_block(lines, i, block_end)
736
- rescue RshBreak
737
- break
738
- rescue RshContinue
739
- next
740
- end
741
- end
742
- i = block_end + 1
743
- next
744
-
745
- elsif line.start_with?("fn ")
746
- parts = line.split
747
- name = parts[1]
748
- argnames = parts[2..-1] || []
749
- block_end = rsh_find_block_end(lines, i - 1)
750
- $rsh_functions[name] = {
751
- args: argnames,
752
- body: lines[i...block_end]
753
- }
754
- i = block_end + 1
755
- next
756
-
757
- else
758
- run_input_line(line)
759
- end
760
- end
761
- end
762
-
763
- def rsh_run_script(script_path, argv)
764
- $rsh_script_mode = true
765
- $rsh_positional = {}
766
- $rsh_positional[0] = File.basename(script_path)
767
- argv.each_with_index do |val, idx|
768
- $rsh_positional[idx + 1] = val
769
- end
770
-
771
- lines = File.readlines(script_path, chomp: true)
772
- if lines[0] && lines[0].start_with?("#!")
773
- lines = lines[1..-1] || []
774
- end
775
- run_rsh_block(lines, 0, lines.length)
776
- end
777
-
778
- def rsh_call_function(name, argv)
779
- fn = $rsh_functions[name]
780
- return unless fn
781
-
782
- saved_positional = $rsh_positional
783
- $rsh_positional = {}
784
- $rsh_positional[0] = name
785
-
786
- fn[:args].each_with_index do |argname, idx|
787
- val = argv[idx] || ""
788
- ENV[argname] = val
789
- $rsh_positional[idx + 1] = val
790
- end
791
-
792
- begin
793
- run_rsh_block(fn[:body], 0, fn[:body].length)
794
- rescue RshReturn
795
- # swallow return
796
- ensure
797
- $rsh_positional = saved_positional
798
- end
799
- end
800
-
801
- # ---------------- External Execution Helper ----------------
802
- def exec_external(args, stdin_file, stdout_file, append)
803
- command_path = args[0]
804
- if command_path && (command_path.include?('/') || command_path.start_with?('.'))
805
- begin
806
- if File.directory?(command_path)
807
- puts color("srsh: #{command_path}: is a directory", 31)
808
- return
809
- end
810
- rescue
811
- end
812
- end
813
-
814
- pid = fork do
815
- Signal.trap("INT","DEFAULT")
816
- if stdin_file
817
- begin
818
- STDIN.reopen(File.open(stdin_file,'r'))
819
- rescue
820
- end
821
- end
822
- if stdout_file
823
- begin
824
- STDOUT.reopen(File.open(stdout_file, append ? 'a' : 'w'))
825
- rescue
826
- end
827
- end
828
- begin
829
- exec(*args)
830
- rescue Errno::ENOENT
831
- puts color("Command not found: #{args[0]}", rainbow_codes.sample)
832
- exit 127
833
- rescue Errno::EACCES
834
- puts color("Permission denied: #{args[0]}", 31)
835
- exit 126
836
- end
837
- end
838
-
839
- $child_pids << pid
840
- begin
841
- Process.wait(pid)
842
- $last_status = $?.exitstatus || 0
843
- rescue Interrupt
844
- ensure
845
- $child_pids.delete(pid)
846
- end
847
- end
848
-
849
- # ---------------- Command Execution ----------------
850
- def run_command(cmd)
851
- cmd = cmd.to_s.strip
852
- return if cmd.empty?
853
-
854
- cmd = expand_aliases(cmd)
855
- cmd = expand_vars(cmd)
856
-
857
- # ---------------- Assignments ----------------
858
- # Expression-style: VAR = Ruby_expression
859
- if (m = cmd.match(/\A([A-Za-z_][A-Za-z0-9_]*)\s+=\s+(.+)\z/))
860
- var = m[1]
861
- rhs = m[2]
862
- begin
863
- value = eval(rhs)
864
- ENV[var] = value.is_a?(String) ? value : value.to_s
865
- rescue Exception
866
- ENV[var] = rhs
867
- end
868
- $last_status = 0
869
- return
870
- end
871
-
872
- # Simple shell-style: VAR=value (no spaces)
873
- if (m = cmd.match(/\A([A-Za-z_][A-Za-z0-9_]*)=(.*)\z/))
874
- var = m[1]
875
- val = m[2] || ""
876
- ENV[var] = val
877
- $last_status = 0
878
- return
879
- end
880
-
881
- # ---------------- Redirections + args ----------------
882
- cmd, stdin_file, stdout_file, append = parse_redirection(cmd)
883
- args = Shellwords.shellsplit(cmd) rescue []
884
- return if args.empty?
885
-
886
- # rsh functions
887
- if $rsh_functions.key?(args[0])
888
- rsh_call_function(args[0], args[1..-1] || [])
889
- $last_status = 0
890
- return
891
- end
892
-
893
- case args[0]
894
- when 'ls'
895
- if args.length == 1
896
- builtin_ls(".")
897
- elsif args.length == 2 && !args[1].start_with?("-")
898
- builtin_ls(args[1])
899
- else
900
- exec_external(args, stdin_file, stdout_file, append)
901
- return
902
- end
903
- $last_status = 0
904
- return
905
-
906
- when 'cd'
907
- path = args[1] ? File.expand_path(args[1]) : ENV['HOME']
908
- if !File.exist?(path)
909
- puts color("cd: no such file or directory: #{args[1]}", 31)
910
- $last_status = 1
911
- elsif !File.directory?(path)
912
- puts color("cd: not a directory: #{args[1]}", 31)
913
- $last_status = 1
914
- else
915
- Dir.chdir(path)
916
- $last_status = 0
917
- end
918
- return
919
-
920
- when 'exit','quit'
921
- $child_pids.each { |pid| Process.kill("TERM", pid) rescue nil }
922
- exit 0
923
-
924
- when 'alias'
925
- if args[1].nil?
926
- $aliases.each { |k,v| puts "#{k}='#{v}'" }
927
- else
928
- arg = args[1..].join(' ')
929
- if arg =~ /^(\w+)=([\"']?)(.+?)\2$/
930
- $aliases[$1] = $3
931
- else
932
- puts color("Invalid alias format", 31)
933
- end
934
- end
935
- $last_status = 0
936
- return
937
-
938
- when 'unalias'
939
- if args[1]
940
- $aliases.delete(args[1])
941
- $last_status = 0
942
- else
943
- puts color("unalias: usage: unalias name", 31)
944
- $last_status = 1
945
- end
946
- return
947
-
948
- when 'help'
949
- builtin_help
950
- $last_status = 0
951
- return
952
-
953
- when 'systemfetch'
954
- builtin_systemfetch
955
- $last_status = 0
956
- return
957
-
958
- when 'jobs'
959
- builtin_jobs
960
- $last_status = 0
961
- return
962
-
963
- when 'pwd'
964
- puts color(Dir.pwd, 36)
965
- $last_status = 0
966
- return
967
-
968
- when 'hist'
969
- builtin_hist
970
- $last_status = 0
971
- return
972
-
973
- when 'clearhist'
974
- builtin_clearhist
975
- $last_status = 0
976
- return
977
-
978
- when 'put'
979
- msg = args[1..-1].join(' ')
980
- puts msg
981
- $last_status = 0
982
- return
983
-
984
- # -------- New scripting builtins --------
985
- when 'break'
986
- raise RshBreak
987
-
988
- when 'continue'
989
- raise RshContinue
990
-
991
- when 'return'
992
- raise RshReturn
993
-
994
- when 'set'
995
- if args.length == 1
996
- ENV.keys.sort.each do |k|
997
- puts "#{k}=#{ENV[k]}"
998
- end
999
- else
1000
- var = args[1]
1001
- val = args[2..-1].join(' ')
1002
- ENV[var] = val
1003
- end
1004
- $last_status = 0
1005
- return
1006
-
1007
- when 'unset'
1008
- if args[1]
1009
- ENV.delete(args[1])
1010
- $last_status = 0
1011
- else
1012
- puts color("unset: usage: unset VAR", 31)
1013
- $last_status = 1
1014
- end
1015
- return
1016
-
1017
- when 'read'
1018
- var = args[1]
1019
- unless var
1020
- puts color("read: usage: read VAR", 31)
1021
- $last_status = 1
1022
- return
1023
- end
1024
- line = STDIN.gets
1025
- ENV[var] = (line ? line.chomp : "")
1026
- $last_status = 0
1027
- return
1028
-
1029
- when 'true'
1030
- $last_status = 0
1031
- return
1032
-
1033
- when 'false'
1034
- $last_status = 1
1035
- return
1036
-
1037
- when 'sleep'
1038
- secs = (args[1] || "1").to_f
1039
- begin
1040
- Kernel.sleep(secs)
1041
- $last_status = 0
1042
- rescue
1043
- $last_status = 1
1044
- end
1045
- return
1046
-
1047
- when 'source', '.'
1048
- file = args[1]
1049
- if file.nil?
1050
- puts color("source: usage: source FILE", 31)
1051
- $last_status = 1
1052
- return
1053
- end
1054
- begin
1055
- rsh_run_script(file, args[2..-1] || [])
1056
- $last_status = 0
1057
- rescue => e
1058
- STDERR.puts "source error: #{e.class}: #{e.message}"
1059
- $last_status = 1
1060
- end
1061
- return
1062
- end
1063
-
1064
- # Fallback to external command
1065
- exec_external(args, stdin_file, stdout_file, append)
1066
- end
1067
-
1068
- # ---------------- Chained Commands ----------------
1069
- def split_commands(input)
1070
- return [] if input.nil?
1071
-
1072
- cmds = []
1073
- buf = +""
1074
- in_single = false
1075
- in_double = false
1076
- escaped = false
1077
- i = 0
1078
-
1079
- while i < input.length
1080
- ch = input[i]
1081
-
1082
- if escaped
1083
- buf << ch
1084
- escaped = false
1085
- elsif ch == '\\'
1086
- escaped = true
1087
- buf << ch
1088
- elsif ch == "'" && !in_double
1089
- in_single = !in_single
1090
- buf << ch
1091
- elsif ch == '"' && !in_single
1092
- in_double = !in_double
1093
- buf << ch
1094
- elsif !in_single && !in_double && ch == ';'
1095
- cmd = buf.strip
1096
- cmds << cmd unless cmd.empty?
1097
- buf = +""
1098
- elsif !in_single && !in_double && ch == '&' && input[i + 1] == '&'
1099
- cmd = buf.strip
1100
- cmds << cmd unless cmd.empty?
1101
- buf = +""
1102
- i += 1
1103
- else
1104
- buf << ch
1105
- end
1106
-
1107
- i += 1
1108
- end
1109
-
1110
- cmd = buf.strip
1111
- cmds << cmd unless cmd.empty?
1112
- cmds
1113
- end
1114
-
1115
- def run_input_line(input)
1116
- split_commands(input).each do |cmd|
1117
- run_command(cmd)
1118
- end
1119
- end
1120
-
1121
- # ---------------- Prompt ----------------
1122
- hostname = Socket.gethostname
1123
- prompt_color = random_color
1124
-
1125
- def prompt(hostname, prompt_color)
1126
- "#{color(Dir.pwd,33)} #{color(hostname,36)}#{color(' > ', prompt_color)}"
1127
- end
1128
-
1129
- # ---------------- Ghost + Completion Helpers ----------------
1130
- def history_ghost_for(line)
1131
- return nil if line.nil? || line.empty?
1132
- HISTORY.reverse_each do |h|
1133
- next if h.nil? || h.empty?
1134
- next if h.start_with?("[completions:")
1135
- next unless h.start_with?(line)
1136
- next if h == line
1137
- return h
1138
- end
1139
- nil
1140
- end
1141
-
1142
- def tab_completions_for(prefix, first_word, at_first_word)
1143
- prefix ||= ""
1144
-
1145
- dir = "."
1146
- base = prefix
1147
-
1148
- if prefix.include?('/')
1149
- if prefix.end_with?('/')
1150
- dir = prefix.chomp('/')
1151
- base = ""
1152
- else
1153
- dir = File.dirname(prefix)
1154
- base = File.basename(prefix)
1155
- end
1156
- dir = "." if dir.nil? || dir.empty?
1157
- end
1158
-
1159
- file_completions = []
1160
- if Dir.exist?(dir)
1161
- Dir.children(dir).each do |entry|
1162
- next unless entry.start_with?(base)
1163
- full = File.join(dir, entry)
1164
-
1165
- rel =
1166
- if dir == "."
1167
- entry
1168
- else
1169
- File.join(File.dirname(prefix), entry)
1170
- end
1171
-
1172
- case first_word
1173
- when "cd"
1174
- next unless File.directory?(full)
1175
- rel = rel + "/" unless rel.end_with?("/")
1176
- file_completions << rel
1177
- when "cat"
1178
- next unless File.file?(full)
1179
- file_completions << rel
1180
- else
1181
- rel = rel + "/" if File.directory?(full) && !rel.end_with?("/")
1182
- file_completions << rel
1183
- end
1184
- end
1185
- end
1186
-
1187
- exec_completions = []
1188
- if first_word != "cat" && first_word != "cd" && at_first_word && !prefix.include?('/')
1189
- path_entries = (ENV['PATH'] || "").split(':')
1190
- execs = path_entries.flat_map do |p|
1191
- Dir.glob("#{p}/*").map { |f|
1192
- File.basename(f) if File.executable?(f) && !File.directory?(f)
1193
- }.compact rescue []
1194
- end
1195
- exec_completions = execs.grep(/^#{Regexp.escape(prefix)}/)
1196
- end
1197
-
1198
- (file_completions + exec_completions).uniq
1199
- end
1200
-
1201
- def longest_common_prefix(strings)
1202
- return "" if strings.empty?
1203
- shortest = strings.min_by(&:length)
1204
- shortest.length.times do |i|
1205
- c = shortest[i]
1206
- strings.each do |s|
1207
- return shortest[0...i] if s[i] != c
1208
- end
1209
- end
1210
- shortest
1211
- end
1212
-
1213
- def render_line(prompt_str, buffer, cursor, show_ghost = true)
1214
- buffer = buffer || ""
1215
- cursor = [[cursor, 0].max, buffer.length].min
1216
-
1217
- ghost_tail = ""
1218
- if show_ghost && cursor == buffer.length
1219
- suggestion = history_ghost_for(buffer)
1220
- ghost_tail = suggestion ? suggestion[buffer.length..-1].to_s : ""
1221
- end
1222
-
1223
- width = terminal_width
1224
- prompt_vis = strip_ansi(prompt_str).length
1225
- total_vis = prompt_vis + buffer.length + ghost_tail.length
1226
- rows = [(total_vis.to_f / width).ceil, 1].max
1227
-
1228
- # Clear previous render block (only what we drew last time)
1229
- if $last_render_rows && $last_render_rows > 0
1230
- STDOUT.print("\r")
1231
- ($last_render_rows - 1).times do
1232
- STDOUT.print("\e[1A\r") # move up a line, to column 0
1233
- end
1234
- $last_render_rows.times do |i|
1235
- STDOUT.print("\e[0K") # clear this line
1236
- STDOUT.print("\n") if i < $last_render_rows - 1
1237
- end
1238
- ($last_render_rows - 1).times do
1239
- STDOUT.print("\e[1A\r") # move back up to first line of block
1240
- end
1241
- end
1242
-
1243
- STDOUT.print("\r")
1244
- STDOUT.print(prompt_str)
1245
- STDOUT.print(buffer)
1246
- STDOUT.print(color(ghost_tail, "2")) unless ghost_tail.empty?
1247
-
1248
- move_left = ghost_tail.length + (buffer.length - cursor)
1249
- STDOUT.print("\e[#{move_left}D") if move_left > 0
1250
- STDOUT.flush
1251
-
1252
- $last_render_rows = rows
1253
- end
1254
-
1255
- # --------- NEAT MULTI-COLUMN TAB LIST (bash-style) ----------
1256
- def print_tab_list(comps)
1257
- return if comps.empty?
1258
-
1259
- width = terminal_width
1260
- max_len = comps.map { |s| s.length }.max || 0
1261
- col_width = [max_len + 2, 4].max
1262
- cols = [width / col_width, 1].max
1263
- rows = (comps.length.to_f / cols).ceil
1264
-
1265
- STDOUT.print("\r\n")
1266
- rows.times do |r|
1267
- line = ""
1268
- cols.times do |c|
1269
- idx = c * rows + r
1270
- break if idx >= comps.length
1271
- item = comps[idx]
1272
- padding = col_width - item.length
1273
- line << item << (" " * padding)
1274
- end
1275
- STDOUT.print("\r")
1276
- STDOUT.print(line.rstrip)
1277
- STDOUT.print("\n")
1278
- end
1279
- STDOUT.print("\r\n")
1280
- STDOUT.flush
1281
- end
1282
-
1283
- def handle_tab_completion(prompt_str, buffer, cursor, last_tab_prefix, tab_cycle)
1284
- buffer = buffer || ""
1285
- cursor = [[cursor, 0].max, buffer.length].min
1286
-
1287
- wstart = buffer.rindex(/[ \t]/, cursor - 1) || -1
1288
- wstart += 1
1289
- prefix = buffer[wstart...cursor] || ""
1290
-
1291
- before_word = buffer[0...wstart]
1292
- at_first_word = before_word.strip.empty?
1293
- first_word = buffer.strip.split(/\s+/, 2)[0] || ""
1294
-
1295
- comps = tab_completions_for(prefix, first_word, at_first_word)
1296
- return [buffer, cursor, nil, 0, false] if comps.empty?
1297
-
1298
- if comps.size == 1
1299
- new_word = comps.first
1300
- buffer = buffer[0...wstart] + new_word + buffer[cursor..-1].to_s
1301
- cursor = wstart + new_word.length
1302
- return [buffer, cursor, nil, 0, true]
1303
- end
1304
-
1305
- if prefix != last_tab_prefix
1306
- lcp = longest_common_prefix(comps)
1307
- if lcp && lcp.length > prefix.length
1308
- buffer = buffer[0...wstart] + lcp + buffer[cursor..-1].to_s
1309
- cursor = wstart + lcp.length
1310
- else
1311
- STDOUT.print("\a")
1312
- end
1313
- last_tab_prefix = prefix
1314
- tab_cycle = 1
1315
- return [buffer, cursor, last_tab_prefix, tab_cycle, false]
1316
- else
1317
- # Second tab on same prefix: show list
1318
- render_line(prompt_str, buffer, cursor, false)
1319
- print_tab_list(comps)
1320
- last_tab_prefix = prefix
1321
- tab_cycle += 1
1322
- return [buffer, cursor, last_tab_prefix, tab_cycle, true]
1323
- end
1324
- end
1325
-
1326
- def read_line_with_ghost(prompt_str)
1327
- buffer = ""
1328
- cursor = 0
1329
- hist_index = HISTORY.length
1330
- saved_line_for_history = ""
1331
- last_tab_prefix = nil
1332
- tab_cycle = 0
1333
-
1334
- render_line(prompt_str, buffer, cursor)
1335
-
1336
- status = :ok
1337
-
1338
- IO.console.raw do |io|
1339
- loop do
1340
- ch = io.getch
1341
-
1342
- case ch
1343
- when "\r", "\n"
1344
- cursor = buffer.length
1345
- render_line(prompt_str, buffer, cursor, false)
1346
- STDOUT.print("\r\n")
1347
- STDOUT.flush
1348
- break
1349
-
1350
- when "\u0003" # Ctrl-C
1351
- STDOUT.print("^C\r\n")
1352
- STDOUT.flush
1353
- status = :interrupt
1354
- buffer = ""
1355
- break
1356
-
1357
- when "\u0004" # Ctrl-D
1358
- if buffer.empty?
1359
- status = :eof
1360
- buffer = nil
1361
- STDOUT.print("\r\n")
1362
- STDOUT.flush
1363
- break
1364
- else
1365
- # ignore when line not empty
1366
- end
1367
-
1368
- when "\u0001" # Ctrl-A - move to beginning of line
1369
- cursor = 0
1370
- last_tab_prefix = nil
1371
- tab_cycle = 0
1372
-
1373
- when "\u007F", "\b" # Backspace
1374
- if cursor > 0
1375
- buffer.slice!(cursor - 1)
1376
- cursor -= 1
1377
- end
1378
- last_tab_prefix = nil
1379
- tab_cycle = 0
1380
-
1381
- when "\t" # Tab completion
1382
- buffer, cursor, last_tab_prefix, tab_cycle, printed =
1383
- handle_tab_completion(prompt_str, buffer, cursor, last_tab_prefix, tab_cycle)
1384
- $last_render_rows = 1 if printed
1385
-
1386
- when "\e" # Escape sequences (arrows, home/end)
1387
- seq1 = io.getch
1388
- seq2 = io.getch
1389
- if seq1 == "[" && seq2
1390
- case seq2
1391
- when "A" # Up
1392
- if hist_index == HISTORY.length
1393
- saved_line_for_history = buffer.dup
1394
- end
1395
- if hist_index > 0
1396
- hist_index -= 1
1397
- buffer = HISTORY[hist_index] || ""
1398
- cursor = buffer.length
1399
- end
1400
- when "B" # Down
1401
- if hist_index < HISTORY.length - 1
1402
- hist_index += 1
1403
- buffer = HISTORY[hist_index] || ""
1404
- cursor = buffer.length
1405
- elsif hist_index == HISTORY.length - 1
1406
- hist_index = HISTORY.length
1407
- buffer = saved_line_for_history || ""
1408
- cursor = buffer.length
1409
- end
1410
- when "C" # Right
1411
- if cursor < buffer.length
1412
- cursor += 1
1413
- else
1414
- suggestion = history_ghost_for(buffer)
1415
- if suggestion
1416
- buffer = suggestion
1417
- cursor = buffer.length
1418
- end
1419
- end
1420
- when "D" # Left
1421
- cursor -= 1 if cursor > 0
1422
- when "H" # Home
1423
- cursor = 0
1424
- when "F" # End
1425
- cursor = buffer.length
1426
- end
1427
- end
1428
- last_tab_prefix = nil
1429
- tab_cycle = 0
1430
-
1431
- else
1432
- if ch.ord >= 32 && ch.ord != 127
1433
- buffer.insert(cursor, ch)
1434
- cursor += 1
1435
- hist_index = HISTORY.length
1436
- last_tab_prefix = nil
1437
- tab_cycle = 0
1438
- end
1439
- end
1440
-
1441
- render_line(prompt_str, buffer, cursor) if status == :ok
1442
- end
1443
- end
1444
-
1445
- [status, buffer]
1446
- end
1447
-
1448
- # ---------------- Welcome ----------------
1449
- def print_welcome
1450
- puts color("Welcome to srsh #{SRSH_VERSION} - your simple Ruby shell!",36)
1451
- puts color("Current Time:",36) + " " + color(current_time,34)
1452
- puts cpu_info
1453
- puts ram_info
1454
- puts storage_info
1455
- puts dynamic_quote
1456
- puts
1457
- puts color("Coded with love by https://github.com/RobertFlexx",90)
1458
- puts
1459
- end
1460
-
1461
- # ---------------- Script vs interactive entry ----------------
1462
- if ARGV[0]
1463
- script_path = ARGV.shift
1464
- begin
1465
- rsh_run_script(script_path, ARGV)
1466
- rescue => e
1467
- STDERR.puts "rsh script error: #{e.class}: #{e.message}"
1468
- end
1469
- exit 0
1470
- end
1471
-
1472
- print_welcome
1473
-
1474
- # ---------------- Main Loop ----------------
1475
- loop do
1476
- print "\033]0;srsh-#{SRSH_VERSION}\007"
1477
- prompt_str = prompt(hostname, prompt_color)
1478
-
1479
- status, input = read_line_with_ghost(prompt_str)
1480
-
1481
- break if status == :eof
1482
- next if status == :interrupt
1483
-
1484
- next if input.nil?
1485
- input = input.strip
1486
- next if input.empty?
1487
-
1488
- HISTORY << input
1489
-
1490
- run_input_line(input)
1491
- end