-
Notifications
You must be signed in to change notification settings - Fork 10
/
ames.sh
executable file
·501 lines (441 loc) · 13.6 KB
/
ames.sh
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
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
#!/usr/bin/env bash
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
set -euo pipefail
AUDIO_FIELD="audio"
SCREENSHOT_FIELD="image"
SENTENCE_FIELD="Sentence"
# leave OUTPUT_MONITOR blank to autoselect a monitor.
OUTPUT_MONITOR=""
AUDIO_BITRATE="64k"
AUDIO_FORMAT="opus"
AUDIO_VOLUME="1"
MINIMUM_DURATION="0"
IMAGE_FORMAT="webp"
# -2 to calculate dimension while preserving aspect ratio.
IMAGE_WIDTH="-2"
IMAGE_HEIGHT="300"
get_config_dir() {
# get the configuration directory
# adapted from https://xdgbasedirectoryspecification.com/
local config_dir="${XDG_CONFIG_HOME-}"
if [ -z "$config_dir" ] || [ "${config_dir::1}" != '/' ]; then
echo -n "$HOME/.config/ames"
else
echo -n "$config_dir/ames"
fi
}
# the config is sourced at the bottom of this file to overwrite functions.
CONFIG_FILE_PATH="$(get_config_dir)/config"
usage() {
# display help
echo "-h: display this help message"
echo "-r: record audio toggle"
echo "-s: interactive screenshot"
echo "-a: screenshot same region again (defaults to -s if no region)"
echo "-w: screenshot currently active window (xdotool)"
echo "-c: export copied text (contents of the CLIPBOARD selection)"
}
notify_message() {
# send a notification with a message to the user.
# $1 is the string containing the message text.
#
# notifies both the console and with libnotify.
echo "$1"
notify-send --hint=int:transient:1 -t 500 -u normal "$1"
}
check_response() {
# check the JSON response of a request to Anki.
# $1 is the response from ankiconnect_request().
local -r get_error='s/.*"error"[[:space:]]*:[[:space:]]*\([^,}]*\).*/\1/p'
local -r strip_whitespace='s/[[:space:]]*$//'
# if the error string itself contains "," or "}" this will end early
local -r error="$(echo "$1" \
| sed --posix -n "$get_error" \
| sed --posix "$strip_whitespace")"
if [[ "$error" != null ]]; then
notify_message "${error:1:-1}"
exit 1
fi
}
notify_screenshot_add() {
# notify the user that a screenshot was added.
if [[ "$LANG" == en* ]]; then
notify_message "Screenshot added"
fi
if [[ "$LANG" == ja* ]]; then
notify_message "スクリーンショット付けました"
fi
}
notify_record_start() {
# notify the user that a recording started.
if [[ "$LANG" == en* ]]; then
notify_message "Recording started..."
fi
if [[ "$LANG" == ja* ]]; then
notify_message "録音しています..."
fi
}
notify_record_stop() {
# notify the user that a recording stopped.
if [[ "$LANG" == en* ]]; then
notify_message "Recording added"
fi
if [[ "$LANG" == ja* ]]; then
notify_message "録音付けました"
fi
}
notify_sentence_add() {
# notify the user that a sentence was added.
if [[ "$LANG" == en* ]]; then
notify_message "Sentence added"
fi
if [[ "$LANG" == ja* ]]; then
notify_message "例文付けました"
fi
}
maxn() {
# compute the max element of a list.
tr -d ' ' | tr ',' '\n' | awk '
BEGIN {
max = 0
}
{
if ($0 > max) {
max = $0
}
}
END {
print max
}
'
}
escape() {
# serialize an arbitrary string for use in JSON.
# $1 is the string to serialize.
local escaped="${1//\\/\\\\}"
escaped="${escaped//\"/\\\"}"
local -r newline="
"
escaped="${escaped//$newline/\\n}"
echo -n "$escaped"
}
get_last_id() {
# get the id of the last card added to Anki.
# result is stored in the global variable newest_card_id.
local -r new_card_request='{
"action": "findNotes",
"version": 6,
"params": {
"query": "added:1"
}
}'
local new_card_response list
new_card_response="$(ankiconnect_request "$new_card_request")"
check_response "$new_card_response"
list="$(echo "$new_card_response" | cut -d "[" -f2 | cut -d "]" -f1)"
newest_card_id="$(echo "$list" | maxn)"
}
store_file() {
# store a media file.
local -r dir="${1:?}"
local -r name="$(basename -- "$dir")"
local request='{
"action": "storeMediaFile",
"version": 6,
"params": {
"filename": "<name>",
"path": "<dir>"
}
}'
request="${request//<name>/$name}"
request="${request/<dir>/$dir}"
check_response "$(ankiconnect_request "$request")"
}
gui_browse() {
# open the gui card browser and point the modified card.
local -r query="${1:-nid:1}"
local request='{
"action": "guiBrowse",
"version": 6,
"params": {
"query": "<QUERY>"
}
}'
request="${request/<QUERY>/$query}"
check_response "$(ankiconnect_request "$request")"
}
ankiconnect_request() {
# send data to Anki through a HTTP request to AnkiConnect.
# $1 is the data to send.
curl --silent localhost:8765 -X POST -d "${1:?}" || \
echo '{"error": "Empty response from AnkiConnect. Is Anki running?"}'
}
safe_request() {
# only send requests after opening the gui browser.
gui_browse "nid:1"
check_response "$(ankiconnect_request "${1:?}")"
gui_browse "nid:${newest_card_id:?Newest card is not known.}"
}
update_sentence() {
# update card with sentence.
# $1 is the sentence.
get_last_id
local update_request='{
"action": "updateNoteFields",
"version": 6,
"params": {
"note": {
"id": <id>,
"fields": { "<SENTENCE_FIELD>": "<sentence>" }
}
}
}'
update_request="${update_request/<id>/$newest_card_id}"
update_request="${update_request/<SENTENCE_FIELD>/$SENTENCE_FIELD}"
local -r sentence="$(escape "$1")"
update_request="${update_request/<sentence>/$sentence}"
safe_request "$update_request"
}
update_img() {
# update card with image.
# $1 is the path to the image.
get_last_id
local update_request='{
"action": "updateNoteFields",
"version": 6,
"params": {
"note": {
"id": <id>,
"fields": { "<SCREENSHOT_FIELD>": "<img src=\"<path>\">" }
}
}
}'
update_request="${update_request/<id>/$newest_card_id}"
update_request="${update_request/<SCREENSHOT_FIELD>/$SCREENSHOT_FIELD}"
update_request="${update_request/<path>/$1}"
safe_request "$update_request"
}
update_sound() {
# update card with sound, given by an audio file.
# $1 is the path to the audio file.
get_last_id
local update_request='{
"action": "updateNoteFields",
"version": 6,
"params": {
"note": {
"id": <id>,
"fields": {
"<AUDIO_FIELD>":"[sound:<path>]"
}
}
}
}'
update_request="${update_request/<id>/$newest_card_id}"
update_request="${update_request/<AUDIO_FIELD>/$AUDIO_FIELD}"
update_request="${update_request/<path>/$1}"
safe_request "$update_request"
}
encode_img() {
# use ffmpeg to encode an image to some desired format.
local -r source_path="$1"
local -r dest_path="$2"
ffmpeg -nostdin \
-hide_banner \
-loglevel error \
-i "$source_path" \
-vf scale="$IMAGE_WIDTH:$IMAGE_HEIGHT" \
"$dest_path"
}
get_selection() {
# get a region of the screen for future screenshotting.
slop
}
take_screenshot_region() {
# function to take a screenshot of a given screen region.
# $1 is the geometry of the region from get_selection().
# $2 is the output file name.
local -r geom="$1"
local -r path="$2"
maim --hidecursor "$path" -g "$geom"
}
take_screenshot_window() {
# function to take a screenshot of the current window.
# $1 is the output file name.
local -r path="$1"
maim --hidecursor "$path" -i "$(xdotool getactivewindow)"
}
screenshot() {
# take a screenshot by prompting the user for a selection
# and then add this image to the last Anki card.
local -r geom="$(get_selection)"
local -r path="$(mktemp /tmp/maim-screenshot.XXXXXX.png)"
local -r base_path="$(basename -- "$path" | cut -d "." -f-2)"
local -r converted_path="/tmp/$base_path.$IMAGE_FORMAT"
take_screenshot_region "$geom" "$path"
encode_img "$path" "$converted_path"
rm "$path"
echo "$geom" >/tmp/previous-maim-screenshot
store_file "$converted_path"
update_img "$(basename -- "$converted_path")"
notify_screenshot_add
}
again() {
# if screenshot() has been called, then repeat take another screenshot
# with the same dimensions as last time and add to the last Anki card.
# otherwise, call screenshot().
local -r path="$(mktemp /tmp/maim-screenshot.XXXXXX.png)"
local -r base_path="$(basename -- "$path" | cut -d "." -f-2)"
local -r converted_path="/tmp/$base_path.$IMAGE_FORMAT"
if [[ -f /tmp/previous-maim-screenshot ]]; then
take_screenshot_region "$(cat /tmp/previous-maim-screenshot)" "$path"
encode_img "$path" "$converted_path"
rm "$path"
store_file "$converted_path"
get_last_id
update_img "$(basename -- "$converted_path")"
notify_screenshot_add
else
screenshot
fi
}
screenshot_window() {
# take a screenshot of the active window and add to the last Anki card.
local -r path="$(mktemp /tmp/maim-screenshot.XXXXXX.png)"
local -r base_path="$(basename -- "$path" | cut -d "." -f-2)"
local -r converted_path="/tmp/$base_path.$IMAGE_FORMAT"
take_screenshot_window "$path"
encode_img "$path" "$converted_path"
rm "$path"
store_file "$converted_path"
update_img "$(basename -- "$converted_path")"
notify_screenshot_add
}
current_time() {
# current time as an integer number of milliseconds since the epoch.
echo "$(date '+%s')$(date '+%N' | awk '{ print substr($1, 0, 3) }')"
}
record_function() {
# function to record desktop audio.
# $1 is the name of the output monitor.
# $2 is the output file name.
# the last function called here MUST be the call to
# ffmpeg or some other program that does recording.
# when -r is called again, the pid of the last function call is killed.
local -r output="$1"
local -r audio_file="$2"
ffmpeg -nostdin \
-y \
-loglevel error \
-f pulse \
-i "$output" \
-ac 2 \
-af "volume=${AUDIO_VOLUME},silenceremove=1:0:-50dB" \
-ab "$AUDIO_BITRATE" \
"$audio_file" 1> /dev/null &
}
record_start() {
# begin recording audio.
local -r audio_file="$(mktemp \
"/tmp/ffmpeg-recording.XXXXXX.$AUDIO_FORMAT")"
echo "$audio_file" >"$recording_toggle"
if [ "$OUTPUT_MONITOR" == "" ]; then
local -r output="$(pactl info \
| grep 'Default Sink' \
| awk '{print $NF ".monitor"}')"
else
local -r output="$OUTPUT_MONITOR"
fi
record_function "$output" "$audio_file"
echo "$!" >> "$recording_toggle"
current_time >> "$recording_toggle"
notify_record_start
}
record_end() {
# end recording.
local -r audio_file="$(sed -n "1p" "$recording_toggle")"
local -r pid="$(sed -n "2p" "$recording_toggle")"
local -r start_time="$(sed -n "3p" "$recording_toggle")"
local -r duration="$(($(current_time) - start_time))"
if [ "$duration" -le "$MINIMUM_DURATION" ]; then
sleep "$((MINIMUM_DURATION - duration))e-3"
fi
rm "$recording_toggle"
kill -15 "$pid"
while [ "$(du "$audio_file" | awk '{ print $1 }')" -eq 0 ]; do
true
done
local -r audio_backup="/tmp/ffmpeg-recording-audio-backup.$AUDIO_FORMAT"
cp "$audio_file" "$audio_backup"
ffmpeg -nostdin \
-y \
-loglevel error \
-i "$audio_backup" \
-c copy \
-to "${duration}ms" \
"$audio_file" 1> /dev/null
store_file "${audio_file}"
update_sound "$(basename -- "$audio_file")"
notify_record_stop
}
record() {
# this section is a heavily modified version of the linux audio
# script written by salamander on qm's animecards.
recording_toggle="/tmp/ffmpeg-recording-audio"
if [[ ! -f /tmp/ffmpeg-recording-audio ]]; then
record_start
else
record_end
fi
}
copied_text() {
# get the contents of the clipboard.
if command -v xclip &> /dev/null
then
xclip -o -selection clipboard
elif command -v xsel &> /dev/null
then
xsel -b
else
echo "Couldn't find xclip or xsel." >&2
exit 1
fi
}
clipboard() {
# get the current clipboard, and add this text to the last Anki card.
local -r sentence="$(copied_text)"
update_sentence "${sentence}"
notify_sentence_add
}
if [[ -f "$CONFIG_FILE_PATH" ]]; then
# shellcheck disable=SC1090
source "$CONFIG_FILE_PATH"
fi
if [[ -z "${1-}" ]]; then
usage
exit 1
fi
while getopts 'hrsawc' flag; do
case "${flag}" in
h) usage ;;
r) record ;;
s) screenshot ;;
a) again ;;
w) screenshot_window ;;
c) clipboard ;;
*) ;;
esac
done