srsh 0.7.0 → 0.7.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/bin/srsh +608 -341
  3. data/lib/srsh/version.rb +1 -1
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 405e071c7030f9eb0a2af1212dfd1357b047ec0c6aa37a7df490300b28d4a033
4
- data.tar.gz: dd50fbc336a12486d5479e547a964e7129d4d0a1f51fe680ec394e9cf6d02792
3
+ metadata.gz: 0c0910382ad05f07b93041cfbecb7d3db98f24e23df1ecd5d23243aab35a7f63
4
+ data.tar.gz: c39ce2184076112bb950035ea2f09939bef9f4105b2c207e6ec73ee152fc3c89
5
5
  SHA512:
6
- metadata.gz: fc2058fd9a0d13b6e91d2a4200acfab1c414040212c1d51c8755be0bb3d81279f7e86cfeeab14e13422f6454f463109fcdd1d1e0989191c68078b1ae10107930
7
- data.tar.gz: 6057d29d402120f7dae36741aaacbbfa4cda2c09d5181c944fdc5bdaf03357c5c4cfecf6a1ad6fe4aca22b0044de4500cb3c0feceb4a3f0f2c476250bb731e98
6
+ metadata.gz: ee7bd7e5ae91582ed612171eda2f3440d16d8d07988b465fd61470e094d80b0f766cfc6f248d2980f0371a0db228593a326934fe7f6c4367fb8e9b009213e26f
7
+ data.tar.gz: 6195a9d026471e57b1b7b8f87970362261302d0e7f8b936a030ecc02bce7de2c56d3420d2e7cee0b7810f60acc32f5a171bd45acd1181ffc7d2469389f37ebd7
data/bin/srsh CHANGED
@@ -7,7 +7,7 @@ require 'rbconfig'
7
7
  require 'io/console'
8
8
 
9
9
  # ---------------- Version ----------------
10
- SRSH_VERSION = "0.7.0"
10
+ SRSH_VERSION = "0.7.1"
11
11
 
12
12
  $0 = "srsh-#{SRSH_VERSION}"
13
13
  ENV['SHELL'] = "srsh-#{SRSH_VERSION}"
@@ -15,6 +15,7 @@ print "\033]0;srsh-#{SRSH_VERSION}\007"
15
15
 
16
16
  Dir.chdir(ENV['HOME']) if ENV['HOME']
17
17
 
18
+ # ---------------- Globals ----------------
18
19
  $child_pids = []
19
20
  $aliases = {}
20
21
  $last_render_rows = 0
@@ -26,345 +27,380 @@ $rsh_script_mode = false
26
27
 
27
28
  Signal.trap("INT", "IGNORE")
28
29
 
29
- # ---------------- History ----------------
30
- HISTORY_FILE = File.join(Dir.home, ".srsh_history")
31
- HISTORY = if File.exist?(HISTORY_FILE)
32
- File.readlines(HISTORY_FILE, chomp: true)
33
- else
34
- []
35
- end
30
+ # Control-flow exceptions for the scripting engine
31
+ class RshBreak < StandardError; end
32
+ class RshContinue < StandardError; end
33
+ class RshReturn < StandardError; end
36
34
 
37
- at_exit do
38
- begin
39
- File.open(HISTORY_FILE, "w") do |f|
40
- HISTORY.each { |line| f.puts line }
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
41
  end
42
- rescue
43
- end
44
- end
45
42
 
46
- # ---------------- RC file (create if missing) ----------------
47
- RC_FILE = File.join(Dir.home, ".srshrc")
48
- begin
49
- unless File.exist?(RC_FILE)
50
- File.write(RC_FILE, <<~RC)
51
- # ~/.srshrc — srsh configuration
52
- # This file was created automatically by srsh v#{SRSH_VERSION}.
53
- # You can keep personal notes or planned settings here.
54
- # (Currently not sourced by srsh runtime.)
55
- RC
56
- end
57
- rescue
58
- end
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
59
51
 
60
- # ---------------- Utilities ----------------
61
- def color(text, code)
62
- "\e[#{code}m#{text}\e[0m"
63
- end
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
64
65
 
65
- def random_color
66
- [31, 32, 33, 34, 35, 36, 37].sample
67
- end
66
+ # ---------------- Utilities ----------------
67
+ def color(text, code)
68
+ "\e[#{code}m#{text}\e[0m"
69
+ end
68
70
 
69
- def rainbow_codes
70
- [31, 33, 32, 36, 34, 35, 91, 93, 92, 96, 94, 95]
71
- end
71
+ def random_color
72
+ [31, 32, 33, 34, 35, 36, 37].sample
73
+ end
72
74
 
73
- # variable expansion: $VAR and $1, $2, $0 (script/function args)
74
- def expand_vars(str)
75
- s = str.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)/) do
76
- ENV[$1] || ""
77
- end
78
- s.gsub(/\$(\d+)/) do
79
- idx = $1.to_i
80
- ($rsh_positional && $rsh_positional[idx]) || ""
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
81
293
  end
294
+ [cores, freqs.first(cores)]
82
295
  end
83
296
 
84
- def parse_redirection(cmd)
85
- stdin_file = nil
86
- stdout_file = nil
87
- append = false
88
-
89
- if cmd =~ /(.*)>>\s*(\S+)/
90
- cmd = $1.strip
91
- stdout_file = $2.strip
92
- append = true
93
- elsif cmd =~ /(.*)>\s*(\S+)/
94
- cmd = $1.strip
95
- stdout_file = $2.strip
96
- end
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
97
316
 
98
- if cmd =~ /(.*)<\s*(\S+)/
99
- cmd = $1.strip
100
- stdin_file = $2.strip
101
- end
317
+ raw_freq_hz = begin
318
+ `sysctl -n hw.cpufrequency 2>/dev/null`.to_i
319
+ rescue
320
+ 0
321
+ end
102
322
 
103
- [cmd, stdin_file, stdout_file, append]
104
- end
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
105
330
 
106
- def human_bytes(bytes)
107
- units = ['B', 'KB', 'MB', 'GB', 'TB']
108
- size = bytes.to_f
109
- unit = units.shift
110
- while size > 1024 && !units.empty?
111
- size /= 1024
112
- unit = units.shift
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
113
344
  end
114
- "#{format('%.2f', size)} #{unit}"
115
- end
116
345
 
117
- def nice_bar(p, w = 30, code = 32)
118
- p = [[p, 0.0].max, 1.0].min
119
- f = (p * w).round
120
- b = "█" * f + "░" * (w - f)
121
- pct = (p * 100).to_i
122
- "#{color("[#{b}]", code)} #{color(sprintf("%3d%%", pct), 37)}"
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)}"
123
349
  end
124
350
 
125
- def terminal_width
126
- IO.console.winsize[1]
127
- rescue
128
- 80
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
129
401
  end
130
402
 
131
- def strip_ansi(str)
132
- str.to_s.gsub(/\e\[[0-9;]*m/, '')
133
- end
134
-
135
- # ---------------- Aliases ----------------
136
- def expand_aliases(cmd, seen = [])
137
- return cmd if cmd.nil? || cmd.strip.empty?
138
- first_word, rest = cmd.strip.split(' ', 2)
139
- return cmd if seen.include?(first_word)
140
- seen << first_word
141
-
142
- if $aliases.key?(first_word)
143
- replacement = $aliases[first_word]
144
- expanded = expand_aliases(replacement, seen)
145
- rest ? "#{expanded} #{rest}" : expanded
146
- else
147
- cmd
148
- end
149
- end
150
-
151
- # ---------------- System Info ----------------
152
- def current_time
153
- Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")
154
- end
155
-
156
- def detect_distro
157
- if File.exist?('/etc/os-release')
158
- line = File.read('/etc/os-release').lines.find { |l|
159
- l.start_with?('PRETTY_NAME="') || l.start_with?('PRETTY_NAME=')
160
- }
161
- return line.split('=').last.strip.delete('"') if line
162
- end
163
- "#{RbConfig::CONFIG['host_os']}"
164
- end
165
-
166
- def os_type
167
- host = RbConfig::CONFIG['host_os'].to_s
168
- case host
169
- when /linux/i
170
- :linux
171
- when /darwin/i
172
- :mac
173
- when /bsd/i
174
- :bsd
175
- else
176
- :other
177
- end
178
- end
179
-
180
- # ---------------- Quotes ----------------
181
- QUOTES = [
182
- "Keep calm and code on.",
183
- "Did you try turning it off and on again?",
184
- "There’s no place like 127.0.0.1.",
185
- "To iterate is human, to recurse divine.",
186
- "sudo rm -rf / – Just kidding, don’t do that!",
187
- "The shell is mightier than the sword.",
188
- "A journey of a thousand commits begins with a single push.",
189
- "In case of fire: git commit, git push, leave building.",
190
- "Debugging is like being the detective in a crime movie where you are also the murderer.",
191
- "Unix is user-friendly. It's just selective about who its friends are.",
192
- "Old sysadmins never die, they just become daemons.",
193
- "Listen you flatpaker! – Totally Terry Davis",
194
- "How is #{detect_distro}? 🤔",
195
- "Life is short, but your command history is eternal.",
196
- "If at first you don’t succeed, git commit and push anyway.",
197
- "rm -rf: the ultimate trust exercise.",
198
- "Coding is like magic, but with more coffee.",
199
- "There’s no bug, only undocumented features.",
200
- "Keep your friends close and your aliases closer.",
201
- "Why wait for the future when you can Ctrl+Z it?",
202
- "A watched process never completes.",
203
- "When in doubt, make it a function.",
204
- "Some call it procrastination, we call it debugging curiosity.",
205
- "Life is like a terminal; some commands just don’t execute.",
206
- "Good code is like a good joke; it needs no explanation.",
207
- "sudo: because sometimes responsibility is overrated.",
208
- "Pipes make the world go round.",
209
- "In bash we trust, in Ruby we wonder.",
210
- "A system without errors is like a day without coffee.",
211
- "Keep your loops tight and your sleeps short.",
212
- "Stack traces are just life giving you directions.",
213
- "Your mom called, she wants her semicolons back."
214
- ]
215
-
216
- $current_quote = QUOTES.sample
217
-
218
- def dynamic_quote
219
- chars = $current_quote.chars
220
- rainbow = rainbow_codes.cycle
221
- chars.map { |c| color(c, rainbow.next) }.join
222
- end
223
-
224
- # ---------------- CPU / RAM / Storage ----------------
225
- def read_cpu_times
226
- return [] unless File.exist?('/proc/stat')
227
- cpu_line = File.readlines('/proc/stat').find { |line| line.start_with?('cpu ') }
228
- return [] unless cpu_line
229
- cpu_line.split[1..-1].map(&:to_i)
230
- end
231
-
232
- def calculate_cpu_usage(prev, current)
233
- return 0.0 if prev.empty? || current.empty?
234
- prev_idle = prev[3] + (prev[4] || 0)
235
- idle = current[3] + (current[4] || 0)
236
- prev_non_idle = prev[0] + prev[1] + prev[2] +
237
- (prev[5] || 0) + (prev[6] || 0) + (prev[7] || 0)
238
- non_idle = current[0] + current[1] + current[2] +
239
- (current[5] || 0) + (current[6] || 0) + (current[7] || 0)
240
- prev_total = prev_idle + prev_non_idle
241
- total = idle + non_idle
242
- totald = total - prev_total
243
- idled = idle - prev_idle
244
- return 0.0 if totald <= 0
245
- ((totald - idled).to_f / totald) * 100
246
- end
247
-
248
- def cpu_cores_and_freq
249
- return [0, []] unless File.exist?('/proc/cpuinfo')
250
- cores = 0
251
- freqs = []
252
- File.foreach('/proc/cpuinfo') do |line|
253
- cores += 1 if line =~ /^processor\s*:\s*\d+/
254
- if line =~ /^cpu MHz\s*:\s*([\d.]+)/
255
- freqs << $1.to_f
256
- end
257
- end
258
- [cores, freqs.first(cores)]
259
- end
260
-
261
- def cpu_info
262
- usage = 0.0
263
- cores = 0
264
- freq_display = "N/A"
265
-
266
- case os_type
267
- when :linux
268
- prev = read_cpu_times
269
- sleep 0.05
270
- current = read_cpu_times
271
- usage = calculate_cpu_usage(prev, current).round(1)
272
- cores, freqs = cpu_cores_and_freq
273
- freq_display = freqs.empty? ? "N/A" : freqs.map { |f| "#{f.round(0)}MHz" }.join(', ')
274
- else
275
- cores = begin
276
- `sysctl -n hw.ncpu 2>/dev/null`.to_i
277
- rescue
278
- 0
279
- end
280
-
281
- raw_freq_hz = begin
282
- `sysctl -n hw.cpufrequency 2>/dev/null`.to_i
283
- rescue
284
- 0
285
- end
286
-
287
- freq_display =
288
- if raw_freq_hz > 0
289
- mhz = (raw_freq_hz.to_f / 1_000_000.0).round(0)
290
- "#{mhz.to_i}MHz"
291
- else
292
- "N/A"
293
- end
294
-
295
- usage = begin
296
- ps_output = `ps -A -o %cpu 2>/dev/null`
297
- lines = ps_output.lines
298
- values = lines[1..-1] || []
299
- sum = values.map { |l| l.to_f }.inject(0.0, :+)
300
- if cores > 0
301
- (sum / cores).round(1)
302
- else
303
- sum.round(1)
304
- end
305
- rescue
306
- 0.0
307
- end
308
- end
309
-
310
- "#{color("CPU Usage:",36)} #{color("#{usage}%",33)} | " \
311
- "#{color("Cores:",36)} #{color(cores.to_s,32)} | " \
312
- "#{color("Freqs:",36)} #{color(freq_display,35)}"
313
- end
314
-
315
- def ram_info
316
- case os_type
317
- when :linux
318
- if File.exist?('/proc/meminfo')
319
- meminfo = {}
320
- File.read('/proc/meminfo').each_line do |line|
321
- key, val = line.split(':')
322
- meminfo[key.strip] = val.strip.split.first.to_i * 1024 if key && val
323
- end
324
- total = meminfo['MemTotal'] || 0
325
- free = (meminfo['MemFree'] || 0) + (meminfo['Buffers'] || 0) + (meminfo['Cached'] || 0)
326
- used = total - free
327
- "#{color("RAM Usage:",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
328
- else
329
- "#{color("RAM Usage:",36)} Info not available"
330
- end
331
- else
332
- begin
333
- if os_type == :mac
334
- total = `sysctl -n hw.memsize 2>/dev/null`.to_i
335
- return "#{color("RAM Usage:",36)} Info not available" if total <= 0
336
-
337
- vm = `vm_stat 2>/dev/null`
338
- page_size = vm[/page size of (\d+) bytes/, 1].to_i
339
- page_size = 4096 if page_size <= 0
340
-
341
- stats = {}
342
- vm.each_line do |line|
343
- if line =~ /^(.+):\s+(\d+)\./
344
- stats[$1] = $2.to_i
345
- end
346
- end
347
-
348
- used_pages = 0
349
- %w[Pages active Pages wired down Pages occupied by compressor].each do |k|
350
- used_pages += stats[k].to_i
351
- end
352
- used = used_pages * page_size
353
-
354
- "#{color("RAM Usage:",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
355
- else
356
- total = `sysctl -n hw.physmem 2>/dev/null`.to_i
357
- total = `sysctl -n hw.realmem 2>/dev/null`.to_i if total <= 0
358
- return "#{color("RAM Usage:",36)} Info not available" if total <= 0
359
- "#{color("RAM Usage:",36)} #{color("Unknown",33)} / #{color(human_bytes(total),32)}"
360
- end
361
- rescue
362
- "#{color("RAM Usage:",36)} Info not available"
363
- end
364
- end
365
- end
366
-
367
- def storage_info
403
+ def storage_info
368
404
  begin
369
405
  require 'sys/filesystem'
370
406
  stat = Sys::Filesystem.stat(Dir.pwd)
@@ -395,6 +431,14 @@ def builtin_help
395
431
  puts color(sprintf("%-15s", "hist"), "1;36") + "Show shell history"
396
432
  puts color(sprintf("%-15s", "clearhist"), "1;36") + "Clear saved history (memory + file)"
397
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"
398
442
  puts color(sprintf("%-15s", "help"), "1;36") + "Show this help message"
399
443
  puts color('=' * 60, "1;35")
400
444
  end
@@ -602,7 +646,7 @@ def rsh_find_if_bounds(lines, start_idx)
602
646
  else_idx = nil
603
647
  i = start_idx + 1
604
648
  while i < lines.length
605
- line = lines[i].to_s.strip
649
+ line = strip_rsh_comment(lines[i].to_s).strip
606
650
  if line.start_with?("if ")
607
651
  depth += 1
608
652
  elsif line.start_with?("while ")
@@ -624,7 +668,7 @@ def rsh_find_block_end(lines, start_idx)
624
668
  depth = 1
625
669
  i = start_idx + 1
626
670
  while i < lines.length
627
- line = lines[i].to_s.strip
671
+ line = strip_rsh_comment(lines[i].to_s).strip
628
672
  if line.start_with?("if ") || line.start_with?("while ") || line.start_with?("fn ")
629
673
  depth += 1
630
674
  elsif line == "end"
@@ -636,14 +680,40 @@ def rsh_find_block_end(lines, start_idx)
636
680
  raise "Unmatched block in rsh script"
637
681
  end
638
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
+
639
708
  def run_rsh_block(lines, start_idx, end_idx)
640
709
  i = start_idx
641
710
  while i < end_idx
642
- raw = lines[i]
643
- i += 1
711
+ raw = lines[i]
712
+ i += 1
644
713
  next if raw.nil?
645
- line = raw.strip
646
- next if line.empty? || line.start_with?("#")
714
+
715
+ line = strip_rsh_comment(raw).strip
716
+ next if line.empty?
647
717
 
648
718
  if line.start_with?("if ")
649
719
  cond_expr = line[3..-1].strip
@@ -656,14 +726,22 @@ def run_rsh_block(lines, start_idx, end_idx)
656
726
  end
657
727
  i = end_idx_2 + 1
658
728
  next
729
+
659
730
  elsif line.start_with?("while ")
660
731
  cond_expr = line[6..-1].strip
661
732
  block_end = rsh_find_block_end(lines, i - 1)
662
733
  while eval_rsh_expr(cond_expr)
663
- run_rsh_block(lines, i, block_end)
734
+ begin
735
+ run_rsh_block(lines, i, block_end)
736
+ rescue RshBreak
737
+ break
738
+ rescue RshContinue
739
+ next
740
+ end
664
741
  end
665
742
  i = block_end + 1
666
743
  next
744
+
667
745
  elsif line.start_with?("fn ")
668
746
  parts = line.split
669
747
  name = parts[1]
@@ -675,6 +753,7 @@ def run_rsh_block(lines, start_idx, end_idx)
675
753
  }
676
754
  i = block_end + 1
677
755
  next
756
+
678
757
  else
679
758
  run_input_line(line)
680
759
  end
@@ -703,15 +782,20 @@ def rsh_call_function(name, argv)
703
782
  saved_positional = $rsh_positional
704
783
  $rsh_positional = {}
705
784
  $rsh_positional[0] = name
785
+
706
786
  fn[:args].each_with_index do |argname, idx|
707
787
  val = argv[idx] || ""
708
788
  ENV[argname] = val
709
789
  $rsh_positional[idx + 1] = val
710
790
  end
711
791
 
712
- run_rsh_block(fn[:body], 0, fn[:body].length)
713
- ensure
714
- $rsh_positional = saved_positional
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
715
799
  end
716
800
 
717
801
  # ---------------- External Execution Helper ----------------
@@ -764,9 +848,37 @@ end
764
848
 
765
849
  # ---------------- Command Execution ----------------
766
850
  def run_command(cmd)
767
- cmd = cmd.to_s
768
- cmd = expand_aliases(cmd.strip)
769
- cmd = expand_vars(cmd.strip)
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 ----------------
770
882
  cmd, stdin_file, stdout_file, append = parse_redirection(cmd)
771
883
  args = Shellwords.shellsplit(cmd) rescue []
772
884
  return if args.empty?
@@ -774,6 +886,7 @@ def run_command(cmd)
774
886
  # rsh functions
775
887
  if $rsh_functions.key?(args[0])
776
888
  rsh_call_function(args[0], args[1..-1] || [])
889
+ $last_status = 0
777
890
  return
778
891
  end
779
892
 
@@ -781,26 +894,33 @@ def run_command(cmd)
781
894
  when 'ls'
782
895
  if args.length == 1
783
896
  builtin_ls(".")
784
- return
785
897
  elsif args.length == 2 && !args[1].start_with?("-")
786
898
  builtin_ls(args[1])
899
+ else
900
+ exec_external(args, stdin_file, stdout_file, append)
787
901
  return
788
902
  end
789
- exec_external(args, stdin_file, stdout_file, append)
903
+ $last_status = 0
790
904
  return
905
+
791
906
  when 'cd'
792
907
  path = args[1] ? File.expand_path(args[1]) : ENV['HOME']
793
908
  if !File.exist?(path)
794
909
  puts color("cd: no such file or directory: #{args[1]}", 31)
910
+ $last_status = 1
795
911
  elsif !File.directory?(path)
796
912
  puts color("cd: not a directory: #{args[1]}", 31)
913
+ $last_status = 1
797
914
  else
798
915
  Dir.chdir(path)
916
+ $last_status = 0
799
917
  end
800
918
  return
919
+
801
920
  when 'exit','quit'
802
921
  $child_pids.each { |pid| Process.kill("TERM", pid) rescue nil }
803
922
  exit 0
923
+
804
924
  when 'alias'
805
925
  if args[1].nil?
806
926
  $aliases.each { |k,v| puts "#{k}='#{v}'" }
@@ -812,46 +932,188 @@ def run_command(cmd)
812
932
  puts color("Invalid alias format", 31)
813
933
  end
814
934
  end
935
+ $last_status = 0
815
936
  return
937
+
816
938
  when 'unalias'
817
939
  if args[1]
818
940
  $aliases.delete(args[1])
941
+ $last_status = 0
819
942
  else
820
943
  puts color("unalias: usage: unalias name", 31)
944
+ $last_status = 1
821
945
  end
822
946
  return
947
+
823
948
  when 'help'
824
949
  builtin_help
950
+ $last_status = 0
825
951
  return
952
+
826
953
  when 'systemfetch'
827
954
  builtin_systemfetch
955
+ $last_status = 0
828
956
  return
957
+
829
958
  when 'jobs'
830
959
  builtin_jobs
960
+ $last_status = 0
831
961
  return
962
+
832
963
  when 'pwd'
833
964
  puts color(Dir.pwd, 36)
965
+ $last_status = 0
834
966
  return
967
+
835
968
  when 'hist'
836
969
  builtin_hist
970
+ $last_status = 0
837
971
  return
972
+
838
973
  when 'clearhist'
839
974
  builtin_clearhist
975
+ $last_status = 0
840
976
  return
977
+
841
978
  when 'put'
842
979
  msg = args[1..-1].join(' ')
843
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
844
1061
  return
845
1062
  end
846
1063
 
1064
+ # Fallback to external command
847
1065
  exec_external(args, stdin_file, stdout_file, append)
848
1066
  end
849
1067
 
850
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
+
851
1115
  def run_input_line(input)
852
- commands = input.split(/&&|;/).map(&:strip)
853
- commands.each do |cmd|
854
- next if cmd.empty?
1116
+ split_commands(input).each do |cmd|
855
1117
  run_command(cmd)
856
1118
  end
857
1119
  end
@@ -1084,12 +1346,14 @@ def read_line_with_ghost(prompt_str)
1084
1346
  STDOUT.print("\r\n")
1085
1347
  STDOUT.flush
1086
1348
  break
1349
+
1087
1350
  when "\u0003" # Ctrl-C
1088
1351
  STDOUT.print("^C\r\n")
1089
1352
  STDOUT.flush
1090
1353
  status = :interrupt
1091
1354
  buffer = ""
1092
1355
  break
1356
+
1093
1357
  when "\u0004" # Ctrl-D
1094
1358
  if buffer.empty?
1095
1359
  status = :eof
@@ -1100,10 +1364,12 @@ def read_line_with_ghost(prompt_str)
1100
1364
  else
1101
1365
  # ignore when line not empty
1102
1366
  end
1367
+
1103
1368
  when "\u0001" # Ctrl-A - move to beginning of line
1104
1369
  cursor = 0
1105
1370
  last_tab_prefix = nil
1106
1371
  tab_cycle = 0
1372
+
1107
1373
  when "\u007F", "\b" # Backspace
1108
1374
  if cursor > 0
1109
1375
  buffer.slice!(cursor - 1)
@@ -1111,12 +1377,12 @@ def read_line_with_ghost(prompt_str)
1111
1377
  end
1112
1378
  last_tab_prefix = nil
1113
1379
  tab_cycle = 0
1380
+
1114
1381
  when "\t" # Tab completion
1115
1382
  buffer, cursor, last_tab_prefix, tab_cycle, printed =
1116
1383
  handle_tab_completion(prompt_str, buffer, cursor, last_tab_prefix, tab_cycle)
1117
- # After showing completion list, reset render rows so the next prompt
1118
- # redraw only clears the current input line, not the completion block.
1119
1384
  $last_render_rows = 1 if printed
1385
+
1120
1386
  when "\e" # Escape sequences (arrows, home/end)
1121
1387
  seq1 = io.getch
1122
1388
  seq2 = io.getch
@@ -1161,6 +1427,7 @@ def read_line_with_ghost(prompt_str)
1161
1427
  end
1162
1428
  last_tab_prefix = nil
1163
1429
  tab_cycle = 0
1430
+
1164
1431
  else
1165
1432
  if ch.ord >= 32 && ch.ord != 127
1166
1433
  buffer.insert(cursor, ch)
data/lib/srsh/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # lib/srsh/version.rb
2
2
  module Srsh
3
- VERSION = "0.7.0"
3
+ VERSION = "0.7.1"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: srsh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - RobertFlexx