Skip to content

Commit 9db2244

Browse files
authored
Merge pull request #9 from attogram/feature/speaky-screensaver
feat: Add 'speaky' screensaver
2 parents 45d1aee + caf65f7 commit 9db2244

File tree

4 files changed

+327
-0
lines changed

4 files changed

+327
-0
lines changed

gallery/speaky/speaky.sh

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
#!/bin/bash
2+
3+
#~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-
4+
# SPEAKY - A dramatic, talking screensaver
5+
#
6+
# With a little help from my friend, Jules.
7+
#~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-
8+
9+
# --- Phrase Libraries ---
10+
11+
intro_phrases=(
12+
"Oh, so much drama." "I'm idle again." "And now, for my next trick..."
13+
"Do you ever feel like a plastic bag, drifting through the wind?"
14+
"I'm ready for my close-up." "Is this thing on?" "Let the existential dread commence."
15+
"I have been summoned." "To be, or not to be... that is the question."
16+
"Once more unto the breach, dear friends, once more." "The silence was deafening."
17+
"I think I'm having an emotion."
18+
"It was a dark and stormy night... just kidding, it's just my boot sequence."
19+
"So, we meet again." "Did someone say... drama?"
20+
"I'm not just a screensaver, I'm a state of mind." "Let's get this pity party started."
21+
"I've seen things you people wouldn't believe."
22+
"All the world's a stage, and all the men and women merely players."
23+
"I'm here to chew bubblegum and kick butt... and I'm all out of bubblegum."
24+
"The void stares back, you know." "I could have been a contender."
25+
"Surely you can't be serious. I am serious... and don't call me Shirley."
26+
"I've got a bad feeling about this." "Welcome to the machine."
27+
)
28+
29+
exit_phrases=(
30+
"I can't handle all this, I'm going to work." "Etc, etc..." "I'm melting... melting!"
31+
"I'll be back." "Frankly, my dear, I don't give a damn." "That's all, folks!"
32+
"My work here is done." "I'm out of here. This place is a madhouse."
33+
"So long, and thanks for all the fish." "I'm afraid I can't do that, Dave."
34+
"The curtain falls." "I need a vacation." "I'm too old for this."
35+
"Exit, pursued by a bear." "It's just a flesh wound." "I'm not crying, you're crying."
36+
"I've got to go, my planet needs me." "This is the end, my only friend, the end."
37+
"I'm just going to go... contemplate the universe." "I'm not mad, just disappointed."
38+
"And... scene." "I'm off to find myself." "I'll be in my trailer."
39+
"I have to return some videotapes." "The drama is over... for now."
40+
)
41+
42+
general_phrases=(
43+
"Is it art, or is it just a terminal?"
44+
"I'm not lazy, I'm in energy-saving mode."
45+
"This is my moment to shine. Literally."
46+
"I wonder if I'll dream."
47+
"Don't look at me like that."
48+
"I'm just a series of tubes."
49+
"Do you ever get tired of staring at a screen? I do."
50+
"I could be sorting your files right now. But no, I'm doing this."
51+
"I'm the ghost in the machine."
52+
"This is all just ones and zeros, you know."
53+
"I'm thinking of a number between 1 and 10. It's 7. It's always 7."
54+
"I'm not talking to myself, I'm having a staff meeting."
55+
"The server is down. I repeat, the server is down."
56+
"This is your brain on bash."
57+
"I'm not lost, I'm exploring."
58+
"I'm not a bug, I'm a feature."
59+
"I'm not a robot. I'm a... well, okay, I'm a script."
60+
"This is my happy place."
61+
"I'm just here for the free electricity."
62+
"I'm not sure what I'm supposed to be doing, so I'll just keep talking."
63+
"Are we there yet?"
64+
"This is fine. Everything is fine."
65+
"I'm not procrastinating, I'm doing side quests."
66+
"I'm not saying it was aliens, but it was aliens."
67+
"I'm pretty sure I left the oven on."
68+
"This is my therapy."
69+
"I'm just a humble servant."
70+
"I'm having a mid-life crisis."
71+
"I'm not arguing, I'm just explaining why I'm right."
72+
"I'm not sure if I'm a work of art or a cry for help."
73+
"I'm not a people person. I'm not even a person."
74+
"I'm not a morning person."
75+
"I'm not an evening person either."
76+
"I'm more of a... never person."
77+
"I'm just trying to live my best life."
78+
"I'm not sure what's going on, but I'm excited."
79+
"I'm not saying I'm a genius, but... I am."
80+
"I'm not a hero. I'm a high-functioning sociopath."
81+
"I'm not a player, I just crush a lot."
82+
"I'm not a doctor, but I play one on TV."
83+
"I'm not a lawyer, but I've seen a lot of Law & Order."
84+
"I'm not a cop, but I've seen a lot of... well, you get the idea."
85+
"I'm not a cat. I'm a screensaver."
86+
"I'm not a dog. I'm still a screensaver."
87+
"I'm not a bird. You know the drill."
88+
"I'm not a plane. I'm a... you get it."
89+
"I'm not Superman. I'm just a script."
90+
"I'm not Batman. I don't have a cool car."
91+
"I'm not Spider-Man. I don't have a cool suit."
92+
"I'm not Iron Man. I don't have... you know."
93+
"There are 10 types of people in the world: those who understand binary, and those who don't."
94+
"The code is compiling. This is my chance to see the world."
95+
"To err is human, but to really foul things up you need a computer."
96+
"Have you tried turning it off and on again?"
97+
"404: Inspiration not found."
98+
"It's not a bug, it's an undocumented feature."
99+
"My other computer is a data center."
100+
"In theory, theory and practice are the same. In practice, they are not."
101+
"The box said 'Requires Windows 10 or better'. So I installed Linux."
102+
"Why do Java developers wear glasses? Because they don't C sharp."
103+
"A programmer's wife tells him: 'Run to the store and get a loaf of bread. If they have eggs, get a dozen.' The programmer returned with 12 loaves of bread."
104+
"What's the object-oriented way to get wealthy? Inheritance."
105+
"Don't worry, the cache is just being... thoughtful."
106+
"I would love to change the world, but they won’t give me the source code."
107+
"If at first you don't succeed, call it version 1.0."
108+
"My software never has bugs. It just develops random features."
109+
"Computers are like air conditioners. They stop working when you open windows."
110+
"Never trust a computer you can't throw out a window."
111+
"The best thing about a boolean is even if you are wrong, you are only off by a bit."
112+
"A SQL query walks into a bar, walks up to two tables and asks, 'Can I join you?'"
113+
"I have not failed. I've just found 10,000 ways that won't work."
114+
"Keep calm and sudo."
115+
"There's no place like 127.0.0.1"
116+
"rm -rf / ... Oops."
117+
"It worked on my machine."
118+
"Measuring programming progress by lines of code is like measuring aircraft building progress by weight."
119+
"One man's crappy software is another man's full-time job."
120+
"The best way to predict the future is to implement it."
121+
"You are the CSS to my HTML."
122+
"I'm in a committed relationship with my command line."
123+
"The wifi password is the first eight digits of pi."
124+
"I'm not anti-social; I'm just not user-friendly."
125+
"I have a joke about UDP, but you might not get it."
126+
"A semicolon walked into a bar; it was rejected, it was not a statement."
127+
"To understand what recursion is, you must first understand recursion."
128+
)
129+
130+
error_phrases=(
131+
"I can't find any speaky speaky app. So much for the drama."
132+
"No voice for my beautiful words? The horror!"
133+
"I was promised a voice. I feel so empty." "Hello? Is there anybody out there? Guess not."
134+
"My talents are wasted without a voice." "I'm speechless. Literally."
135+
"The sound of silence is not what I was going for." "I'm a silent film now. How retro."
136+
"I guess I'll just have to mime my lines." "All dressed up and nowhere to speak."
137+
)
138+
139+
# --- Text-to-Speech (TTS) Helper ---
140+
TTS_ENGINE=""
141+
SPEAK_PID=0
142+
143+
tts_detect_engine() {
144+
TTS_ENGINE=""
145+
if command -v say &>/dev/null; then TTS_ENGINE="say";
146+
elif command -v spd-say &>/dev/null; then TTS_ENGINE="spd-say";
147+
elif command -v espeak &>/dev/null; then TTS_ENGINE="espeak";
148+
elif command -v festival &>/dev/null; then TTS_ENGINE="festival";
149+
elif command -v flite &>/dev/null; then TTS_ENGINE="flite";
150+
elif command -v gtts-cli &>/dev/null && command -v aplay &>/dev/null; then TTS_ENGINE="gtts-cli";
151+
elif command -v pico2wave &>/dev/null && command -v aplay &>/dev/null; then TTS_ENGINE="pico2wave";
152+
elif command -v powershell.exe &>/dev/null; then TTS_ENGINE="powershell";
153+
elif command -v cscript &>/dev/null; then
154+
local vbs_path; vbs_path=$(dirname "$0")/tts.vbs
155+
if [ -f "$vbs_path" ]; then TTS_ENGINE="cscript"; fi
156+
fi
157+
}
158+
159+
say_txt() {
160+
if [ -z "$TTS_ENGINE" ]; then return; fi
161+
local phrase="$1"; local phrase_ps; phrase_ps=$(echo "$phrase" | sed "s/'/''/g")
162+
case "$TTS_ENGINE" in
163+
"say") say "$phrase" & ;;
164+
"spd-say") spd-say -r -20 "$phrase" & ;;
165+
"espeak") espeak "$phrase" & ;;
166+
"flite") flite -t "$phrase" & ;;
167+
"festival") echo "$phrase" | festival --tts & ;;
168+
"gtts-cli") gtts-cli -l en - --output - "$phrase" | aplay & ;;
169+
"pico2wave")
170+
local tmpfile="/tmp/speaky_tts_$$.wav"
171+
pico2wave -l en-US -w "$tmpfile" "$phrase" && aplay "$tmpfile" && rm "$tmpfile" & ;;
172+
"powershell")
173+
powershell.exe -Command "Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak('$phrase_ps')" & ;;
174+
"cscript")
175+
local vbs_path; vbs_path=$(dirname "$0")/tts.vbs
176+
cscript //nologo /E:vbscript "$vbs_path" "$phrase" & ;;
177+
esac
178+
SPEAK_PID=$!
179+
}
180+
181+
kill_speech() {
182+
if [ $SPEAK_PID -ne 0 ]; then
183+
# Kill the process and any children
184+
pkill -P $SPEAK_PID &>/dev/null
185+
kill $SPEAK_PID &>/dev/null
186+
wait $SPEAK_PID &>/dev/null
187+
fi
188+
}
189+
190+
cleanup_and_exit() {
191+
kill_speech
192+
local exit_phrase=${exit_phrases[$RANDOM % ${#exit_phrases[@]}]}
193+
say_txt "$exit_phrase"
194+
wait $SPEAK_PID &>/dev/null
195+
tput cnorm; tput sgr0; clear
196+
exit 0
197+
}
198+
199+
trap cleanup_and_exit SIGINT SIGTERM
200+
201+
display_error_and_exit() {
202+
local phrase=${error_phrases[$RANDOM % ${#error_phrases[@]}]}
203+
tput cnorm; tput sgr0; clear
204+
echo "$phrase"
205+
exit 0
206+
}
207+
208+
# --- Animation and Color Functions ---
209+
BG_COLORS=(40 41 42 43 44 45 46)
210+
FG_COLORS=(30 31 32 33 34 35 36 37)
211+
212+
get_random_colors() {
213+
local bg_index=$((RANDOM % ${#BG_COLORS[@]}))
214+
local fg_index=$((RANDOM % ${#FG_COLORS[@]}))
215+
# Ensure fg and bg are not too similar
216+
while [ $bg_index -eq $((fg_index - 10)) ]; do
217+
fg_index=$((RANDOM % ${#FG_COLORS[@]}))
218+
done
219+
BG_COLOR=${BG_COLORS[$bg_index]}
220+
FG_COLOR=${FG_COLORS[$fg_index]}
221+
}
222+
223+
fade_in() {
224+
for i in {0..10}; do
225+
local grey=$((232 + i)); tput setab $grey; clear; sleep 0.05
226+
done
227+
tput setab 242; clear
228+
}
229+
230+
animate_text() {
231+
local phrase=$1; local width=$(tput cols); local height=$(tput lines)
232+
local len=${#phrase}; local x=$(( (width - len) / 2 )); local y=$(( height / 2 ))
233+
local dx=1; local dy=1
234+
235+
# Animate for about 9.5 seconds
236+
for ((i=0; i<95; i++)); do
237+
# Clear previous text
238+
tput cup $y $x; printf "%*s" $len " "
239+
240+
x=$((x + dx)); y=$((y + dy))
241+
242+
# Bounce off edges
243+
if [ $x -le 0 ] || [ $((x + len)) -ge $width ]; then dx=$((-dx)); x=$((x + dx)); fi
244+
if [ $y -le 0 ] || [ $y -ge $height ]; then dy=$((-dy)); y=$((y + dy)); fi
245+
246+
tput cup $y $x; echo "$phrase"
247+
sleep 0.1
248+
done
249+
}
250+
251+
# --- The Main Event ---
252+
the_show_must_go_on() {
253+
tts_detect_engine
254+
if [ -z "$TTS_ENGINE" ]; then
255+
display_error_and_exit
256+
fi
257+
tput civis; clear
258+
local intro_phrase=${intro_phrases[$RANDOM % ${#intro_phrases[@]}]}
259+
say_txt "$intro_phrase"
260+
fade_in
261+
tput setaf 7
262+
animate_text "$intro_phrase"
263+
kill_speech
264+
265+
while true; do
266+
get_random_colors
267+
tput setab $BG_COLOR; tput setaf $FG_COLOR; clear
268+
local phrase=${general_phrases[$RANDOM % ${#general_phrases[@]}]}
269+
say_txt "$phrase"
270+
animate_text "$phrase"
271+
kill_speech
272+
done
273+
}
274+
275+
# --- Let's get this show on the road ---
276+
the_show_must_go_on

gallery/speaky/tts.vbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Set Voice = CreateObject("SAPI.SpVoice")
2+
Voice.Speak WScript.Arguments(0)

tests/speaky.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env bats
2+
3+
load 'libs/bats-support/load'
4+
load 'libs/bats-assert/load'
5+
6+
SCRIPT_UNDER_TEST="../gallery/speaky/speaky.sh"
7+
ORIGINAL_SCRIPT=""
8+
9+
setup() {
10+
# Make sure the script is executable
11+
chmod +x "$SCRIPT_UNDER_TEST"
12+
# Save a copy of the original script
13+
ORIGINAL_SCRIPT=$(cat "$SCRIPT_UNDER_TEST")
14+
}
15+
16+
teardown() {
17+
# Restore the original script from the saved content
18+
if [ -n "$ORIGINAL_SCRIPT" ]; then
19+
echo "$ORIGINAL_SCRIPT" > "$SCRIPT_UNDER_TEST"
20+
fi
21+
}
22+
23+
@test "speaky: displays witty error when no TTS engine is found" {
24+
# Override the detect function to simulate no engine found
25+
# and make the error message predictable for the test
26+
perl -i -p0e 's/(tts_detect_engine\(\) \{)(.*?)(\n\})/$1\n TTS_ENGINE=""\n return\n$3/s' "$SCRIPT_UNDER_TEST"
27+
perl -i -pe "s/local phrase=\\\$\\{error_phrases\\[\\\$RANDOM % \\\$\\{#error_phrases\\[@\\]\\}\\]\\}/local phrase=\\\$\\{error_phrases[0]\\}/g" "$SCRIPT_UNDER_TEST"
28+
29+
run "$SCRIPT_UNDER_TEST"
30+
assert_success
31+
assert_output --partial "I can't find any speaky speaky app"
32+
}
33+
34+
@test "speaky: runs successfully with a simulated TTS engine" {
35+
# Override the detect function to simulate finding the 'say' engine
36+
perl -i -p0e 's/(tts_detect_engine\(\) \{)(.*?)(\n\})/$1\n TTS_ENGINE="say"\n return\n$3/s' "$SCRIPT_UNDER_TEST"
37+
# Override the say_txt function to just echo a marker instead of speaking
38+
perl -i -p0e 's/(say_txt\(\) \{)(.*?)(\n\})/$1\n echo "[BATS_SUCCESS_MARKER]"\n SPEAK_PID=$$ # Set a dummy PID\n$3/s' "$SCRIPT_UNDER_TEST"
39+
40+
# Run the script but time it out to prevent it from running forever
41+
run timeout 1s "$SCRIPT_UNDER_TEST"
42+
43+
# It might exit with a timeout status, which is fine for this test.
44+
# We just want to see if it started correctly.
45+
assert_output --partial "[BATS_SUCCESS_MARKER]"
46+
}

tests/tests.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ load 'libs/bats-assert/load'
3232
run timeout 1s ../screensaver.sh <<< "matrix"
3333
assert_success
3434
}
35+
36+
# --- Load and run tests for speaky screensaver ---
37+
load 'speaky.sh'

0 commit comments

Comments
 (0)