srsh 0.6.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.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +17 -0
  3. data/README.md +235 -0
  4. data/bin/srsh +882 -0
  5. data/lib/srsh/version.rb +4 -0
  6. metadata +46 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9e757aeabce3a17e17c7ffcf6c201abbc8345d9b75d349d879781902c2420351
4
+ data.tar.gz: 4f90b0e077c2eae3223bff3a6e9bf53c955b8bcfa9e64dcd43b201b3ecf445db
5
+ SHA512:
6
+ metadata.gz: e34fa22368e54984df32590ff9cfb9ccc550e4a25d5ae6c827ba9d2651b6b8e942cae846e8249950a3576b88afd8c64e531d1b536a39ba140a63d6b9e7ce6dcc
7
+ data.tar.gz: 527072fa0f7f38e9e0358bfc70c57947e2c2dbcbc46ca2e78c713e9d5e5252d1a9ac005c199a90b60fb8ef60561387bc3ea59010c30da6765a76af44c0432b73
data/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Copyright (C) 2025 RobertFlexx
2
+
3
+ This software is provided 'as-is', without any express or implied
4
+ warranty. In no event will the authors be held liable for any damages
5
+ arising from the use of this software.
6
+
7
+ Permission is granted to anyone to use this software for any purpose,
8
+ including commercial applications, and to alter it and redistribute it
9
+ freely, subject to the following restrictions:
10
+
11
+ 1. The origin of this software must not be misrepresented; you must not
12
+ claim that you wrote the original software. If you use this software
13
+ in a product, an acknowledgment in the product documentation would be
14
+ appreciated but is not required.
15
+ 2. Altered source versions must be plainly marked as such, and must not be
16
+ misrepresented as being the original software.
17
+ 3. This notice may not be removed or altered from any source distribution.
data/README.md ADDED
@@ -0,0 +1,235 @@
1
+ This is version 0.6.0, if things dont work, or work optimally. If you notice anything wrong, please consult me.
2
+ (Fixed Control-C, fixed some bugs, added new features (check via help command)
3
+ THIS IS A BETA RELEASE, IT MAY NOT WORK.
4
+
5
+ The code itself is written by RobertFlexx, but the comments are written by ChatGPT.
6
+
7
+ ## Known Issues:
8
+
9
+ * Flatpak chaining with 'and' doesn't work.
10
+ * Running Shell Scripts might not always work. Sometimes it works, other times not so much.
11
+
12
+ ## Please Consult:
13
+
14
+ * if you have any issues with this SRSh version, please post an issue.
15
+
16
+ ## How to Install:
17
+
18
+ ### Clone the repository
19
+
20
+ ```console
21
+ git clone https://github.com/RobertFlexx/RSH
22
+ ```
23
+
24
+ ### Change the directory to where the Ruby Script is located
25
+
26
+ ```console
27
+ cd RSH
28
+ ```
29
+
30
+ ### And finally run it
31
+
32
+ ```console
33
+ ./rsh
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Requirements
39
+
40
+ * Ruby installed (2.7+ is recommended; newer is better).
41
+ * A POSIX-ish terminal (Linux, *BSD, macOS Terminal, iTerm2, etc).
42
+ * `./rsh` needs the executable bit set:
43
+
44
+ ```console
45
+ chmod +x rsh
46
+ ```
47
+
48
+ If `./rsh` complains or behaves oddly, **run it directly in the repo first** before messing with symlinks or PATH.
49
+
50
+ ---
51
+
52
+ ## Basic Usage
53
+
54
+ Once you’re in the repo:
55
+
56
+ ```console
57
+ ./rsh
58
+ ```
59
+
60
+ Inside `srsh` / `rsh` you can:
61
+
62
+ * Use normal commands (`ls`, `cat`, `grep`, etc.).
63
+ * Use built-ins:
64
+
65
+ * `help` – show builtin help with all srsh-specific commands.
66
+ * `systemfetch` – prints system info with nice bars.
67
+ * `hist` – view shell history.
68
+ * `clearhist` – clear history (memory + file).
69
+ * `alias` / `unalias` – manage aliases.
70
+ * Enjoy:
71
+
72
+ * **Autosuggestions** (ghost text from history).
73
+ * **Smart Tab completion**:
74
+
75
+ * Completes commands, files, dirs.
76
+ * `cd` → only directories.
77
+ * `cat` → only files.
78
+
79
+ ---
80
+
81
+ ## Adding `rsh` / `srsh` to your PATH
82
+
83
+ So you don’t have to always `cd` into the repo and run `./rsh`, you can either:
84
+
85
+ 1. Add the repo directory to your `PATH`, or
86
+ 2. Symlink the script into a directory that’s already on your `PATH`.
87
+
88
+ > ⚠️ There is a *system* command called `rsh` on some systems.
89
+ > To avoid conflict, using the name `srsh` for the installed command is usually safer.
90
+
91
+ ### Option 1 — Add the repo directory to PATH (Linux & macOS, bash/zsh)
92
+
93
+ Assuming the repo is at `~/RSH`:
94
+
95
+ ```console
96
+ chmod +x ~/RSH/rsh
97
+ ```
98
+
99
+ #### For `bash` (Linux, older macOS)
100
+
101
+ Add this line to `~/.bashrc` (or `~/.bash_profile` on macOS):
102
+
103
+ ```bash
104
+ export PATH="$HOME/RSH:$PATH"
105
+ ```
106
+
107
+ Then reload:
108
+
109
+ ```console
110
+ source ~/.bashrc
111
+ ```
112
+
113
+ #### For `zsh` (default on modern macOS)
114
+
115
+ Add this line to `~/.zshrc`:
116
+
117
+ ```zsh
118
+ export PATH="$HOME/RSH:$PATH"
119
+ ```
120
+
121
+ Reload:
122
+
123
+ ```console
124
+ source ~/.zshrc
125
+ ```
126
+
127
+ Now you can just run:
128
+
129
+ ```console
130
+ rsh
131
+ # or if you prefer to rename it:
132
+ srsh
133
+ ```
134
+
135
+ ---
136
+
137
+ ### Option 2 — Symlink into `/usr/local/bin` (Linux & macOS)
138
+
139
+ This keeps your PATH clean and gives you a nice command name.
140
+
141
+ From the repo directory:
142
+
143
+ ```console
144
+ chmod +x rsh
145
+ sudo ln -s "$(pwd)/rsh" /usr/local/bin/srsh
146
+ ```
147
+
148
+ Now you can just type:
149
+
150
+ ```console
151
+ srsh
152
+ ```
153
+
154
+ from anywhere.
155
+
156
+ If you really, really want to override the system `rsh` (not recommended):
157
+
158
+ ```console
159
+ sudo ln -s "$(pwd)/rsh" /usr/local/bin/rsh
160
+ ```
161
+
162
+ ---
163
+
164
+ ### *BSD: Adding to PATH
165
+
166
+ On *BSD, the default shell might be `sh`, `ksh`, `csh`, or `tcsh`. Same idea, different config files.
167
+
168
+ Assuming repo at `~/RSH`:
169
+
170
+ ```console
171
+ chmod +x ~/RSH/rsh
172
+ ```
173
+
174
+ #### For `sh` / `ksh` / `ash` / `dash` style shells
175
+
176
+ Add to `~/.profile`:
177
+
178
+ ```sh
179
+ export PATH="$HOME/RSH:$PATH"
180
+ ```
181
+
182
+ Then either log out and back in, or:
183
+
184
+ ```console
185
+ . ~/.profile
186
+ ```
187
+
188
+ #### For `csh` / `tcsh`
189
+
190
+ Edit `~/.cshrc` (or `~/.tcshrc`) and add:
191
+
192
+ ```csh
193
+ set path = ( $HOME/RSH $path )
194
+ ```
195
+
196
+ Reload it:
197
+
198
+ ```console
199
+ source ~/.cshrc
200
+ ```
201
+
202
+ Now you should be able to run:
203
+
204
+ ```console
205
+ rsh
206
+ # or rename / symlink it as srsh if you want:
207
+ srsh
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Tips / Notes
213
+
214
+ * If the command **isn’t found** after editing PATH:
215
+
216
+ * Check which shell you’re actually using:
217
+
218
+ ```console
219
+ echo $SHELL
220
+ ```
221
+ * Make sure you edited the correct rc file for that shell.
222
+ * Print your PATH to confirm:
223
+
224
+ ```console
225
+ echo "$PATH"
226
+ ```
227
+ * If things feel off, run it directly from the repo with:
228
+
229
+ ```console
230
+ ./rsh
231
+ ```
232
+
233
+ to see if the issue is PATH-related or shell-related.
234
+
235
+ And as i say : if anything looks cursed, **consult me and/or open an issue** :D
data/bin/srsh ADDED
@@ -0,0 +1,882 @@
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.6.0"
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
+ $child_pids = []
19
+ $aliases = {}
20
+
21
+ Signal.trap("INT", "IGNORE")
22
+
23
+ # ---------------- History ----------------
24
+ HISTORY_FILE = File.join(Dir.home, ".srsh_history")
25
+ HISTORY = if File.exist?(HISTORY_FILE)
26
+ File.readlines(HISTORY_FILE, chomp: true)
27
+ else
28
+ []
29
+ end
30
+
31
+ at_exit do
32
+ begin
33
+ File.open(HISTORY_FILE, "w") do |f|
34
+ HISTORY.each { |line| f.puts line }
35
+ end
36
+ rescue
37
+ end
38
+ end
39
+
40
+ # ---------------- RC file (create if missing) ----------------
41
+ RC_FILE = File.join(Dir.home, ".srshrc")
42
+ begin
43
+ 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
50
+ end
51
+ rescue
52
+ end
53
+
54
+ # ---------------- Utilities ----------------
55
+ def color(text, code)
56
+ "\e[#{code}m#{text}\e[0m"
57
+ end
58
+
59
+ def random_color
60
+ [31,32,33,34,35,36,37].sample
61
+ end
62
+
63
+ def rainbow_codes
64
+ [31,33,32,36,34,35,91,93,92,96,94,95]
65
+ end
66
+
67
+ def expand_vars(str)
68
+ str.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)/) { ENV[$1] || "" }
69
+ end
70
+
71
+ def parse_redirection(cmd)
72
+ stdin_file = nil
73
+ stdout_file = nil
74
+ append = false
75
+
76
+ if cmd =~ /(.*)>>\s*(\S+)/
77
+ cmd = $1.strip
78
+ stdout_file = $2.strip
79
+ append = true
80
+ elsif cmd =~ /(.*)>\s*(\S+)/
81
+ cmd = $1.strip
82
+ stdout_file = $2.strip
83
+ end
84
+
85
+ if cmd =~ /(.*)<\s*(\S+)/
86
+ cmd = $1.strip
87
+ stdin_file = $2.strip
88
+ end
89
+
90
+ [cmd, stdin_file, stdout_file, append]
91
+ end
92
+
93
+ def human_bytes(bytes)
94
+ units = ['B','KB','MB','GB','TB']
95
+ size = bytes.to_f
96
+ unit = units.shift
97
+ while size > 1024 && !units.empty?
98
+ size /= 1024
99
+ unit = units.shift
100
+ end
101
+ "#{format('%.2f', size)} #{unit}"
102
+ end
103
+
104
+ def nice_bar(p, w = 30, code = 32)
105
+ p = [[p, 0.0].max, 1.0].min
106
+ f = (p * w).round
107
+ b = "█" * f + "░" * (w - f)
108
+ pct = (p * 100).to_i
109
+ "#{color("[#{b}]", code)} #{color(sprintf("%3d%%", pct), 37)}"
110
+ end
111
+
112
+ def terminal_width
113
+ IO.console.winsize[1]
114
+ rescue
115
+ 80
116
+ end
117
+
118
+ def strip_ansi(str)
119
+ 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
321
+ end
322
+ rescue
323
+ 0.0
324
+ end
325
+
326
+ puts color('=' * 60, "1;35")
327
+ puts color("srsh System Information", "1;33")
328
+ puts color("User: ", "1;36") + color("#{user}@#{host}", "0;37")
329
+ puts color("OS: ", "1;36") + color(os, "0;37")
330
+ puts color("Shell: ", "1;36") + color("srsh v#{SRSH_VERSION}", "0;37")
331
+ puts color("Ruby: ", "1;36") + color(ruby_ver, "0;37")
332
+ puts color("CPU Usage: ", "1;36") + nice_bar(cpu_percent / 100.0, 30, 32)
333
+ puts color("RAM Usage: ", "1;36") + nice_bar(mem_percent / 100.0, 30, 35)
334
+ puts color('=' * 60, "1;35")
335
+ end
336
+
337
+ def builtin_jobs
338
+ if $child_pids.empty?
339
+ puts color("No tracked child jobs.", 36)
340
+ return
341
+ end
342
+ $child_pids.each do |pid|
343
+ status = begin
344
+ Process.kill(0, pid)
345
+ 'running'
346
+ rescue Errno::ESRCH
347
+ 'done'
348
+ rescue Errno::EPERM
349
+ 'running'
350
+ end
351
+ puts "[#{pid}] #{status}"
352
+ end
353
+ end
354
+
355
+ def builtin_hist
356
+ HISTORY.each_with_index do |h, i|
357
+ printf "%5d %s\n", i + 1, h
358
+ end
359
+ end
360
+
361
+ def builtin_clearhist
362
+ HISTORY.clear
363
+ if File.exist?(HISTORY_FILE)
364
+ begin
365
+ File.delete(HISTORY_FILE)
366
+ rescue
367
+ end
368
+ end
369
+ puts color("History cleared (memory + file).", 32)
370
+ end
371
+
372
+ # -------- Pretty column printer for colored text (used by ls) --------
373
+ def print_columns_colored(labels)
374
+ return if labels.nil? || labels.empty?
375
+
376
+ width = terminal_width
377
+ 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
382
+
383
+ rows.times do |r|
384
+ line = ""
385
+ cols.times do |c|
386
+ idx = c * rows + r
387
+ break if idx >= labels.length
388
+ label = labels[idx]
389
+ visible = strip_ansi(label).length
390
+ padding = col_width - visible
391
+ line << label << (" " * padding)
392
+ end
393
+ STDOUT.print("\r")
394
+ STDOUT.print(line.rstrip)
395
+ STDOUT.print("\n")
396
+ end
397
+ end
398
+
399
+ def builtin_ls(path = ".")
400
+ begin
401
+ entries = Dir.children(path).sort
402
+ rescue => e
403
+ puts color("ls: #{e.message}", 31)
404
+ return
405
+ end
406
+
407
+ labels = entries.map do |name|
408
+ full = File.join(path, name)
409
+ begin
410
+ if File.directory?(full)
411
+ color("#{name}/", 36)
412
+ elsif File.executable?(full)
413
+ color("#{name}*", 32)
414
+ else
415
+ color(name, 37)
416
+ end
417
+ rescue
418
+ name
419
+ end
420
+ end
421
+
422
+ print_columns_colored(labels)
423
+ end
424
+
425
+ # ---------------- External Execution Helper ----------------
426
+ def exec_external(args, stdin_file, stdout_file, append)
427
+ command_path = args[0]
428
+ if command_path && (command_path.include?('/') || command_path.start_with?('.'))
429
+ begin
430
+ if File.directory?(command_path)
431
+ puts color("srsh: #{command_path}: is a directory", 31)
432
+ return
433
+ end
434
+ rescue
435
+ end
436
+ end
437
+
438
+ pid = fork do
439
+ Signal.trap("INT","DEFAULT")
440
+ if stdin_file
441
+ begin
442
+ STDIN.reopen(File.open(stdin_file,'r'))
443
+ rescue
444
+ end
445
+ end
446
+ if stdout_file
447
+ begin
448
+ STDOUT.reopen(File.open(stdout_file, append ? 'a' : 'w'))
449
+ rescue
450
+ end
451
+ end
452
+ begin
453
+ exec(*args)
454
+ rescue Errno::ENOENT
455
+ puts color("Command not found: #{args[0]}", rainbow_codes.sample)
456
+ exit 127
457
+ rescue Errno::EACCES
458
+ puts color("Permission denied: #{args[0]}", 31)
459
+ exit 126
460
+ end
461
+ end
462
+
463
+ $child_pids << pid
464
+ begin
465
+ Process.wait(pid)
466
+ rescue Interrupt
467
+ ensure
468
+ $child_pids.delete(pid)
469
+ end
470
+ end
471
+
472
+ # ---------------- Command Execution ----------------
473
+ def run_command(cmd)
474
+ cmd = cmd.to_s
475
+ cmd = expand_aliases(cmd.strip)
476
+ cmd = expand_vars(cmd.strip)
477
+ cmd, stdin_file, stdout_file, append = parse_redirection(cmd)
478
+ args = Shellwords.shellsplit(cmd) rescue []
479
+ return if args.empty?
480
+
481
+ case args[0]
482
+ when 'ls'
483
+ if args.length == 1
484
+ builtin_ls(".")
485
+ return
486
+ elsif args.length == 2 && !args[1].start_with?("-")
487
+ builtin_ls(args[1])
488
+ return
489
+ end
490
+ exec_external(args, stdin_file, stdout_file, append)
491
+ return
492
+ when 'cd'
493
+ path = args[1] ? File.expand_path(args[1]) : ENV['HOME']
494
+ if !File.exist?(path)
495
+ puts color("cd: no such file or directory: #{args[1]}", 31)
496
+ elsif !File.directory?(path)
497
+ puts color("cd: not a directory: #{args[1]}", 31)
498
+ else
499
+ Dir.chdir(path)
500
+ end
501
+ return
502
+ when 'exit','quit'
503
+ $child_pids.each { |pid| Process.kill("TERM", pid) rescue nil }
504
+ exit 0
505
+ when 'alias'
506
+ if args[1].nil?
507
+ $aliases.each { |k,v| puts "#{k}='#{v}'" }
508
+ else
509
+ arg = args[1..].join(' ')
510
+ if arg =~ /^(\w+)=([\"']?)(.+?)\2$/
511
+ $aliases[$1] = $3
512
+ else
513
+ puts color("Invalid alias format", 31)
514
+ end
515
+ end
516
+ return
517
+ when 'unalias'
518
+ if args[1]
519
+ $aliases.delete(args[1])
520
+ else
521
+ puts color("unalias: usage: unalias name", 31)
522
+ end
523
+ return
524
+ when 'help'
525
+ builtin_help
526
+ return
527
+ when 'systemfetch'
528
+ builtin_systemfetch
529
+ return
530
+ when 'jobs'
531
+ builtin_jobs
532
+ return
533
+ when 'pwd'
534
+ puts color(Dir.pwd, 36)
535
+ return
536
+ when 'hist'
537
+ builtin_hist
538
+ return
539
+ when 'clearhist'
540
+ builtin_clearhist
541
+ return
542
+ end
543
+
544
+ exec_external(args, stdin_file, stdout_file, append)
545
+ end
546
+
547
+ # ---------------- Chained Commands ----------------
548
+ def run_input_line(input)
549
+ commands = input.split(/&&|;/).map(&:strip)
550
+ commands.each do |cmd|
551
+ next if cmd.empty?
552
+ run_command(cmd)
553
+ end
554
+ end
555
+
556
+ # ---------------- Prompt ----------------
557
+ hostname = Socket.gethostname
558
+ prompt_color = random_color
559
+
560
+ def prompt(hostname, prompt_color)
561
+ "#{color(Dir.pwd,33)} #{color(hostname,36)}#{color(' > ', prompt_color)}"
562
+ end
563
+
564
+ # ---------------- Ghost + Completion Helpers ----------------
565
+ def history_ghost_for(line)
566
+ return nil if line.nil? || line.empty?
567
+ HISTORY.reverse_each do |h|
568
+ next if h.nil? || h.empty?
569
+ next if h.start_with?("[completions:")
570
+ next unless h.start_with?(line)
571
+ next if h == line
572
+ return h
573
+ end
574
+ nil
575
+ end
576
+
577
+ def tab_completions_for(prefix, first_word, at_first_word)
578
+ prefix ||= ""
579
+
580
+ dir = "."
581
+ base = prefix
582
+
583
+ if prefix.include?('/')
584
+ if prefix.end_with?('/')
585
+ dir = prefix.chomp('/')
586
+ base = ""
587
+ else
588
+ dir = File.dirname(prefix)
589
+ base = File.basename(prefix)
590
+ end
591
+ dir = "." if dir.nil? || dir.empty?
592
+ end
593
+
594
+ file_completions = []
595
+ if Dir.exist?(dir)
596
+ Dir.children(dir).each do |entry|
597
+ next unless entry.start_with?(base)
598
+ full = File.join(dir, entry)
599
+
600
+ rel =
601
+ if dir == "."
602
+ entry
603
+ else
604
+ File.join(File.dirname(prefix), entry)
605
+ end
606
+
607
+ case first_word
608
+ when "cd"
609
+ next unless File.directory?(full)
610
+ rel = rel + "/" unless rel.end_with?("/")
611
+ file_completions << rel
612
+ when "cat"
613
+ next unless File.file?(full)
614
+ file_completions << rel
615
+ else
616
+ rel = rel + "/" if File.directory?(full) && !rel.end_with?("/")
617
+ file_completions << rel
618
+ end
619
+ end
620
+ end
621
+
622
+ exec_completions = []
623
+ if first_word != "cat" && first_word != "cd" && at_first_word && !prefix.include?('/')
624
+ path_entries = (ENV['PATH'] || "").split(':')
625
+ execs = path_entries.flat_map do |p|
626
+ Dir.glob("#{p}/*").map { |f|
627
+ File.basename(f) if File.executable?(f) && !File.directory?(f)
628
+ }.compact rescue []
629
+ end
630
+ exec_completions = execs.grep(/^#{Regexp.escape(prefix)}/)
631
+ end
632
+
633
+ (file_completions + exec_completions).uniq
634
+ end
635
+
636
+ def longest_common_prefix(strings)
637
+ return "" if strings.empty?
638
+ shortest = strings.min_by(&:length)
639
+ shortest.length.times do |i|
640
+ c = shortest[i]
641
+ strings.each do |s|
642
+ return shortest[0...i] if s[i] != c
643
+ end
644
+ end
645
+ shortest
646
+ end
647
+
648
+ def render_line(prompt_str, buffer, cursor, show_ghost = true)
649
+ buffer ||= ""
650
+ cursor = [[cursor, 0].max, buffer.length].min
651
+
652
+ ghost_tail = ""
653
+ if show_ghost && cursor == buffer.length
654
+ suggestion = history_ghost_for(buffer)
655
+ ghost_tail = suggestion ? suggestion[buffer.length..-1].to_s : ""
656
+ end
657
+
658
+ STDOUT.print("\r")
659
+ STDOUT.print("\e[0K")
660
+ STDOUT.print(prompt_str)
661
+ STDOUT.print(buffer)
662
+ STDOUT.print(color(ghost_tail, "2")) unless ghost_tail.empty?
663
+
664
+ move_left = ghost_tail.length + (buffer.length - cursor)
665
+ STDOUT.print("\e[#{move_left}D") if move_left > 0
666
+ STDOUT.flush
667
+ end
668
+
669
+ # --------- NEAT MULTI-COLUMN TAB LIST (bash-style) ----------
670
+ def print_tab_list(comps)
671
+ return if comps.empty?
672
+
673
+ width = terminal_width
674
+ max_len = comps.map { |s| s.length }.max || 0
675
+ col_width = [max_len + 2, 4].max
676
+ cols = [width / col_width, 1].max
677
+ rows = (comps.length.to_f / cols).ceil
678
+
679
+ STDOUT.print("\r\n")
680
+ rows.times do |r|
681
+ line = ""
682
+ cols.times do |c|
683
+ idx = c * rows + r
684
+ break if idx >= comps.length
685
+ item = comps[idx]
686
+ padding = col_width - item.length
687
+ line << item << (" " * padding)
688
+ end
689
+ STDOUT.print("\r")
690
+ STDOUT.print(line.rstrip)
691
+ STDOUT.print("\n")
692
+ end
693
+ STDOUT.print("\r\n")
694
+ STDOUT.flush
695
+ end
696
+
697
+ def handle_tab_completion(prompt_str, buffer, cursor, last_tab_prefix, tab_cycle)
698
+ buffer ||= ""
699
+ cursor = [[cursor, 0].max, buffer.length].min
700
+
701
+ wstart = buffer.rindex(/[ \t]/, cursor - 1) || -1
702
+ wstart += 1
703
+ prefix = buffer[wstart...cursor] || ""
704
+
705
+ before_word = buffer[0...wstart]
706
+ at_first_word = before_word.strip.empty?
707
+ first_word = buffer.strip.split(/\s+/, 2)[0] || ""
708
+
709
+ comps = tab_completions_for(prefix, first_word, at_first_word)
710
+ return [buffer, cursor, nil, 0, false] if comps.empty?
711
+
712
+ if comps.size == 1
713
+ new_word = comps.first
714
+ buffer = buffer[0...wstart] + new_word + buffer[cursor..-1].to_s
715
+ cursor = wstart + new_word.length
716
+ return [buffer, cursor, nil, 0, true]
717
+ end
718
+
719
+ if prefix != last_tab_prefix
720
+ lcp = longest_common_prefix(comps)
721
+ if lcp && lcp.length > prefix.length
722
+ buffer = buffer[0...wstart] + lcp + buffer[cursor..-1].to_s
723
+ cursor = wstart + lcp.length
724
+ else
725
+ STDOUT.print("\a")
726
+ end
727
+ last_tab_prefix = prefix
728
+ tab_cycle = 1
729
+ return [buffer, cursor, last_tab_prefix, tab_cycle, false]
730
+ else
731
+ # Second tab on same prefix: show list (after erasing ghost on current line)
732
+ render_line(prompt_str, buffer, cursor, false)
733
+ print_tab_list(comps)
734
+ last_tab_prefix = prefix
735
+ tab_cycle += 1
736
+ return [buffer, cursor, last_tab_prefix, tab_cycle, true]
737
+ end
738
+ end
739
+
740
+ def read_line_with_ghost(prompt_str)
741
+ buffer = ""
742
+ cursor = 0
743
+ hist_index = HISTORY.length
744
+ saved_line_for_history = ""
745
+ last_tab_prefix = nil
746
+ tab_cycle = 0
747
+
748
+ render_line(prompt_str, buffer, cursor)
749
+
750
+ status = :ok
751
+
752
+ IO.console.raw do |io|
753
+ loop do
754
+ ch = io.getch
755
+
756
+ case ch
757
+ when "\r", "\n"
758
+ cursor = buffer.length
759
+ render_line(prompt_str, buffer, cursor, false)
760
+ STDOUT.print("\r\n")
761
+ STDOUT.flush
762
+ break
763
+ when "\u0003" # Ctrl-C
764
+ STDOUT.print("^C\r\n")
765
+ STDOUT.flush
766
+ status = :interrupt
767
+ buffer = ""
768
+ break
769
+ when "\u0004" # Ctrl-D
770
+ if buffer.empty?
771
+ status = :eof
772
+ buffer = nil
773
+ STDOUT.print("\r\n")
774
+ STDOUT.flush
775
+ break
776
+ else
777
+ # ignore when line not empty
778
+ end
779
+ when "\u007F", "\b" # Backspace
780
+ if cursor > 0
781
+ buffer.slice!(cursor - 1)
782
+ cursor -= 1
783
+ end
784
+ last_tab_prefix = nil
785
+ tab_cycle = 0
786
+ when "\t" # Tab completion
787
+ buffer, cursor, last_tab_prefix, tab_cycle, _printed =
788
+ handle_tab_completion(prompt_str, buffer, cursor, last_tab_prefix, tab_cycle)
789
+ when "\e" # Escape sequences (arrows, home/end)
790
+ seq1 = io.getch
791
+ seq2 = io.getch
792
+ if seq1 == "[" && seq2
793
+ case seq2
794
+ when "A" # Up
795
+ if hist_index == HISTORY.length
796
+ saved_line_for_history = buffer.dup
797
+ end
798
+ if hist_index > 0
799
+ hist_index -= 1
800
+ buffer = HISTORY[hist_index] || ""
801
+ cursor = buffer.length
802
+ end
803
+ when "B" # Down
804
+ if hist_index < HISTORY.length - 1
805
+ hist_index += 1
806
+ buffer = HISTORY[hist_index] || ""
807
+ cursor = buffer.length
808
+ elsif hist_index == HISTORY.length - 1
809
+ hist_index = HISTORY.length
810
+ buffer = saved_line_for_history || ""
811
+ cursor = buffer.length
812
+ end
813
+ when "C" # Right
814
+ if cursor < buffer.length
815
+ cursor += 1
816
+ else
817
+ suggestion = history_ghost_for(buffer)
818
+ if suggestion
819
+ buffer = suggestion
820
+ cursor = buffer.length
821
+ end
822
+ end
823
+ when "D" # Left
824
+ cursor -= 1 if cursor > 0
825
+ when "H" # Home
826
+ cursor = 0
827
+ when "F" # End
828
+ cursor = buffer.length
829
+ end
830
+ end
831
+ last_tab_prefix = nil
832
+ tab_cycle = 0
833
+ else
834
+ if ch.ord >= 32 && ch.ord != 127
835
+ buffer.insert(cursor, ch)
836
+ cursor += 1
837
+ hist_index = HISTORY.length
838
+ last_tab_prefix = nil
839
+ tab_cycle = 0
840
+ end
841
+ end
842
+
843
+ render_line(prompt_str, buffer, cursor) if status == :ok
844
+ end
845
+ end
846
+
847
+ [status, buffer]
848
+ end
849
+
850
+ # ---------------- Welcome ----------------
851
+ def print_welcome
852
+ puts color("Welcome to srsh #{SRSH_VERSION} - your simple Ruby shell!",36)
853
+ puts color("Current Time:",36) + " " + color(current_time,34)
854
+ puts cpu_info
855
+ puts ram_info
856
+ puts storage_info
857
+ puts dynamic_quote
858
+ puts
859
+ puts color("Coded with love by https://github.com/RobertFlexx",90)
860
+ puts
861
+ end
862
+
863
+ print_welcome
864
+
865
+ # ---------------- Main Loop ----------------
866
+ loop do
867
+ print "\033]0;srsh-#{SRSH_VERSION}\007"
868
+ prompt_str = prompt(hostname, prompt_color)
869
+
870
+ status, input = read_line_with_ghost(prompt_str)
871
+
872
+ break if status == :eof
873
+ next if status == :interrupt
874
+
875
+ next if input.nil?
876
+ input = input.strip
877
+ next if input.empty?
878
+
879
+ HISTORY << input
880
+
881
+ run_input_line(input)
882
+ end
@@ -0,0 +1,4 @@
1
+ # lib/srsh/version.rb
2
+ module Srsh
3
+ VERSION = "0.6.0"
4
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: srsh
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.0
5
+ platform: ruby
6
+ authors:
7
+ - RobertFlexx
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: srsh is a small Ruby-written interactive shell. It is its own shell and
13
+ does not wrap bash or sh.
14
+ email:
15
+ - robertwilliamnelson2008@gmail.com
16
+ executables:
17
+ - srsh
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - LICENSE
22
+ - README.md
23
+ - bin/srsh
24
+ - lib/srsh/version.rb
25
+ homepage: https://github.com/RobertFlexx/RSH
26
+ licenses:
27
+ - Zlib
28
+ metadata: {}
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '2.7'
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubygems_version: 3.6.7
44
+ specification_version: 4
45
+ summary: srsh – a simple Ruby shell.
46
+ test_files: []