-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmatrix.sh
More file actions
executable file
·312 lines (258 loc) · 8.5 KB
/
matrix.sh
File metadata and controls
executable file
·312 lines (258 loc) · 8.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
#!/usr/bin/env bash
# Matrix-style falling character rain for your terminal
# Ensure UTF-8 locale for multibyte character handling
export LC_ALL="${LC_ALL:-en_US.UTF-8}"
VERSION="1.0.0"
CODE_MODE=0
REPO_PATH="."
usage() {
cat <<'USAGE'
Usage: matrix [options] [path]
Options:
--code Use characters from repository source files instead of random characters
-h Show help
Arguments:
path Path to repository (default: current directory)
Controls:
Any key Exit
Examples:
matrix # Random chars, current directory
matrix --code # Repo source chars from current directory
matrix --code ~/myproject # Repo source chars from specific repo
USAGE
exit 0
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--code)
CODE_MODE=1
shift
;;
-h|--help)
usage
;;
*)
REPO_PATH="$1"
shift
;;
esac
done
# Validate path
if [[ ! -d "$REPO_PATH" ]]; then
echo "Error: '$REPO_PATH' is not a valid directory" >&2
exit 1
fi
# --- Character Pool ---
CHAR_POOL=""
build_random_pool() {
# Katakana-inspired chars, digits, symbols
CHAR_POOL='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()_+-=[]{}|;:<>?/~ヲアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン'
}
build_code_pool() {
local repo="$1"
local files=""
# Get file list: prefer git ls-files, fallback to find
if git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
files=$(git -C "$repo" ls-files --cached --others --exclude-standard 2>/dev/null)
else
files=$(find "$repo" -type f \
! -path '*/\.*' \
! -name '*.png' ! -name '*.jpg' ! -name '*.jpeg' ! -name '*.gif' \
! -name '*.ico' ! -name '*.svg' ! -name '*.woff' ! -name '*.woff2' \
! -name '*.ttf' ! -name '*.eot' ! -name '*.mp3' ! -name '*.mp4' \
! -name '*.zip' ! -name '*.tar' ! -name '*.gz' ! -name '*.bin' \
! -name '*.exe' ! -name '*.dll' ! -name '*.so' ! -name '*.dylib' \
! -name '*.pdf' ! -name '*.lock' \
2>/dev/null)
fi
# Read file contents (capped at ~500KB total), extract unique printable chars
local max_size=500000
CHAR_POOL=$(
printf '%s\n' "$files" | head -200 | while IFS= read -r file; do
[[ -z "$file" ]] && continue
local full_path="$repo/$file"
[[ -f "$full_path" ]] && head -c 5000 "$full_path" 2>/dev/null
done | head -c "$max_size" | LC_ALL=C tr -dc '!-~' | grep -o . | sort -u | tr -d '\n'
)
# Fallback if pool is empty
if [[ -z "$CHAR_POOL" ]]; then
echo "Warning: No source characters found, falling back to random mode" >&2
build_random_pool
fi
}
# Split pool into array for safe multibyte indexing
declare -a CHAR_ARRAY=()
POOL_SIZE=0
build_char_array() {
CHAR_ARRAY=()
local i
for (( i=0; i<${#CHAR_POOL}; i++ )); do
CHAR_ARRAY+=("${CHAR_POOL:$i:1}")
done
POOL_SIZE=${#CHAR_ARRAY[@]}
}
# Get a random character from the pool (sets RCHAR variable, no subshell)
random_char() {
RCHAR="${CHAR_ARRAY[RANDOM % POOL_SIZE]}"
}
if [[ "$CODE_MODE" -eq 1 ]]; then
build_code_pool "$REPO_PATH"
else
build_random_pool
fi
build_char_array
# --- Terminal Setup ---
COLS=0
LINES_COUNT=0
ORIG_STTY=""
get_dimensions() {
COLS=$(tput cols)
LINES_COUNT=$(tput lines)
}
KEY_READER_PID=""
cleanup() {
# Kill background key reader if running
[[ -n "$KEY_READER_PID" ]] && kill "$KEY_READER_PID" 2>/dev/null
# Restore terminal
tput cnorm # Show cursor
tput sgr0 # Reset colors
printf '\033[2J' # Clear screen
printf '\033[H' # Move to top-left
if [[ -n "$ORIG_STTY" ]]; then
stty "$ORIG_STTY" 2>/dev/null
fi
exit 0
}
init_terminal() {
ORIG_STTY=$(stty -g 2>/dev/null)
stty -echo # Disable echo (we handle display ourselves)
tput civis # Hide cursor
printf '\033[2J' # Clear screen
printf '\033[H' # Move to top-left
get_dimensions
}
# Trap signals for clean exit
trap cleanup EXIT INT TERM
trap 'get_dimensions; init_streams' WINCH
# --- Stream State ---
# Each column has: position (head row), length, speed counter, speed threshold, active flag, pause counter
declare -a STREAM_POS # Current head position (row)
declare -a STREAM_LEN # Trail length
declare -a STREAM_SPEED # Speed counter (increments each frame)
declare -a STREAM_THRESHOLD # Frames between advances
declare -a STREAM_ACTIVE # 1=active, 0=paused
declare -a STREAM_PAUSE # Pause frames remaining before respawn
init_stream() {
local col=$1
STREAM_POS[$col]=$(( (RANDOM % LINES_COUNT) * -1 )) # Start above screen
STREAM_LEN[$col]=$(( RANDOM % 5 + 4 )) # Length 4-8
STREAM_SPEED[$col]=0
STREAM_THRESHOLD[$col]=$(( RANDOM % 3 + 2 )) # Speed 2-4
STREAM_ACTIVE[$col]=1
STREAM_PAUSE[$col]=0
}
init_streams() {
for (( col=0; col<COLS; col++ )); do
if (( RANDOM % 3 == 0 )); then
# Start some columns paused for staggered effect
STREAM_ACTIVE[$col]=0
STREAM_PAUSE[$col]=$(( RANDOM % 30 + 5 ))
STREAM_POS[$col]=0
STREAM_LEN[$col]=0
STREAM_SPEED[$col]=0
STREAM_THRESHOLD[$col]=1
else
init_stream "$col"
STREAM_POS[$col]=$(( (RANDOM % LINES_COUNT) * -1 ))
fi
done
}
# --- Rendering ---
# Frame buffer — accumulate all output, flush once per frame
FRAME_BUF=""
# Move cursor and print a colored character at (row, col)
put_char() {
local row=$1 col=$2 char=$3 color=$4
# Only draw within bounds
if (( row >= 0 && row < LINES_COUNT && col >= 0 && col < COLS )); then
FRAME_BUF+="\033[$((row + 1));$((col + 1))H${color}${char}"
fi
}
# Clear a cell
clear_cell() {
local row=$1 col=$2
if (( row >= 0 && row < LINES_COUNT )); then
FRAME_BUF+="\033[$((row + 1));$((col + 1))H "
fi
}
# Color codes for trail gradient
COLOR_HEAD='\033[1;97m' # Bright white (head)
COLOR_NEAR='\033[1;32m' # Bright green
COLOR_MID='\033[0;32m' # Normal green
COLOR_DIM='\033[2;32m' # Dim green
render_frame() {
for (( col=0; col<COLS; col++ )); do
# Handle paused streams
if [[ ${STREAM_ACTIVE[$col]} -eq 0 ]]; then
STREAM_PAUSE[$col]=$(( STREAM_PAUSE[$col] - 1 ))
if [[ ${STREAM_PAUSE[$col]} -le 0 ]]; then
init_stream "$col"
fi
continue
fi
# Speed control: only advance when counter reaches threshold
STREAM_SPEED[$col]=$(( STREAM_SPEED[$col] + 1 ))
if [[ ${STREAM_SPEED[$col]} -lt ${STREAM_THRESHOLD[$col]} ]]; then
continue
fi
STREAM_SPEED[$col]=0
local pos=${STREAM_POS[$col]}
local len=${STREAM_LEN[$col]}
# Draw head (bright white)
random_char; put_char "$pos" "$col" "$RCHAR" "$COLOR_HEAD"
# Recolor previous head position to bright green
random_char; put_char $((pos - 1)) "$col" "$RCHAR" "$COLOR_NEAR"
# Mid trail
random_char; put_char $((pos - 2)) "$col" "$RCHAR" "$COLOR_MID"
# Dim trail
for (( t=3; t<=len; t++ )); do
random_char; put_char $((pos - t)) "$col" "$RCHAR" "$COLOR_DIM"
done
# Clear tail (character falling off the trail)
clear_cell $((pos - len - 1)) "$col"
# Advance position
STREAM_POS[$col]=$(( pos + 1 ))
# Check if stream has fully exited the screen
if (( pos - len > LINES_COUNT )); then
STREAM_ACTIVE[$col]=0
STREAM_PAUSE[$col]=$(( RANDOM % 20 + 5 )) # Pause 5-24 frames
fi
done
FRAME_BUF+="\033[0m"
printf '%b' "$FRAME_BUF"
FRAME_BUF=""
}
# --- Main Loop ---
main() {
init_terminal
if (( COLS < 2 || LINES_COUNT < 2 )); then
echo "Terminal too small" >&2
exit 1
fi
init_streams
# Background process: wait for any keypress, then signal main process
GOT_KEY=0
trap 'GOT_KEY=1' USR1
( read -rsn1 _ < /dev/tty 2>/dev/null; kill -USR1 $$ 2>/dev/null ) &
KEY_READER_PID=$!
while true; do
render_frame
sleep 0.05
# Check if background reader caught a keypress
if (( GOT_KEY )); then
break
fi
done
}
main