guile-clipboard-speaker

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | LICENSE

commit 9a6a9e13aba798872e10399d04b516516fee0413
parent 8b493f674c1f42c480c582d10cd50bdae0926555
Author: Yuval Langer <yuval.langer@gmail.com>
Date:   Sat, 30 Dec 2023 22:54:36 +0200

Define (clipboard-speaker) as a R7RS library.  Remove the main call from the end of the script.

Diffstat:
Mclipboard-speaker.scm | 674+++++++++++++++++++++++++++++++++++++++++--------------------------------------
1 file changed, 347 insertions(+), 327 deletions(-)

diff --git a/clipboard-speaker.scm b/clipboard-speaker.scm @@ -1,344 +1,364 @@ -#!/usr/bin/env guile +#!/usr/bin/env -S guile --r7rs -e (clipboard-speaker) -s !# -(use-modules (srfi srfi-26) - ((srfi srfi-180) #:prefix json:) - - (ice-9 match) - (ice-9 popen) - (ice-9 textual-ports) - - ((config) #:prefix config:) - ((config api) #:prefix config:api:) - ((config licenses) #:prefix config:licenses:) - ((config parser sexp) #:prefix config:parser:sexp:) - ) - - -(define espeak-ng-pid #f) - - -(define (dp x) - ;; Debug print. - (display x) (newline)) - +(define-library (clipboard-speaker) + (import + (scheme base) + (scheme process-context) + (scheme write) + + (only (guile) + EXIT_FAILURE + EXIT_SUCCESS + F_GETFL + F_SETFL + LOCK_EX + LOCK_NB + O_NONBLOCK + SIGTERM + cdadr + compose + fcntl + file-exists? + filter + flock + force-output + format + getpw + getuid + kill + logior + mknod + open-file + passwd:dir + pipe + setvbuf + sigaction + spawn + string-ci=? + string-join + string-split + string-trim-both + waitpid + ) + (srfi srfi-26) + (prefix (srfi srfi-180) json:) + + (ice-9 match) + (ice-9 popen) + (ice-9 textual-ports) + + (prefix (config) config:) + (prefix (config api) config:api:) + (prefix (config licenses) config:licenses:) + (prefix (config parser sexp) config:parser:sexp:)) + (export main) + + (begin + (define *espeak-ng-pid* #f) + + (define (dp x) + ;; Debug print. + (display x) (newline)) + + (define (dp-id x) + ;; Debug print, but it's just like the identity procedure. + (display x) (newline) + x) + + (define (debug-notify-send x) + (spawn "notify-send" (list "notify-send" + "--expire-time=1000" + (format #f "~s~%" x)))) + + (define (get-pid-of-file-locking-process options) + (define lslocks-output-pipe (pipe)) + (define lslocks-pid + (spawn "lslocks" + (list "lslocks" + "--json" + "--notruncate" + "--output=pid,path") + #:output (cdr lslocks-output-pipe))) + + (close-port (cdr lslocks-output-pipe)) + + (define lslocks-output (json:json-read (car lslocks-output-pipe))) + + (waitpid lslocks-pid) + (close-port (car lslocks-output-pipe)) + + (define result + (match (filter (lambda (x) + (equal? (cdadr x) + (config:option-ref options 'lock-file-path))) + ((compose + vector->list + cdar) + lslocks-output)) + (((('pid . pid) ('path . path))) + pid) + (() + #f))) + + result) + + (define (open-file-locked filename) + (let ([file-port (open-file filename "w")]) + (with-exception-handler + (lambda (an-exception) + (close-port file-port) + #f) + (lambda () + (flock file-port + (logior LOCK_EX + LOCK_NB)) + file-port) + #:unwind? #t))) + + (define (get-home-directory) + (passwd:dir (getpw (getuid)))) + + (define DEFAULT-WORDS-PER-MINUTE 200) + + (define DEFAULT-CLIPBOARD-SPEAKER-HOME-PATH ".config/clipboard-speaker/") + + (define DEFAULT-LOCK-FILE-PATH + (string-append DEFAULT-CLIPBOARD-SPEAKER-HOME-PATH + "lock")) + + (define DEFAULT-FIFO-FILE-PATH + (string-append DEFAULT-CLIPBOARD-SPEAKER-HOME-PATH + "fifo")) + + (define config + (config:api:configuration + (name 'clipboard-speaker) + (synopsis "Speak clipboard contents.") + (author "Yuval Langer") + (copyright '(2023)) + (license config:licenses:agpl3+) + (version "0.1") + (parser config:parser:sexp:sexp-parser) + (keywords + (list + (config:api:switch + (name 'clipboard-type) + (synopsis "Type of clipboard (b, s, or p)") + (character #f) + (example "b/s/p (choose b or s or p)") + (default "b") + (test (lambda (x) + (or (string-ci=? x "b") + (string-ci=? x "s") + (string-ci=? x "p")))) + ;;(handler identity) + ) + (config:api:switch + (name 'words-per-minute) + (synopsis "Words per minute spoken.") + (character #f) + (example "170") + (default 200) + (test integer?) + (handler string->number)) + (config:api:switch + (name 'kill) + (synopsis "Kill the espeak-ng loop process.") + (default #f) + (test boolean?) + (character #f)) + (config:api:switch + (name 'clipboard-speaker-path) + (synopsis "Path to the clipboard-speaker settings directory.") + (default DEFAULT-CLIPBOARD-SPEAKER-HOME-PATH) + (character #f) + ) + (config:api:setting + (name 'words-per-minute) + (synopsis "Words per minute spoken.") + (default 200) + (handler string->number) + (test number?) + (example "230")) + (config:api:setting + (name 'fifo-file-path) + (synopsis "Path to FIFO file.") + (default (string-append (get-home-directory) + "/" + DEFAULT-FIFO-FILE-PATH))) + (config:api:setting + (name 'lock-file-path) + (synopsis "Path to lock file.") + (default (string-append (get-home-directory) + "/" + DEFAULT-LOCK-FILE-PATH))))) + (directory + (config:api:in-home DEFAULT-CLIPBOARD-SPEAKER-HOME-PATH)))) + + (define (make-config-if-does-not-exist path value) + (if (file-exists? path) + #f + (let ([file-port (open-file path "w")]) + (put-string file-port value)))) + + (define (mkfifo path mode) + (mknod path 'fifo mode 0)) + + (define (read-clipboard clipboard-type) + ;; Popen `xsel -p` or `xsel -b` as an output port. + (unless (or (equal? clipboard-type "p") + (equal? clipboard-type "s") + (equal? clipboard-type "b")) + (error "Bad clipboard type:" clipboard-type)) + + (define xsel-stdout-pipe (pipe)) + (define xsel-pid + (spawn "xsel" + (list "xsel" + (string-append "-" clipboard-type)) + #:output (cdr xsel-stdout-pipe))) + + (close-port (cdr xsel-stdout-pipe)) + + ;; Read lines from xsel port and for each line replace + ;; the newlines in it with spaces. + (define xsel-output ((compose (cut string-join <> " ") + (cut map string-trim-both <>) + (cut string-split <> #\newline) + get-string-all) + (car xsel-stdout-pipe))) + + (close-port (car xsel-stdout-pipe)) + + (waitpid xsel-pid) + + xsel-output) + + (define (make-fifo-ports fifo-file-path) + (when (not (file-exists? fifo-file-path)) + (mkfifo fifo-file-path + #o600)) + + (define fifo-port-write + (open-file fifo-file-path + "wl+")) + + (define fifo-port-read + (open-file fifo-file-path + "rl+")) + + (let ([flags (fcntl fifo-port-read + F_GETFL)]) + (fcntl fifo-port-read + F_SETFL + (logior O_NONBLOCK + flags))) + + (cons fifo-port-read fifo-port-write)) + + (define (espeak-loop fifo-read-port words-per-minute) + (define espeak-ng-input-pipe (pipe)) + + (set! *espeak-ng-pid* + (spawn "espeak-ng" + (list "espeak-ng" + "-s" + (number->string words-per-minute)) + #:input (car espeak-ng-input-pipe))) + + (let loop ((text-to-speak (get-line fifo-read-port))) + (format #t "Server received: ~A\n" text-to-speak) + + (put-string (cdr espeak-ng-input-pipe) + text-to-speak) + ;; XXX: These ports are line buffered(?). Always write to ports + ;; with a newline at their end if you want the other side to read + ;; them now. + (put-char (cdr espeak-ng-input-pipe) + #\newline) + + ;; XXX: You also have you flush the port, otherwise the TTS keeps + ;; silent. Please someone explain this to me. + (force-output (cdr espeak-ng-input-pipe)) + + (loop (get-line fifo-read-port)))) + + (define (make-pipes buffering-type) + (define pipes (pipe)) + + (setvbuf (car pipes) buffering-type) + (setvbuf (cdr pipes) buffering-type) + + pipes) + + (define (kill-server options) + (cond + ((file-exists? (config:option-ref options + 'lock-file-path)) + (let ([pid (get-pid-of-file-locking-process options)]) + (cond + ((number? pid) + (kill pid SIGTERM) + (format #t "Killed ~S.~%" pid) + (exit EXIT_SUCCESS)) + (else + (format #t "clipboard-speaker is not running.~%") + (exit EXIT_FAILURE))))) + (else + (format #t "No pid file: ~S~%" (config:option-ref options 'lock-file-path)) + (exit EXIT_FAILURE)))) -(define (dp-id x) - ;; Debug print, but it's just like the identity procedure. - (display x) (newline) - x) + (define (main args) + (define options (config:getopt-config-auto args config)) -(define (debug-notify-send x) - (spawn "notify-send" (list "notify-send" - "--expire-time=1000" - (format #f "~s~%" x)))) - - -(define (get-pid-of-file-locking-process options) - (define lslocks-output-pipe (pipe)) - (define lslocks-pid - (spawn "lslocks" - (list "lslocks" - "--json" - "--notruncate" - "--output=pid,path") - #:output (cdr lslocks-output-pipe))) - - (close-port (cdr lslocks-output-pipe)) - - (define lslocks-output (json:json-read (car lslocks-output-pipe))) - - (waitpid lslocks-pid) - (close-port (car lslocks-output-pipe)) - - (define result - (match (filter (lambda (x) - (equal? (cdadr x) - (config:option-ref options 'lock-file-path))) - ((compose - vector->list - cdar) - lslocks-output)) - (((('pid . pid) ('path . path))) - pid) - (() - #f))) - - result) - - -(define (open-file-locked filename) - (let ([file-port (open-file filename "w")]) - (with-exception-handler - (lambda (an-exception) - (close-port file-port) - #f) - (lambda () - (flock file-port - (logior LOCK_EX - LOCK_NB)) - file-port) - #:unwind? #t))) - - -(define (get-home-directory) - (passwd:dir (getpw (getuid)))) - - -(define DEFAULT-WORDS-PER-MINUTE 200) - - -(define DEFAULT-CLIPBOARD-SPEAKER-HOME-PATH ".config/clipboard-speaker/") - - -(define DEFAULT-LOCK-FILE-PATH - (string-append DEFAULT-CLIPBOARD-SPEAKER-HOME-PATH - "lock")) - - -(define DEFAULT-FIFO-FILE-PATH - (string-append DEFAULT-CLIPBOARD-SPEAKER-HOME-PATH - "fifo")) - - -(define config - (config:api:configuration - (name 'clipboard-speaker) - (synopsis "Speak clipboard contents.") - (author "Yuval Langer") - (copyright '(2023)) - (license config:licenses:agpl3+) - (version "0.1") - (parser config:parser:sexp:sexp-parser) - (keywords - (list - (config:api:switch - (name 'clipboard-type) - (synopsis "Type of clipboard (b, s, or p)") - (character #f) - (example "b/s/p (choose b or s or p)") - (default "b") - (test (lambda (x) - (or (string-ci=? x "b") - (string-ci=? x "s") - (string-ci=? x "p")))) - ;;(handler identity) - ) - (config:api:switch - (name 'words-per-minute) - (synopsis "Words per minute spoken.") - (character #f) - (example "170") - (default 200) - (test integer?) - (handler string->number)) - (config:api:switch - (name 'kill) - (synopsis "Kill the espeak-ng loop process.") - (default #f) - (test boolean?) - (character #f)) - (config:api:switch - (name 'clipboard-speaker-path) - (synopsis "Path to the clipboard-speaker settings directory.") - (default DEFAULT-CLIPBOARD-SPEAKER-HOME-PATH) - (character #f) - ) - (config:api:setting - (name 'words-per-minute) - (synopsis "Words per minute spoken.") - (default 200) - (handler string->number) - (test number?) - (example "230")) - (config:api:setting - (name 'fifo-file-path) - (synopsis "Path to FIFO file.") - (default (string-append (get-home-directory) - "/" - DEFAULT-FIFO-FILE-PATH))) - (config:api:setting - (name 'lock-file-path) - (synopsis "Path to lock file.") - (default (string-append (get-home-directory) - "/" - DEFAULT-LOCK-FILE-PATH))))) - (directory - (config:api:in-home DEFAULT-CLIPBOARD-SPEAKER-HOME-PATH)))) - - -(define (make-config-if-does-not-exist path value) - (if (file-exists? path) - #f - (let ([file-port (open-file path "w")]) - (put-string file-port value)))) - - -(define (mkfifo path mode) - (mknod path 'fifo mode 0)) - - -(define (read-clipboard clipboard-type) - ;; Popen `xsel -p` or `xsel -b` as an output port. - (unless (or (equal? clipboard-type "p") - (equal? clipboard-type "s") - (equal? clipboard-type "b")) - (error "Bad clipboard type:" clipboard-type)) - - (define xsel-stdout-pipe (pipe)) - (define xsel-pid - (spawn "xsel" - (list "xsel" - (string-append "-" clipboard-type)) - #:output (cdr xsel-stdout-pipe))) - - (close-port (cdr xsel-stdout-pipe)) - - ;; Read lines from xsel port and for each line replace - ;; the newlines in it with spaces. - (define xsel-output ((compose (cut string-join <> " ") - (cut map string-trim-both <>) - (cut string-split <> #\newline) - get-string-all) - (car xsel-stdout-pipe))) - - (close-port (car xsel-stdout-pipe)) - - (waitpid xsel-pid) - - xsel-output) - -(define (make-fifo-ports fifo-file-path) - (when (not (file-exists? fifo-file-path)) - (mkfifo fifo-file-path - #o600)) - - (define fifo-port-write - (open-file fifo-file-path - "wl+")) - - (define fifo-port-read - (open-file fifo-file-path - "rl+")) - - (let ([flags (fcntl fifo-port-read - F_GETFL)]) - (fcntl fifo-port-read - F_SETFL - (logior O_NONBLOCK - flags))) - - (cons fifo-port-read fifo-port-write)) - - -(define (espeak-loop fifo-read-port words-per-minute) - (define espeak-ng-input-pipe (pipe)) - - (set! espeak-ng-pid - (spawn "espeak-ng" - (list "espeak-ng" - "-s" - (number->string words-per-minute)) - #:input (car espeak-ng-input-pipe))) - - (let loop ((text-to-speak (get-line fifo-read-port))) - (format #t "Server received: ~A\n" text-to-speak) - - (put-string (cdr espeak-ng-input-pipe) - text-to-speak) - ;; XXX: These ports are line buffered(?). Always write to ports - ;; with a newline at their end if you want the other side to read - ;; them now. - (put-char (cdr espeak-ng-input-pipe) - #\newline) + (when (config:option-ref options + 'kill) + (kill-server options)) - ;; XXX: You also have you flush the port, otherwise the TTS keeps - ;; silent. Please someone explain this to me. - (force-output (cdr espeak-ng-input-pipe)) + ;; Make / open read and write fifo files. + (define fifo-r+w (make-fifo-ports (config:option-ref options + 'fifo-file-path))) - (loop (get-line fifo-read-port)))) + (define clipboard-output + (read-clipboard (config:option-ref options + 'clipboard-type))) + (format #t "Client sends: ~A~%" clipboard-output) -(define (make-pipes buffering-type) - (define pipes (pipe)) + ;; Write clipboard contents into the FIFO file for the TTS server to + ;; read. + (put-string (cdr fifo-r+w) + clipboard-output) + ;; XXX: These ports are line buffered. Always write to ports with + ;; a newline at their end if you want the other side to read them + ;; now. + (put-char (cdr fifo-r+w) #\newline) - (setvbuf (car pipes) buffering-type) - (setvbuf (cdr pipes) buffering-type) + ;; XXX: We don't need to write to FIFO anymore. This will make sure + ;; the message passes through. Please someone explain this to me. + (close-port (cdr fifo-r+w)) - pipes) + ;; Try to lock the lock file. + (define lock-file-port (open-file-locked (config:option-ref options + 'lock-file-path))) + (when lock-file-port + ;; If we have the lock, we're the speaker server. -(define (kill-server options) - (cond - ((file-exists? (config:option-ref options - 'lock-file-path)) - (let ([pid (get-pid-of-file-locking-process options)]) - (cond - ((number? pid) - (kill pid SIGTERM) - (format #t "Killed ~S.~%" pid) - (exit EXIT_SUCCESS)) - (else - (format #t "clipboard-speaker is not running.~%") - (exit EXIT_FAILURE))))) - (else - (format #t "No pid file: ~S~%" (config:option-ref options 'lock-file-path)) - (exit EXIT_FAILURE)))) - - -(define (main args) - (define options (config:getopt-config-auto args config)) - - (when (config:option-ref options - 'kill) - (kill-server options)) - - ;; Make / open read and write fifo files. - (define fifo-r+w (make-fifo-ports (config:option-ref options - 'fifo-file-path))) - - (define clipboard-output - (read-clipboard (config:option-ref options - 'clipboard-type))) - - (format #t "Client sends: ~A~%" clipboard-output) - - ;; Write clipboard contents into the FIFO file for the TTS server to - ;; read. - (put-string (cdr fifo-r+w) - clipboard-output) - ;; XXX: These ports are line buffered. Always write to ports with - ;; a newline at their end if you want the other side to read them - ;; now. - (put-char (cdr fifo-r+w) #\newline) - - ;; XXX: We don't need to write to FIFO anymore. This will make sure - ;; the message passes through. Please someone explain this to me. - (close-port (cdr fifo-r+w)) - - ;; Try to lock the lock file. - (define lock-file-port (open-file-locked (config:option-ref options - 'lock-file-path))) - - (when lock-file-port - ;; If we have the lock, we're the speaker server. - - ;; If we receive a divine SIGTERM, kill our offsprings and commit - ;; suicide. Truly, an Abrahamic horror story. - (sigaction SIGTERM - (lambda (received-signal) + ;; If we receive a divine SIGTERM, kill our offsprings and commit + ;; suicide. Truly, an Abrahamic horror story. + (sigaction SIGTERM + (lambda (received-signal) ;;; XXX: BUG: In Gnome, (kill 0 received-signal), which kills all ;;; processes in our group, results in killing the keybinding reading -;;; process that runs clipboard-speaker, hence espeak-ng-pid. - (when (number? espeak-ng-pid) - (kill espeak-ng-pid received-signal)) - (exit EXIT_SUCCESS))) - - (espeak-loop (car fifo-r+w) - (config:option-ref options 'words-per-minute))) - - (exit EXIT_SUCCESS)) +;;; process that runs clipboard-speaker, hence *espeak-ng-pid*. + (when (number? *espeak-ng-pid*) + (kill *espeak-ng-pid* received-signal)) + (exit EXIT_SUCCESS))) + (espeak-loop (car fifo-r+w) + (config:option-ref options 'words-per-minute))) -(main (command-line)) + (exit EXIT_SUCCESS))))