mirror of
https://github.com/SqrtMinusOne/dotfiles.git
synced 2025-12-10 19:23:03 +03:00
Compare commits
3 commits
b0837527ef
...
d1b69e6cbc
| Author | SHA1 | Date | |
|---|---|---|---|
| d1b69e6cbc | |||
| f10fb1a047 | |||
| 96445d5c02 |
12 changed files with 695 additions and 78 deletions
|
|
@ -7,7 +7,7 @@
|
||||||
"ffmpeg"
|
"ffmpeg"
|
||||||
"krita"
|
"krita"
|
||||||
"gimp"
|
"gimp"
|
||||||
"libreoffice"
|
"libreoffice-fresh"
|
||||||
"zathura-djvu"
|
"zathura-djvu"
|
||||||
"zathura-pdf-mupdf"
|
"zathura-pdf-mupdf"
|
||||||
"zathura-ps"
|
"zathura-ps"
|
||||||
|
|
|
||||||
|
|
@ -277,8 +277,8 @@ DIR is either 'left or 'right."
|
||||||
(cl-loop while (windmove-find-other-window opposite-dir)
|
(cl-loop while (windmove-find-other-window opposite-dir)
|
||||||
do (windmove-do-window-select opposite-dir))))))
|
do (windmove-do-window-select opposite-dir))))))
|
||||||
|
|
||||||
(defun my/exwm-refresh-monitors ()
|
(defun my/exwm-refresh-monitors (&optional refresh)
|
||||||
(interactive)
|
(interactive (list t))
|
||||||
(setq my/exwm-monitor-list (my/exwm-xrandr-monitor-list))
|
(setq my/exwm-monitor-list (my/exwm-xrandr-monitor-list))
|
||||||
(cl-loop for i from 0 to (1- exwm-workspace-number)
|
(cl-loop for i from 0 to (1- exwm-workspace-number)
|
||||||
for monitor = (plist-get exwm-randr-workspace-monitor-plist
|
for monitor = (plist-get exwm-randr-workspace-monitor-plist
|
||||||
|
|
@ -287,7 +287,8 @@ DIR is either 'left or 'right."
|
||||||
do
|
do
|
||||||
(setf (plist-get exwm-randr-workspace-monitor-plist i)
|
(setf (plist-get exwm-randr-workspace-monitor-plist i)
|
||||||
(car my/exwm-monitor-list)))
|
(car my/exwm-monitor-list)))
|
||||||
(exwm-randr-refresh))
|
(when refresh
|
||||||
|
(exwm-randr-refresh)))
|
||||||
|
|
||||||
(use-package ivy-posframe
|
(use-package ivy-posframe
|
||||||
:straight t
|
:straight t
|
||||||
|
|
|
||||||
|
|
@ -277,3 +277,8 @@
|
||||||
(message "Processed %s as emacs config module" (buffer-file-name))))
|
(message "Processed %s as emacs config module" (buffer-file-name))))
|
||||||
|
|
||||||
(add-hook 'org-babel-post-tangle-hook #'my/modules--post-tangle)
|
(add-hook 'org-babel-post-tangle-hook #'my/modules--post-tangle)
|
||||||
|
|
||||||
|
(defun my/emacs-tramp ()
|
||||||
|
(interactive)
|
||||||
|
(with-environment-variables (("EMACS_ENV" "remote"))
|
||||||
|
(start-process "emacs-tramp" nil "emacs")))
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,61 @@
|
||||||
(line-beginning-position)
|
(line-beginning-position)
|
||||||
(line-end-position)))))
|
(line-end-position)))))
|
||||||
|
|
||||||
|
(defvar my/edit-elisp-string--window-config nil)
|
||||||
|
|
||||||
|
(defun my/edit-elisp-string ()
|
||||||
|
(interactive)
|
||||||
|
(if (org-src-edit-buffer-p)
|
||||||
|
(org-edit-src-exit)
|
||||||
|
(let* ((bounds (bounds-of-thing-at-point 'string))
|
||||||
|
(orig-buf (current-buffer))
|
||||||
|
(orig-str (when bounds
|
||||||
|
(buffer-substring-no-properties (car bounds) (cdr bounds)))))
|
||||||
|
(unless bounds
|
||||||
|
(user-error "No string under cursor"))
|
||||||
|
;; Not sure if there's a better way
|
||||||
|
(let* ((mode (intern
|
||||||
|
(completing-read
|
||||||
|
"Major mode: " obarray
|
||||||
|
(lambda (sym)
|
||||||
|
(and (commandp sym)
|
||||||
|
(string-suffix-p "-mode" (symbol-name sym))))
|
||||||
|
t)))
|
||||||
|
(edit-buf (generate-new-buffer "*string-edit*")))
|
||||||
|
(setq edit-elisp-string--window-config (current-window-configuration))
|
||||||
|
(with-current-buffer edit-buf
|
||||||
|
(insert (string-replace
|
||||||
|
"\\\"" "\"" (substring orig-str 1 -1)))
|
||||||
|
(funcall mode)
|
||||||
|
(use-local-map (copy-keymap (current-local-map)))
|
||||||
|
;; Confirm edit
|
||||||
|
(local-set-key
|
||||||
|
(kbd "C-c '")
|
||||||
|
(lambda ()
|
||||||
|
(interactive)
|
||||||
|
(let ((new-str (buffer-substring-no-properties
|
||||||
|
(point-min) (point-max))))
|
||||||
|
(with-current-buffer orig-buf
|
||||||
|
(delete-region (car bounds) (cdr bounds))
|
||||||
|
(goto-char (car bounds))
|
||||||
|
(insert (prin1-to-string new-str))))
|
||||||
|
(kill-buffer edit-buf)
|
||||||
|
(when edit-elisp-string--window-config
|
||||||
|
(set-window-configuration edit-elisp-string--window-config))))
|
||||||
|
;; Cancel edit
|
||||||
|
(local-set-key
|
||||||
|
(kbd "C-c C-k")
|
||||||
|
(lambda ()
|
||||||
|
(interactive)
|
||||||
|
(kill-buffer edit-buf)
|
||||||
|
(when edit-elisp-string--window-config
|
||||||
|
(set-window-configuration edit-elisp-string--window-config)))))
|
||||||
|
(pop-to-buffer edit-buf)))))
|
||||||
|
|
||||||
|
(general-define-key
|
||||||
|
:keymaps '(emacs-lisp-mode-map lisp-interaction-mode-map)
|
||||||
|
"C-c '" #'my/edit-elisp-string)
|
||||||
|
|
||||||
(use-package projectile
|
(use-package projectile
|
||||||
:straight t
|
:straight t
|
||||||
:config
|
:config
|
||||||
|
|
|
||||||
|
|
@ -37,14 +37,17 @@ The return value is an alist; see `my/index--tree-get' for details."
|
||||||
(setf (alist-get :symlink val) symlink))
|
(setf (alist-get :symlink val) symlink))
|
||||||
(when (org-element-property :PROJECT heading)
|
(when (org-element-property :PROJECT heading)
|
||||||
(setf (alist-get :project val) t))
|
(setf (alist-get :project val) t))
|
||||||
(when-let* ((kind-str (org-element-property :KIND heading))
|
(when-let* ((kind-str (org-element-property :KIND heading)))
|
||||||
(kind (intern kind-str)))
|
(when (string-match-p (rx bos "rclone:") kind-str)
|
||||||
(setf (alist-get :kind val) kind)
|
(setf (alist-get :remote val)
|
||||||
(when (equal kind 'git)
|
(substring kind-str 7))
|
||||||
|
(setq kind-str "rclone"))
|
||||||
|
(when (equal kind-str "git")
|
||||||
(let ((remote (org-element-property :REMOTE heading)))
|
(let ((remote (org-element-property :REMOTE heading)))
|
||||||
(unless remote
|
(unless remote
|
||||||
(user-error "No remote for %s" (alist-get :name val)))
|
(user-error "No remote for %s" (alist-get :name val)))
|
||||||
(setf (alist-get :remote val) remote))))
|
(setf (alist-get :remote val) remote)))
|
||||||
|
(setf (alist-get :kind val) (intern kind-str)))
|
||||||
(setf (alist-get :name val) (org-element-property :raw-value heading)
|
(setf (alist-get :name val) (org-element-property :raw-value heading)
|
||||||
(alist-get :path val) new-path)
|
(alist-get :path val) new-path)
|
||||||
val)))
|
val)))
|
||||||
|
|
@ -320,6 +323,232 @@ The return value is a list of commands as defined by
|
||||||
(substring path 0 (1- (length path))))
|
(substring path 0 (1- (length path))))
|
||||||
"Mega remove sync" 4)))))
|
"Mega remove sync" 4)))))
|
||||||
|
|
||||||
|
(defconst my/index--rclone-options
|
||||||
|
`("--create-empty-src-dirs"
|
||||||
|
"--resilient"
|
||||||
|
"--metadata"
|
||||||
|
"--filters-file"
|
||||||
|
,(expand-file-name "~/.config/rclone/filters-bisync")))
|
||||||
|
|
||||||
|
(defconst my/index--rclone-script-path "~/bin/rclone-scripts/")
|
||||||
|
|
||||||
|
(defun my/index--rclone-get-folders (tree)
|
||||||
|
"Get TREE nodes to be synced with rclone.
|
||||||
|
|
||||||
|
Return a list of alists with the following keys:
|
||||||
|
- `:local-path' - path in the local filesystem
|
||||||
|
- `:remote-path' - path in the remote
|
||||||
|
- `:remote' - name of the remote."
|
||||||
|
(cl-loop for node in tree
|
||||||
|
if (eq (alist-get :kind node) 'rclone)
|
||||||
|
collect
|
||||||
|
`((:local-path . ,(file-name-as-directory (alist-get :path node)))
|
||||||
|
(:remote-path
|
||||||
|
. ,(concat (alist-get :remote node)
|
||||||
|
":" (my/index--mega-local-path
|
||||||
|
(file-name-as-directory (alist-get :path node)))))
|
||||||
|
(:remote . ,(alist-get :remote node)))
|
||||||
|
append (my/index--rclone-get-folders
|
||||||
|
(alist-get :children node))))
|
||||||
|
|
||||||
|
(defun my/index--rclone-make-command (local-path remote-path remote)
|
||||||
|
"Make a bisync command to sync LOCAL-PATH and REMOTE-PATH.
|
||||||
|
|
||||||
|
REMOTE is the name of the remote."
|
||||||
|
(string-join
|
||||||
|
`("rclone"
|
||||||
|
"bisync"
|
||||||
|
,(format "\"%s\"" local-path)
|
||||||
|
,(format "\"%s\"" remote-path)
|
||||||
|
,@my/index--rclone-options
|
||||||
|
"--check-filename"
|
||||||
|
,(format ".rclone-test-%s" remote))
|
||||||
|
" "))
|
||||||
|
|
||||||
|
(defun my/index--rclone-script (remote folders)
|
||||||
|
(let ((script "
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
REMOTE = '<rclone-remote>'
|
||||||
|
FOLDERS = json.loads('<rclone-folders-json>')
|
||||||
|
OPTIONS = json.loads('<rclone-options>')
|
||||||
|
|
||||||
|
|
||||||
|
def rclone_make_command(local_path, remote_path, remote):
|
||||||
|
return [
|
||||||
|
'rclone',
|
||||||
|
'bisync',
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
*OPTIONS,
|
||||||
|
'--check-filename',
|
||||||
|
f'.rclone-test-{REMOTE}',
|
||||||
|
'--verbose',
|
||||||
|
'--color',
|
||||||
|
'NEVER',
|
||||||
|
'--use-json-log',
|
||||||
|
'--stats',
|
||||||
|
'9999m'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rclone_stats(log_output):
|
||||||
|
log = log_output.splitlines()
|
||||||
|
log.reverse()
|
||||||
|
for line in log:
|
||||||
|
try:
|
||||||
|
log_entry = json.loads(line)
|
||||||
|
if 'stats' in log_entry:
|
||||||
|
return log_entry['stats']
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def rclone_run(folder):
|
||||||
|
command = rclone_make_command(
|
||||||
|
folder['local-path'], folder['remote-path'], folder['remote']
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, check=True, capture_output=True, text=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f'=== Error syncing {folder['local-path']} ===')
|
||||||
|
print(f'Command: {' '.join(command)}')
|
||||||
|
print(f'--- STDOUT ---')
|
||||||
|
print(e.stdout if e.stdout else '(empty)')
|
||||||
|
print(f'--- STDERR ---')
|
||||||
|
print(e.stderr if e.stderr else '(empty)')
|
||||||
|
return {'success': False, 'stats': {}}
|
||||||
|
return {'success': True, 'stats': parse_rclone_stats(result.stderr)}
|
||||||
|
|
||||||
|
|
||||||
|
def notify(summary, body, level='normal', expire_time=5000):
|
||||||
|
subprocess.run(['notify-send', '-u', level, '-t', str(expire_time), summary, body])
|
||||||
|
|
||||||
|
# Source: https://stackoverflow.com/questions/1094841/get-a-human-readable-version-of-a-file-size
|
||||||
|
def sizeof_fmt(num, suffix='B'):
|
||||||
|
for unit in ('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi'):
|
||||||
|
if abs(num) < 1024.0:
|
||||||
|
return f'{num:3.1f}{unit}{suffix}'
|
||||||
|
num /= 1024.0
|
||||||
|
return f'{num:.1f}Yi{suffix}'
|
||||||
|
|
||||||
|
|
||||||
|
def rclone_run_all(folders):
|
||||||
|
error_folders = []
|
||||||
|
total_bytes = 0
|
||||||
|
total_transfers = 0
|
||||||
|
total_deleted = 0
|
||||||
|
total_renamed = 0
|
||||||
|
for folder in folders:
|
||||||
|
print(f'Running rclone for {folder}')
|
||||||
|
res = rclone_run(folder)
|
||||||
|
if not res['success']:
|
||||||
|
error_folders.append(folder['local-path'])
|
||||||
|
else:
|
||||||
|
total_bytes += res.get('stats', {}).get('bytes', 0)
|
||||||
|
total_transfers += res.get('stats', {}).get('transfers', 0)
|
||||||
|
total_deleted += res.get('stats', {}).get('deletes', 0)
|
||||||
|
total_renamed += res.get('stats', {}).get('renames', 0)
|
||||||
|
if len(error_folders) > 0:
|
||||||
|
error_msg = f'Sync error for remote {REMOTE}!'
|
||||||
|
for folder in error_folders:
|
||||||
|
error_msg += '''\n- ''' + folder
|
||||||
|
notify(f'rclone sync {REMOTE}', error_msg, level='error')
|
||||||
|
else:
|
||||||
|
msg = ''
|
||||||
|
if total_transfers > 0:
|
||||||
|
msg += f'''Transferred {total_transfers} files ({sizeof_fmt(total_bytes)})\n'''
|
||||||
|
if total_deleted > 0:
|
||||||
|
msg += f'''Deleted {total_transfers} files\n'''
|
||||||
|
if total_renamed > 0:
|
||||||
|
msg += f'''Renamed {total_renamed} files\n'''
|
||||||
|
if len(msg) > 0:
|
||||||
|
notify(f'rclone sync {REMOTE}', msg)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
rclone_run_all(FOLDERS)
|
||||||
|
"))
|
||||||
|
(setq script
|
||||||
|
(thread-last script
|
||||||
|
(string-trim)
|
||||||
|
(string-replace "<rclone-remote>" remote)
|
||||||
|
(string-replace "<rclone-folders-json>"
|
||||||
|
(json-encode folders))
|
||||||
|
(string-replace "<rclone-options>"
|
||||||
|
(json-encode my/index--rclone-options))))
|
||||||
|
script))
|
||||||
|
|
||||||
|
(defun my/index--rclone-script-loc (remote)
|
||||||
|
(concat (file-name-as-directory
|
||||||
|
(expand-file-name my/index--rclone-script-path))
|
||||||
|
(format "rclone_%s.py" remote)))
|
||||||
|
|
||||||
|
(defun my/index--rclone-script-saved-p (remote folders)
|
||||||
|
(let* ((script (my/index--rclone-script remote folders))
|
||||||
|
(script-loc (my/index--rclone-script-loc remote)))
|
||||||
|
(when (file-exists-p script-loc)
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert-file-contents script-loc)
|
||||||
|
(equal (string-trim (buffer-string)) script)))))
|
||||||
|
|
||||||
|
(defun my/index--rclone-commands (tree)
|
||||||
|
"Get commands to set up sync with rclone in TREE.
|
||||||
|
|
||||||
|
TREE is a form a defined by `my/index--tree-get'. This is supposed to
|
||||||
|
be the tree narrowed to the current machine (`my/index--tree-narrow').
|
||||||
|
|
||||||
|
The return value is a list of commands as defined by
|
||||||
|
`my/index--commands-display'."
|
||||||
|
(let ((folders (my/index--rclone-get-folders tree))
|
||||||
|
commands
|
||||||
|
sync-items-per-remote)
|
||||||
|
(dolist (folder folders)
|
||||||
|
(pcase-let*
|
||||||
|
((`((:local-path . ,local-path) (:remote-path . ,remote-path)
|
||||||
|
(:remote . ,remote))
|
||||||
|
folder)
|
||||||
|
(test-file-name (format ".rclone-test-%s" remote))
|
||||||
|
(test-file-local (concat local-path test-file-name))
|
||||||
|
(test-file-remote (concat remote-path test-file-name)))
|
||||||
|
(unless (file-exists-p test-file-local)
|
||||||
|
(push
|
||||||
|
(list (format "touch \"%s\"" test-file-local) "Create local test files" 3)
|
||||||
|
commands)
|
||||||
|
(push
|
||||||
|
(list (format "rclone mkdir \"%s\"" remote-path)
|
||||||
|
"Create remote directories" 3)
|
||||||
|
commands)
|
||||||
|
(push
|
||||||
|
(list (format "rclone touch \"%s\"" test-file-remote) "Create remote test-file" 4)
|
||||||
|
commands)
|
||||||
|
(push
|
||||||
|
(list
|
||||||
|
(concat (my/index--rclone-make-command local-path remote-path remote)
|
||||||
|
" --resync")
|
||||||
|
(format "Initial sync for %s" remote) 8)
|
||||||
|
commands))
|
||||||
|
(push folder
|
||||||
|
(alist-get remote sync-items-per-remote nil nil #'equal))))
|
||||||
|
(unless (file-exists-p my/index--rclone-script-path)
|
||||||
|
(push (list (format "mkdir -p \"%s\"" (expand-file-name
|
||||||
|
my/index--rclone-script-path))
|
||||||
|
"Create rclone sync scripts directory" 9)
|
||||||
|
commands))
|
||||||
|
(cl-loop for (remote . folders) in sync-items-per-remote
|
||||||
|
unless (my/index--rclone-script-saved-p remote folders)
|
||||||
|
do (push
|
||||||
|
(list
|
||||||
|
(format "cat <<EOF > %s\n%s\nEOF"
|
||||||
|
(my/index--rclone-script-loc remote)
|
||||||
|
(my/index--rclone-script remote folders))
|
||||||
|
"Update rclone sync script" 10)
|
||||||
|
commands))
|
||||||
|
(nreverse commands)))
|
||||||
|
|
||||||
(defun my/index--git-commands (tree)
|
(defun my/index--git-commands (tree)
|
||||||
"Get commands to clone the yet uncloned git repos in TREE.
|
"Get commands to clone the yet uncloned git repos in TREE.
|
||||||
|
|
||||||
|
|
@ -500,13 +729,14 @@ is still valid. Otherwise, it re-parses the index file."
|
||||||
(let* ((full-tree (my/index--tree-retrive)))
|
(let* ((full-tree (my/index--tree-retrive)))
|
||||||
(my/index--tree-verify full-tree)
|
(my/index--tree-verify full-tree)
|
||||||
(let* ((tree (my/index--tree-narrow full-tree))
|
(let* ((tree (my/index--tree-narrow full-tree))
|
||||||
(mega-commands (my/index--mega-commands full-tree tree))
|
;; (mega-commands (my/index--mega-commands full-tree tree))
|
||||||
|
(rclone-commands (my/index--rclone-commands tree))
|
||||||
(mapping (my/index--filesystem-tree-mapping full-tree tree))
|
(mapping (my/index--filesystem-tree-mapping full-tree tree))
|
||||||
(folder-commands (my/index--filesystem-commands mapping))
|
(folder-commands (my/index--filesystem-commands mapping))
|
||||||
(git-commands (my/index--git-commands tree))
|
(git-commands (my/index--git-commands tree))
|
||||||
(waka-commands (my/index--wakatime-commands tree))
|
(waka-commands (my/index--wakatime-commands tree))
|
||||||
(symlink-commands (my/index-get-symlink-commands tree)))
|
(symlink-commands (my/index-get-symlink-commands tree)))
|
||||||
(my/index--commands-display (append mega-commands folder-commands git-commands
|
(my/index--commands-display (append rclone-commands folder-commands git-commands
|
||||||
waka-commands symlink-commands)))))
|
waka-commands symlink-commands)))))
|
||||||
|
|
||||||
(defun my/index--nav-extend (name path)
|
(defun my/index--nav-extend (name path)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,6 @@
|
||||||
(quit-window t))
|
(quit-window t))
|
||||||
|
|
||||||
(setq initial-major-mode 'fundamental-mode)
|
(setq initial-major-mode 'fundamental-mode)
|
||||||
(setq initial-scratch-message "Hello there <3\n\n")
|
(setq initial-scratch-message "Hallo Leben")
|
||||||
|
|
||||||
(provide 'sqrt-misc-initial)
|
(provide 'sqrt-misc-initial)
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,10 @@
|
||||||
("beamer" "\\documentclass[presentation]{beamer}"
|
("beamer" "\\documentclass[presentation]{beamer}"
|
||||||
("\\section{%s}" . "\\section*{%s}")
|
("\\section{%s}" . "\\section*{%s}")
|
||||||
("\\subsection{%s}" . "\\subsection*{%s}")
|
("\\subsection{%s}" . "\\subsection*{%s}")
|
||||||
("\\subsubsection{%s}" . "\\subsubsection*{%s}")))))
|
("\\subsubsection{%s}" . "\\subsubsection*{%s}"))))
|
||||||
|
(with-eval-after-load 'ox-beamer
|
||||||
|
(add-to-list 'org-beamer-environments-extra
|
||||||
|
'("dummy" "d" "\\begin{dummyenv}" "\\end{dummyenv}"))))
|
||||||
|
|
||||||
;; Make sure to eval the function when org-latex-classes list already exists
|
;; Make sure to eval the function when org-latex-classes list already exists
|
||||||
(with-eval-after-load 'ox-latex
|
(with-eval-after-load 'ox-latex
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,8 @@
|
||||||
(my-leader-def
|
(my-leader-def
|
||||||
:keymaps 'org-mode-map
|
:keymaps 'org-mode-map
|
||||||
"SPC b" '(:wk "org-babel")
|
"SPC b" '(:wk "org-babel")
|
||||||
"SPC b" org-babel-map))
|
"SPC b" org-babel-map
|
||||||
|
"SPC h" #'consult-org-heading))
|
||||||
|
|
||||||
(defun my/org-prj-dir (path)
|
(defun my/org-prj-dir (path)
|
||||||
(expand-file-name path (org-entry-get nil "PRJ-DIR" t)))
|
(expand-file-name path (org-entry-get nil "PRJ-DIR" t)))
|
||||||
|
|
|
||||||
|
|
@ -892,21 +892,6 @@ KEYS is a list of cons cells like (<label> . <time>)."
|
||||||
(message "Got error: %S" error-thrown)))))
|
(message "Got error: %S" error-thrown)))))
|
||||||
my/weather-value)
|
my/weather-value)
|
||||||
|
|
||||||
(defun my/get-mood ()
|
|
||||||
(let* ((crm-separator " ")
|
|
||||||
(crm-local-completion-map
|
|
||||||
(let ((map (make-sparse-keymap)))
|
|
||||||
(set-keymap-parent map crm-local-completion-map)
|
|
||||||
(define-key map " " 'self-insert-command)
|
|
||||||
map))
|
|
||||||
(vertico-sort-function nil))
|
|
||||||
(mapconcat
|
|
||||||
#'identity
|
|
||||||
(completing-read-multiple
|
|
||||||
"How do you feel: "
|
|
||||||
my/mood-list)
|
|
||||||
" ")))
|
|
||||||
|
|
||||||
(defun my/set-journal-header ()
|
(defun my/set-journal-header ()
|
||||||
(org-set-property "Emacs" emacs-version)
|
(org-set-property "Emacs" emacs-version)
|
||||||
(org-set-property "Hostname" (my/system-name))
|
(org-set-property "Hostname" (my/system-name))
|
||||||
|
|
@ -932,9 +917,7 @@ KEYS is a list of cons cells like (<label> . <time>)."
|
||||||
(when title
|
(when title
|
||||||
(setq string (concat string title)))
|
(setq string (concat string title)))
|
||||||
(when (> (length string) 0)
|
(when (> (length string) 0)
|
||||||
(org-set-property "EMMS_Track" string))))))
|
(org-set-property "EMMS_Track" string)))))))
|
||||||
(when-let (mood (my/get-mood))
|
|
||||||
(org-set-property "Mood" mood)))
|
|
||||||
|
|
||||||
(add-hook 'org-journal-after-entry-create-hook
|
(add-hook 'org-journal-after-entry-create-hook
|
||||||
#'my/set-journal-header)
|
#'my/set-journal-header)
|
||||||
|
|
|
||||||
|
|
@ -683,8 +683,8 @@ So here is my implementation of that. It always does =windmove-do-select-window=
|
||||||
*** Update the monitor list
|
*** Update the monitor list
|
||||||
|
|
||||||
#+begin_src emacs-lisp
|
#+begin_src emacs-lisp
|
||||||
(defun my/exwm-refresh-monitors ()
|
(defun my/exwm-refresh-monitors (&optional refresh)
|
||||||
(interactive)
|
(interactive (list t))
|
||||||
(setq my/exwm-monitor-list (my/exwm-xrandr-monitor-list))
|
(setq my/exwm-monitor-list (my/exwm-xrandr-monitor-list))
|
||||||
(cl-loop for i from 0 to (1- exwm-workspace-number)
|
(cl-loop for i from 0 to (1- exwm-workspace-number)
|
||||||
for monitor = (plist-get exwm-randr-workspace-monitor-plist
|
for monitor = (plist-get exwm-randr-workspace-monitor-plist
|
||||||
|
|
@ -693,7 +693,8 @@ So here is my implementation of that. It always does =windmove-do-select-window=
|
||||||
do
|
do
|
||||||
(setf (plist-get exwm-randr-workspace-monitor-plist i)
|
(setf (plist-get exwm-randr-workspace-monitor-plist i)
|
||||||
(car my/exwm-monitor-list)))
|
(car my/exwm-monitor-list)))
|
||||||
(exwm-randr-refresh))
|
(when refresh
|
||||||
|
(exwm-randr-refresh)))
|
||||||
#+end_src
|
#+end_src
|
||||||
** Completions
|
** Completions
|
||||||
Setting up some completion interfaces that fit particularly well to use with EXWM. While rofi also works, I want to use Emacs functionality wherever possible to have one completion interface everywhere.
|
Setting up some completion interfaces that fit particularly well to use with EXWM. While rofi also works, I want to use Emacs functionality wherever possible to have one completion interface everywhere.
|
||||||
|
|
@ -4215,7 +4216,7 @@ This section generates manifests for various desktop software that I'm using.
|
||||||
** Office & Multimedia
|
** Office & Multimedia
|
||||||
| Category | Guix dependency |
|
| Category | Guix dependency |
|
||||||
|----------+-----------------|
|
|----------+-----------------|
|
||||||
| office | libreoffice |
|
| office | libreoffice-fresh |
|
||||||
| office | gimp |
|
| office | gimp |
|
||||||
| office | krita |
|
| office | krita |
|
||||||
| office | ffmpeg |
|
| office | ffmpeg |
|
||||||
|
|
|
||||||
418
Emacs.org
418
Emacs.org
|
|
@ -497,6 +497,14 @@ Finally, some post-processing of the tangled files:
|
||||||
(add-hook 'org-babel-post-tangle-hook #'my/modules--post-tangle)
|
(add-hook 'org-babel-post-tangle-hook #'my/modules--post-tangle)
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
|
Launch Emacs with the =remote= env (for TRAMP):
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(defun my/emacs-tramp ()
|
||||||
|
(interactive)
|
||||||
|
(with-environment-variables (("EMACS_ENV" "remote"))
|
||||||
|
(start-process "emacs-tramp" nil "emacs")))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
** Performance
|
** Performance
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:MODULE_NAME: performance
|
:MODULE_NAME: performance
|
||||||
|
|
@ -656,7 +664,7 @@ So until I've made a better loading screen, this will do.
|
||||||
|
|
||||||
#+begin_src emacs-lisp
|
#+begin_src emacs-lisp
|
||||||
(setq initial-major-mode 'fundamental-mode)
|
(setq initial-major-mode 'fundamental-mode)
|
||||||
(setq initial-scratch-message "Hello there <3\n\n")
|
(setq initial-scratch-message "Hallo Leben")
|
||||||
#+end_src
|
#+end_src
|
||||||
* General settings
|
* General settings
|
||||||
** Keybindings
|
** Keybindings
|
||||||
|
|
@ -1490,6 +1498,66 @@ Input accented characters.
|
||||||
(line-beginning-position)
|
(line-beginning-position)
|
||||||
(line-end-position)))))
|
(line-end-position)))))
|
||||||
#+end_src
|
#+end_src
|
||||||
|
**** Edit string a point
|
||||||
|
A function to edit the string at point in a temporary buffer with chosen major mode. Somewhat like =org-edit-special=.
|
||||||
|
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(defvar my/edit-elisp-string--window-config nil)
|
||||||
|
|
||||||
|
(defun my/edit-elisp-string ()
|
||||||
|
(interactive)
|
||||||
|
(if (org-src-edit-buffer-p)
|
||||||
|
(org-edit-src-exit)
|
||||||
|
(let* ((bounds (bounds-of-thing-at-point 'string))
|
||||||
|
(orig-buf (current-buffer))
|
||||||
|
(orig-str (when bounds
|
||||||
|
(buffer-substring-no-properties (car bounds) (cdr bounds)))))
|
||||||
|
(unless bounds
|
||||||
|
(user-error "No string under cursor"))
|
||||||
|
;; Not sure if there's a better way
|
||||||
|
(let* ((mode (intern
|
||||||
|
(completing-read
|
||||||
|
"Major mode: " obarray
|
||||||
|
(lambda (sym)
|
||||||
|
(and (commandp sym)
|
||||||
|
(string-suffix-p "-mode" (symbol-name sym))))
|
||||||
|
t)))
|
||||||
|
(edit-buf (generate-new-buffer "*string-edit*")))
|
||||||
|
(setq edit-elisp-string--window-config (current-window-configuration))
|
||||||
|
(with-current-buffer edit-buf
|
||||||
|
(insert (string-replace
|
||||||
|
"\\\"" "\"" (substring orig-str 1 -1)))
|
||||||
|
(funcall mode)
|
||||||
|
(use-local-map (copy-keymap (current-local-map)))
|
||||||
|
;; Confirm edit
|
||||||
|
(local-set-key
|
||||||
|
(kbd "C-c '")
|
||||||
|
(lambda ()
|
||||||
|
(interactive)
|
||||||
|
(let ((new-str (buffer-substring-no-properties
|
||||||
|
(point-min) (point-max))))
|
||||||
|
(with-current-buffer orig-buf
|
||||||
|
(delete-region (car bounds) (cdr bounds))
|
||||||
|
(goto-char (car bounds))
|
||||||
|
(insert (prin1-to-string new-str))))
|
||||||
|
(kill-buffer edit-buf)
|
||||||
|
(when edit-elisp-string--window-config
|
||||||
|
(set-window-configuration edit-elisp-string--window-config))))
|
||||||
|
;; Cancel edit
|
||||||
|
(local-set-key
|
||||||
|
(kbd "C-c C-k")
|
||||||
|
(lambda ()
|
||||||
|
(interactive)
|
||||||
|
(kill-buffer edit-buf)
|
||||||
|
(when edit-elisp-string--window-config
|
||||||
|
(set-window-configuration edit-elisp-string--window-config)))))
|
||||||
|
(pop-to-buffer edit-buf)))))
|
||||||
|
|
||||||
|
(general-define-key
|
||||||
|
:keymaps '(emacs-lisp-mode-map lisp-interaction-mode-map)
|
||||||
|
"C-c '" #'my/edit-elisp-string)
|
||||||
|
#+end_src
|
||||||
|
|
||||||
** Working with projects
|
** Working with projects
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:MODULE_NAME: general-config
|
:MODULE_NAME: general-config
|
||||||
|
|
@ -5715,7 +5783,8 @@ Some keybindings:
|
||||||
(my-leader-def
|
(my-leader-def
|
||||||
:keymaps 'org-mode-map
|
:keymaps 'org-mode-map
|
||||||
"SPC b" '(:wk "org-babel")
|
"SPC b" '(:wk "org-babel")
|
||||||
"SPC b" org-babel-map))
|
"SPC b" org-babel-map
|
||||||
|
"SPC h" #'consult-org-heading))
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
*** Managing a literate programming project
|
*** Managing a literate programming project
|
||||||
|
|
@ -6993,31 +7062,12 @@ Also, I want to add some extra information to the journal. Here's a functionalit
|
||||||
my/weather-value)
|
my/weather-value)
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
Let's also try to log the current mood:
|
|
||||||
#+begin_src emacs-lisp
|
|
||||||
(defun my/get-mood ()
|
|
||||||
(let* ((crm-separator " ")
|
|
||||||
(crm-local-completion-map
|
|
||||||
(let ((map (make-sparse-keymap)))
|
|
||||||
(set-keymap-parent map crm-local-completion-map)
|
|
||||||
(define-key map " " 'self-insert-command)
|
|
||||||
map))
|
|
||||||
(vertico-sort-function nil))
|
|
||||||
(mapconcat
|
|
||||||
#'identity
|
|
||||||
(completing-read-multiple
|
|
||||||
"How do you feel: "
|
|
||||||
my/mood-list)
|
|
||||||
" ")))
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
And here's the function that creates a drawer with such information. At the moment, it's:
|
And here's the function that creates a drawer with such information. At the moment, it's:
|
||||||
- Emacs version
|
- Emacs version
|
||||||
- Hostname
|
- Hostname
|
||||||
- Location
|
- Location
|
||||||
- Weather
|
- Weather
|
||||||
- Current EMMS track
|
- Current EMMS track
|
||||||
- Current mood
|
|
||||||
|
|
||||||
#+begin_src emacs-lisp
|
#+begin_src emacs-lisp
|
||||||
(defun my/set-journal-header ()
|
(defun my/set-journal-header ()
|
||||||
|
|
@ -7045,9 +7095,7 @@ And here's the function that creates a drawer with such information. At the mome
|
||||||
(when title
|
(when title
|
||||||
(setq string (concat string title)))
|
(setq string (concat string title)))
|
||||||
(when (> (length string) 0)
|
(when (> (length string) 0)
|
||||||
(org-set-property "EMMS_Track" string))))))
|
(org-set-property "EMMS_Track" string)))))))
|
||||||
(when-let (mood (my/get-mood))
|
|
||||||
(org-set-property "Mood" mood)))
|
|
||||||
|
|
||||||
(add-hook 'org-journal-after-entry-create-hook
|
(add-hook 'org-journal-after-entry-create-hook
|
||||||
#'my/set-journal-header)
|
#'my/set-journal-header)
|
||||||
|
|
@ -8027,7 +8075,10 @@ Add a custom LaTeX template without default packages. Packages are indented to b
|
||||||
("beamer" "\\documentclass[presentation]{beamer}"
|
("beamer" "\\documentclass[presentation]{beamer}"
|
||||||
("\\section{%s}" . "\\section*{%s}")
|
("\\section{%s}" . "\\section*{%s}")
|
||||||
("\\subsection{%s}" . "\\subsection*{%s}")
|
("\\subsection{%s}" . "\\subsection*{%s}")
|
||||||
("\\subsubsection{%s}" . "\\subsubsection*{%s}")))))
|
("\\subsubsection{%s}" . "\\subsubsection*{%s}"))))
|
||||||
|
(with-eval-after-load 'ox-beamer
|
||||||
|
(add-to-list 'org-beamer-environments-extra
|
||||||
|
'("dummy" "d" "\\begin{dummyenv}" "\\end{dummyenv}"))))
|
||||||
|
|
||||||
;; Make sure to eval the function when org-latex-classes list already exists
|
;; Make sure to eval the function when org-latex-classes list already exists
|
||||||
(with-eval-after-load 'ox-latex
|
(with-eval-after-load 'ox-latex
|
||||||
|
|
@ -12268,14 +12319,15 @@ Perhaps this is too verbose (=10.03.R.01=), but it works for now.
|
||||||
**** Tools choice
|
**** Tools choice
|
||||||
As I mentioned earlier, my current options to manage a particular node are:
|
As I mentioned earlier, my current options to manage a particular node are:
|
||||||
- [[https://git-scm.com/][git]];
|
- [[https://git-scm.com/][git]];
|
||||||
- [[https://mega.nz/][MEGA]] - for files that don't fit into git, such as DOCX documents, photos, etc.;
|
- [[https://mega.nz/][MEGA]] - for files that don't fit into git, such as DOCX documents, photos, etc. (*UPD 2025.10.22* - MEGA has been banned in Russia, of course...);
|
||||||
|
- [[https://owncloud.dev/ocis/][ownCloud Infinite Scale]] (self-hosted by me) via [[https://rclone.org/docs/][rclone]] - instead of MEGA.
|
||||||
- "nothing" - for something that I don't need to sync across machines, e.g. database dumps.
|
- "nothing" - for something that I don't need to sync across machines, e.g. database dumps.
|
||||||
|
|
||||||
Another tool I considered was [[https://github.com/restic/restic][restic]]. It's an interesting backup & sync solution with built-in encryption, snapshots, etc.
|
=rclone= is nice because it provides a single interface for multiple services like Google Drive, Dropbox, S3, WebDAV. It also supported MEGA, but it required turning off 2FA, which I didn't want.
|
||||||
|
|
||||||
However, a challenge I encountered is that its repositories are only accessible via restic. So, even if I use something like MEGA as a backend, I won't be able to use the MEGA file-sharing features, which I occasionally want for document or photo folders. Hence, for now, I'm more interested in synchronizing the file tree in MEGA with [[https://github.com/meganz/MEGAcmd][MEGAcmd]] (and also clean up the mess up there).
|
So the MEGA functionality used [[https://github.com/meganz/MEGAcmd][MEGAcmd]]. I'll keep it for now.
|
||||||
|
|
||||||
Another interesting tool is [[https://rclone.org/][rclone]], which provides a single interface for multiple services like Google Drive, Dropbox, S3, WebDAV. It also supports MEGA, but it requires turning off the two-factor authentication, which I don't want.
|
Another tool I considered was [[https://github.com/restic/restic][restic]]. It's an interesting backup & sync solution with built-in encryption, snapshots, etc., but the repositories are only accessible via restic, which prevents using file-sharing features.
|
||||||
|
|
||||||
*** Implementation
|
*** Implementation
|
||||||
**** Dependencies
|
**** Dependencies
|
||||||
|
|
@ -12304,7 +12356,7 @@ The org tree is located in my =org-mode= folder in a file called =index.org=:
|
||||||
|
|
||||||
Each "area" is an Org header with the =folder= tag; the Org hierarchy forms the file tree. A header can have the following properties:
|
Each "area" is an Org header with the =folder= tag; the Org hierarchy forms the file tree. A header can have the following properties:
|
||||||
- =machine= - a list of hostnames for which the node is active (or =nil=)
|
- =machine= - a list of hostnames for which the node is active (or =nil=)
|
||||||
- =kind= - =mega=, =git=, or =dummy=
|
- =kind= - =mega=, =rclone:<remote>=, =git=, or =dummy=. For now, only one option per node; I'll probably change that in the future.
|
||||||
- =remote= - remote URL for =git=
|
- =remote= - remote URL for =git=
|
||||||
- =symlink= - in case the folder has to be symlinked somewhere else
|
- =symlink= - in case the folder has to be symlinked somewhere else
|
||||||
|
|
||||||
|
|
@ -12319,16 +12371,16 @@ E.g. a part of the tree above:
|
||||||
:END:
|
:END:
|
||||||
,**** 10.03.A Artifacts
|
,**** 10.03.A Artifacts
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:kind: mega
|
:kind: rclone:ocis
|
||||||
:END:
|
:END:
|
||||||
,**** 10.03.D Documents
|
,**** 10.03.D Documents
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:kind: mega
|
:kind: rclone:ocis
|
||||||
:END:
|
:END:
|
||||||
,**** 10.03.R Repos
|
,**** 10.03.R Repos
|
||||||
,***** 10.03.R.00 digital-trajectories-deploy
|
,***** 10.03.R.00 digital-trajectories-deploy
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:kind: mega
|
:kind: rclone:ocis
|
||||||
:END:
|
:END:
|
||||||
,***** 10.03.R.01 digital-trajectories-backend
|
,***** 10.03.R.01 digital-trajectories-backend
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
|
|
@ -12374,14 +12426,17 @@ The return value is an alist; see `my/index--tree-get' for details."
|
||||||
(setf (alist-get :symlink val) symlink))
|
(setf (alist-get :symlink val) symlink))
|
||||||
(when (org-element-property :PROJECT heading)
|
(when (org-element-property :PROJECT heading)
|
||||||
(setf (alist-get :project val) t))
|
(setf (alist-get :project val) t))
|
||||||
(when-let* ((kind-str (org-element-property :KIND heading))
|
(when-let* ((kind-str (org-element-property :KIND heading)))
|
||||||
(kind (intern kind-str)))
|
(when (string-match-p (rx bos "rclone:") kind-str)
|
||||||
(setf (alist-get :kind val) kind)
|
(setf (alist-get :remote val)
|
||||||
(when (equal kind 'git)
|
(substring kind-str 7))
|
||||||
|
(setq kind-str "rclone"))
|
||||||
|
(when (equal kind-str "git")
|
||||||
(let ((remote (org-element-property :REMOTE heading)))
|
(let ((remote (org-element-property :REMOTE heading)))
|
||||||
(unless remote
|
(unless remote
|
||||||
(user-error "No remote for %s" (alist-get :name val)))
|
(user-error "No remote for %s" (alist-get :name val)))
|
||||||
(setf (alist-get :remote val) remote))))
|
(setf (alist-get :remote val) remote)))
|
||||||
|
(setf (alist-get :kind val) (intern kind-str)))
|
||||||
(setf (alist-get :name val) (org-element-property :raw-value heading)
|
(setf (alist-get :name val) (org-element-property :raw-value heading)
|
||||||
(alist-get :path val) new-path)
|
(alist-get :path val) new-path)
|
||||||
val)))
|
val)))
|
||||||
|
|
@ -12714,6 +12769,288 @@ The return value is a list of commands as defined by
|
||||||
|
|
||||||
#+RESULTS:
|
#+RESULTS:
|
||||||
: my/index--mega-commands
|
: my/index--mega-commands
|
||||||
|
**** rclone
|
||||||
|
This section wraps the [[https://rclone.org/bisync/][bisync]] command for rclone, which implements two-way sync.
|
||||||
|
|
||||||
|
The general approach is:
|
||||||
|
- Consider the directory unsynced if it doesn't have the =.rclone-test-<remote>= file, which is used with the [[https://rclone.org/bisync/#check-access][--check-access]] and [[https://rclone.org/bisync/#check-filename][--check-filename]] path. =--check-access= prevents rclone from running unless the check file exists in both places (i.e., locally and remotely).
|
||||||
|
- For unsynced directories, run =rclone mkdir= to create remote directory, =touch= and =rclone touch= to create the check file.
|
||||||
|
- For unsynced directories, run the sync command with the [[https://rclone.org/bisync/#resync][--resync]] flag for initial sync.
|
||||||
|
- For all directories, create a bash script that would run the sync command normally.
|
||||||
|
|
||||||
|
First, default options for =rclone bisync=:
|
||||||
|
#+NAME: rclone-options
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(defconst my/index--rclone-options
|
||||||
|
`("--create-empty-src-dirs"
|
||||||
|
"--resilient"
|
||||||
|
"--metadata"
|
||||||
|
"--filters-file"
|
||||||
|
,(expand-file-name "~/.config/rclone/filters-bisync")))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
A [[https://rclone.org/bisync/#filtering][filters file]] for rclone:
|
||||||
|
#+begin_src text :tangle ~/.config/rclone/filters-bisync
|
||||||
|
- .*
|
||||||
|
- ~*
|
||||||
|
- .debris
|
||||||
|
#+end_src
|
||||||
|
Not yet sure what's that supposed to be.
|
||||||
|
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(defconst my/index--rclone-script-path "~/bin/rclone-scripts/")
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
First, get folders to be synced from the tree:
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(defun my/index--rclone-get-folders (tree)
|
||||||
|
"Get TREE nodes to be synced with rclone.
|
||||||
|
|
||||||
|
Return a list of alists with the following keys:
|
||||||
|
- `:local-path' - path in the local filesystem
|
||||||
|
- `:remote-path' - path in the remote
|
||||||
|
- `:remote' - name of the remote."
|
||||||
|
(cl-loop for node in tree
|
||||||
|
if (eq (alist-get :kind node) 'rclone)
|
||||||
|
collect
|
||||||
|
`((:local-path . ,(file-name-as-directory (alist-get :path node)))
|
||||||
|
(:remote-path
|
||||||
|
. ,(concat (alist-get :remote node)
|
||||||
|
":" (my/index--mega-local-path
|
||||||
|
(file-name-as-directory (alist-get :path node)))))
|
||||||
|
(:remote . ,(alist-get :remote node)))
|
||||||
|
append (my/index--rclone-get-folders
|
||||||
|
(alist-get :children node))))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Then, get the sync command:
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(defun my/index--rclone-make-command (local-path remote-path remote)
|
||||||
|
"Make a bisync command to sync LOCAL-PATH and REMOTE-PATH.
|
||||||
|
|
||||||
|
REMOTE is the name of the remote."
|
||||||
|
(string-join
|
||||||
|
`("rclone"
|
||||||
|
"bisync"
|
||||||
|
,(format "\"%s\"" local-path)
|
||||||
|
,(format "\"%s\"" remote-path)
|
||||||
|
,@my/index--rclone-options
|
||||||
|
"--check-filename"
|
||||||
|
,(format ".rclone-test-%s" remote))
|
||||||
|
" "))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
A python script to run rclone.
|
||||||
|
#+NAME: rclone-sync-script
|
||||||
|
#+begin_src python :tangle no
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
REMOTE = '<rclone-remote>'
|
||||||
|
FOLDERS = json.loads('<rclone-folders-json>')
|
||||||
|
OPTIONS = json.loads('<rclone-options>')
|
||||||
|
|
||||||
|
|
||||||
|
def rclone_make_command(local_path, remote_path, remote):
|
||||||
|
return [
|
||||||
|
'rclone',
|
||||||
|
'bisync',
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
,*OPTIONS,
|
||||||
|
'--check-filename',
|
||||||
|
f'.rclone-test-{REMOTE}',
|
||||||
|
'--verbose',
|
||||||
|
'--color',
|
||||||
|
'NEVER',
|
||||||
|
'--use-json-log',
|
||||||
|
'--stats',
|
||||||
|
'9999m'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rclone_stats(log_output):
|
||||||
|
log = log_output.splitlines()
|
||||||
|
log.reverse()
|
||||||
|
for line in log:
|
||||||
|
try:
|
||||||
|
log_entry = json.loads(line)
|
||||||
|
if 'stats' in log_entry:
|
||||||
|
return log_entry['stats']
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def process_output(output):
|
||||||
|
if output is None:
|
||||||
|
print('(empty)')
|
||||||
|
for line in output.splitlines():
|
||||||
|
try:
|
||||||
|
datum = json.loads(line)
|
||||||
|
print(datum['msg'])
|
||||||
|
except Exception:
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
def rclone_run(folder):
|
||||||
|
command = rclone_make_command(
|
||||||
|
folder['local-path'], folder['remote-path'], folder['remote']
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, check=True, capture_output=True, text=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f'=== Error syncing {folder['local-path']} ===')
|
||||||
|
print(f'Command: {' '.join(command)}')
|
||||||
|
print(f'--- STDOUT ---')
|
||||||
|
process_output(e.stdout)
|
||||||
|
print(f'--- STDERR ---')
|
||||||
|
process_output(e.stderr)
|
||||||
|
return {'success': False, 'stats': {}}
|
||||||
|
return {'success': True, 'stats': parse_rclone_stats(result.stderr)}
|
||||||
|
|
||||||
|
|
||||||
|
def notify(summary, body, level='normal', expire_time=5000):
|
||||||
|
subprocess.run(['notify-send', '-u', level, '-t', str(expire_time), summary, body])
|
||||||
|
|
||||||
|
# Source: https://stackoverflow.com/questions/1094841/get-a-human-readable-version-of-a-file-size
|
||||||
|
def sizeof_fmt(num, suffix='B'):
|
||||||
|
for unit in ('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi'):
|
||||||
|
if abs(num) < 1024.0:
|
||||||
|
return f'{num:3.1f}{unit}{suffix}'
|
||||||
|
num /= 1024.0
|
||||||
|
return f'{num:.1f}Yi{suffix}'
|
||||||
|
|
||||||
|
|
||||||
|
def rclone_run_all(folders):
|
||||||
|
error_folders = []
|
||||||
|
total_bytes = 0
|
||||||
|
total_transfers = 0
|
||||||
|
total_deleted = 0
|
||||||
|
total_renamed = 0
|
||||||
|
for folder in folders:
|
||||||
|
print(f'Running rclone for {folder}')
|
||||||
|
res = rclone_run(folder)
|
||||||
|
if not res['success']:
|
||||||
|
error_folders.append(folder['local-path'])
|
||||||
|
else:
|
||||||
|
total_bytes += res.get('stats', {}).get('bytes', 0)
|
||||||
|
total_transfers += res.get('stats', {}).get('transfers', 0)
|
||||||
|
total_deleted += res.get('stats', {}).get('deletes', 0)
|
||||||
|
total_renamed += res.get('stats', {}).get('renames', 0)
|
||||||
|
if len(error_folders) > 0:
|
||||||
|
error_msg = f'Sync error for remote {REMOTE}!'
|
||||||
|
for folder in error_folders:
|
||||||
|
error_msg += '''\n- ''' + folder
|
||||||
|
notify(f'rclone sync {REMOTE}', error_msg, level='critical')
|
||||||
|
else:
|
||||||
|
msg = ''
|
||||||
|
if total_transfers > 0:
|
||||||
|
msg += f'''Transferred {total_transfers} files ({sizeof_fmt(total_bytes)})\n'''
|
||||||
|
if total_deleted > 0:
|
||||||
|
msg += f'''Deleted {total_transfers} files\n'''
|
||||||
|
if total_renamed > 0:
|
||||||
|
msg += f'''Renamed {total_renamed} files\n'''
|
||||||
|
if len(msg) > 0:
|
||||||
|
notify(f'rclone sync {REMOTE}', msg)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
rclone_run_all(FOLDERS)
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
A function that templates the script above:
|
||||||
|
#+begin_src emacs-lisp :noweb yes
|
||||||
|
(defun my/index--rclone-script (remote folders)
|
||||||
|
(let ((script "
|
||||||
|
<<rclone-sync-script>>
|
||||||
|
"))
|
||||||
|
(setq script
|
||||||
|
(thread-last script
|
||||||
|
(string-trim)
|
||||||
|
(string-replace "<rclone-remote>" remote)
|
||||||
|
(string-replace "<rclone-folders-json>"
|
||||||
|
(json-encode folders))
|
||||||
|
(string-replace "<rclone-options>"
|
||||||
|
(json-encode my/index--rclone-options))))
|
||||||
|
script))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(defun my/index--rclone-script-loc (remote)
|
||||||
|
(concat (file-name-as-directory
|
||||||
|
(expand-file-name my/index--rclone-script-path))
|
||||||
|
(format "rclone_%s.py" remote)))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
And checks if the script needs to be updated:
|
||||||
|
#+begin_src emacs-lisp :noweb yes
|
||||||
|
(defun my/index--rclone-script-saved-p (remote folders)
|
||||||
|
(let* ((script (my/index--rclone-script remote folders))
|
||||||
|
(script-loc (my/index--rclone-script-loc remote)))
|
||||||
|
(when (file-exists-p script-loc)
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert-file-contents script-loc)
|
||||||
|
(equal (string-trim (buffer-string)) script)))))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Putting everything together:
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(defun my/index--rclone-commands (tree)
|
||||||
|
"Get commands to set up sync with rclone in TREE.
|
||||||
|
|
||||||
|
TREE is a form a defined by `my/index--tree-get'. This is supposed to
|
||||||
|
be the tree narrowed to the current machine (`my/index--tree-narrow').
|
||||||
|
|
||||||
|
The return value is a list of commands as defined by
|
||||||
|
`my/index--commands-display'."
|
||||||
|
(let ((folders (my/index--rclone-get-folders tree))
|
||||||
|
commands
|
||||||
|
sync-items-per-remote)
|
||||||
|
(dolist (folder folders)
|
||||||
|
(pcase-let*
|
||||||
|
((`((:local-path . ,local-path) (:remote-path . ,remote-path)
|
||||||
|
(:remote . ,remote))
|
||||||
|
folder)
|
||||||
|
(test-file-name (format ".rclone-test-%s" remote))
|
||||||
|
(test-file-local (concat local-path test-file-name))
|
||||||
|
(test-file-remote (concat remote-path test-file-name)))
|
||||||
|
(unless (file-exists-p test-file-local)
|
||||||
|
(push
|
||||||
|
(list (format "touch \"%s\"" test-file-local) "Create local test files" 3)
|
||||||
|
commands)
|
||||||
|
(push
|
||||||
|
(list (format "rclone mkdir \"%s\"" remote-path)
|
||||||
|
"Create remote directories" 3)
|
||||||
|
commands)
|
||||||
|
(push
|
||||||
|
(list (format "rclone touch \"%s\"" test-file-remote) "Create remote test-file" 4)
|
||||||
|
commands)
|
||||||
|
(push
|
||||||
|
(list
|
||||||
|
(concat (my/index--rclone-make-command local-path remote-path remote)
|
||||||
|
" --resync")
|
||||||
|
(format "Initial sync for %s" remote) 8)
|
||||||
|
commands))
|
||||||
|
(push folder
|
||||||
|
(alist-get remote sync-items-per-remote nil nil #'equal))))
|
||||||
|
(unless (file-exists-p my/index--rclone-script-path)
|
||||||
|
(push (list (format "mkdir -p \"%s\"" (expand-file-name
|
||||||
|
my/index--rclone-script-path))
|
||||||
|
"Create rclone sync scripts directory" 9)
|
||||||
|
commands))
|
||||||
|
(cl-loop for (remote . folders) in sync-items-per-remote
|
||||||
|
unless (my/index--rclone-script-saved-p remote folders)
|
||||||
|
do (push
|
||||||
|
(list
|
||||||
|
(format "cat <<EOF > %s\n%s\nEOF"
|
||||||
|
(my/index--rclone-script-loc remote)
|
||||||
|
(my/index--rclone-script remote (nreverse folders)))
|
||||||
|
"Update rclone sync script" 10)
|
||||||
|
commands))
|
||||||
|
(nreverse commands)))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
**** Git repos
|
**** Git repos
|
||||||
To sync git, we just need to clone the required git repos. Removing the repos is handled by the folder sync commands.
|
To sync git, we just need to clone the required git repos. Removing the repos is handled by the folder sync commands.
|
||||||
|
|
||||||
|
|
@ -12936,13 +13273,14 @@ With that, we can make the main entrypoint.
|
||||||
(let* ((full-tree (my/index--tree-retrive)))
|
(let* ((full-tree (my/index--tree-retrive)))
|
||||||
(my/index--tree-verify full-tree)
|
(my/index--tree-verify full-tree)
|
||||||
(let* ((tree (my/index--tree-narrow full-tree))
|
(let* ((tree (my/index--tree-narrow full-tree))
|
||||||
(mega-commands (my/index--mega-commands full-tree tree))
|
;; (mega-commands (my/index--mega-commands full-tree tree))
|
||||||
|
(rclone-commands (my/index--rclone-commands tree))
|
||||||
(mapping (my/index--filesystem-tree-mapping full-tree tree))
|
(mapping (my/index--filesystem-tree-mapping full-tree tree))
|
||||||
(folder-commands (my/index--filesystem-commands mapping))
|
(folder-commands (my/index--filesystem-commands mapping))
|
||||||
(git-commands (my/index--git-commands tree))
|
(git-commands (my/index--git-commands tree))
|
||||||
(waka-commands (my/index--wakatime-commands tree))
|
(waka-commands (my/index--wakatime-commands tree))
|
||||||
(symlink-commands (my/index-get-symlink-commands tree)))
|
(symlink-commands (my/index-get-symlink-commands tree)))
|
||||||
(my/index--commands-display (append mega-commands folder-commands git-commands
|
(my/index--commands-display (append rclone-commands folder-commands git-commands
|
||||||
waka-commands symlink-commands)))))
|
waka-commands symlink-commands)))))
|
||||||
#+end_src
|
#+end_src
|
||||||
*** Navigation
|
*** Navigation
|
||||||
|
|
|
||||||
4
Mail.org
4
Mail.org
|
|
@ -126,7 +126,7 @@ remotehost = mbox.etu.ru
|
||||||
remoteuser = <<mail-username()>>
|
remoteuser = <<mail-username()>>
|
||||||
remotepass = <<mail-password()>>
|
remotepass = <<mail-password()>>
|
||||||
remoteport = 993
|
remoteport = 993
|
||||||
cert_fingerprint = 416b1f7f18d68a65f5e75bb1af5d65ea5e20739e
|
cert_fingerprint = 20bbfdcb617e4695c47a90af96e40d72a57adee4
|
||||||
#+end_src
|
#+end_src
|
||||||
* Notmuch
|
* Notmuch
|
||||||
| Guix dependency |
|
| Guix dependency |
|
||||||
|
|
@ -389,7 +389,7 @@ host mbox.etu.ru
|
||||||
port 465
|
port 465
|
||||||
tls on
|
tls on
|
||||||
tls_starttls off
|
tls_starttls off
|
||||||
tls_fingerprint 69:E9:61:A0:DE:8B:AD:F2:C0:90:2F:55:F9:D6:80:7F:0B:AB:7E:1B:FD:56:C0:EE:35:ED:E2:EB:DD:80:AD:C3
|
tls_fingerprint AD:1D:38:93:43:18:F2:DF:0C:62:80:81:55:74:B0:FB:A7:2B:FF:BD:FC:60:05:02:89:AB:F3:C2:33:57:E1:96
|
||||||
from pvkorytov@etu.ru
|
from pvkorytov@etu.ru
|
||||||
user pvkorytov
|
user pvkorytov
|
||||||
passwordeval "pass show Job/Digital/Email/pvkorytov@etu.ru | head -n 1"
|
passwordeval "pass show Job/Digital/Email/pvkorytov@etu.ru | head -n 1"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue