mirror of
https://github.com/SqrtMinusOne/dotfiles.git
synced 2025-12-10 19:23:03 +03:00
feat(emacs): first version of rdrview & pandoc
This commit is contained in:
parent
86bb113b32
commit
a33abd485d
3 changed files with 695 additions and 10 deletions
247
.emacs.d/init.el
247
.emacs.d/init.el
|
|
@ -1,3 +1,5 @@
|
||||||
|
;;; -*- lexical-binding: t -*-
|
||||||
|
|
||||||
(defvar bootstrap-version)
|
(defvar bootstrap-version)
|
||||||
(let ((bootstrap-file
|
(let ((bootstrap-file
|
||||||
(expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
|
(expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
|
||||||
|
|
@ -1583,7 +1585,11 @@ Returns (<buffer> . <workspace-index>) or nil."
|
||||||
|
|
||||||
(use-package php-mode
|
(use-package php-mode
|
||||||
:straight t
|
:straight t
|
||||||
:mode "\\.php\\'")
|
:mode "\\.php\\'"
|
||||||
|
:config
|
||||||
|
(add-hook 'php-mode-hook #'smartparens-mode)
|
||||||
|
(add-hook 'php-mode-hook #'lsp)
|
||||||
|
(my/set-smartparens-indent 'php-mode))
|
||||||
|
|
||||||
(use-package tex
|
(use-package tex
|
||||||
:straight auctex
|
:straight auctex
|
||||||
|
|
@ -3495,6 +3501,7 @@ Returns (<buffer> . <workspace-index>) or nil."
|
||||||
"=" 'dired-narrow
|
"=" 'dired-narrow
|
||||||
"-" 'dired-create-empty-file
|
"-" 'dired-create-empty-file
|
||||||
"~" 'vterm
|
"~" 'vterm
|
||||||
|
"M-r" 'wdired-change-to-wdired-mode
|
||||||
"<left>" 'dired-up-directory
|
"<left>" 'dired-up-directory
|
||||||
"<right>" 'dired-find-file
|
"<right>" 'dired-find-file
|
||||||
"M-<return>" 'dired-open-xdg))
|
"M-<return>" 'dired-open-xdg))
|
||||||
|
|
@ -3942,6 +3949,9 @@ Returns (<buffer> . <workspace-index>) or nil."
|
||||||
(defface elfeed-blogs-entry nil
|
(defface elfeed-blogs-entry nil
|
||||||
"Face for the elfeed entries with tag \"blogs\"")
|
"Face for the elfeed entries with tag \"blogs\"")
|
||||||
|
|
||||||
|
(defface elfeed-govt-entry nil
|
||||||
|
"Face for the elfeed entries with tag \"blogs\"")
|
||||||
|
|
||||||
(my/use-doom-colors
|
(my/use-doom-colors
|
||||||
(elfeed-search-tag-face :foreground (doom-color 'yellow))
|
(elfeed-search-tag-face :foreground (doom-color 'yellow))
|
||||||
(elfeed-videos-entry :foreground (doom-color 'red))
|
(elfeed-videos-entry :foreground (doom-color 'red))
|
||||||
|
|
@ -3949,13 +3959,15 @@ Returns (<buffer> . <workspace-index>) or nil."
|
||||||
(elfeed-emacs-entry :foreground (doom-color 'magenta))
|
(elfeed-emacs-entry :foreground (doom-color 'magenta))
|
||||||
(elfeed-music-entry :foreground (doom-color 'green))
|
(elfeed-music-entry :foreground (doom-color 'green))
|
||||||
(elfeed-podcasts-entry :foreground (doom-color 'yellow))
|
(elfeed-podcasts-entry :foreground (doom-color 'yellow))
|
||||||
(elfeed-blogs-entry :foreground (doom-color 'orange)))
|
(elfeed-blogs-entry :foreground (doom-color 'orange))
|
||||||
|
(elfeed-govt-entry :foreground (doom-color 'dark-cyan)))
|
||||||
|
|
||||||
(with-eval-after-load 'elfeed
|
(with-eval-after-load 'elfeed
|
||||||
(setq elfeed-search-face-alist
|
(setq elfeed-search-face-alist
|
||||||
'((twitter elfeed-twitter-entry)
|
'((podcasts elfeed-podcasts-entry)
|
||||||
(podcasts elfeed-podcasts-entry)
|
|
||||||
(music elfeed-music-entry)
|
(music elfeed-music-entry)
|
||||||
|
(gov elfeed-govt-entry)
|
||||||
|
(twitter elfeed-twitter-entry)
|
||||||
(videos elfeed-videos-entry)
|
(videos elfeed-videos-entry)
|
||||||
(emacs elfeed-emacs-entry)
|
(emacs elfeed-emacs-entry)
|
||||||
(blogs elfeed-blogs-entry)
|
(blogs elfeed-blogs-entry)
|
||||||
|
|
@ -3978,7 +3990,9 @@ Returns (<buffer> . <workspace-index>) or nil."
|
||||||
|
|
||||||
(use-package elfeed-summary
|
(use-package elfeed-summary
|
||||||
:commands (elfeed-summary)
|
:commands (elfeed-summary)
|
||||||
:straight t)
|
:straight t
|
||||||
|
:config
|
||||||
|
(setq elfeed-summary-filter-by-title t))
|
||||||
|
|
||||||
(defun my/elfeed-toggle-score-sort ()
|
(defun my/elfeed-toggle-score-sort ()
|
||||||
(interactive)
|
(interactive)
|
||||||
|
|
@ -4260,6 +4274,229 @@ Returns (<buffer> . <workspace-index>) or nil."
|
||||||
"+" 'text-scale-increase
|
"+" 'text-scale-increase
|
||||||
"-" 'text-scale-decrease)
|
"-" 'text-scale-decrease)
|
||||||
|
|
||||||
|
(defun my/rdrview-get (url callback)
|
||||||
|
(let* ((buffer (generate-new-buffer "rdrview"))
|
||||||
|
(proc (start-process "rdrview" buffer "rdrview"
|
||||||
|
url "-T" "title,sitename,body"
|
||||||
|
"-H")))
|
||||||
|
(set-process-sentinel
|
||||||
|
proc
|
||||||
|
(lambda (process _msg)
|
||||||
|
(let ((status (process-status process))
|
||||||
|
(code (process-exit-status process)))
|
||||||
|
(cond ((and (eq status 'exit) (= code 0))
|
||||||
|
(progn
|
||||||
|
(funcall callback
|
||||||
|
(with-current-buffer (process-buffer process)
|
||||||
|
(buffer-string)))
|
||||||
|
(kill-buffer (process-buffer process))) )
|
||||||
|
((or (and (eq status 'exit) (> code 0))
|
||||||
|
(eq status 'signal))
|
||||||
|
(let ((err (with-current-buffer (process-buffer process)
|
||||||
|
(buffer-string))))
|
||||||
|
(kill-buffer (process-buffer process))
|
||||||
|
(user-error "Error in rdrview: %s" err)))))))
|
||||||
|
proc))
|
||||||
|
|
||||||
|
(defun my/rdrview-parse (dom-string)
|
||||||
|
(let ((dom (with-temp-buffer
|
||||||
|
(insert dom-string)
|
||||||
|
(libxml-parse-html-region (point-min) (point-max)))))
|
||||||
|
(let (title sitename content)
|
||||||
|
(dolist (child (dom-children (car (dom-by-id dom "readability-page-1"))))
|
||||||
|
(when (listp child)
|
||||||
|
(cond
|
||||||
|
((eq (car child) 'h1)
|
||||||
|
(setq title (dom-text child)))
|
||||||
|
((eq (car child) 'h2)
|
||||||
|
(setq sitename (dom-text child)))
|
||||||
|
((eq (car child) 'div)
|
||||||
|
(setq content child)))))
|
||||||
|
(dom-search
|
||||||
|
content
|
||||||
|
(lambda (el)
|
||||||
|
(when (listp el)
|
||||||
|
(pcase (car el)
|
||||||
|
('h2 (setf (car el) 'h1))
|
||||||
|
('h3 (setf (car el) 'h2))
|
||||||
|
('h4 (setf (car el) 'h3))
|
||||||
|
('h5 (setf (car el) 'h4))
|
||||||
|
('h6 (setf (car el) 'h5))))))
|
||||||
|
`((title . ,title)
|
||||||
|
(sitename . ,sitename)
|
||||||
|
(content . ,(with-temp-buffer
|
||||||
|
(dom-print content)
|
||||||
|
(buffer-string)))))))
|
||||||
|
|
||||||
|
(setq my/rdrview-template (expand-file-name
|
||||||
|
(concat user-emacs-directory "rdrview.tex")))
|
||||||
|
|
||||||
|
(cl-defun my/rdrview-render (content type variables callback
|
||||||
|
&key file-name overwrite)
|
||||||
|
(unless file-name
|
||||||
|
(setq file-name (format "/tmp/%d.pdf" (random 100000000))))
|
||||||
|
(let (params
|
||||||
|
(temp-file-name (format "/tmp/%d.%s" (random 100000000) type)))
|
||||||
|
(cl-loop for (key . value) in variables
|
||||||
|
when value
|
||||||
|
do (progn
|
||||||
|
(push "--variable" params)
|
||||||
|
(push (format "%s=%s" key value) params)))
|
||||||
|
(setq params (nreverse params))
|
||||||
|
(if (and (file-exists-p file-name) (not overwrite))
|
||||||
|
(funcall callback file-name)
|
||||||
|
(with-temp-file temp-file-name
|
||||||
|
(insert content))
|
||||||
|
(let ((proc (apply #'start-process
|
||||||
|
"pandoc" (get-buffer-create "*Pandoc*") "pandoc"
|
||||||
|
temp-file-name "-o" file-name
|
||||||
|
"--pdf-engine=xelatex" "--template" my/rdrview-template
|
||||||
|
params)))
|
||||||
|
(set-process-sentinel
|
||||||
|
proc
|
||||||
|
(lambda (process _msg)
|
||||||
|
(let ((status (process-status process))
|
||||||
|
(code (process-exit-status process)))
|
||||||
|
(cond ((and (eq status 'exit) (= code 0))
|
||||||
|
(progn
|
||||||
|
(message "Done!")
|
||||||
|
(funcall callback file-name)))
|
||||||
|
((or (and (eq status 'exit) (> code 0))
|
||||||
|
(eq status 'signal))
|
||||||
|
(user-error "Error in pandoc. Check the *Pandoc* buffer"))))))))))
|
||||||
|
|
||||||
|
(defun my/get-languages (url)
|
||||||
|
(let ((main-lang "english")
|
||||||
|
(other-lang "russian"))
|
||||||
|
(when (string-match-p (rx ".ru") url)
|
||||||
|
(setq main-lang "russian"
|
||||||
|
other-lang "english"))
|
||||||
|
(list main-lang other-lang)))
|
||||||
|
|
||||||
|
(defun my/rdrview-open (url overwrite)
|
||||||
|
(interactive
|
||||||
|
(let ((url (read-from-minibuffer
|
||||||
|
"URL: "
|
||||||
|
(if (bound-and-true-p elfeed-show-entry)
|
||||||
|
(elfeed-entry-link elfeed-show-entry)))))
|
||||||
|
(when (string-empty-p url)
|
||||||
|
(user-error "URL is empty"))
|
||||||
|
(list url current-prefix-arg)))
|
||||||
|
(my/rdrview-get
|
||||||
|
url
|
||||||
|
(lambda (res)
|
||||||
|
(let ((data (my/rdrview-parse res))
|
||||||
|
(langs (my/get-languages url)))
|
||||||
|
(my/rdrview-render
|
||||||
|
(alist-get 'content data)
|
||||||
|
'html
|
||||||
|
`((title . ,(alist-get 'title data))
|
||||||
|
(subtitle . ,(alist-get 'sitename data))
|
||||||
|
(main-lang . ,(nth 0 langs))
|
||||||
|
(other-lang . ,(nth 1 langs)))
|
||||||
|
(lambda (file-name)
|
||||||
|
(start-process "xdg-open" nil "xdg-open" file-name)))))))
|
||||||
|
|
||||||
|
(setq my/elfeed-pdf-dir (expand-file-name "~/.elfeed/pdf/"))
|
||||||
|
|
||||||
|
(defun my/elfeed-open-pdf (entry overwrite)
|
||||||
|
(interactive (list elfeed-show-entry current-prefix-arg))
|
||||||
|
(let ((authors (mapcar (lambda (m) (plist-get m :name)) (elfeed-meta entry :authors)))
|
||||||
|
(feed-title (elfeed-feed-title (elfeed-entry-feed entry)))
|
||||||
|
(tags (mapconcat #'symbol-name (elfeed-entry-tags entry) ", "))
|
||||||
|
(date (format-time-string "%a, %e %b %Y" (seconds-to-time (elfeed-entry-date entry))))
|
||||||
|
(content (elfeed-deref (elfeed-entry-content entry)))
|
||||||
|
(file-name (concat my/elfeed-pdf-dir
|
||||||
|
(elfeed-ref-id (elfeed-entry-content entry))
|
||||||
|
".pdf"))
|
||||||
|
(main-language "english")
|
||||||
|
(other-language "russian"))
|
||||||
|
(unless content
|
||||||
|
(user-error "No content!"))
|
||||||
|
(setq subtitle
|
||||||
|
(cond
|
||||||
|
((seq-empty-p authors) feed-title)
|
||||||
|
((and (not (seq-empty-p (car authors)))
|
||||||
|
(string-match-p (regexp-quote (car authors)) feed-title)) feed-title)
|
||||||
|
(t (concat (string-join authors ", ") "\\\\" feed-title))))
|
||||||
|
(when (member 'ru (elfeed-entry-tags entry))
|
||||||
|
(setq main-language "russian")
|
||||||
|
(setq other-language "english"))
|
||||||
|
(my/rdrview-render
|
||||||
|
(if (bound-and-true-p my/elfeed-show-rdrview-html)
|
||||||
|
my/elfeed-show-rdrview-html
|
||||||
|
content)
|
||||||
|
(elfeed-entry-content-type entry)
|
||||||
|
`((title . ,(elfeed-entry-title entry))
|
||||||
|
(subtitle . ,subtitle)
|
||||||
|
(date . ,date)
|
||||||
|
(tags . ,tags)
|
||||||
|
(main-lang . ,main-language)
|
||||||
|
(other-lang . ,other-language))
|
||||||
|
(lambda (file-name)
|
||||||
|
(start-process "xdg-open" nil "xdg-open" file-name))
|
||||||
|
:file-name file-name
|
||||||
|
:overwrite current-prefix-arg)))
|
||||||
|
|
||||||
|
(defvar-local my/elfeed-show-rdrview-html nil)
|
||||||
|
|
||||||
|
(defun my/rdrview-elfeed-show ()
|
||||||
|
(interactive)
|
||||||
|
(unless elfeed-show-entry
|
||||||
|
(user-error "No elfeed entry in this buffer!"))
|
||||||
|
(my/rdrview-get
|
||||||
|
(elfeed-entry-link elfeed-show-entry)
|
||||||
|
(lambda (result)
|
||||||
|
(let* ((data (my/rdrview-parse result))
|
||||||
|
(inhibit-read-only t)
|
||||||
|
(title (elfeed-entry-title elfeed-show-entry))
|
||||||
|
(date (seconds-to-time (elfeed-entry-date elfeed-show-entry)))
|
||||||
|
(authors (elfeed-meta elfeed-show-entry :authors))
|
||||||
|
(link (elfeed-entry-link elfeed-show-entry))
|
||||||
|
(tags (elfeed-entry-tags elfeed-show-entry))
|
||||||
|
(tagsstr (mapconcat #'symbol-name tags ", "))
|
||||||
|
(nicedate (format-time-string "%a, %e %b %Y %T %Z" date))
|
||||||
|
(content (alist-get 'content data))
|
||||||
|
(feed (elfeed-entry-feed elfeed-show-entry))
|
||||||
|
(feed-title (elfeed-feed-title feed))
|
||||||
|
(base (and feed (elfeed-compute-base (elfeed-feed-url feed)))))
|
||||||
|
(erase-buffer)
|
||||||
|
(insert (format (propertize "Title: %s\n" 'face 'message-header-name)
|
||||||
|
(propertize title 'face 'message-header-subject)))
|
||||||
|
(when elfeed-show-entry-author
|
||||||
|
(dolist (author authors)
|
||||||
|
(let ((formatted (elfeed--show-format-author author)))
|
||||||
|
(insert
|
||||||
|
(format (propertize "Author: %s\n" 'face 'message-header-name)
|
||||||
|
(propertize formatted 'face 'message-header-to))))))
|
||||||
|
(insert (format (propertize "Date: %s\n" 'face 'message-header-name)
|
||||||
|
(propertize nicedate 'face 'message-header-other)))
|
||||||
|
(insert (format (propertize "Feed: %s\n" 'face 'message-header-name)
|
||||||
|
(propertize feed-title 'face 'message-header-other)))
|
||||||
|
(when tags
|
||||||
|
(insert (format (propertize "Tags: %s\n" 'face 'message-header-name)
|
||||||
|
(propertize tagsstr 'face 'message-header-other))))
|
||||||
|
(insert (propertize "Link: " 'face 'message-header-name))
|
||||||
|
(elfeed-insert-link link link)
|
||||||
|
(insert "\n")
|
||||||
|
(cl-loop for enclosure in (elfeed-entry-enclosures elfeed-show-entry)
|
||||||
|
do (insert (propertize "Enclosure: " 'face 'message-header-name))
|
||||||
|
do (elfeed-insert-link (car enclosure))
|
||||||
|
do (insert "\n"))
|
||||||
|
(insert "\n")
|
||||||
|
(if content
|
||||||
|
(elfeed-insert-html content base)
|
||||||
|
(insert (propertize "(empty)\n" 'face 'italic)))
|
||||||
|
(setq-local my/elfeed-show-rdrview-html content)
|
||||||
|
(goto-char (point-min))))))
|
||||||
|
|
||||||
|
(with-eval-after-load 'elfeed
|
||||||
|
(general-define-key
|
||||||
|
:states '(normal)
|
||||||
|
:keymaps 'elfeed-show-mode-map
|
||||||
|
"gp" #'my/rdrview-elfeed-show
|
||||||
|
"gv" #'my/elfeed-open-pdf))
|
||||||
|
|
||||||
(use-package erc
|
(use-package erc
|
||||||
:commands (erc erc-tls)
|
:commands (erc erc-tls)
|
||||||
:straight (:type built-in)
|
:straight (:type built-in)
|
||||||
|
|
|
||||||
170
.emacs.d/rdrview.tex
Normal file
170
.emacs.d/rdrview.tex
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
\documentclass[a4paper, 12pt]{extarticle}
|
||||||
|
|
||||||
|
% ====== Math ======
|
||||||
|
\usepackage{amsmath} % Math stuff
|
||||||
|
\usepackage{amssymb}
|
||||||
|
\usepackage{mathspec}
|
||||||
|
|
||||||
|
% ====== List ======
|
||||||
|
\usepackage{enumitem}
|
||||||
|
\usepackage{etoolbox}
|
||||||
|
\setlist{nosep, topsep=-10pt} % Remove sep-s beetween list elements
|
||||||
|
\setlist[enumerate]{label*=\arabic*.}
|
||||||
|
\setlist[enumerate,1]{after=\vspace{0.5\baselineskip}}
|
||||||
|
\setlist[itemize,1]{after=\vspace{0.5\baselineskip}}
|
||||||
|
|
||||||
|
\AtBeginEnvironment{itemize}{%
|
||||||
|
\setlist[enumerate]{label=\arabic*.}
|
||||||
|
\setlist[enumerate,1]{after=\vspace{0\baselineskip}}
|
||||||
|
}
|
||||||
|
|
||||||
|
\providecommand{\tightlist}{%
|
||||||
|
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
|
||||||
|
|
||||||
|
% ====== Link ======
|
||||||
|
|
||||||
|
\usepackage{xcolor}
|
||||||
|
\usepackage{hyperref} % Links
|
||||||
|
\hypersetup{
|
||||||
|
colorlinks=true,
|
||||||
|
citecolor=blue,
|
||||||
|
filecolor=blue,
|
||||||
|
linkcolor=blue,
|
||||||
|
urlcolor=blue,
|
||||||
|
}
|
||||||
|
|
||||||
|
% Linebreaks for urls
|
||||||
|
\expandafter\def\expandafter\UrlBreaks\expandafter{\UrlBreaks% save the current one
|
||||||
|
\do\a\do\b\do\c\do\d\do\e\do\f\do\g\do\h\do\i\do\j%
|
||||||
|
\do\k\do\l\do\m\do\n\do\o\do\p\do\q\do\r\do\s\do\t%
|
||||||
|
\do\u\do\v\do\w\do\x\do\y\do\z\do\A\do\B\do\C\do\D%
|
||||||
|
\do\E\do\F\do\G\do\H\do\I\do\J\do\K\do\L\do\M\do\N%
|
||||||
|
\do\O\do\P\do\Q\do\R\do\S\do\T\do\U\do\V\do\W\do\X%
|
||||||
|
\do\Y\do\Z}
|
||||||
|
|
||||||
|
% ====== Captions ======
|
||||||
|
% TODO
|
||||||
|
|
||||||
|
% ====== Table ======
|
||||||
|
\usepackage{array}
|
||||||
|
\usepackage{booktabs}
|
||||||
|
\usepackage{longtable}
|
||||||
|
\usepackage{multirow}
|
||||||
|
\usepackage{calc}
|
||||||
|
|
||||||
|
% ====== Images ======
|
||||||
|
\usepackage{graphicx} % Pictures
|
||||||
|
|
||||||
|
\makeatletter
|
||||||
|
\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}
|
||||||
|
\def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi}
|
||||||
|
\makeatother
|
||||||
|
% Scale images if necessary, so that they will not overflow the page
|
||||||
|
% margins by default, and it is still possible to overwrite the defaults
|
||||||
|
% using explicit options in \includegraphics[width, height, ...]{}
|
||||||
|
\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio}
|
||||||
|
% Set default figure placement to htbp
|
||||||
|
\makeatletter
|
||||||
|
\def\fps@figure{htbp}
|
||||||
|
\makeatother
|
||||||
|
|
||||||
|
\newcommand{\noimage}{%
|
||||||
|
\setlength{\fboxsep}{-\fboxrule}%
|
||||||
|
\fbox{\phantom{\rule{150pt}{100pt}}}% Framed box
|
||||||
|
}
|
||||||
|
|
||||||
|
\makeatletter
|
||||||
|
\patchcmd{\Gin@ii}
|
||||||
|
{\begingroup}% <search>
|
||||||
|
{\begingroup\renewcommand{\@latex@error}[2]{\noimage}}% <replace>
|
||||||
|
{}% <success>
|
||||||
|
{}% <failure>
|
||||||
|
\makeatother
|
||||||
|
% ====== Misc ======
|
||||||
|
\usepackage{fancyvrb}
|
||||||
|
|
||||||
|
\usepackage{csquotes}
|
||||||
|
|
||||||
|
\usepackage[normalem]{ulem}
|
||||||
|
|
||||||
|
% Quotes and verses style
|
||||||
|
\AtBeginEnvironment{quote}{\singlespacing}
|
||||||
|
\AtBeginEnvironment{verse}{\singlespacing}
|
||||||
|
|
||||||
|
% ====== Text spacing ======
|
||||||
|
\usepackage{setspace} % String spacing
|
||||||
|
\onehalfspacing{}
|
||||||
|
|
||||||
|
\usepackage{indentfirst}
|
||||||
|
\setlength\parindent{0cm}
|
||||||
|
\setlength\parskip{6pt}
|
||||||
|
|
||||||
|
% ====== Page layout ======
|
||||||
|
\usepackage[ % Margins
|
||||||
|
left=2cm,
|
||||||
|
right=2cm,
|
||||||
|
top=2cm,
|
||||||
|
bottom=2cm
|
||||||
|
]{geometry}
|
||||||
|
|
||||||
|
% ====== Document sectioning ======
|
||||||
|
\usepackage{titlesec}
|
||||||
|
|
||||||
|
\titleformat*{\section}{\bfseries}
|
||||||
|
\titleformat*{\subsection}{\bfseries}
|
||||||
|
\titleformat*{\subsubsection}{\bfseries}
|
||||||
|
\titleformat*{\paragraph}{\bfseries}
|
||||||
|
\titleformat*{\subparagraph}{\bfseries\itshape}% chktex 6
|
||||||
|
|
||||||
|
\titlespacing*{\section}{0cm}{12pt}{3pt}
|
||||||
|
\titlespacing*{\subsection}{0cm}{12pt}{3pt}
|
||||||
|
\titlespacing*{\subsubsection}{0cm}{12pt}{0pt}
|
||||||
|
\titlespacing*{\paragraph}{0pt}{6pt}{6pt}
|
||||||
|
\titlespacing*{\subparagraph}{0pt}{6pt}{3pt}
|
||||||
|
|
||||||
|
\makeatletter
|
||||||
|
\providecommand{\subtitle}[1]{
|
||||||
|
\apptocmd{\@title}{\par {\large #1 \par}}{}{}
|
||||||
|
}
|
||||||
|
\makeatother
|
||||||
|
|
||||||
|
% ====== Pandoc =======
|
||||||
|
$if(highlighting-macros)$
|
||||||
|
$highlighting-macros$
|
||||||
|
$endif$
|
||||||
|
|
||||||
|
% ====== Language ======
|
||||||
|
\usepackage{polyglossia}
|
||||||
|
\setdefaultlanguage{$main-lang$}
|
||||||
|
\setotherlanguage{$other-lang$}
|
||||||
|
\defaultfontfeatures{Ligatures={TeX}}
|
||||||
|
\setmainfont{Open Sans}
|
||||||
|
\newfontfamily\cyrillicfont{Open Sans}
|
||||||
|
|
||||||
|
\setmonofont[Scale=0.9]{DejaVu Sans Mono}
|
||||||
|
\newfontfamily{\cyrillicfonttt}{DejaVu Sans Mono}[Scale=0.8]
|
||||||
|
|
||||||
|
\usepackage{bidi}
|
||||||
|
|
||||||
|
\usepackage{microtype}
|
||||||
|
\setlength{\emergencystretch}{3pt}
|
||||||
|
|
||||||
|
$if(title)$
|
||||||
|
\title{$title$}
|
||||||
|
$endif$
|
||||||
|
$if(subtitle)$
|
||||||
|
\subtitle{$subtitle$}
|
||||||
|
$endif$
|
||||||
|
|
||||||
|
$if(author)$
|
||||||
|
\author{$for(author)$$author$$sep$ \and $endfor$}
|
||||||
|
$endif$
|
||||||
|
$if(date)$
|
||||||
|
\date{$date$}
|
||||||
|
$endif$
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
\maketitle{}
|
||||||
|
|
||||||
|
$body$
|
||||||
|
\end{document}
|
||||||
288
Emacs.org
288
Emacs.org
|
|
@ -290,6 +290,11 @@ I decided not to keep configs for features that I do not use anymore because thi
|
||||||
* Bootstrap
|
* Bootstrap
|
||||||
Setting up the environment, performance tuning and a few basic settings.
|
Setting up the environment, performance tuning and a few basic settings.
|
||||||
|
|
||||||
|
First things first, lexical binding.
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
;;; -*- lexical-binding: t -*-
|
||||||
|
#+end_src
|
||||||
|
|
||||||
** Packages
|
** Packages
|
||||||
*** straight.el
|
*** straight.el
|
||||||
Straight.el is my Emacs package manager of choice. Its advantages & disadvantages over other options are listed pretty thoroughly in the README file in the repo.
|
Straight.el is my Emacs package manager of choice. Its advantages & disadvantages over other options are listed pretty thoroughly in the README file in the repo.
|
||||||
|
|
@ -2615,7 +2620,11 @@ Vue settings
|
||||||
#+begin_src emacs-lisp
|
#+begin_src emacs-lisp
|
||||||
(use-package php-mode
|
(use-package php-mode
|
||||||
:straight t
|
:straight t
|
||||||
:mode "\\.php\\'")
|
:mode "\\.php\\'"
|
||||||
|
:config
|
||||||
|
(add-hook 'php-mode-hook #'smartparens-mode)
|
||||||
|
(add-hook 'php-mode-hook #'lsp)
|
||||||
|
(my/set-smartparens-indent 'php-mode))
|
||||||
#+end_src
|
#+end_src
|
||||||
** LaTeX
|
** LaTeX
|
||||||
*** AUCTeX
|
*** AUCTeX
|
||||||
|
|
@ -5178,6 +5187,7 @@ My config mostly follows ranger's and vifm's keybindings which I'm used to.
|
||||||
"=" 'dired-narrow
|
"=" 'dired-narrow
|
||||||
"-" 'dired-create-empty-file
|
"-" 'dired-create-empty-file
|
||||||
"~" 'vterm
|
"~" 'vterm
|
||||||
|
"M-r" 'wdired-change-to-wdired-mode
|
||||||
"<left>" 'dired-up-directory
|
"<left>" 'dired-up-directory
|
||||||
"<right>" 'dired-find-file
|
"<right>" 'dired-find-file
|
||||||
"M-<return>" 'dired-open-xdg))
|
"M-<return>" 'dired-open-xdg))
|
||||||
|
|
@ -5694,6 +5704,7 @@ My notmuch config now resides in [[file:Mail.org][Mail.org]].
|
||||||
(message "Can't load mail.el"))))
|
(message "Can't load mail.el"))))
|
||||||
#+end_src
|
#+end_src
|
||||||
*** Elfeed
|
*** Elfeed
|
||||||
|
**** General settings
|
||||||
[[https://github.com/skeeto/elfeed][elfeed]] is an Emacs RSS client.
|
[[https://github.com/skeeto/elfeed][elfeed]] is an Emacs RSS client.
|
||||||
|
|
||||||
The advice there sets =shr-use-fonts= to nil while rendering HTML, so the =elfeed-show= buffer will use monospace font.
|
The advice there sets =shr-use-fonts= to nil while rendering HTML, so the =elfeed-show= buffer will use monospace font.
|
||||||
|
|
@ -5785,6 +5796,9 @@ Setting up custom faces for certain tags to make the feed look a bit nicer.
|
||||||
(defface elfeed-blogs-entry nil
|
(defface elfeed-blogs-entry nil
|
||||||
"Face for the elfeed entries with tag \"blogs\"")
|
"Face for the elfeed entries with tag \"blogs\"")
|
||||||
|
|
||||||
|
(defface elfeed-govt-entry nil
|
||||||
|
"Face for the elfeed entries with tag \"blogs\"")
|
||||||
|
|
||||||
(my/use-doom-colors
|
(my/use-doom-colors
|
||||||
(elfeed-search-tag-face :foreground (doom-color 'yellow))
|
(elfeed-search-tag-face :foreground (doom-color 'yellow))
|
||||||
(elfeed-videos-entry :foreground (doom-color 'red))
|
(elfeed-videos-entry :foreground (doom-color 'red))
|
||||||
|
|
@ -5792,13 +5806,15 @@ Setting up custom faces for certain tags to make the feed look a bit nicer.
|
||||||
(elfeed-emacs-entry :foreground (doom-color 'magenta))
|
(elfeed-emacs-entry :foreground (doom-color 'magenta))
|
||||||
(elfeed-music-entry :foreground (doom-color 'green))
|
(elfeed-music-entry :foreground (doom-color 'green))
|
||||||
(elfeed-podcasts-entry :foreground (doom-color 'yellow))
|
(elfeed-podcasts-entry :foreground (doom-color 'yellow))
|
||||||
(elfeed-blogs-entry :foreground (doom-color 'orange)))
|
(elfeed-blogs-entry :foreground (doom-color 'orange))
|
||||||
|
(elfeed-govt-entry :foreground (doom-color 'dark-cyan)))
|
||||||
|
|
||||||
(with-eval-after-load 'elfeed
|
(with-eval-after-load 'elfeed
|
||||||
(setq elfeed-search-face-alist
|
(setq elfeed-search-face-alist
|
||||||
'((twitter elfeed-twitter-entry)
|
'((podcasts elfeed-podcasts-entry)
|
||||||
(podcasts elfeed-podcasts-entry)
|
|
||||||
(music elfeed-music-entry)
|
(music elfeed-music-entry)
|
||||||
|
(gov elfeed-govt-entry)
|
||||||
|
(twitter elfeed-twitter-entry)
|
||||||
(videos elfeed-videos-entry)
|
(videos elfeed-videos-entry)
|
||||||
(emacs elfeed-emacs-entry)
|
(emacs elfeed-emacs-entry)
|
||||||
(blogs elfeed-blogs-entry)
|
(blogs elfeed-blogs-entry)
|
||||||
|
|
@ -5830,7 +5846,9 @@ The default interface of elfeed is just a list of all entries, so it gets hard t
|
||||||
#+begin_src emacs-lisp
|
#+begin_src emacs-lisp
|
||||||
(use-package elfeed-summary
|
(use-package elfeed-summary
|
||||||
:commands (elfeed-summary)
|
:commands (elfeed-summary)
|
||||||
:straight t)
|
:straight t
|
||||||
|
:config
|
||||||
|
(setq elfeed-summary-filter-by-title t))
|
||||||
#+end_src
|
#+end_src
|
||||||
**** elfeed-score
|
**** elfeed-score
|
||||||
[[https://github.com/sp1ff/elfeed-score][elfeed-score]] is a package that implements scoring for the elfeed entries. Entries are scored by a set of rules for tags/title/content/etc and sorted by that score.
|
[[https://github.com/sp1ff/elfeed-score][elfeed-score]] is a package that implements scoring for the elfeed entries. Entries are scored by a set of rules for tags/title/content/etc and sorted by that score.
|
||||||
|
|
@ -6248,6 +6266,266 @@ I use it occasionally to open links in elfeed.
|
||||||
"+" 'text-scale-increase
|
"+" 'text-scale-increase
|
||||||
"-" 'text-scale-decrease)
|
"-" 'text-scale-decrease)
|
||||||
#+end_src
|
#+end_src
|
||||||
|
*** Reader View & PDFs
|
||||||
|
**** rdrview
|
||||||
|
[[https://github.com/eafer/rdrview][rdrview]] is a command-line tool that provides Firefox Reader view as a command-line tool. A Guix definition is available in [[https://github.com/SqrtMinusOne/channel-q][my Guix channel]].
|
||||||
|
|
||||||
|
The basic idea here is to take an arbitrary web page and convert it to PDF via pandoc.
|
||||||
|
|
||||||
|
So, first we need to get the =rdrview= representation of the URL:
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(defun my/rdrview-get (url callback)
|
||||||
|
(let* ((buffer (generate-new-buffer "rdrview"))
|
||||||
|
(proc (start-process "rdrview" buffer "rdrview"
|
||||||
|
url "-T" "title,sitename,body"
|
||||||
|
"-H")))
|
||||||
|
(set-process-sentinel
|
||||||
|
proc
|
||||||
|
(lambda (process _msg)
|
||||||
|
(let ((status (process-status process))
|
||||||
|
(code (process-exit-status process)))
|
||||||
|
(cond ((and (eq status 'exit) (= code 0))
|
||||||
|
(progn
|
||||||
|
(funcall callback
|
||||||
|
(with-current-buffer (process-buffer process)
|
||||||
|
(buffer-string)))
|
||||||
|
(kill-buffer (process-buffer process))) )
|
||||||
|
((or (and (eq status 'exit) (> code 0))
|
||||||
|
(eq status 'signal))
|
||||||
|
(let ((err (with-current-buffer (process-buffer process)
|
||||||
|
(buffer-string))))
|
||||||
|
(kill-buffer (process-buffer process))
|
||||||
|
(user-error "Error in rdrview: %s" err)))))))
|
||||||
|
proc))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
After that, process the rdrview output. First, it outputs metadata to the resulting HTML, so this part parses the DOM and retrieves the header and the name of the site.
|
||||||
|
|
||||||
|
Second, for some reason the header enumeration starts with =<h2>=, so this also shifts headers up by one.
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(defun my/rdrview-parse (dom-string)
|
||||||
|
(let ((dom (with-temp-buffer
|
||||||
|
(insert dom-string)
|
||||||
|
(libxml-parse-html-region (point-min) (point-max)))))
|
||||||
|
(let (title sitename content)
|
||||||
|
(dolist (child (dom-children (car (dom-by-id dom "readability-page-1"))))
|
||||||
|
(when (listp child)
|
||||||
|
(cond
|
||||||
|
((eq (car child) 'h1)
|
||||||
|
(setq title (dom-text child)))
|
||||||
|
((eq (car child) 'h2)
|
||||||
|
(setq sitename (dom-text child)))
|
||||||
|
((eq (car child) 'div)
|
||||||
|
(setq content child)))))
|
||||||
|
(dom-search
|
||||||
|
content
|
||||||
|
(lambda (el)
|
||||||
|
(when (listp el)
|
||||||
|
(pcase (car el)
|
||||||
|
('h2 (setf (car el) 'h1))
|
||||||
|
('h3 (setf (car el) 'h2))
|
||||||
|
('h4 (setf (car el) 'h3))
|
||||||
|
('h5 (setf (car el) 'h4))
|
||||||
|
('h6 (setf (car el) 'h5))))))
|
||||||
|
`((title . ,title)
|
||||||
|
(sitename . ,sitename)
|
||||||
|
(content . ,(with-temp-buffer
|
||||||
|
(dom-print content)
|
||||||
|
(buffer-string)))))))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
**** Opening stuff in PDF viewer
|
||||||
|
Now, we need to render the resulting HTML to a pdf. To do that, we can use =pandoc= with a [[file:.emacs.d/rdrview.tex][custom template]].
|
||||||
|
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(setq my/rdrview-template (expand-file-name
|
||||||
|
(concat user-emacs-directory "rdrview.tex")))
|
||||||
|
|
||||||
|
(cl-defun my/rdrview-render (content type variables callback
|
||||||
|
&key file-name overwrite)
|
||||||
|
(unless file-name
|
||||||
|
(setq file-name (format "/tmp/%d.pdf" (random 100000000))))
|
||||||
|
(let (params
|
||||||
|
(temp-file-name (format "/tmp/%d.%s" (random 100000000) type)))
|
||||||
|
(cl-loop for (key . value) in variables
|
||||||
|
when value
|
||||||
|
do (progn
|
||||||
|
(push "--variable" params)
|
||||||
|
(push (format "%s=%s" key value) params)))
|
||||||
|
(setq params (nreverse params))
|
||||||
|
(if (and (file-exists-p file-name) (not overwrite))
|
||||||
|
(funcall callback file-name)
|
||||||
|
(with-temp-file temp-file-name
|
||||||
|
(insert content))
|
||||||
|
(let ((proc (apply #'start-process
|
||||||
|
"pandoc" (get-buffer-create "*Pandoc*") "pandoc"
|
||||||
|
temp-file-name "-o" file-name
|
||||||
|
"--pdf-engine=xelatex" "--template" my/rdrview-template
|
||||||
|
params)))
|
||||||
|
(set-process-sentinel
|
||||||
|
proc
|
||||||
|
(lambda (process _msg)
|
||||||
|
(let ((status (process-status process))
|
||||||
|
(code (process-exit-status process)))
|
||||||
|
(cond ((and (eq status 'exit) (= code 0))
|
||||||
|
(progn
|
||||||
|
(message "Done!")
|
||||||
|
(funcall callback file-name)))
|
||||||
|
((or (and (eq status 'exit) (> code 0))
|
||||||
|
(eq status 'signal))
|
||||||
|
(user-error "Error in pandoc. Check the *Pandoc* buffer"))))))))))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
And putting all of this together to get a PDF representation of an arbitrary URL.
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(defun my/get-languages (url)
|
||||||
|
(let ((main-lang "english")
|
||||||
|
(other-lang "russian"))
|
||||||
|
(when (string-match-p (rx ".ru") url)
|
||||||
|
(setq main-lang "russian"
|
||||||
|
other-lang "english"))
|
||||||
|
(list main-lang other-lang)))
|
||||||
|
|
||||||
|
(defun my/rdrview-open (url overwrite)
|
||||||
|
(interactive
|
||||||
|
(let ((url (read-from-minibuffer
|
||||||
|
"URL: "
|
||||||
|
(if (bound-and-true-p elfeed-show-entry)
|
||||||
|
(elfeed-entry-link elfeed-show-entry)))))
|
||||||
|
(when (string-empty-p url)
|
||||||
|
(user-error "URL is empty"))
|
||||||
|
(list url current-prefix-arg)))
|
||||||
|
(my/rdrview-get
|
||||||
|
url
|
||||||
|
(lambda (res)
|
||||||
|
(let ((data (my/rdrview-parse res))
|
||||||
|
(langs (my/get-languages url)))
|
||||||
|
(my/rdrview-render
|
||||||
|
(alist-get 'content data)
|
||||||
|
'html
|
||||||
|
`((title . ,(alist-get 'title data))
|
||||||
|
(subtitle . ,(alist-get 'sitename data))
|
||||||
|
(main-lang . ,(nth 0 langs))
|
||||||
|
(other-lang . ,(nth 1 langs)))
|
||||||
|
(lambda (file-name)
|
||||||
|
(start-process "xdg-open" nil "xdg-open" file-name)))))))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
**** Rendering elfeed entries as PDFs
|
||||||
|
This also goes really well with elfeed, because for these RSS feeds that have a well-formed HTML part there's even no need to invoke =rdrview=, we can just feed the HTML to =pandoc=.
|
||||||
|
|
||||||
|
TODO escape title
|
||||||
|
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(setq my/elfeed-pdf-dir (expand-file-name "~/.elfeed/pdf/"))
|
||||||
|
|
||||||
|
(defun my/elfeed-open-pdf (entry overwrite)
|
||||||
|
(interactive (list elfeed-show-entry current-prefix-arg))
|
||||||
|
(let ((authors (mapcar (lambda (m) (plist-get m :name)) (elfeed-meta entry :authors)))
|
||||||
|
(feed-title (elfeed-feed-title (elfeed-entry-feed entry)))
|
||||||
|
(tags (mapconcat #'symbol-name (elfeed-entry-tags entry) ", "))
|
||||||
|
(date (format-time-string "%a, %e %b %Y" (seconds-to-time (elfeed-entry-date entry))))
|
||||||
|
(content (elfeed-deref (elfeed-entry-content entry)))
|
||||||
|
(file-name (concat my/elfeed-pdf-dir
|
||||||
|
(elfeed-ref-id (elfeed-entry-content entry))
|
||||||
|
".pdf"))
|
||||||
|
(main-language "english")
|
||||||
|
(other-language "russian"))
|
||||||
|
(unless content
|
||||||
|
(user-error "No content!"))
|
||||||
|
(setq subtitle
|
||||||
|
(cond
|
||||||
|
((seq-empty-p authors) feed-title)
|
||||||
|
((and (not (seq-empty-p (car authors)))
|
||||||
|
(string-match-p (regexp-quote (car authors)) feed-title)) feed-title)
|
||||||
|
(t (concat (string-join authors ", ") "\\\\" feed-title))))
|
||||||
|
(when (member 'ru (elfeed-entry-tags entry))
|
||||||
|
(setq main-language "russian")
|
||||||
|
(setq other-language "english"))
|
||||||
|
(my/rdrview-render
|
||||||
|
(if (bound-and-true-p my/elfeed-show-rdrview-html)
|
||||||
|
my/elfeed-show-rdrview-html
|
||||||
|
content)
|
||||||
|
(elfeed-entry-content-type entry)
|
||||||
|
`((title . ,(elfeed-entry-title entry))
|
||||||
|
(subtitle . ,subtitle)
|
||||||
|
(date . ,date)
|
||||||
|
(tags . ,tags)
|
||||||
|
(main-lang . ,main-language)
|
||||||
|
(other-lang . ,other-language))
|
||||||
|
(lambda (file-name)
|
||||||
|
(start-process "xdg-open" nil "xdg-open" file-name))
|
||||||
|
:file-name file-name
|
||||||
|
:overwrite current-prefix-arg)))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
**** Viewing elfeed entries view rdrview
|
||||||
|
However, in some cases RSS feeds supply only a short description of the content instead of the actual content. If that's the case, we can use =rdrview= to replace the actual content.
|
||||||
|
|
||||||
|
So, the following is the corresponding modification of =elfeed-show-refresh--mail-style= function:
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(defvar-local my/elfeed-show-rdrview-html nil)
|
||||||
|
|
||||||
|
(defun my/rdrview-elfeed-show ()
|
||||||
|
(interactive)
|
||||||
|
(unless elfeed-show-entry
|
||||||
|
(user-error "No elfeed entry in this buffer!"))
|
||||||
|
(my/rdrview-get
|
||||||
|
(elfeed-entry-link elfeed-show-entry)
|
||||||
|
(lambda (result)
|
||||||
|
(let* ((data (my/rdrview-parse result))
|
||||||
|
(inhibit-read-only t)
|
||||||
|
(title (elfeed-entry-title elfeed-show-entry))
|
||||||
|
(date (seconds-to-time (elfeed-entry-date elfeed-show-entry)))
|
||||||
|
(authors (elfeed-meta elfeed-show-entry :authors))
|
||||||
|
(link (elfeed-entry-link elfeed-show-entry))
|
||||||
|
(tags (elfeed-entry-tags elfeed-show-entry))
|
||||||
|
(tagsstr (mapconcat #'symbol-name tags ", "))
|
||||||
|
(nicedate (format-time-string "%a, %e %b %Y %T %Z" date))
|
||||||
|
(content (alist-get 'content data))
|
||||||
|
(feed (elfeed-entry-feed elfeed-show-entry))
|
||||||
|
(feed-title (elfeed-feed-title feed))
|
||||||
|
(base (and feed (elfeed-compute-base (elfeed-feed-url feed)))))
|
||||||
|
(erase-buffer)
|
||||||
|
(insert (format (propertize "Title: %s\n" 'face 'message-header-name)
|
||||||
|
(propertize title 'face 'message-header-subject)))
|
||||||
|
(when elfeed-show-entry-author
|
||||||
|
(dolist (author authors)
|
||||||
|
(let ((formatted (elfeed--show-format-author author)))
|
||||||
|
(insert
|
||||||
|
(format (propertize "Author: %s\n" 'face 'message-header-name)
|
||||||
|
(propertize formatted 'face 'message-header-to))))))
|
||||||
|
(insert (format (propertize "Date: %s\n" 'face 'message-header-name)
|
||||||
|
(propertize nicedate 'face 'message-header-other)))
|
||||||
|
(insert (format (propertize "Feed: %s\n" 'face 'message-header-name)
|
||||||
|
(propertize feed-title 'face 'message-header-other)))
|
||||||
|
(when tags
|
||||||
|
(insert (format (propertize "Tags: %s\n" 'face 'message-header-name)
|
||||||
|
(propertize tagsstr 'face 'message-header-other))))
|
||||||
|
(insert (propertize "Link: " 'face 'message-header-name))
|
||||||
|
(elfeed-insert-link link link)
|
||||||
|
(insert "\n")
|
||||||
|
(cl-loop for enclosure in (elfeed-entry-enclosures elfeed-show-entry)
|
||||||
|
do (insert (propertize "Enclosure: " 'face 'message-header-name))
|
||||||
|
do (elfeed-insert-link (car enclosure))
|
||||||
|
do (insert "\n"))
|
||||||
|
(insert "\n")
|
||||||
|
(if content
|
||||||
|
(elfeed-insert-html content base)
|
||||||
|
(insert (propertize "(empty)\n" 'face 'italic)))
|
||||||
|
(setq-local my/elfeed-show-rdrview-html content)
|
||||||
|
(goto-char (point-min))))))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Setting keybindings for elfeed:
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(with-eval-after-load 'elfeed
|
||||||
|
(general-define-key
|
||||||
|
:states '(normal)
|
||||||
|
:keymaps 'elfeed-show-mode-map
|
||||||
|
"gp" #'my/rdrview-elfeed-show
|
||||||
|
"gv" #'my/elfeed-open-pdf))
|
||||||
|
#+end_src
|
||||||
*** ERC
|
*** ERC
|
||||||
ERC is a built-it Emacs IRC client.
|
ERC is a built-it Emacs IRC client.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue