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.
- checksums.yaml +7 -0
- data/LICENSE +17 -0
- data/README.md +235 -0
- data/bin/srsh +882 -0
- data/lib/srsh/version.rb +4 -0
- 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
|
data/lib/srsh/version.rb
ADDED
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: []
|