srsh 0.8.0 → 1.0.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +12 -3
  3. data/README.md +446 -8
  4. data/bin/srsh +71 -0
  5. data/docs/assets/slut.txt +4 -0
  6. data/docs/assets/srsh-mark.svg +12 -0
  7. data/docs/css/style.css +696 -0
  8. data/docs/index.html +703 -0
  9. data/docs/js/app.js +203 -0
  10. data/examples/bridge.rsh +8 -0
  11. data/examples/calculator.rsh +253 -0
  12. data/examples/defer.rsh +14 -0
  13. data/examples/hot.rsh +14 -0
  14. data/examples/meta.rsh +20 -0
  15. data/examples/modules/text.rsh +6 -0
  16. data/examples/modules.rsh +6 -0
  17. data/examples/paste.rsh +15 -0
  18. data/examples/plugin.rb +8 -0
  19. data/examples/power.rsh +65 -0
  20. data/examples/tour.rsh +38 -0
  21. data/ext/srsh_native/extconf.rb +3 -0
  22. data/ext/srsh_native/srsh_native.c +48 -0
  23. data/language-docs/LANGUAGE.md +670 -0
  24. data/language-docs/MIGRATION.md +44 -0
  25. data/language-docs/SECURITY.md +44 -0
  26. data/lib/srsh/app.rb +261 -0
  27. data/lib/srsh/builtins.rb +492 -0
  28. data/lib/srsh/editor.rb +530 -0
  29. data/lib/srsh/errors.rb +23 -0
  30. data/lib/srsh/history.rb +74 -0
  31. data/lib/srsh/language/evaluator.rb +1175 -0
  32. data/lib/srsh/language/lexer.rb +316 -0
  33. data/lib/srsh/language/parser.rb +997 -0
  34. data/lib/srsh/language/token.rb +5 -0
  35. data/lib/srsh/language/values.rb +392 -0
  36. data/lib/srsh/paths.rb +29 -0
  37. data/lib/srsh/plugins.rb +59 -0
  38. data/lib/srsh/process_identity.rb +38 -0
  39. data/lib/srsh/security.rb +38 -0
  40. data/lib/srsh/shell/executor.rb +1182 -0
  41. data/lib/srsh/shell/job.rb +101 -0
  42. data/lib/srsh/shell/lexer.rb +114 -0
  43. data/lib/srsh/shell/terminal.rb +26 -0
  44. data/lib/srsh/state.rb +136 -0
  45. data/lib/srsh/theme.rb +108 -0
  46. data/lib/srsh/version.rb +3 -0
  47. data/lib/srsh.rb +11 -5
  48. metadata +61 -14
  49. data/exe/srsh +0 -6
  50. data/lib/srsh/runner.rb +0 -2416
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a57dd7a03d68aed0d91904d56099b2b3fdf8766b62a98c5acd4bb86e6593293
4
- data.tar.gz: 1d1c804627a25573bb2338434ff9b96131cb4edfe0697396b524b7a8586b9c5d
3
+ metadata.gz: 5b4ebcca8a9c5952dd9608cd8fe5dd311aae5445224896c4e190db2c7981a4dc
4
+ data.tar.gz: 17f76ba91a316ebd35dfdfaf5c260baf778348a73e4f930c8c61fb0de2222cc3
5
5
  SHA512:
6
- metadata.gz: 0a4c5d4c62a654284d241bc2a9eab9108559689522fbed7cf25fbb7740049ec5aa55b9bf760029b54e1953c9fd200754131c1f1efdb8bc339349530537f426f7
7
- data.tar.gz: 3802eb18db7d0d2f49934d6aab2fbdfd0f191ebe30778ddb57534a6045a18df031dc037823d59c4467e9dfe53a76d6114d06642bbec7ef57511a872dd3721afc
6
+ metadata.gz: 37f82634b0c88623c50d1aa3eee65d2b219ed317c87ea6e9abe42c904a9989aec5a0fd360715a33879a5dfc44ecf020611c5880b0a024868bb4f6627cabf95d0
7
+ data.tar.gz: 787a567cfed45fdd641874c8a0e9f43b919645ff5c882c5be0aac6bde2899a6cb4c89f2f5f032bfe0b1c2a274df4df66800542834937fe8d12dec0f3932db835
data/LICENSE CHANGED
@@ -1,12 +1,21 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 RobertFlexx
3
+ Copyright (c) 2026 RobertFlexx
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
7
7
  in the Software without restriction, including without limitation the rights
8
8
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
9
  copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions.
10
+ furnished to do so, subject to the following conditions:
11
11
 
12
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,14 +1,452 @@
1
- # srsh
1
+ # srsh, Simple Ruby Shell
2
2
 
3
- srsh is a Ruby-based interactive shell with its own scripting language (RSH),
4
- theme system, plugin support, and advanced line-editing features.
3
+ `srsh` is a Unix shell written in Ruby. Its scripting language is **RSH**.
5
4
 
6
- ## Usage
5
+ The basic idea is pretty simple: keep the parts of a normal shell that are already good, then stop making people switch languages when a script gets serious.
7
6
 
8
- After installing the gem:
7
+ ```rsh
8
+ printf 'c\na\nb\n' | sort
9
9
 
10
- srsh
10
+ files := glob("src/**/*.rb")
11
+ |> reject(::p => contains(p, "/vendor/"))
12
+ |> sort
11
13
 
12
- You can also run RSH scripts:
14
+ ? files |> len > 20 => = "that's a lot of Ruby"
15
+ ```
13
16
 
14
- srsh script.rsh arg1 arg2
17
+ `|` is still a Unix process pipe. `|>` is an RSH value pipe. They look related because they *are* related, but they move different things.
18
+
19
+ This tree reports **1.0.0**. It is the unreleased 1.0 line; the older 1.1/1.2 numbers were development snapshots and were a dumb way to version something that had not shipped yet.
20
+
21
+ ## What SRSH is trying to be
22
+
23
+ Bash is fantastic at launching programs. It gets a lot less fun when you need nested data, workers, reusable modules, objects, error handling, higher-order functions, or a pile of state that has to remain understandable next month.
24
+
25
+ RSH is meant to cover that gap without turning the shell into a foreign environment:
26
+
27
+ ```rsh
28
+ branch := $(git branch --show-current)
29
+
30
+ fn changed(path) => cmd("git", "diff", "--quiet", "--", path).result().status != 0
31
+
32
+ dirty := glob("src/**/*") |> filter(changed)
33
+ = "#{branch}: #{dirty |> len} changed files"
34
+ ```
35
+
36
+ It is **not a Bash parser**. Existing Bash scripts still belong to Bash. The goal is that new automation can start as a shell one-liner and grow into a proper RSH program without a rewrite.
37
+
38
+ ## Build and run
39
+
40
+ Ruby 3.2+ is required.
41
+
42
+ ```sh
43
+ make test
44
+ ./bin/srsh
45
+ ```
46
+
47
+ The shell is fully usable as Ruby-only code. There is a tiny optional C extension for hot lexer work and Linux process naming:
48
+
49
+ ```sh
50
+ make native
51
+ ./bin/srsh
52
+ ```
53
+
54
+ YJIT is enabled when the running Ruby supports it. Set `SRSH_YJIT=0` if you want it off.
55
+
56
+ Install somewhere else with:
57
+
58
+ ```sh
59
+ make PREFIX="$HOME/.local" install
60
+ ```
61
+
62
+ ## The syntax has a reason
63
+
64
+ RSH has some odd-looking syntax, but the goal is not to win a weird-language contest.
65
+
66
+ There are a few boundaries you hit constantly in shell programming, and RSH gives each one a visible shape:
67
+
68
+ ```text
69
+ command | command bytes/processes move between Unix programs
70
+ value |> function RSH values move through functions
71
+ $(command) process output crosses into the value layer
72
+ name := value local RSH binding
73
+ $NAME := value exported process environment
74
+ ::x => expression tiny callable value
75
+ bridge ... from ... RSH crosses into a native C ABI
76
+ ```
77
+
78
+ That makes parsing less ambiguous around arbitrary Unix command names, and it lets hot scripts stay short without turning maintained code into punctuation soup.
79
+
80
+ Readable forms exist too:
81
+
82
+ ```rsh
83
+ fn deploy(files, dry := no)
84
+ if dry
85
+ = "would deploy #{files |> len} files"
86
+ else
87
+ each files -> path
88
+ emit path
89
+ end
90
+ end
91
+ end
92
+ ```
93
+
94
+ The short forms lower to the same AST:
95
+
96
+ ```rsh
97
+ :: twice(x) => x * 2
98
+ ? ready => emit "go"
99
+ @ jobs -> job => = job.name
100
+ ```
101
+
102
+ Use whichever one fits the size of the job.
103
+
104
+ ## The REPL is actually an RSH REPL
105
+
106
+ Bindings, expressions and blocks work directly at the shell prompt:
107
+
108
+ ```text
109
+ > x := 21
110
+ > = x * 2
111
+ 42
112
+
113
+ > fn triple(n)
114
+ ... return n * 3
115
+ ... end
116
+ > = triple(10)
117
+ 30
118
+ ```
119
+
120
+ Multiline list/map/call expressions also continue properly:
121
+
122
+ ```text
123
+ > jobs := [
124
+ ... fetch("a"),
125
+ ... fetch("b")
126
+ ... ]
127
+ ```
128
+
129
+ That same completeness logic is used for manually entered continuations. For an actual terminal paste, SRSH enables bracketed-paste mode and captures the whole clipboard payload before parsing any of it. Multi-line pastes get a small preview; press Enter once to run the whole program or Ctrl-C to throw it away. Embedded newlines never become a queue of accidental commands.
130
+
131
+ The editor keeps the old SRSH behavior too: prefix history prediction, Right Arrow to accept it, UTF-8 cursor movement, history navigation, and first-Tab/second-Tab completion.
132
+
133
+ ## Shell side
134
+
135
+ Normal Unix muscle memory is supposed to work:
136
+
137
+ ```sh
138
+ cat *.log | grep ERROR | sort -u
139
+ make -j8 && put "built"
140
+ long_job &
141
+ jobs
142
+ fg %1
143
+ ```
144
+
145
+ Implemented shell features include:
146
+
147
+ - `fork`/`exec` pipelines and process groups
148
+ - `|`, `&&`, `||`, `;`, background `&`
149
+ - `<`, `>`, `>>`, `2>`, `2>>`
150
+ - `jobs`, `fg`, `bg`
151
+ - command substitution with nested `$(...)`
152
+ - aliases
153
+ - `$VAR`, `${VAR}`, `$?`, `$!`, positional arguments
154
+ - pathname globbing (`*`, `?`, `[...]`, and recursive `**` where Ruby's glob supports it)
155
+ - `~` expansion
156
+ - command lookup caching
157
+ - `cd`, `pwd`, `ls`, `printf`, `export`, `read`, `source`, `which`/`type`
158
+ - `pushd`, `popd`, `dirs`, `umask`, `kill`
159
+ - `option pipefail`, `option nounset`, `option noclobber`
160
+ - `option strict yes` as a shortcut for `pipefail + nounset`
161
+
162
+ No-match globs stay literal, which is a much less surprising default for an interactive shell.
163
+
164
+ ## Values and functional scripting
165
+
166
+ RSH has integers, floats, strings, booleans, `void`, lists, maps, ranges, lambdas, code values, tasks, prototypes, namespaces, native handles and structured commands.
167
+
168
+ ```rsh
169
+ users := json(readfile("users.json"))
170
+
171
+ enabled := users
172
+ |> filter(::u => u?.enabled ?? no)
173
+ |> sort(::u => lower(u.name))
174
+
175
+ = enabled |> map(::u => u.name)
176
+ ```
177
+
178
+ The usual functional operations are built in:
179
+
180
+ ```text
181
+ map filter reject fold find any all count sum
182
+ sort uniq flat zip enumerate take drop chunk group
183
+ each tap partial compose
184
+ ```
185
+
186
+ Named functions are first-class too:
187
+
188
+ ```rsh
189
+ fn square(x) => x * x
190
+ = 1 .. 10 |> map(square) |> sum
191
+ ```
192
+
193
+ ## Objects without a ceremony tax
194
+
195
+ RSH uses prototypes plus traits. There is no class-inheritance maze to climb through just to hold some state.
196
+
197
+ ```rsh
198
+ trait Named
199
+ fn label() => "#{self.name}: #{self.value}"
200
+ end
201
+
202
+ proto Counter(name, start := 0) with Named
203
+ slot name := name
204
+ slot value := start
205
+
206
+ fn inc(by := 1)
207
+ self.value += by
208
+ return self
209
+ end
210
+ end
211
+
212
+ hits := Counter("hits")
213
+ hits.inc(3)
214
+ = hits.label()
215
+ ```
216
+
217
+ Object slot updates are synchronized, so objects can be an explicit shared-state tool between tasks.
218
+
219
+ ## Concurrency
220
+
221
+ Async RSH functions return task values immediately:
222
+
223
+ ```rsh
224
+ task fetch(url)
225
+ return cmd("curl", "-fsS", url).check().out
226
+ end
227
+
228
+ jobs := urls |> map(fetch)
229
+ pages := await_all(jobs)
230
+ ```
231
+
232
+ You can spawn a lambda directly:
233
+
234
+ ```rsh
235
+ job := &:: => expensive_io()
236
+ = job.await(2.0)
237
+ ```
238
+
239
+ Shared mutation is explicit:
240
+
241
+ ```rsh
242
+ hits := atom(0)
243
+ workers := 0 ..< 32 |> map(::i => &:: => hits.swap(::n => n + 1))
244
+ await_all(workers)
245
+ = hits.get()
246
+ ```
247
+
248
+ There are channels too:
249
+
250
+ ```rsh
251
+ queue := chan(8)
252
+ queue.send("hello")
253
+ = queue.recv(1)
254
+ queue.close()
255
+ ```
256
+
257
+ `parallel()` is a Ruby thread pool, which is great for I/O and subprocess waits. On CRuby the GVL still exists; SRSH does not pretend otherwise. `pmap()` uses Unix fork workers when you actually want CPU parallelism.
258
+
259
+ `race()` cancels losing task handles. A failing `await_all()` cancels still-running siblings before propagating the error.
260
+
261
+ ## Modules, namespaces and cleanup
262
+
263
+ Namespaces can live in the same file:
264
+
265
+ ```rsh
266
+ space mathx
267
+ bias := 10
268
+ fn bump(x) => x + bias
269
+ end
270
+
271
+ = mathx.bump(5)
272
+ ```
273
+
274
+ Or load a file as a namespace:
275
+
276
+ ```rsh
277
+ use "./lib/http.rsh" as http
278
+ = http.get("https://example.com")
279
+ ```
280
+
281
+ Module paths are resolved relative to the importing script when possible, and import cycles are rejected.
282
+
283
+ Cleanup is LIFO and tied to the current execution scope:
284
+
285
+ ```rsh
286
+ fn build()
287
+ tmp := "/tmp/my-build.lock"
288
+ writefile(tmp, "busy")
289
+ defer rmfile(tmp)
290
+
291
+ # return, error, whatever; the cleanup still runs
292
+ return compile()
293
+ end
294
+ ```
295
+
296
+ There is a block form when cleanup takes more than one statement:
297
+
298
+ ```rsh
299
+ defer
300
+ close_stuff()
301
+ emit "cleaned up"
302
+ end
303
+ ```
304
+
305
+ ## Structured process values
306
+
307
+ Shell strings are convenient. They are not always what you want for untrusted filenames or arguments.
308
+
309
+ ```rsh
310
+ result := cmd("git", "rev-parse", "--verify", ref).check()
311
+ sha := result.out.trim()
312
+ ```
313
+
314
+ `cmd()` keeps argv as argv. It does not feed your data back through shell parsing.
315
+
316
+ Available methods include `.argv()`, `.run()`, `.result()`, `.capture()`, `.check()` and `.task()`.
317
+
318
+ ## C interop: `bridge`
319
+
320
+ This is the native boundary:
321
+
322
+ ```rsh
323
+ bridge c from "@self"
324
+ strlen(cstr) -> usize
325
+ end
326
+
327
+ = c.strlen("hello")
328
+ ```
329
+
330
+ Or load a specific shared library:
331
+
332
+ ```rsh
333
+ bridge math from "libm.so.6"
334
+ cos(f64) -> f64
335
+ pow(f64, f64) -> f64
336
+ end
337
+
338
+ = math.cos(0.0)
339
+ ```
340
+
341
+ For output buffers/pointer APIs:
342
+
343
+ ```rsh
344
+ bridge libc from "libc.so.6"
345
+ gethostname(ptr, usize) -> i32
346
+ end
347
+
348
+ buf := cbuf(256)
349
+ libc.gethostname(buf, buf.size())
350
+ = buf.string()
351
+ ```
352
+
353
+ Supported ABI names are:
354
+
355
+ ```text
356
+ void bool
357
+ i8 u8 i16 u16 i32 u32 i64 u64
358
+ isize usize
359
+ f32 f64
360
+ cstr ptr
361
+ ```
362
+
363
+ `@self` means the current process' symbol table. Bridges resolve their symbols once when declared, so calls do not repeatedly search the library.
364
+
365
+ This is real FFI. A wrong C signature can crash the SRSH process, because C does not care about your feelings. See `docs/SECURITY.md` before feeding native pointers to random libraries.
366
+
367
+ ## Errors and strict scripts
368
+
369
+ ```rsh
370
+ try
371
+ cfg := json(readfile("config.json"))
372
+ catch err
373
+ = "config failed: #{err.message}"
374
+ cfg := %[]
375
+ finally
376
+ audit("config attempted")
377
+ end
378
+ ```
379
+
380
+ For value pipelines:
381
+
382
+ ```rsh
383
+ result := attempt(:: => risky())
384
+ ? !result.ok => = result.error.message
385
+ ```
386
+
387
+ For shell-side stricter behavior:
388
+
389
+ ```rsh
390
+ option strict yes
391
+ option noclobber yes
392
+ ```
393
+
394
+ `strict` currently means `nounset + pipefail`; it does **not** try to clone all of Bash's `set -e` edge cases.
395
+
396
+ ## Metascripting
397
+
398
+ RSH code can be a value instead of a string you hope parses later:
399
+
400
+ ```rsh
401
+ code cleanup
402
+ @ glob("tmp/*.log") -> file
403
+ rm $file
404
+ .@
405
+ end
406
+
407
+ ? dry => = sourceof(cleanup)
408
+ ? !dry => run(cleanup)
409
+ ```
410
+
411
+ The reflective toolbox includes `code()`, `run()`, `eval()`, `sourceof()`, `valid()`, `locals()`, `fns()`, `protos()` and `traits()`.
412
+
413
+ ## Ruby plugins
414
+
415
+ Ruby is still part of the point. A Ruby plugin can register builtins, hooks and themes through the SRSH API. Plugins are trusted code and are permission-checked before auto-loading.
416
+
417
+ RSH plugins are supported too if you want to stay entirely in the language.
418
+
419
+ ## Process identity
420
+
421
+ SRSH sets `$SHELL`, argv0, and the Linux process name to `srsh`. That keeps process-oriented tools from calling your active shell `ruby` just because Ruby is the engine.
422
+
423
+ The executable behind the process is still Ruby. That is intentional, not something SRSH tries to disguise.
424
+
425
+ ## Repo layout
426
+
427
+ ```text
428
+ bin/srsh
429
+ lib/srsh/
430
+ app.rb
431
+ editor.rb
432
+ builtins.rb
433
+ history.rb
434
+ plugins.rb
435
+ process_identity.rb
436
+ state.rb
437
+ theme.rb
438
+ language/
439
+ shell/
440
+ ext/srsh_native/ optional accelerator
441
+ examples/
442
+ docs/
443
+ test/
444
+ ```
445
+
446
+ The code is split by job, not by architectural cosplay. If a file gets big because the job is big, it gets split when that actually makes it easier to work on.
447
+
448
+ ## Current status
449
+
450
+ The project is still unreleased. `1.0.0` means “the 1.0 tree,” not “independently audited and incapable of bugs.” A shell has too much surface area for that kind of claim.
451
+
452
+ Run the tests, beat on it, and report the ugly cases. Those are usually more useful than another feature bullet.
data/bin/srsh ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
5
+ require 'srsh'
6
+
7
+ Srsh::ProcessIdentity.install!(executable: File.expand_path(__FILE__))
8
+
9
+ if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable) && ENV.fetch('SRSH_YJIT', '1') != '0'
10
+ RubyVM::YJIT.enable rescue nil
11
+ end
12
+
13
+ app = Srsh::App.new
14
+ args = ARGV.dup
15
+ app.disable_startup! if args.delete('--norc')
16
+
17
+ case args[0]
18
+ when '--version', '-V'
19
+ puts "srsh #{Srsh::VERSION} (Simple Ruby Shell; #{RUBY_ENGINE} #{RUBY_VERSION})"
20
+ exit 0
21
+ when '--help', '-h'
22
+ puts <<~TXT
23
+ usage: srsh [options] [script.rsh [args...]]
24
+
25
+ -c COMMAND run one shell command
26
+ -e EXPR evaluate and print one RSH expression
27
+ --check FILE parse an RSH script without running it
28
+ --norc skip ~/.srshrc and plugins for -c/interactive startup
29
+ -V, --version show version
30
+ -h, --help show this help
31
+ TXT
32
+ exit 0
33
+ when '--check'
34
+ file = args[1] or abort 'srsh: --check needs a file'
35
+ exit app.check_script(file)
36
+ when '-c'
37
+ command = args[1] or abort 'srsh: -c needs a command'
38
+ begin
39
+ exit app.run_command(command).to_i
40
+ rescue StandardError => e
41
+ warn "srsh: #{e.class}: #{e.message}"
42
+ exit 1
43
+ ensure
44
+ app.history.flush
45
+ end
46
+ when '-e'
47
+ expression = args[1] or abort 'srsh: -e needs an expression'
48
+ begin
49
+ exit app.run_expression(expression).to_i
50
+ rescue StandardError => e
51
+ warn "srsh: #{e.class}: #{e.message}"
52
+ exit 2
53
+ ensure
54
+ app.history.flush
55
+ end
56
+ else
57
+ if args[0]
58
+ script = args.shift
59
+ app.startup!
60
+ begin
61
+ exit app.run_script(script, args).to_i
62
+ rescue StandardError => e
63
+ warn "srsh: #{e.class}: #{e.message}"
64
+ exit 1
65
+ ensure
66
+ app.history.flush
67
+ end
68
+ end
69
+ end
70
+
71
+ exit app.interactive
@@ -0,0 +1,4 @@
1
+ i put this here cuz funny
2
+
3
+
4
+ (ALSO MY ART IS PEAK BTW FR FR ONG)
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="srsh">
3
+ <rect x="10" y="18" width="108" height="92" rx="16" fill="#0c0f14"/>
4
+ <rect x="16" y="26" width="96" height="16" rx="8" fill="#141824"/>
5
+ <circle cx="28" cy="34" r="5" fill="#ff5f57"/>
6
+ <circle cx="44" cy="34" r="5" fill="#febc2e"/>
7
+ <circle cx="60" cy="34" r="5" fill="#28c840"/>
8
+ <path d="M28 60L44 68L28 76" stroke="#e6edf3" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
9
+ <path d="M54 78H86" stroke="#e6edf3" stroke-width="7" stroke-linecap="round"/>
10
+ <path d="M54 60H92" stroke="#7ee787" stroke-width="7" stroke-linecap="round" opacity="0.9"/>
11
+ <text x="64" y="106" text-anchor="middle" font-family="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace" font-size="14" fill="#9aa4b2">srsh</text>
12
+ </svg>