(defvar vz/kdeconnect-daemon-service "org.kde.kdeconnect.daemon" "Name of the KDE Connect daemon DBus service.") (defvar vz/kdeconnect-device-interface "org.kde.kdeconnect.device" "Name of the KDE Connect device DBus interface.") (defun vz/kdeconnect--devices () "Return a list of full paths of all devices available. Every device under the path /modules/kdeconnect/devices is returned without any filtering." (mapcar (lambda (x) (concat "/modules/kdeconnect/devices/" x)) (dbus-call-method :session vz/kdeconnect-daemon-service "/modules/kdeconnect" vz/kdeconnect-daemon-service "devices"))) (defun vz/kdeconnect-device-reachable-p (device) "Return non-nil if remote device DEVICE is paired and reachable." ;; See (dbus-introspect :session vz/kdeconnect-daemon-service "/modules/kdeconnect/devices/76e88a7fc6d6b3e8") (and (dbus-get-property :session vz/kdeconnect-daemon-service device vz/kdeconnect-device-interface "isPaired") (dbus-get-property :session vz/kdeconnect-daemon-service device vz/kdeconnect-device-interface "isReachable") device)) (defun vz/kdeconnect-reachable-devices () "Return a list of paths of paired and reachable devices." (delq nil (mapcar #'vz/kdeconnect-device-reachable-p (vz/kdeconnect--devices)))) (defun vz/kdeconnect--device-name (path) "Return the device name of device with path PATH." (condition-case nil (dbus-get-property :session vz/kdeconnect-daemon-service path vz/kdeconnect-device-interface "name") ;; This can happen sometimes. (error (file-name-nondirectory path)))) (defun vz/kdeconnect-find-device-by-name (name) "Return path of remote device with display name NAME, nil if not." (let ((devices (vz/kdeconnect--devices)) donep) (while (or (null donep) devices) (when (equal name (vz/kdeconnect--device-name (car devices))) (setq donep (car devices))) (setq devices (cdr devices))) donep)) (defun vz/read-kdeconnect-device (prompt &optional devices) "Read device connected via KDE Connect and return its DBus path. The prompt string is given by PROMPT. If no devices are connected, this function errors. If optional argument DEVICES is non-nil, then use that instead of reachable devices." (let ((devices (mapcar (lambda (x) (cons (vz/kdeconnect--device-name x) x)) (or devices (vz/kdeconnect-reachable-devices))))) (if devices (assoc-default (completing-read (format-prompt prompt (caar devices)) devices nil 'req-match nil nil (caar devices)) devices) (error "No devices connected")))) (defun vz/kdeconnect--plugin-enabled-p (device plugin) "Return non-nil if plugin PLUGIN is enabled for device DEVICE." (dbus-call-method :session vz/kdeconnect-daemon-service device vz/kdeconnect-device-interface "isPluginEnabled" plugin)) (defvar vz/kdeconnect-device-share-interface (concat vz/kdeconnect-device-interface ".share") "Interface name for the `share' plugin.") (defun vz/kdeconnect-send-files (device files) "Send FILES to DEVICE via KDE Connect. FILES is a list of files to send, and DEVICE is the DBus path of the device to send it to. This function errors when DEVICE does not have the `share' plugin enabled." (unless (vz/kdeconnect--plugin-enabled-p device "kdeconnect_share") (error "`kdeconnect_share' plugin is not enabled in device %s" device)) (dbus-call-method :session vz/kdeconnect-daemon-service (concat device "/share") vz/kdeconnect-device-share-interface "shareUrls" (mapcar (lambda (x) (concat "file://" (url-hexify-string (expand-file-name x) (cons ?/ url-unreserved-chars)))) files))) (defun vz/kdeconnect-share-urls (device urls) "Send URLS to DEVICE via KDE Connect. URLS is a list of URL to send, and DEVICE is the DBus path of the device to send it to. This function errors when DEVICE does not have the `share' plugin enabled." (unless (vz/kdeconnect--plugin-enabled-p device "kdeconnect_share") (error "`kdeconnect_share' plugin is not enabled in device %s" device)) (dbus-call-method :session vz/kdeconnect-daemon-service (concat device "/share") vz/kdeconnect-device-share-interface "shareUrls" urls)) (defvar vz/kdeconnect-url-regexp (rx bos (or (seq "http" (? "s")) "magnet") ":") "Regexp a given string should match to be sent as a URL.") (defun vz/kdeconnect-send-region (begin end device) "Send BEGIN..END as URL or text depending on its nature to DEVICE. See `vz/kdeconnect-url-regexp'." (interactive (let ((beg (and (use-region-p) (region-beginning))) (end (and (use-region-p) (region-end)))) (list beg end (vz/read-kdeconnect-device "Select device")))) (let ((string (if (and begin end) (buffer-substring-no-properties begin end) (read-string "Send to device: ")))) (dbus-call-method :session vz/kdeconnect-daemon-service (concat device "/share") vz/kdeconnect-device-share-interface (if (string-match-p vz/kdeconnect-url-regexp string) "shareUrl" "shareText") string))) ;; (define-key vz/ctl-z-map (kbd "C-s") #'vz/kdeconnect-send-region) (defvar vz/kdeconnect-sftp-interface (concat vz/kdeconnect-device-interface ".sftp") "Interface name for filesystem share plugin.") (defvar vz/kdeconnect--mounted-devices '() "List of devices mounted.") (defun vz/kdeconnect--mounted-directories (device) "Return mounted directories of device DEVICE. An alist (DIRNAME . DIR) where DIRNAME is the name of the directory named in the device, and DIR is the absolute filename of the directory." (let ((dir (dbus-call-method :session vz/kdeconnect-daemon-service (concat device "/sftp") vz/kdeconnect-sftp-interface "getDirectories")) ret) (dolist (d dir) (push (cons (caadr d) (car d)) ret)) ret)) (defun vz/kdeconnect-mount-device (device) "Mount remote device DEVICE and visit exposed directory in `dired'. Exposed directories to visit is read from the user." (interactive (list (vz/read-kdeconnect-device "Mount device"))) (unless (vz/kdeconnect--plugin-enabled-p device "kdeconnect_sftp") (error "Plugin `kdeconnect_sftp' not enabled for device `%s'" device)) ;; TODO: This fails to wait? After mounting the device, the ;; directory was not accessible! (unless (dbus-call-method :session vz/kdeconnect-daemon-service (concat device "/sftp") vz/kdeconnect-sftp-interface "mountAndWait") (error "Failed to mount device `%s'" device)) (push device vz/kdeconnect--mounted-devices) (let* ((dirs (vz/kdeconnect--mounted-directories device)) (dir (completing-read "Goto directory: " dirs nil t))) (with-current-buffer (dired (assoc-default dir dirs)) (rename-buffer (concat "*" (vz/kdeconnect--device-name device) ": " dir "*"))))) (defun vz/kdeconnect-unmount-device (device) "Unmount mounted remote device DEVICE." (interactive (list (vz/read-kdeconnect-device "Unmount device" vz/kdeconnect--mounted-devices))) (dbus-call-method :session vz/kdeconnect-daemon-service (concat device "/sftp") vz/kdeconnect-sftp-interface "unmount") (setq vz/kdeconnect--mounted-devices (delete device vz/kdeconnect--mounted-devices))) (defvar vz/kdeconnect-device-battery-interface (concat vz/kdeconnect-device-interface ".battery") "Interface name for battery plugin.") (defun vz/kdeconnect-device-battery (device) "Report battery charge and charging status of DEVICE." (interactive (list (vz/read-kdeconnect-device "Report battery of device"))) (unless (vz/kdeconnect--plugin-enabled-p device "kdeconnect_battery") (error "Plugin `kdeconnect_battery' not enabled for device `%s'" device)) (message "%d%%%s" (dbus-get-property :session vz/kdeconnect-daemon-service (concat device "/battery") vz/kdeconnect-device-battery-interface "charge") (if (dbus-get-property :session vz/kdeconnect-daemon-service (concat device "/battery") vz/kdeconnect-device-battery-interface "isCharging") "+" ""))) (defun vz/dired-send-via-kdeconnect (device) "Send the marked files to DEVICE via kdeconnect." (interactive (condition-case nil (list (vz/read-kdeconnect-device "Select the device, good sire")) (error (user-error "No device connected"))) dired-mode) (vz/kdeconnect-send-files device (dired-get-marked-files))) (with-eval-after-load 'dired (define-key dired-mode-map (kbd "C-c C-s") #'vz/dired-send-via-kdeconnect))