URL로부터 Title을 가져오고 Web Archive 링크를 생성하는 Emacs 함수
ohyecloudy.com/lifelog/와 ohyecloudy.com/pnotes/ 블로그에 개시된 과거 글을 보면 끊긴 링크가 많다. 참고한 링크가 실린 웹사이트보다 더 오래 살아남아서 어쩔 수 없는 걸까? 링크를 건 글 중에 좋은 글도 많았는데, 지금은 볼 수 없어서 아쉽다.
이런 아쉬움 때문일까? 인터넷 아카이브를 제공하는 ’Wayback Machine - web.archive.org’ 서비스가 있다. 글에 링크를 붙일 때, Web Archive 링크도 같이 붙이면 해당 글이 사라지더라도 내용을 확인할 수 있다. 링크를 무조건 Web Archive 주소로 거는 건 올바르지 않다고 생각한다. 글 작성자에게 트래픽을 주는 게 맞다고 생각한다. 그래서 원본 링크(archive link)
이런 식으로 링크를 두 개 추가하는 기능을 추가하려고 한다. Bash 스크립트로? 아니 Emacs Lisp 함수로.
필요한 기능 프로토타이핑
필요한 기능을 Emacs Lisp로 구현할 수 있는지 확인하는 걸 먼저 해본다.
Web Archive 링크를 가져오는 방법
Web Archive 링크를 저장하려면 어떻게 해야 하나? 가장 핵심적인 기능이다. 생각보다 간편하다. http://web.archive.org/save/아카이빙할주소
이런 주소로 HTTP GET 메서드를 호출하면 된다.
http://ohyecloudy.com/pnotes/archives/retrospective-2023/ 페이지를 Curl 프로그램을 사용해 archive 주소를 가져오는 걸 살펴보자.
진행 상황이나 다른 정보가 필요 없으니 --slient
옵션을 붙여주고 archive 내용이 아니라 주소만 필요하니 --head
옵션으로 응답 header만 출력하게 한다.
curl --silent --head http://web.archive.org/save/http://ohyecloudy.com/pnotes/archives/retrospective-2023/
header의 location
필드에 archive 주소가 있다. HTTP 응답은 3XX로 redirection 이다.
HTTP/1.1 302 FOUND
...
location: http://web.archive.org/web/20240325031430/http://ohyecloudy.com/pnotes/archives/retrospective-2023/
...
Emacs Lisp로 curl 프로그램 실행
사용이 간편한 ’tkf/emacs-request - github.com’ 라이브러리를 사용하려고 했지만 redirection 응답이 왔을 때, 해당 페이지를 방문하지 않고 header의 location 필드만 얻을 방법을 못 찾았다. url-retrieve 내장 함수를 사용할까 했는데, 사용 방법이 복잡해서 잘 손이 가지 않는다.
그래서 Curl 직접 호출해 본다. curl 인자를 만들고 shell-command-to-string 함수를 호출해 프로그램을 실행한다. 출력에서 location
필드를 파싱한다.
(defun retrieve-web-archive-url (url)
(message "Starts saving web archive url: %s" url)
(let* ((start-time (current-time))
(request-url (format "http://web.archive.org/save/%s" url))
(command (format "curl --silent --head %s" request-url))
(response (replace-regexp-in-string "\r+$" "" (shell-command-to-string command)))
(elapsed (float-time (time-subtract (current-time) start-time))))
(message "End saving web archive url: %s, %S sec" url elapsed)
(save-match-data
(if (string-match "location: \\(.*\\)" response)
(match-string 1 response)
(message "Failed grep location values: %s" url)
(message "Failed response: %S" response)))))
(retrieve-web-archive-url
"http://ohyecloudy.com/pnotes/archives/retrospective-2023/")
Starts saving web archive url: http://ohyecloudy.com/pnotes/archives/retrospective-2023/
End saving web archive url: http://ohyecloudy.com/pnotes/archives/retrospective-2023/, 6.955126 sec
http://web.archive.org/web/20240325031430/http://ohyecloudy.com/pnotes/archives/retrospective-2023/
7초 정도가 걸린다. 동기(Synchronous) 호출이라 7초 동안 Emacs가 먹통된다. 비동기(Asynchronous)로 바꿔야 한다.
Asynchronous Process로 Web Archive 링크 가져오기
start-process 함수로 비동기 프로세스를 만들어서 실행한다. 프로세스 출력 결과에서 location
필드 값을 얻어와야 한다. set-process-filter 함수로 출력값을 파싱하게 했다.
(defun async-retrieve-web-archive-url (url)
(let* ((request-url (format "http://web.archive.org/save/%s" url))
(proc (start-process "web-archive"
nil
"curl"
"-s"
"--head"
request-url)))
(set-process-filter proc (lambda (proc output)
(let ((web-archive-url (web-archive-extract-redirect-url output)))
(message "url: %S\nresult: %S" url web-archive-url))))
proc))
(defun web-archive-extract-redirect-url (str)
;; replace ANSI escape codes and ^M with an empty string
(let ((str (replace-regexp-in-string "\033\\[[0-9;]*m\\|\033\\]8;.*?\033\\\\\\|\r+$" "" str)))
(save-match-data
(if (string-match "location: \\(.*\\)" str)
(match-string 1 str)))))
(async-retrieve-web-archive-url
"http://ohyecloudy.com/pnotes/archives/retrospective-2023/")
url: "http://ohyecloudy.com/pnotes/archives/retrospective-2023/"
result: "http://web.archive.org/web/20240325031430/http://ohyecloudy.com/pnotes/archives/retrospective-2023/"
Emacs가 블러킹되지 않고 잘 된다. 한 개씩 Web Archive 주소를 얻을 때도 있지만 ’Emacs org 문서에서 링크를 긁어서 링크 섹션을 만들기’ 블로그 포스트에 추가한 함수처럼 글에 있는 모든 링크를 긁어서 링크 센셕을 만들 때도 사용할 예정이다. URL 여러 개로 각각 Web Archive 주소를 만드는 기능도 필요하다.
여러 Async 프로세스를 기다리기
Web Archive 링크를 여러 개 가져올 때, 완료됐는지 여부를 알아야 한다. 아직 응답이 안 온 요청이 몇 개인지 사람이 기억하는 건 가혹하다.
비동기로 실행할 커맨드 개수를 구하고 비동기 프로세스가 종료할 때, 카운팅을 한다. 프로세스 종료 이벤트 감지는 set-process-sentinel 함수를 사용한다. Closure를 사용해야 하므로 반드시 Lexical binding을 사용해야 한다. 근본 없는 Dynamic binding이 디폴트 scoping rule이다.
# -*- mode: emacs-lisp; lexical-binding: t -*-
파일 변수를 사용하는 게 가장 일반적이다. 파일 가장 위에 정의해주면 된다.
(defun async-many (commands)
(message "start main thread: %S" main-thread)
(let ((total (length commands))
(count 0))
(defun process-sentinel (proc event)
(when (string-equal event "finished\n")
(setq count (1+ count))
(if (>= count total)
(message "%S All processes done!" (current-thread))
(message "%S Progress %S/%S" (current-thread) count total))))
(dolist (c commands)
(let ((proc (apply #'start-process (append (list "async-process" nil) c))))
(set-process-sentinel proc 'process-sentinel)))))
(async-many '(("sleep" "1")
("sleep" "5")
("sleep" "10")))
count
값을 증가할 때, 경쟁 상태(Race Condition)는 없다. 모두 main thread에서 실행된다.
start main thread: #<thread 00007ff747f01880>
#<thread 00007ff747f01880> Progress 1/3
#<thread 00007ff747f01880> Progress 2/3
#<thread 00007ff747f01880> All processes done!
my/web-archive
패키지 작성
필요한 기능 프로토타이핑이 끝났다. 이제 본격적으로 패키지를 만들어본다. 공유를 염두에 두지 않은 작은 패키지를 만들 거라 이름을 my/web-archive
라고 지었다.
두 개의 버퍼를 사용한다. 로그 버퍼와 결과 버퍼이다. 비동기로 Web Archive URL을 요청한다. Org-cliplink처럼 바로 버퍼에 링크를 추가하지 않는다. 최소 5초가 걸리는 작업이라 의도하지 않은 곳에 링크가 삽입될 수 있기 때문이다. 대신 결과 버퍼를 만들어서 응답을 모두 받으면 보여줘서 복사&붙이기가 가능하게 할 예정이다.
로그 버퍼와 결과 버퍼
로그 버퍼와 결과 버퍼 이름을 정의하고 버퍼에 출력하는 함수를 만든다.
로그 버퍼와 결과 버퍼 관련 설정
다른 이름을 설정할 일이 있나 싶다. 상수로 버퍼 이름을 정의한다.
(defconst my/web-archive-result-buffer-name "*my/web-archive-result*")
(defconst my/web-archive-log-buffer-name "*my/web-archive-log*")
Doom Emacs에 있는 Popup 패키지를 사용해서 포커스를 가지지 않고 하단에 pop up 형식으로 원도가 보이게 설정한다.
(when (functionp #'set-popup-rule!)
(set-popup-rule! my/web-archive-result-buffer-name :size 0.25 :ttl nil :vslot -1))
Web Archive 작업이 끝나면 my/web-archive-result-buffer-name
버퍼를 화면에 보여줄 텐데 Popup 패키지를 사용해서 방해하지 않게 한다.
메시지 출력 함수
로그 버퍼와 결과 버퍼 메시지에 사용할 쓰기 함수를 작성한다. 결과 버퍼는 org-mode syntax로 출력해서 조금 다르다. 그것만 빼고는 같으므로 write-line 함수는 같이 사용한다. 읽기 전용 버퍼로 만들고 제일 마지막 줄에 인자로 받은 message를 삽입한다.
(defun my/web-archive--write-line (buffer-name message)
(with-current-buffer (get-buffer-create buffer-name)
;; 명시적으로 호출하지 않으면 evil-mode가 활성화되지 않는다.
;; 정확한 원인은 모르며 workaround로 fundamental-mode 함수를 호출.
(fundamental-mode)
(setq buffer-read-only t)
(goto-char (point-max))
(let ((inhibit-read-only t))
(save-excursion
(insert (format "%s\n" message))))))
읽기 전용 버퍼로 만들어서 함수에서 호출하는 insert
함수도 동작 안 한다. inhibit-read-only
변수에 t
를 할당해서 읽기 전용 버퍼지만 인자로 받은 message를 추가할 수 있게 한다.
fundamental-mode
함수를 호출하는 건 우회책(workaround)이다. 원인은 모르겠다.
로그 함수와 결과 출력 함수
로거는 메시지 출력 함수에 버퍼 이름과 메시지를 인자로 넘기면 된다.
(defun my/web-archive--logger (message)
(my/web-archive--write-line my/web-archive-log-buffer-name
message))
결과 출력 함수는 출력 형식을 Org-mode 마크업을 사용한다. 대부분의 글을 org-mode로 작성한다. web archive 링크 삽입도 대부분 org-mode에서 하기 때문이다.
(defun my/web-archive--process-result (url archive-url)
(my/web-archive--write-line my/web-archive-result-buffer-name
(format "%s [[%s][archive]]\n" url archive-url)))
[[링크][TEXT]]
형식으로 출력한다. 이 결과 버퍼에 있는 내용을 org-mode 버퍼에 복사 붙이기를 하면 되게 했다.
결과 버퍼를 보여주기
web archive 요청을 보내고 응답을 모두 받으면 결과 버퍼를 보여주기 위해 만든다. 위에서 포커스를 가지지 않는 Popup으로 설정해서 편집을 방해하지 않는다.
(defun my/web-archive--display-result-buffer ()
(with-current-buffer (get-buffer-create my/web-archive-result-buffer-name)
(display-buffer (current-buffer))))
Web Archive 링크 요청
로그와 결과를 쓸 수 있는 함수를 만들었다. 이제 앞에서 프로토타이핑해 본 결과를 바탕으로 URL 여러 개를 archive 요청하고 모두 받아서 결과 버퍼에 출력하는 메인 함수를 만들 차례다.
비동기 Web Archive 요청 함수
Curl 프로그램으로 Web Archive를 요청할 수 있게 인자를 만드는 함수다. curl 프로그램이 실행 경로에 있는지 확인해서 빠르게 에러를 내게 한다.
(defun my/web-archive--build-curl-commands (url)
(let* ((curl-executable (executable-find "curl"))
(request-url (format "http://web.archive.org/save/%s" url)))
(unless curl-executable
(error "cannot find 'curl' executable path"))
`(,curl-executable "--silent" "--head" ,request-url)))
’http://ohyecloudy.com/pnotes/archives/retrospective-2023/’ 를 인자로 넘겨서 curl commands를 만들어보면
(my/web-archive--build-curl-commands "http://ohyecloudy.com/pnotes/archives/retrospective-2023/")
;;=> ("c:/git-sdk-64/mingw64/bin/curl.exe"
;; "--silent"
;; "--head"
;; "http://web.archive.org/save/http://ohyecloudy.com/pnotes/archives/retrospective-2023/")
비동기 프로세스 시작 함수에 넘길 인자가 만들어진다.
비동기로 Web Archive에 링크 요청을 하고 결과 처리 함수를 출력할 메인 함수를 작성할 차례다. start-process 함수로 curl 프로그램을 실행한다. 실행 결과를 set-process-filter 함수로 파싱해서 location
필드를 추출한다.
(defun my/web-archive--start-async-request (url logger result-func)
(let* ((start-time (current-time))
(proc (apply #'start-process
(append (list "web-archive" nil)
(my/web-archive--build-curl-commands url)))))
(funcall logger (format "request web archive %S" url))
(set-process-filter proc (lambda (proc output)
(let ((elapsed (float-time (time-subtract (current-time) start-time)))
(web-archive-url (my/web-archive--extract-redirect-url output)))
(funcall result-func url web-archive-url)
(funcall logger
(format "url: %S\nresult: %S\nelapsed: %Ssec"
url
web-archive-url elapsed)))))
proc))
(defun my/web-archive--extract-redirect-url (str)
;; replace ANSI escape codes and ^M with an empty string
(let ((str (replace-regexp-in-string "\033\\[[0-9;]*m\\|\033\\]8;.*?\033\\\\\\|\r+$" "" str)))
(save-match-data
(if (string-match "location: \\(.*\\)" str)
(match-string 1 str)))))
위 my/web-archive--extract-redirect-url
함수가 curl 프로그램 실행 결과에서 location
필드를 추출하는 함수다.
만든 my/web-archive--start-async-request
함수를 호출해 보자.
(my/web-archive--start-async-request "http://ohyecloudy.com/pnotes/archives/retrospective-2023/"
#'my/web-archive--logger
#'my/web-archive--process-result)
*my/web-archive-log*
버퍼에는 다음과 같은 로그가 보인다
request web archive "http://ohyecloudy.com/pnotes/archives/retrospective-2023/"
url: "http://ohyecloudy.com/pnotes/archives/retrospective-2023/"
result: "http://web.archive.org/web/20240326033503/http://ohyecloudy.com/pnotes/archives/retrospective-2023/"
elapsed: 42.806658sec
*my/web-archive-result*
버퍼에는 다음과 같은 결과가 보인다
http://ohyecloudy.com/pnotes/archives/retrospective-2023/ [[http://web.archive.org/web/20240326033503/http://ohyecloudy.com/pnotes/archives/retrospective-2023/][archive]]
비동기 요청을 여러 개 보내고 모두 완료하면 결과 버퍼를 보여주는 함수
입력으로 여러 url을 받아서 위에서 만든 my/web-archive--start-async-request
함수를 호출하고 모두 완료하면 결과 버퍼를 보여주는 함수를 짠다. 프로토타이핑에서 모두 만들어 본 거다.
(defun my/web-archive-async (urls)
(unless (and (listp urls) urls)
(error "urls should be non empty list"))
(let ((total (length urls))
(count 0))
(my/web-archive--logger (format "web archive start... %S urls" total))
(defun process-sentinel (proc event)
(when (string-equal event "finished\n")
(setq count (1+ count))
(if (>= count total)
(progn
(my/web-archive--logger "all web archives done!")
(my/web-archive--display-result-buffer))
(my/web-archive--logger (format "progress %S/%S" count total)))))
(dolist (u urls)
(let ((proc (my/web-archive--start-async-request
u
#'my/web-archive--logger
#'my/web-archive--process-result)))
(set-process-sentinel proc #'process-sentinel)))))
요청한 url이 모두 완료했을 때, my/web-archive--display-result-buffer
함수를 호출해 결과 창을 보여준다.
만든 함수에 url을 여러 개 넘겨서 테스트해 보자.
(my/web-archive-async
'("http://ohyecloudy.com/pnotes/archives/retrospective-2023/"
"http://ohyecloudy.com/pnotes/archives/retrospective-2022/"
"http://ohyecloudy.com/pnotes/archives/retrospective-2021/"))
*my/web-archive-log*
버퍼에 출력되는 결과는 다음과 같다.
web archive start... 3 urls
request web archive "http://ohyecloudy.com/pnotes/archives/retrospective-2023/"
request web archive "http://ohyecloudy.com/pnotes/archives/retrospective-2022/"
request web archive "http://ohyecloudy.com/pnotes/archives/retrospective-2021/"
url: "http://ohyecloudy.com/pnotes/archives/retrospective-2023/"
result: "http://web.archive.org/web/20240326033503/http://ohyecloudy.com/pnotes/archives/retrospective-2023/"
elapsed: 7.196123sec
progress 1/3
url: "http://ohyecloudy.com/pnotes/archives/retrospective-2021/"
result: "http://web.archive.org/web/20240326035204/http://ohyecloudy.com/pnotes/archives/retrospective-2021/"
elapsed: 43.017302sec
progress 2/3
url: "http://ohyecloudy.com/pnotes/archives/retrospective-2022/"
result: "http://web.archive.org/web/20240326035338/http://ohyecloudy.com/pnotes/archives/retrospective-2022/"
elapsed: 139.674683sec
all web archives done!
작업이 완료되면 *my/web-archive-result*
버퍼가 하단에 뜬다.
http://ohyecloudy.com/pnotes/archives/retrospective-2023/ [[http://web.archive.org/web/20240326033503/http://ohyecloudy.com/pnotes/archives/retrospective-2023/][archive]]
http://ohyecloudy.com/pnotes/archives/retrospective-2021/ [[http://web.archive.org/web/20240326035204/http://ohyecloudy.com/pnotes/archives/retrospective-2021/][archive]]
http://ohyecloudy.com/pnotes/archives/retrospective-2022/ [[http://web.archive.org/web/20240326035338/http://ohyecloudy.com/pnotes/archives/retrospective-2022/][archive]]
페이지 타이틀과 URL 그리고 Web Archive 주소
앞에서 짠 Web Archive 링크를 가져오는 함수를 기존에 사용하고 있는 패키지와 같이 쓰려고 한다. URL을 클립보드에 복사하고 호출하면 웹 페이지 타이틀과 같이 org-mode 마크업으로 삽입하는 org-cliplink 패키지. 그리고 문서에서 링크를 긁어 링크 섹션을 org-mode 마크업으로 만들어주는 build-link-section 패키지. 이렇게 두 개의 패키지와 연동해 본다.
org-cliplink로 페이지 타이틀을 가져온다
Web Archive 주소만 가져오려고 함수를 호출하는 일은 드물 것이다. 페이지 타이틀과 같이 삽입할 수 있게 한다.
이 기능 때문에 my-web-archive
패키지가 org-cliplink
패키지를 의존하게 할 필요는 없다. my/web-archive-async
함수를 수정해서 결과를 처리하는 함수를 인자로 받을 수 있게 변경한다. 결과를 처리하는 함수에서 org-cliplink
패키지를 사용하게 해서 필요 없는 의존성을 만들지는 말자.
result-func
인자를 선택적으로 받을 수 있게 한다. 만약 인자로 넘기지 않았다면 my/web-archive--process-result
함수를 디폴트로 사용한다.
(defun my/web-archive-async (urls &optional result-func)
;; ...
(let ( ;; ....
(result-func (or result-func #'my/web-archive--process-result)))
;; ...
(dolist (u urls)
(let ((proc (my/web-archive--start-async-request
u
#'my/web-archive--logger
result-func)))
(set-process-sentinel proc #'process-sentinel)))))
아카이브 URL을 가져온 뒤 org-cliplink 패키지 함수를 호출해 웹페이지 타이틀을 가져오는 결과 처리 함수를 정의한다.
(defun my/web-archive-with-webpage-title (url archive-url)
(org-cliplink-retrieve-title
url
(lambda (url title)
(let* ((origin-org-link (my/org-cliplink-link-transformer url title))
(archive-link (format "([[%s][archive]])" archive-url)))
(my/web-archive--write-line my/web-archive-result-buffer-name
(format "%s%s" origin-org-link archive-link))))))
만든 결과 처리 함수를 인자로 넘겨서 의도대로 동작하는지 확인해 보자.
(my/web-archive-async
'("http://ohyecloudy.com/pnotes/archives/retrospective-2023/"
"http://ohyecloudy.com/pnotes/archives/retrospective-2022/"
"http://ohyecloudy.com/pnotes/archives/retrospective-2021/")
#'my/web-archive-with-webpage-title)
원문 링크(archive)
형식으로 잘 출력된다.
[[http://ohyecloudy.com/pnotes/archives/retrospective-2021/][#retrospective 2021년 회고 - ohyecloudy’s pnotes - ohyecloudy.com]]([[http://web.archive.org/web/20240327030914/http://ohyecloudy.com/pnotes/archives/retrospective-2021/][archive]])
[[http://ohyecloudy.com/pnotes/archives/retrospective-2022/][#retrospective 2022년 회고 - ohyecloudy’s pnotes - ohyecloudy.com]]([[http://web.archive.org/web/20240327031042/http://ohyecloudy.com/pnotes/archives/retrospective-2022/][archive]])
[[http://ohyecloudy.com/pnotes/archives/retrospective-2023/][#retrospective 2023년 회고 - ohyecloudy’s pnotes - ohyecloudy.com]]([[http://web.archive.org/web/20240327030939/http://ohyecloudy.com/pnotes/archives/retrospective-2023/][archive]])
org-cliplink
함수처럼 클립보드에 있는 컨텐츠를 가져와 my/web-archive-async
함수를 호출하는 대화형(interactive) 함수를 정의하자. 이 함수에 키 바인딩을 해서 사용할 예정이다.
(defun my/org-cliplink-with-archive-url ()
(interactive)
(my/web-archive-async (list (org-cliplink-clipboard-content))
#'my/web-archive-with-webpage-title))
http://ohyecloudy.com/pnotes/archives/retrospection-2019/
주소를 복사해서 클립보드에 넣고 M-x my/org-cliplink-with-archive-url
명령한다.
[[http://ohyecloudy.com/pnotes/archives/retrospection-2019/][#retrospective 2019년 회고 - ohyecloudy’s pnotes - ohyecloudy.com]]([[http://web.archive.org/web/20240327033242/http://ohyecloudy.com/pnotes/archives/retrospection-2019/][archive]])
잘 동작한다. SPC m l C
키에 방금 만든 함수를 바인딩한다. SPC m l c
는 타이틀만 가져와서 링크를 만든다. c
를 대문자로 바꾸면 아카이브 URL도 같이 가져오게 했다.
(after! org
(map! :map org-mode-map
:localleader
(:prefix ("l" . "links")
"C" #'my/org-cliplink-with-archive-url)))
org-mode 문서에서 링크를 긁어서 링크 섹션을 만들 때, 아카이브 URL도 추가
Emacs org 문서에서 링크를 긁어서 링크 섹션을 만드는 패키지를 사용하고 있다. 링크를 수집해 my/web-archive-async
함수를 호출하면 된다. 이것도 간단히 적용할 수 있다.
기존에 함수를 리팩터링했다. org-mode 버퍼에서 링크를 수집한 다음 삽입하는 방법을 함수 인자로 추출했다.
(defun my/build-link-section (&optional link-inserter-func)
(interactive)
(let ((links (sort
(delete-dups (my/build-link-section--extract-urls (org-element-parse-buffer)))
'string<))
(link-inserter-func (or link-inserter-func #'my/build-link-section--default-inserter)))
(funcall link-inserter-func links)))
대화형(interactive) 함수로 정의해서 M-x my/build-link-section-with-archive-urls-async
로 호출할 수 있게 한다.
(defun my/build-link-section-with-archive-urls-async ()
(interactive)
(my/build-link-section (lambda (links)
(my/web-archive-async links
#'my/web-archive-with-webpage-title))))
마치며
블로그 포스트에서 참조한 링크가 사라질 때를 대비해 Web Archive 서비스 URL을 가져오는 my/web-archive
패키지를 작성했다. org 문서에 결과를 잘 삽입하고 싶은데, 아카이브 URL을 가져오는 데 시간이 오래 걸려서 결과 버퍼로 출력하는 것으로 타협했다. 아카이브 URL을 가져오는 기능을 Org-cliplink와 build-link-section 패키지와 결합하니 꽤 쓸만하다.
작고 귀여운 금액이지만 Web Archive 서비스에 기부도 했다.
전체 소스 코드는 c5b5b6a2 커밋에서 볼 수 있다.
’인터넷 아카이브의 절체절명의 순간에 살아남기 위한 최후의 노력 - GeekNews - news.hada.io’ 기사를 최근에 봤다. 웨이백 머신은 피해를 안 받았으면 좋겠다.
링크
- ohyecloudy’s pnotes - ohyecloudy.com(archive)
- Doom Emacs 전환 후기 - (emacsian ohyecloudy) - ohyecloudy.com(archive)
- Org-mode 문서에서 링크를 긁어서 링크 섹션을 만들기 - (emacsian ohyecloudy) - ohyecloudy.com(archive)
- exp cabinet - ohyecloudy.com(archive)
- org-cliplink 패키지로 title과 url을 편하게 삽입 - (emacsian ohyecloudy) - ohyecloudy.com(archive)
- #retrospective 2023년 회고 - ohyecloudy’s pnotes - ohyecloudy.com(archive)
- https://antonz.org/curl-by-example/ - antonz.org(archive)
- Race condition - en.wikipedia.org(archive)
- ohyecloudy/dotfiles/commit/c5b5b6a255f081778cf4309dd027602036ca1f3c - github.com
- tkf/emacs-request - github.com
- 인터넷 아카이브의 절체절명의 순간에 살아남기 위한 최후의 노력 - GeekNews - news.hada.io(archive)
- Asynchronous Processes (GNU Emacs Lisp Reference Manual) - gnu.org(archive)
- Dynamic Binding (GNU Emacs Lisp Reference Manual) - gnu.org(archive)
- Filter Functions (GNU Emacs Lisp Reference Manual) - gnu.org(archive)
- Optional Arguments (Programming in Emacs Lisp) - gnu.org(archive)
- Synchronous Processes (GNU Emacs Lisp Reference Manual) - gnu.org(archive)
- Sentinels (GNU Emacs Lisp Reference Manual) - gnu.org(archive)
- Lexical Binding (GNU Emacs Lisp Reference Manual) - gnu.org(archive)
- Retrieving URLs (URL Programmer’s Manual) - gnu.org(archive)
- Specifying File Variables (GNU Emacs Manual) - gnu.org(archive)
C-x C-s C-x C-c