srsh 0.6.0 → 0.6.2

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 +427 -265
  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: 9e757aeabce3a17e17c7ffcf6c201abbc8345d9b75d349d879781902c2420351
4
- data.tar.gz: 4f90b0e077c2eae3223bff3a6e9bf53c955b8bcfa9e64dcd43b201b3ecf445db
3
+ metadata.gz: c04c9c3c80b2bf7d0e2b070ab02439dda32474e2d2e2abb55d326517f24d56ce
4
+ data.tar.gz: 65aa5c728d64a1fd916d1e65bd1b0086c998041e60b4b914dfbbcb35c9e48101
5
5
  SHA512:
6
- metadata.gz: e34fa22368e54984df32590ff9cfb9ccc550e4a25d5ae6c827ba9d2651b6b8e942cae846e8249950a3576b88afd8c64e531d1b536a39ba140a63d6b9e7ce6dcc
7
- data.tar.gz: 527072fa0f7f38e9e0358bfc70c57947e2c2dbcbc46ca2e78c713e9d5e5252d1a9ac005c199a90b60fb8ef60561387bc3ea59010c30da6765a76af44c0432b73
6
+ metadata.gz: 1e5305c079a3fdf81d63aa18afe60ad56ee163df1071fda6ad087bbeac811779423e50e2fd3d3930ce18c5af06e192ae610e085c76f971866f80888e31bd8287
7
+ data.tar.gz: 31900c7a88931ac85ab734cdc153df13c26f6b86ac2049ecb79f0f2dd34decd7d6f157d32f0e72ef970ba73a4c4bebcd18d3a7594d15c9b10913f8331ac17ba3
data/bin/srsh CHANGED
@@ -7,7 +7,7 @@ require 'rbconfig'
7
7
  require 'io/console'
8
8
 
9
9
  # ---------------- Version ----------------
10
- SRSH_VERSION = "0.6.0"
10
+ SRSH_VERSION = "0.6.2"
11
11
 
12
12
  $0 = "srsh-#{SRSH_VERSION}"
13
13
  ENV['SHELL'] = "srsh-#{SRSH_VERSION}"
@@ -15,15 +15,16 @@ print "\033]0;srsh-#{SRSH_VERSION}\007"
15
15
 
16
16
  Dir.chdir(ENV['HOME']) if ENV['HOME']
17
17
 
18
- $child_pids = []
19
- $aliases = {}
18
+ $child_pids = []
19
+ $aliases = {}
20
+ $last_render_rows = 0
20
21
 
21
22
  Signal.trap("INT", "IGNORE")
22
23
 
23
24
  # ---------------- History ----------------
24
25
  HISTORY_FILE = File.join(Dir.home, ".srsh_history")
25
26
  HISTORY = if File.exist?(HISTORY_FILE)
26
- File.readlines(HISTORY_FILE, chomp: true)
27
+ File.readlines(HISTORY_FILE, chomp: true)
27
28
  else
28
29
  []
29
30
  end
@@ -41,12 +42,12 @@ end
41
42
  RC_FILE = File.join(Dir.home, ".srshrc")
42
43
  begin
43
44
  unless File.exist?(RC_FILE)
44
- File.write(RC_FILE, <<~RC)
45
- # ~/.srshrc — srsh configuration
46
- # This file was created automatically by srsh v#{SRSH_VERSION}.
47
- # You can keep personal notes or planned settings here.
48
- # (Currently not sourced by srsh runtime.)
49
- RC
45
+ File.write(RC_FILE, <<~RC)
46
+ # ~/.srshrc — srsh configuration
47
+ # This file was created automatically by srsh v#{SRSH_VERSION}.
48
+ # You can keep personal notes or planned settings here.
49
+ # (Currently not sourced by srsh runtime.)
50
+ RC
50
51
  end
51
52
  rescue
52
53
  end
@@ -57,11 +58,11 @@ def color(text, code)
57
58
  end
58
59
 
59
60
  def random_color
60
- [31,32,33,34,35,36,37].sample
61
+ [31, 32, 33, 34, 35, 36, 37].sample
61
62
  end
62
63
 
63
64
  def rainbow_codes
64
- [31,33,32,36,34,35,91,93,92,96,94,95]
65
+ [31, 33, 32, 36, 34, 35, 91, 93, 92, 96, 94, 95]
65
66
  end
66
67
 
67
68
  def expand_vars(str)
@@ -69,21 +70,21 @@ def expand_vars(str)
69
70
  end
70
71
 
71
72
  def parse_redirection(cmd)
72
- stdin_file = nil
73
+ stdin_file = nil
73
74
  stdout_file = nil
74
- append = false
75
+ append = false
75
76
 
76
77
  if cmd =~ /(.*)>>\s*(\S+)/
77
- cmd = $1.strip
78
+ cmd = $1.strip
78
79
  stdout_file = $2.strip
79
- append = true
80
+ append = true
80
81
  elsif cmd =~ /(.*)>\s*(\S+)/
81
- cmd = $1.strip
82
+ cmd = $1.strip
82
83
  stdout_file = $2.strip
83
84
  end
84
85
 
85
86
  if cmd =~ /(.*)<\s*(\S+)/
86
- cmd = $1.strip
87
+ cmd = $1.strip
87
88
  stdin_file = $2.strip
88
89
  end
89
90
 
@@ -91,12 +92,12 @@ def parse_redirection(cmd)
91
92
  end
92
93
 
93
94
  def human_bytes(bytes)
94
- units = ['B','KB','MB','GB','TB']
95
- size = bytes.to_f
96
- unit = units.shift
95
+ units = ['B', 'KB', 'MB', 'GB', 'TB']
96
+ size = bytes.to_f
97
+ unit = units.shift
97
98
  while size > 1024 && !units.empty?
98
99
  size /= 1024
99
- unit = units.shift
100
+ unit = units.shift
100
101
  end
101
102
  "#{format('%.2f', size)} #{unit}"
102
103
  end
@@ -117,207 +118,344 @@ end
117
118
 
118
119
  def strip_ansi(str)
119
120
  str.to_s.gsub(/\e\[[0-9;]*m/, '')
120
- end
121
-
122
- # ---------------- Aliases ----------------
123
- def expand_aliases(cmd, seen = [])
124
- return cmd if cmd.nil? || cmd.strip.empty?
125
- first_word, rest = cmd.strip.split(' ', 2)
126
- return cmd if seen.include?(first_word)
127
- seen << first_word
128
-
129
- if $aliases.key?(first_word)
130
- replacement = $aliases[first_word]
131
- expanded = expand_aliases(replacement, seen)
132
- rest ? "#{expanded} #{rest}" : expanded
133
- else
134
- cmd
135
- end
136
- end
137
-
138
- # ---------------- System Info ----------------
139
- def current_time
140
- Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")
141
- end
142
-
143
- def detect_distro
144
- if File.exist?('/etc/os-release')
145
- line = File.read('/etc/os-release').lines.find { |l|
146
- l.start_with?('PRETTY_NAME="') || l.start_with?('PRETTY_NAME=')
147
- }
148
- return line.split('=').last.strip.delete('"') if line
149
- end
150
- "#{RbConfig::CONFIG['host_os']}"
151
- end
152
-
153
- # ---------------- Quotes ----------------
154
- QUOTES = [
155
- "Keep calm and code on.",
156
- "Did you try turning it off and on again?",
157
- "There’s no place like 127.0.0.1.",
158
- "To iterate is human, to recurse divine.",
159
- "sudo rm -rf / – Just kidding, don’t do that!",
160
- "The shell is mightier than the sword.",
161
- "A journey of a thousand commits begins with a single push.",
162
- "In case of fire: git commit, git push, leave building.",
163
- "Debugging is like being the detective in a crime movie where you are also the murderer.",
164
- "Unix is user-friendly. It's just selective about who its friends are.",
165
- "Old sysadmins never die, they just become daemons.",
166
- "Listen you flatpaker! – Totally Terry Davis",
167
- "How is #{detect_distro}? 🤔",
168
- "Life is short, but your command history is eternal.",
169
- "If at first you don’t succeed, git commit and push anyway.",
170
- "rm -rf: the ultimate trust exercise.",
171
- "Coding is like magic, but with more coffee.",
172
- "There’s no bug, only undocumented features.",
173
- "Keep your friends close and your aliases closer.",
174
- "Why wait for the future when you can Ctrl+Z it?",
175
- "A watched process never completes.",
176
- "When in doubt, make it a function.",
177
- "Some call it procrastination, we call it debugging curiosity.",
178
- "Life is like a terminal; some commands just don’t execute.",
179
- "Good code is like a good joke; it needs no explanation.",
180
- "sudo: because sometimes responsibility is overrated.",
181
- "Pipes make the world go round.",
182
- "In bash we trust, in Ruby we wonder.",
183
- "A system without errors is like a day without coffee.",
184
- "Keep your loops tight and your sleeps short.",
185
- "Stack traces are just life giving you directions.",
186
- "Your mom called, she wants her semicolons back."
187
- ]
188
-
189
- $current_quote = QUOTES.sample
190
-
191
- def dynamic_quote
192
- chars = $current_quote.chars
193
- rainbow = rainbow_codes.cycle
194
- chars.map { |c| color(c, rainbow.next) }.join
195
- end
196
-
197
- # ---------------- CPU / RAM / Storage ----------------
198
- def read_cpu_times
199
- return [] unless File.exist?('/proc/stat')
200
- cpu_line = File.readlines('/proc/stat').find { |line| line.start_with?('cpu ') }
201
- return [] unless cpu_line
202
- cpu_line.split[1..-1].map(&:to_i)
203
- end
204
-
205
- def calculate_cpu_usage(prev, current)
206
- return 0.0 if prev.empty? || current.empty?
207
- prev_idle = prev[3] + (prev[4] || 0)
208
- idle = current[3] + (current[4] || 0)
209
- prev_non_idle = prev[0] + prev[1] + prev[2] + (prev[5] || 0) + (prev[6] || 0) + (prev[7] || 0)
210
- non_idle = current[0] + current[1] + current[2] + (current[5] || 0) + (current[6] || 0) + (current[7] || 0)
211
- prev_total = prev_idle + prev_non_idle
212
- total = idle + non_idle
213
- totald = total - prev_total
214
- idled = idle - prev_idle
215
- return 0.0 if totald <= 0
216
- ((totald - idled).to_f / totald) * 100
217
- end
218
-
219
- def cpu_cores_and_freq
220
- return [0, []] unless File.exist?('/proc/cpuinfo')
221
- cores = 0
222
- freqs = []
223
- File.foreach('/proc/cpuinfo') do |line|
224
- cores += 1 if line =~ /^processor\s*:\s*\d+/
225
- if line =~ /^cpu MHz\s*:\s*([\d.]+)/
226
- freqs << $1.to_f
227
- end
228
- end
229
- [cores, freqs.first(cores)]
230
- end
231
-
232
- def cpu_info
233
- prev = read_cpu_times
234
- sleep 0.05
235
- current = read_cpu_times
236
- usage = calculate_cpu_usage(prev, current).round(1)
237
- cores, freqs = cpu_cores_and_freq
238
- freq_display = freqs.empty? ? "N/A" : freqs.map { |f| "#{f.round(0)}MHz" }.join(', ')
239
- "#{color("CPU Usage:",36)} #{color("#{usage}%",33)} | " \
240
- "#{color("Cores:",36)} #{color(cores.to_s,32)} | " \
241
- "#{color("Freqs:",36)} #{color(freq_display,35)}"
242
- end
243
-
244
- def ram_info
245
- if File.exist?('/proc/meminfo')
246
- meminfo = {}
247
- File.read('/proc/meminfo').each_line do |line|
248
- key, val = line.split(':')
249
- meminfo[key.strip] = val.strip.split.first.to_i * 1024 if key && val
250
- end
251
- total = meminfo['MemTotal'] || 0
252
- free = (meminfo['MemFree'] || 0) + (meminfo['Buffers'] || 0) + (meminfo['Cached'] || 0)
253
- used = total - free
254
- "#{color("RAM Usage:",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
255
- else
256
- "#{color("RAM Usage:",36)} Info not available"
257
- end
258
- end
259
-
260
- def storage_info
261
- begin
262
- require 'sys/filesystem'
263
- stat = Sys::Filesystem.stat(Dir.pwd)
264
- total = stat.bytes_total
265
- free = stat.bytes_available
266
- used = total - free
267
- "#{color("Storage Usage (#{Dir.pwd}):",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
268
- rescue LoadError
269
- "#{color("Install 'sys-filesystem' gem for storage info:",31)} #{color('gem install sys-filesystem',33)}"
270
- rescue
271
- "#{color("Storage Usage:",36)} Info not available"
272
- end
273
- end
274
-
275
- # ---------------- Builtin helpers ----------------
276
- def builtin_help
277
- puts color('=' * 60, "1;35")
278
- puts color("srsh #{SRSH_VERSION} - Builtin Commands", "1;33")
279
- puts color(sprintf("%-15s%-45s", "Command", "Description"), "1;36")
280
- puts color('-' * 60, "1;34")
281
- puts color(sprintf("%-15s", "cd"), "1;36") + "Change directory"
282
- puts color(sprintf("%-15s", "pwd"), "1;36") + "Print working directory"
283
- puts color(sprintf("%-15s", "exit / quit"), "1;36") + "Exit the shell"
284
- puts color(sprintf("%-15s", "alias"), "1;36") + "Create or list aliases"
285
- puts color(sprintf("%-15s", "unalias"), "1;36") + "Remove alias"
286
- puts color(sprintf("%-15s", "jobs"), "1;36") + "Show background jobs (tracked pids)"
287
- puts color(sprintf("%-15s", "systemfetch"), "1;36") + "Display system information"
288
- puts color(sprintf("%-15s", "hist"), "1;36") + "Show shell history"
289
- puts color(sprintf("%-15s", "clearhist"), "1;36") + "Clear saved history (memory + file)"
290
- puts color(sprintf("%-15s", "help"), "1;36") + "Show this help message"
291
- puts color('=' * 60, "1;35")
292
- end
293
-
294
- def builtin_systemfetch
295
- user = ENV['USER'] || Etc.getlogin || Etc.getpwuid.name rescue ENV['USER'] || Etc.getlogin
296
- host = Socket.gethostname
297
- os = detect_distro
298
- ruby_ver = RUBY_VERSION
299
- cpu_percent = begin
300
- prev = read_cpu_times
301
- sleep 0.05
302
- cur = read_cpu_times
303
- calculate_cpu_usage(prev, cur).round(1)
304
- rescue
305
- 0.0
306
- end
307
-
308
- mem_percent = begin
309
- if File.exist?('/proc/meminfo')
310
- meminfo = {}
311
- File.read('/proc/meminfo').each_line do |line|
312
- k, v = line.split(':')
313
- meminfo[k.strip] = v.strip.split.first.to_i * 1024 if k && v
314
- end
315
- total = meminfo['MemTotal'] || 1
316
- free = (meminfo['MemAvailable'] || meminfo['MemFree'] || 0)
317
- used = total - free
318
- (used.to_f / total.to_f * 100).round(1)
319
- else
320
- 0.0
121
+ end
122
+
123
+ # ---------------- Aliases ----------------
124
+ def expand_aliases(cmd, seen = [])
125
+ return cmd if cmd.nil? || cmd.strip.empty?
126
+ first_word, rest = cmd.strip.split(' ', 2)
127
+ return cmd if seen.include?(first_word)
128
+ seen << first_word
129
+
130
+ if $aliases.key?(first_word)
131
+ replacement = $aliases[first_word]
132
+ expanded = expand_aliases(replacement, seen)
133
+ rest ? "#{expanded} #{rest}" : expanded
134
+ else
135
+ cmd
136
+ end
137
+ end
138
+
139
+ # ---------------- System Info ----------------
140
+ def current_time
141
+ Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")
142
+ end
143
+
144
+ def detect_distro
145
+ if File.exist?('/etc/os-release')
146
+ line = File.read('/etc/os-release').lines.find { |l|
147
+ l.start_with?('PRETTY_NAME="') || l.start_with?('PRETTY_NAME=')
148
+ }
149
+ return line.split('=').last.strip.delete('"') if line
150
+ end
151
+ "#{RbConfig::CONFIG['host_os']}"
152
+ end
153
+
154
+ def os_type
155
+ host = RbConfig::CONFIG['host_os'].to_s
156
+ case host
157
+ when /linux/i
158
+ :linux
159
+ when /darwin/i
160
+ :mac
161
+ when /bsd/i
162
+ :bsd
163
+ else
164
+ :other
165
+ end
166
+ end
167
+
168
+ # ---------------- Quotes ----------------
169
+ QUOTES = [
170
+ "Keep calm and code on.",
171
+ "Did you try turning it off and on again?",
172
+ "There’s no place like 127.0.0.1.",
173
+ "To iterate is human, to recurse divine.",
174
+ "sudo rm -rf / Just kidding, don’t do that!",
175
+ "The shell is mightier than the sword.",
176
+ "A journey of a thousand commits begins with a single push.",
177
+ "In case of fire: git commit, git push, leave building.",
178
+ "Debugging is like being the detective in a crime movie where you are also the murderer.",
179
+ "Unix is user-friendly. It's just selective about who its friends are.",
180
+ "Old sysadmins never die, they just become daemons.",
181
+ "Listen you flatpaker! Totally Terry Davis",
182
+ "How is #{detect_distro}? 🤔",
183
+ "Life is short, but your command history is eternal.",
184
+ "If at first you don’t succeed, git commit and push anyway.",
185
+ "rm -rf: the ultimate trust exercise.",
186
+ "Coding is like magic, but with more coffee.",
187
+ "There’s no bug, only undocumented features.",
188
+ "Keep your friends close and your aliases closer.",
189
+ "Why wait for the future when you can Ctrl+Z it?",
190
+ "A watched process never completes.",
191
+ "When in doubt, make it a function.",
192
+ "Some call it procrastination, we call it debugging curiosity.",
193
+ "Life is like a terminal; some commands just don’t execute.",
194
+ "Good code is like a good joke; it needs no explanation.",
195
+ "sudo: because sometimes responsibility is overrated.",
196
+ "Pipes make the world go round.",
197
+ "In bash we trust, in Ruby we wonder.",
198
+ "A system without errors is like a day without coffee.",
199
+ "Keep your loops tight and your sleeps short.",
200
+ "Stack traces are just life giving you directions.",
201
+ "Your mom called, she wants her semicolons back."
202
+ ]
203
+
204
+ $current_quote = QUOTES.sample
205
+
206
+ def dynamic_quote
207
+ chars = $current_quote.chars
208
+ rainbow = rainbow_codes.cycle
209
+ chars.map { |c| color(c, rainbow.next) }.join
210
+ end
211
+
212
+ # ---------------- CPU / RAM / Storage ----------------
213
+ def read_cpu_times
214
+ return [] unless File.exist?('/proc/stat')
215
+ cpu_line = File.readlines('/proc/stat').find { |line| line.start_with?('cpu ') }
216
+ return [] unless cpu_line
217
+ cpu_line.split[1..-1].map(&:to_i)
218
+ end
219
+
220
+ def calculate_cpu_usage(prev, current)
221
+ return 0.0 if prev.empty? || current.empty?
222
+ prev_idle = prev[3] + (prev[4] || 0)
223
+ idle = current[3] + (current[4] || 0)
224
+ prev_non_idle = prev[0] + prev[1] + prev[2] +
225
+ (prev[5] || 0) + (prev[6] || 0) + (prev[7] || 0)
226
+ non_idle = current[0] + current[1] + current[2] +
227
+ (current[5] || 0) + (current[6] || 0) + (current[7] || 0)
228
+ prev_total = prev_idle + prev_non_idle
229
+ total = idle + non_idle
230
+ totald = total - prev_total
231
+ idled = idle - prev_idle
232
+ return 0.0 if totald <= 0
233
+ ((totald - idled).to_f / totald) * 100
234
+ end
235
+
236
+ def cpu_cores_and_freq
237
+ return [0, []] unless File.exist?('/proc/cpuinfo')
238
+ cores = 0
239
+ freqs = []
240
+ File.foreach('/proc/cpuinfo') do |line|
241
+ cores += 1 if line =~ /^processor\s*:\s*\d+/
242
+ if line =~ /^cpu MHz\s*:\s*([\d.]+)/
243
+ freqs << $1.to_f
244
+ end
245
+ end
246
+ [cores, freqs.first(cores)]
247
+ end
248
+
249
+ def cpu_info
250
+ usage = 0.0
251
+ cores = 0
252
+ freq_display = "N/A"
253
+
254
+ case os_type
255
+ when :linux
256
+ prev = read_cpu_times
257
+ sleep 0.05
258
+ current = read_cpu_times
259
+ usage = calculate_cpu_usage(prev, current).round(1)
260
+ cores, freqs = cpu_cores_and_freq
261
+ freq_display = freqs.empty? ? "N/A" : freqs.map { |f| "#{f.round(0)}MHz" }.join(', ')
262
+ else
263
+ cores = begin
264
+ `sysctl -n hw.ncpu 2>/dev/null`.to_i
265
+ rescue
266
+ 0
267
+ end
268
+
269
+ raw_freq_hz = begin
270
+ `sysctl -n hw.cpufrequency 2>/dev/null`.to_i
271
+ rescue
272
+ 0
273
+ end
274
+
275
+ freq_display =
276
+ if raw_freq_hz > 0
277
+ mhz = (raw_freq_hz.to_f / 1_000_000.0).round(0)
278
+ "#{mhz.to_i}MHz"
279
+ else
280
+ "N/A"
281
+ end
282
+
283
+ usage = begin
284
+ ps_output = `ps -A -o %cpu 2>/dev/null`
285
+ lines = ps_output.lines
286
+ values = lines[1..-1] || []
287
+ sum = values.map { |l| l.to_f }.inject(0.0, :+)
288
+ if cores > 0
289
+ (sum / cores).round(1)
290
+ else
291
+ sum.round(1)
292
+ end
293
+ rescue
294
+ 0.0
295
+ end
296
+ end
297
+
298
+ "#{color("CPU Usage:",36)} #{color("#{usage}%",33)} | " \
299
+ "#{color("Cores:",36)} #{color(cores.to_s,32)} | " \
300
+ "#{color("Freqs:",36)} #{color(freq_display,35)}"
301
+ end
302
+
303
+ def ram_info
304
+ case os_type
305
+ when :linux
306
+ if File.exist?('/proc/meminfo')
307
+ meminfo = {}
308
+ File.read('/proc/meminfo').each_line do |line|
309
+ key, val = line.split(':')
310
+ meminfo[key.strip] = val.strip.split.first.to_i * 1024 if key && val
311
+ end
312
+ total = meminfo['MemTotal'] || 0
313
+ free = (meminfo['MemFree'] || 0) + (meminfo['Buffers'] || 0) + (meminfo['Cached'] || 0)
314
+ used = total - free
315
+ "#{color("RAM Usage:",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
316
+ else
317
+ "#{color("RAM Usage:",36)} Info not available"
318
+ end
319
+ else
320
+ begin
321
+ if os_type == :mac
322
+ total = `sysctl -n hw.memsize 2>/dev/null`.to_i
323
+ return "#{color("RAM Usage:",36)} Info not available" if total <= 0
324
+
325
+ vm = `vm_stat 2>/dev/null`
326
+ page_size = vm[/page size of (\d+) bytes/, 1].to_i
327
+ page_size = 4096 if page_size <= 0
328
+
329
+ stats = {}
330
+ vm.each_line do |line|
331
+ if line =~ /^(.+):\s+(\d+)\./
332
+ stats[$1] = $2.to_i
333
+ end
334
+ end
335
+
336
+ used_pages = 0
337
+ %w[Pages active Pages wired down Pages occupied by compressor].each do |k|
338
+ used_pages += stats[k].to_i
339
+ end
340
+ used = used_pages * page_size
341
+
342
+ "#{color("RAM Usage:",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
343
+ else
344
+ total = `sysctl -n hw.physmem 2>/dev/null`.to_i
345
+ total = `sysctl -n hw.realmem 2>/dev/null`.to_i if total <= 0
346
+ return "#{color("RAM Usage:",36)} Info not available" if total <= 0
347
+ "#{color("RAM Usage:",36)} #{color("Unknown",33)} / #{color(human_bytes(total),32)}"
348
+ end
349
+ rescue
350
+ "#{color("RAM Usage:",36)} Info not available"
351
+ end
352
+ end
353
+ end
354
+
355
+ def storage_info
356
+ begin
357
+ require 'sys/filesystem'
358
+ stat = Sys::Filesystem.stat(Dir.pwd)
359
+ total = stat.bytes_total
360
+ free = stat.bytes_available
361
+ used = total - free
362
+ "#{color("Storage Usage (#{Dir.pwd}):",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
363
+ rescue LoadError
364
+ "#{color("Install 'sys-filesystem' gem for storage info:",31)} #{color('gem install sys-filesystem',33)}"
365
+ rescue
366
+ "#{color("Storage Usage:",36)} Info not available"
367
+ end
368
+ end
369
+
370
+ # ---------------- Builtin helpers ----------------
371
+ def builtin_help
372
+ puts color('=' * 60, "1;35")
373
+ puts color("srsh #{SRSH_VERSION} - Builtin Commands", "1;33")
374
+ puts color(sprintf("%-15s%-45s", "Command", "Description"), "1;36")
375
+ puts color('-' * 60, "1;34")
376
+ puts color(sprintf("%-15s", "cd"), "1;36") + "Change directory"
377
+ puts color(sprintf("%-15s", "pwd"), "1;36") + "Print working directory"
378
+ puts color(sprintf("%-15s", "exit / quit"), "1;36") + "Exit the shell"
379
+ puts color(sprintf("%-15s", "alias"), "1;36") + "Create or list aliases"
380
+ puts color(sprintf("%-15s", "unalias"), "1;36") + "Remove alias"
381
+ puts color(sprintf("%-15s", "jobs"), "1;36") + "Show background jobs (tracked pids)"
382
+ puts color(sprintf("%-15s", "systemfetch"), "1;36") + "Display system information"
383
+ puts color(sprintf("%-15s", "hist"), "1;36") + "Show shell history"
384
+ puts color(sprintf("%-15s", "clearhist"), "1;36") + "Clear saved history (memory + file)"
385
+ puts color(sprintf("%-15s", "help"), "1;36") + "Show this help message"
386
+ puts color('=' * 60, "1;35")
387
+ end
388
+
389
+ def builtin_systemfetch
390
+ user = ENV['USER'] || Etc.getlogin || Etc.getpwuid.name rescue ENV['USER'] || Etc.getlogin
391
+ host = Socket.gethostname
392
+ os = detect_distro
393
+ ruby_ver = RUBY_VERSION
394
+
395
+ cpu_percent = begin
396
+ case os_type
397
+ when :linux
398
+ prev = read_cpu_times
399
+ sleep 0.05
400
+ cur = read_cpu_times
401
+ calculate_cpu_usage(prev, cur).round(1)
402
+ else
403
+ cores = `sysctl -n hw.ncpu 2>/dev/null`.to_i rescue 0
404
+ ps_output = `ps -A -o %cpu 2>/dev/null`
405
+ lines = ps_output.lines
406
+ values = lines[1..-1] || []
407
+ sum = values.map { |l| l.to_f }.inject(0.0, :+)
408
+ if cores > 0
409
+ (sum / cores).round(1)
410
+ else
411
+ sum.round(1)
412
+ end
413
+ end
414
+ rescue
415
+ 0.0
416
+ end
417
+
418
+ mem_percent = begin
419
+ case os_type
420
+ when :linux
421
+ if File.exist?('/proc/meminfo')
422
+ meminfo = {}
423
+ File.read('/proc/meminfo').each_line do |line|
424
+ k, v = line.split(':')
425
+ meminfo[k.strip] = v.strip.split.first.to_i * 1024 if k && v
426
+ end
427
+ total = meminfo['MemTotal'] || 1
428
+ free = (meminfo['MemAvailable'] || meminfo['MemFree'] || 0)
429
+ used = total - free
430
+ (used.to_f / total.to_f * 100).round(1)
431
+ else
432
+ 0.0
433
+ end
434
+ when :mac
435
+ total = `sysctl -n hw.memsize 2>/dev/null`.to_i
436
+ if total <= 0
437
+ 0.0
438
+ else
439
+ vm = `vm_stat 2>/dev/null`
440
+ page_size = vm[/page size of (\d+) bytes/, 1].to_i
441
+ page_size = 4096 if page_size <= 0
442
+
443
+ stats = {}
444
+ vm.each_line do |line|
445
+ if line =~ /^(.+):\s+(\d+)\./
446
+ stats[$1] = $2.to_i
447
+ end
448
+ end
449
+
450
+ used_pages = 0
451
+ %w[Pages active Pages wired down Pages occupied by compressor].each do |k|
452
+ used_pages += stats[k].to_i
453
+ end
454
+ used = used_pages * page_size
455
+ ((used.to_f / total.to_f) * 100).round(1)
456
+ end
457
+ else
458
+ 0.0
321
459
  end
322
460
  rescue
323
461
  0.0
@@ -373,19 +511,19 @@ end
373
511
  def print_columns_colored(labels)
374
512
  return if labels.nil? || labels.empty?
375
513
 
376
- width = terminal_width
514
+ width = terminal_width
377
515
  visible_lengths = labels.map { |s| strip_ansi(s).length }
378
- max_len = visible_lengths.max || 0
379
- col_width = [max_len + 2, 4].max
380
- cols = [width / col_width, 1].max
381
- rows = (labels.length.to_f / cols).ceil
516
+ max_len = visible_lengths.max || 0
517
+ col_width = [max_len + 2, 4].max
518
+ cols = [width / col_width, 1].max
519
+ rows = (labels.length.to_f / cols).ceil
382
520
 
383
521
  rows.times do |r|
384
522
  line = ""
385
523
  cols.times do |c|
386
524
  idx = c * rows + r
387
525
  break if idx >= labels.length
388
- label = labels[idx]
526
+ label = labels[idx]
389
527
  visible = strip_ansi(label).length
390
528
  padding = col_width - visible
391
529
  line << label << (" " * padding)
@@ -554,7 +692,7 @@ def run_input_line(input)
554
692
  end
555
693
 
556
694
  # ---------------- Prompt ----------------
557
- hostname = Socket.gethostname
695
+ hostname = Socket.gethostname
558
696
  prompt_color = random_color
559
697
 
560
698
  def prompt(hostname, prompt_color)
@@ -577,15 +715,15 @@ end
577
715
  def tab_completions_for(prefix, first_word, at_first_word)
578
716
  prefix ||= ""
579
717
 
580
- dir = "."
718
+ dir = "."
581
719
  base = prefix
582
720
 
583
721
  if prefix.include?('/')
584
722
  if prefix.end_with?('/')
585
- dir = prefix.chomp('/')
723
+ dir = prefix.chomp('/')
586
724
  base = ""
587
725
  else
588
- dir = File.dirname(prefix)
726
+ dir = File.dirname(prefix)
589
727
  base = File.basename(prefix)
590
728
  end
591
729
  dir = "." if dir.nil? || dir.empty?
@@ -646,7 +784,7 @@ def longest_common_prefix(strings)
646
784
  end
647
785
 
648
786
  def render_line(prompt_str, buffer, cursor, show_ghost = true)
649
- buffer ||= ""
787
+ buffer = buffer || ""
650
788
  cursor = [[cursor, 0].max, buffer.length].min
651
789
 
652
790
  ghost_tail = ""
@@ -655,8 +793,27 @@ def render_line(prompt_str, buffer, cursor, show_ghost = true)
655
793
  ghost_tail = suggestion ? suggestion[buffer.length..-1].to_s : ""
656
794
  end
657
795
 
796
+ width = terminal_width
797
+ prompt_vis = strip_ansi(prompt_str).length
798
+ total_vis = prompt_vis + buffer.length + ghost_tail.length
799
+ rows = [(total_vis.to_f / width).ceil, 1].max
800
+
801
+ # Clear previous render block (only what we drew last time)
802
+ if $last_render_rows && $last_render_rows > 0
803
+ STDOUT.print("\r")
804
+ ($last_render_rows - 1).times do
805
+ STDOUT.print("\e[1A\r") # move up a line, to column 0
806
+ end
807
+ $last_render_rows.times do |i|
808
+ STDOUT.print("\e[0K") # clear this line
809
+ STDOUT.print("\n") if i < $last_render_rows - 1
810
+ end
811
+ ($last_render_rows - 1).times do
812
+ STDOUT.print("\e[1A\r") # move back up to first line of block
813
+ end
814
+ end
815
+
658
816
  STDOUT.print("\r")
659
- STDOUT.print("\e[0K")
660
817
  STDOUT.print(prompt_str)
661
818
  STDOUT.print(buffer)
662
819
  STDOUT.print(color(ghost_tail, "2")) unless ghost_tail.empty?
@@ -664,17 +821,19 @@ def render_line(prompt_str, buffer, cursor, show_ghost = true)
664
821
  move_left = ghost_tail.length + (buffer.length - cursor)
665
822
  STDOUT.print("\e[#{move_left}D") if move_left > 0
666
823
  STDOUT.flush
824
+
825
+ $last_render_rows = rows
667
826
  end
668
827
 
669
828
  # --------- NEAT MULTI-COLUMN TAB LIST (bash-style) ----------
670
829
  def print_tab_list(comps)
671
830
  return if comps.empty?
672
831
 
673
- width = terminal_width
674
- max_len = comps.map { |s| s.length }.max || 0
832
+ width = terminal_width
833
+ max_len = comps.map { |s| s.length }.max || 0
675
834
  col_width = [max_len + 2, 4].max
676
- cols = [width / col_width, 1].max
677
- rows = (comps.length.to_f / cols).ceil
835
+ cols = [width / col_width, 1].max
836
+ rows = (comps.length.to_f / cols).ceil
678
837
 
679
838
  STDOUT.print("\r\n")
680
839
  rows.times do |r|
@@ -682,9 +841,9 @@ def print_tab_list(comps)
682
841
  cols.times do |c|
683
842
  idx = c * rows + r
684
843
  break if idx >= comps.length
685
- item = comps[idx]
844
+ item = comps[idx]
686
845
  padding = col_width - item.length
687
- line << item << (" " * padding)
846
+ line << item << (" " * padding)
688
847
  end
689
848
  STDOUT.print("\r")
690
849
  STDOUT.print(line.rstrip)
@@ -695,24 +854,24 @@ def print_tab_list(comps)
695
854
  end
696
855
 
697
856
  def handle_tab_completion(prompt_str, buffer, cursor, last_tab_prefix, tab_cycle)
698
- buffer ||= ""
857
+ buffer = buffer || ""
699
858
  cursor = [[cursor, 0].max, buffer.length].min
700
859
 
701
860
  wstart = buffer.rindex(/[ \t]/, cursor - 1) || -1
702
861
  wstart += 1
703
862
  prefix = buffer[wstart...cursor] || ""
704
863
 
705
- before_word = buffer[0...wstart]
864
+ before_word = buffer[0...wstart]
706
865
  at_first_word = before_word.strip.empty?
707
- first_word = buffer.strip.split(/\s+/, 2)[0] || ""
866
+ first_word = buffer.strip.split(/\s+/, 2)[0] || ""
708
867
 
709
868
  comps = tab_completions_for(prefix, first_word, at_first_word)
710
869
  return [buffer, cursor, nil, 0, false] if comps.empty?
711
870
 
712
871
  if comps.size == 1
713
872
  new_word = comps.first
714
- buffer = buffer[0...wstart] + new_word + buffer[cursor..-1].to_s
715
- cursor = wstart + new_word.length
873
+ buffer = buffer[0...wstart] + new_word + buffer[cursor..-1].to_s
874
+ cursor = wstart + new_word.length
716
875
  return [buffer, cursor, nil, 0, true]
717
876
  end
718
877
 
@@ -725,25 +884,25 @@ def handle_tab_completion(prompt_str, buffer, cursor, last_tab_prefix, tab_cycle
725
884
  STDOUT.print("\a")
726
885
  end
727
886
  last_tab_prefix = prefix
728
- tab_cycle = 1
887
+ tab_cycle = 1
729
888
  return [buffer, cursor, last_tab_prefix, tab_cycle, false]
730
889
  else
731
- # Second tab on same prefix: show list (after erasing ghost on current line)
890
+ # Second tab on same prefix: show list
732
891
  render_line(prompt_str, buffer, cursor, false)
733
892
  print_tab_list(comps)
734
893
  last_tab_prefix = prefix
735
- tab_cycle += 1
894
+ tab_cycle += 1
736
895
  return [buffer, cursor, last_tab_prefix, tab_cycle, true]
737
896
  end
738
897
  end
739
898
 
740
899
  def read_line_with_ghost(prompt_str)
741
- buffer = ""
742
- cursor = 0
743
- hist_index = HISTORY.length
900
+ buffer = ""
901
+ cursor = 0
902
+ hist_index = HISTORY.length
744
903
  saved_line_for_history = ""
745
- last_tab_prefix = nil
746
- tab_cycle = 0
904
+ last_tab_prefix = nil
905
+ tab_cycle = 0
747
906
 
748
907
  render_line(prompt_str, buffer, cursor)
749
908
 
@@ -782,10 +941,13 @@ def read_line_with_ghost(prompt_str)
782
941
  cursor -= 1
783
942
  end
784
943
  last_tab_prefix = nil
785
- tab_cycle = 0
944
+ tab_cycle = 0
786
945
  when "\t" # Tab completion
787
- buffer, cursor, last_tab_prefix, tab_cycle, _printed =
946
+ buffer, cursor, last_tab_prefix, tab_cycle, printed =
788
947
  handle_tab_completion(prompt_str, buffer, cursor, last_tab_prefix, tab_cycle)
948
+ # After showing completion list, reset render rows so the next prompt
949
+ # redraw only clears the current input line, not the completion block.
950
+ $last_render_rows = 1 if printed
789
951
  when "\e" # Escape sequences (arrows, home/end)
790
952
  seq1 = io.getch
791
953
  seq2 = io.getch
@@ -797,18 +959,18 @@ def read_line_with_ghost(prompt_str)
797
959
  end
798
960
  if hist_index > 0
799
961
  hist_index -= 1
800
- buffer = HISTORY[hist_index] || ""
801
- cursor = buffer.length
962
+ buffer = HISTORY[hist_index] || ""
963
+ cursor = buffer.length
802
964
  end
803
965
  when "B" # Down
804
966
  if hist_index < HISTORY.length - 1
805
967
  hist_index += 1
806
- buffer = HISTORY[hist_index] || ""
807
- cursor = buffer.length
968
+ buffer = HISTORY[hist_index] || ""
969
+ cursor = buffer.length
808
970
  elsif hist_index == HISTORY.length - 1
809
971
  hist_index = HISTORY.length
810
- buffer = saved_line_for_history || ""
811
- cursor = buffer.length
972
+ buffer = saved_line_for_history || ""
973
+ cursor = buffer.length
812
974
  end
813
975
  when "C" # Right
814
976
  if cursor < buffer.length
@@ -829,14 +991,14 @@ def read_line_with_ghost(prompt_str)
829
991
  end
830
992
  end
831
993
  last_tab_prefix = nil
832
- tab_cycle = 0
994
+ tab_cycle = 0
833
995
  else
834
996
  if ch.ord >= 32 && ch.ord != 127
835
997
  buffer.insert(cursor, ch)
836
998
  cursor += 1
837
- hist_index = HISTORY.length
999
+ hist_index = HISTORY.length
838
1000
  last_tab_prefix = nil
839
- tab_cycle = 0
1001
+ tab_cycle = 0
840
1002
  end
841
1003
  end
842
1004
 
data/lib/srsh/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # lib/srsh/version.rb
2
2
  module Srsh
3
- VERSION = "0.6.0"
3
+ VERSION = "0.6.2"
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.6.0
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - RobertFlexx