echo Impossible|sed 's/Im/To be /'

December 29, 2017

Lisp Web - Parenscript Emacs Trident-mode slime skewer - HowTo

想用 Lisp 寫 web ,如果是全端工程師,前後台都包的話,Caveman2 + Parenscript 這樣的工作流程就很方便。但要與他人合作的話,而且要切進個別案子或是即有的工作流程那就有點點尷尬了。Team 的後台主力開發工具是 node.js ,前台是 angular 或 react ,所以協同開發輸出 JS 變成是必要的。

將 Parenscript 轉成 JS 的工具不少,自已也寫了一個簡單的版本,實作上不難。 github 上就有好幾個像是 pikey sigil lisp2js ,就連 parenscript-react-examples 裏面都有個 ps-compile.lisp 在處理 parenscript to JS,而在 npm 上也有 sigil_sigil-cli ,估計大家都有這樣的需求。

Trident-mode

Emacs 上有個 Trident-mode 搭配 slime 可以作到同樣的效果。更特別的是它可以配合 skewer-mode + simple-httpd + browser (firefox/chrome),直接把轉譯好的 JS 送到 browser 端執行,達到 REPL 的效果。

大概的關係圖如下:

                      /+-- slime                    <--> SBCL Lisp (in-package :parenscript)
Emacs (Trident-mode) /
                     \
                      \+-- (skewer + simple-httpd)  <--> Browser (firefox/chrome open http://localhost:8080/skewer.html)

[Trident] Paren -> Paren [slime|SBCL|Parenscript] JS -> [Trident] JS -> [skewer|simple-httpd|browser] eval(JS)

Trident-mode Install & Setup

請參照 https://github.com/johnmastro/trident-mode.el/blob/master/README.org 幾乎可以無痛完成。

測試

  • 用 emacs 打開檔案 test.paren (進入 trident-mode)
  • 在 emacs 編輯區下輸入 (lisp *ps-lisp-library*)
  • 在 emacs 下鍵入 Mx trident-expand-sexp RET
$> emacs test.paren

(lisp *ps-lisp-library*)

M-x trident-expand-sexp
emacs-trident-mode

如果執行有錯,請確認 slime 上目前是處於 PS>

emacs-slime-ps

emacs after-save-hook trident-compile-buffer-to-file

另一個方便的設定則是透過 after-save-hook 在每次存檔時喚起 trident-compile-buffer-to-file 將 paren 轉成 JS。在每個 paren 的檔尾加上設定如下:

;; Local Variables:
;; eval: (add-hook 'after-save-hook (lambda () (trident-compile-buffer-to-file)) nil t)
;; End:

Skewer live web development with Emacs

skewer-mode 是個方便開發 Web front end 工具,可以把 js, css, html 注入 browser 中,由 browser 來解譯與呈現。

skewer-mode Install & Setup

  • 透過 MELPA 安裝 simple-httpd js2-mode 及 skewer-mode

  • 設定好 simple-httpd httpd-root

    (require 'simple-httpd)
    ;; set root folder for httpd server
    (setq httpd-root "~/public_html")
    
    ;; chrome open http://127.0.0.1:8080/skewer.html
    (httpd-start)
    
  • 設定 skewer mode

    (add-hook 'js2-mode-hook 'skewer-mode)
    (add-hook 'css-mode-hook 'skewer-css-mode)
    (add-hook 'html-mode-hook 'skewer-html-mode)
    
  • 建立文件 ~/public-html/skewer.html 提供給 browser 載入 skewer

    <!doctype html>
    <html>
    <head>
        <!-- Include skewer.js as a script -->
        <script src="http://localhost:8080/skewer"></script>
        <!-- Testing.js for testing or mark it and skip. -->
        <script src="testing.js"></script>
    </head>
    <body>
        <p>Hello world skewer</p>
    </body>
    </html>
    
  • browser 開啟 http://localhost:8080/skewer.html

  • emacs M-x skewer-repl 進入交談介面,測試看看。

小結

emacs + trident-mode + skewer-mode 提供一個與 browser 互動 REPL 的交談介面。單用 trident-mode 幫忙將 Parenscript 轉成 JS 就非常實用了。

與 Team 的合作模式,則是自已寫用 Parenscript ,提交到 git 跟 Team 合作仍然使用 JS 溝通。

Posted by Lloyd Huang in on December 29, 2017

December 28, 2017

List ip address of G suite

早先用 emacs lisp 寫了一段 code 列出 G suite 所有的 ip address ,但因斷電後 code 找不到了。改用 CL 重寫後留個記錄方便查詢。另一個心得就是 CL :inferior-shell 很好用。

G suite document

查詢 G suite 所有的 ip address 的文件 https://support.google.com/a/answer/60764?hl=zh-Hant

Launch lisp from shell command

/usr/bin/sbcl --noinform --load gmail-suite.lisp --quit

Output

ip route add 64.233.160.0/19 via 192.168.1.1 dev enp2s0
ip route add 66.102.0.0/20 via 192.168.1.1 dev enp2s0
.. skip ..

Load script from lisp repl

(load "~/.org/gmail-suite.lisp")

Main program - lisp script

(ql:quickload :inferior-shell :silent t)
(ql:quickload :cl-ppcre :silent t)

(defpackage "GMAIL-SUITE"
(:use "COMMON-LISP" "INFERIOR-SHELL" "CL-PPCRE"))
(in-package "GMAIL-SUITE")

(let ((rev nil) (gw "192.168.1.1"))
(labels ((grep (match-str lst &key (start 0))
     (if (> start 0)
         (mapcar #'(lambda (x) (subseq x start))
                 (remove-if-not #'(lambda (x) (search match-str x)) lst))
         (remove-if-not #'(lambda (x) (search match-str x)) lst)))

       (spf-nslookup-raw-data-grep-spf1 (domain)
     (split " "
            (first
             (grep "spf1"
                   (run/lines (format nil "nslookup -q=txt ~A 8.8.8.8" domain)))))))

    (setq rev
      (mapcar #'(lambda (domain)
              (grep "ip4:" (spf-nslookup-raw-data-grep-spf1 domain) :start (length "ip4:")))
          (grep "include:" (spf-nslookup-raw-data-grep-spf1 "_spf.google.com") :start (length "include:"))))

    (map nil
     #'(lambda (x)
     (format t "ip route add ~A via ~A dev enp2s0~%" x gw))
     (reduce #'append rev))))

Other

Rewirte grep function with defmacro

grep 有段 code 是重複的,試著用 macro 寫寫看。心得是 macro 不好閱讀是真的,但沒有想像中的難上手,不過 macro 的特性讓人印象深刻,很特別可以用 code 生成 code。

(defmacro grep (match-str lst &key (start 0))
  (let (( grep-filter `(remove-if-not #'(lambda (x) (search ,match-str x)) ,lst )))
  (if (> start 0)
    `(mapcar #'(lambda (x) (subseq x ,start)) ,grep-filter)
    `,grep-filter)))
GS> (macroexpand-1 '(grep  "5:" '("1:2" "2:2" "3:2" "4:4" "5:5")))
(REMOVE-IF-NOT #'(LAMBDA (X) (SEARCH "5:" X)) '("1:2" "2:2" "3:2" "4:4" "5:5"))

GS> (macroexpand-1 '(grep  "3:" '("1:2" "2:2" "3:2" "4:4" "5:5") :start 2))
(MAPCAR #'(LAMBDA (X) (SUBSEQ X 2))
        (REMOVE-IF-NOT #'(LAMBDA (X) (SEARCH "3:" X))
                 '("1:2" "2:2" "3:2" "4:4" "5:5")))

December 25, 2017

Lisp Web - Caveman2 Clack Hunchentoot Woo - Framework

使用 Framework 開發,可以降低工作、減少出錯、方便移植等優點。Caveman2 架構在 Clack 上,可以抽換底層使用的 Web Server,無論是 Hunchentoot FastCGI Wookie Toot 或是 Woo。並且它也協助作好串接資料庫的介面。

以下文件大量參考 https://github.com/fukamachi/caveman/blob/master/README.markdown

Install and hello world

由於使用 quicklisp 作為套件管理,在建立專案時請把路徑放在 quicklisp/local-projects/ 底下,方便之後用 quickload 載入。

(ql:quickload :caveman2)
(ql:quickload :hunchentoot)

(caveman2:make-project #P"~/quicklisp/local-projects/caveman2-test/"
                 :author "<Your full name>")

;; writing ~/quicklisp/local-projects/caveman2-test/caveman2-test.asd
;; writing ~/quicklisp/local-projects/caveman2-test/caveman2-test-test.asd
;; skip .....

(ql:quickload :caveman2-test)
;; To load "caveman2-test":

(caveman2-test:start)
;; Hunchentoot server is started. <--- Hunchentoot
;; Listening on localhost:5000.   <--- port 5000
(caveman2-test:stop)

(ql:quickload :woo)
(caveman2-test:start :server :woo :port 8080)
;; Woo server is started.      <--- Woo
;; Listening on localhost:8080 <--- Port 8080
(caveman2-test:stop)

(caveman2-test:start :server :woo :address "192.168.1.1")
;; woo 需要指定對外的 ip address
;; hunchentoot 預設對外 ip 及 localhost 同時監聽
(caveman2-test:stop)

(caveman2-test:start)
;; 切回預設值 hunchentoot port 5000

打開 browser 訪問 http://localhost:5000 您將會看到 Caveman2 Welcome Page,這即表示建好 project 了。

Routing

routeing 的表示方式有二種 -- @route 與 defroute。以下範例請在 lisp repl 交談模式下鍵入。

(ql:quickload :caveman2-test)
(caveman2-test:start)
(in-package :caveman2-test.web)

;; A route with no name.
@route GET "/welcome0"
(lambda (&key (|name| "Guest"))
  (format nil "Welcome0, ~A" |name|))

;; A route with welcome1 function name.
@route GET "/welcome1"
(defun welcome1 (&key (|name| "Guest"))
  (format nil "Welcome1, ~A" |name|))

;; A route with no name.
(defroute "/welcome2" (&key (|name| "Guest"))
  (format nil "Welcome2, ~A" |name|))

;; 多個參數傳遞
(defroute "/welcome3" (&key (|n| "Guest") (|l| "who"))
  (format nil "Welcome3, ~A ~A" |n| |l|))

以 curl 的方式測試如下:

$> curl "http://localhost:5000/welcome0"
Welcome0, Guest

$> curl "http://localhost:5000/welcome1?"
Welcome1, Guest

$> curl "http://localhost:5000/welcome2?name=Joe"
Welcome2, Joe

$> curl "http://localhost:5000/welcome3?n=Joe&l=Jo"
Welcome3, Joe Jo

Route pattern *keyword*

URL path 可以用 *keyword* 的方式傳入程式中。

(defroute "/hello/:name/:last-name/" (&key name last-name)
  (if (string= name "nobody")
   (next-route)
   (if (string= name "yoda")
    (format nil "You are master ~A ~A" name last-name)
    (format nil "Hello, ~A ~A" name last-name))))

(defroute "/hello/*/*" ()
  "You missed!")

(defroute "/hello/*" ()
  "You missed!")

以 curl 的方式測試如下:

$> curl "http://localhost:5000/hello/Joe/Jo/"
Hello, Joe Jo

$> curl "http://localhost:5000/hello/Joe/"
You missed!

$> curl "http://localhost:5000/hello/"
You missed!

$> curl "http://localhost:5000/hello/yoda/lin/"
You are master yoda lin

$> curl "http://localhost:5000/hello/nobody/who/"
You missed!

Route pattern *wildcard* (星字元/萬用字元)

URL path 也可以用 * 萬用字元來表示,而把參數傳入程式中。

(defroute "/say/*/to/*" (&key splat)
  (format nil "~S" splat))

(defroute "/download/*.*" (&key splat)
  (format nil "~S" splat))

以 curl 的方式測試如下:

$> curl "http://localhost:5000/say/hello/to/world"
("hello" "world")

$> curl "http://localhost:5000/download/path/to/file.xml"
("path/to/file" "xml")
$> curl "http://localhost:5000/download/path/to/file.xml.txt"
("path/to/file" "xml.txt")

Route pattern with regular expression (正規表示式)

URL path 也可以用 regular expression (正規表示式),把需要的參數傳入程式中。

(defroute ("/regexp_hello/([\\w]+)" :regexp t) (&key captures)
  (format nil "Hello, ~S" captures))

(defroute ("/regexp-hello/([\\w]+)" :regexp t) (&key captures)
  (format nil "Hello, ~S" captures))
$> curl "http://localhost:50000/regexp_hello/yoda is master/YO!"
Hello, ("yoda")

# I got the problem!! why ("y") not ("yoda"). Need time for check it.
$> curl "http://localhost:50000/regexp-hello/yoda is master/YO!"
Hello, ("y")

Structured query/post parameters

以下是 form-data 分別透過 get 與 post 的方式傳遞表單,最後以 plist 的資料方式呈現。

(ql:quickload :cl-who)

(defroute "/edit-form" ()
  (cl-who:with-html-output-to-string (s nil :prologue t :indent t)
    (:form :action "/edit"
     (:input :type "text" :name "person[name]")
     (:input :type "text" :name "person[email]")
     (:input :type "text" :name "person[birth][year]")
     (:input :type "text" :name "person[birth][month]")
     (:input :type "text" :name "person[birth][day]")
     (:input :type "submit" :value "Add"))))

(defroute "/edit" (&key _parsed)
  (format nil "~S" (cdr (assoc "person" _parsed :test #'string=))))

(defroute "/edit-form-post" ()
  (cl-who:with-html-output-to-string (s nil :prologue t :indent t)
    (:form :action "/edit-post" :method "post"              ;; add method = post
     (:input :type "text" :name "person[name]")
     (:input :type "text" :name "person[email]")
     (:input :type "text" :name "person[birth][year]")
     (:input :type "text" :name "person[birth][month]")
     (:input :type "text" :name "person[birth][day]")
     (:input :type "submit" :value "Add"))))

(defroute ("/edit-post" :method :POST) (&key _parsed) ;; add method = post
  (format nil "~S" (cdr (assoc "person" _parsed :test #'string=))))

(defroute "/add-form" ()
  (cl-who:with-html-output-to-string (s nil :prologue t :indent t)
    (:form :action "/add"
     (:input :type "text" :name "items[][name]")
     (:input :type "text" :name "items[][price]")
     (:input :type "text" :name "items[][name]")
     (:input :type "text" :name "items[][price]")
     (:input :type "submit" :value "Add"))))

(defroute "/add-form-post" ()
  (cl-who:with-html-output-to-string (s nil :prologue t :indent t)
    (:form :action "/add" :method "post"        ;; add method = post
     (:input :type "text" :name "items[][name]")
     (:input :type "text" :name "items[][price]")
     (:input :type "text" :name "items[][name]")
     (:input :type "text" :name "items[][price]")
     (:input :type "submit" :value "Add"))))

(defroute ("/add"  :method :ANY) (&key _parsed) ;; add method = any 同時收 get/post
  (format nil "~S" (assoc "items" _parsed :test #'string=)))

輸出範例:

http://localhost:5000/edit-form output
http://localhost:5000/edit-form-post output
(("name" . "Eitaro Fukamachi") ("email" . "e.arrows@gmail.com") ("birth" ("year" . "2018") ("month" . "01") ("day" . "01")))

http://localhost:5000/add-from output
http://localhost:5000/add-from-post output
("items" (("name" . "pencil") ("price" . "5")) (("name" . "stapler") ("price" . "70")))

Templates

考慮用前端工具 vue.js ,brower 畫面由 client side browser render 完成,故這段暫時略過。

JSON API

json api 很常用,修改範例如下:

(defroute "/user.json" (&key |id|)
  ;; (let ((person (find-person-from-db |id|)))
  (let ((person '(:|name| "Eitaro Fukamachi" :|email| "e.arrows@gmail.com")))
    (render-json person)))
$> curl "http://localhost:5000/user.json?id=0001"
{"name":"Eitaro Fukamachi","email":"e.arrows@gmail.com"}

Static file

靜態檔案擺放的位置,可透過修改 app.lisp 更改位置,但不建議。

/images/logo.png => {PROJECT_ROOT}/static/images/logo.png
/css/main.css    => {PROJECT_ROOT}/static/css/main.css
/js/app/index.js => {PROJECT_ROOT}/static/js/app/index.js
/robot.txt       => {PROJECT_ROOT}/static/robot.txt
/favicon.ico     => {PROJECT_ROOT}/static/favicon.ico

寫入檔案 reload project 執行測試

新增套件相依

修改 caveman2-test.asd 把 cl-who 加入套件相依如下:

--- caveman2-test.asd.org  2017-12-18 14:33:18.509177123 +0800
+++ caveman2-test.asd      2017-12-18 17:37:03.675672573 +0800
@@ -8,7 +8,8 @@
            "envy"
            "cl-ppcre"
            "uiop"
-
+              "cl-who"
+
            ;; for @route annotation
            "cl-syntax-annot

修改 src/web.lisp

新增二項

  • import **cl-whowith-html-output-to-string* from *
  • add defroute "/welcome"
--- web.lisp.org        2017-12-19 09:03:17.750820058 +0800
+++ web.lisp            2017-12-19 16:49:56.151354622 +0800
@@ -7,6 +7,8 @@
     :caveman2-test.db
     :datafly
     :sxql)
+  (:import-from :cl-who
+               :with-html-output-to-string)
   (:export :*web*))
 (in-package :caveman2-test.web)

@@ -26,6 +28,12 @@
 (defroute "/" ()
   (render #P"index.html"))

+(defroute "/welcome" ()
+  (with-html-output-to-string (s nil :prologue t :indent t)
+    (:html
+     (:head (:title "Welcome to Caveman2!"))
+     (:body (:h1 "Blah blah blah.")))))
+
 ;;
 ;; Error pages

reload 執行

以下在 lisp repl 介面下執行,停掉、reload、重啟服務。

(caveman2-test:stop)
(ql:quickload :caveman2-test)
(caveman2-test:start)

測試

$> curl "http://localhost:5000/welcome"
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html>
  <head>
    <title>Welcome to Caveman!
    </title>
  </head>
  <body>
    <h1>Blah blah blah.
    </h1>
  </body>
</html>

小結

CL (common lisp) 在 backend 的開發上已相當的成熟了,而且 Woo 的 效能測試 非常讓人驚豔,雖然仍在 beta 狀態中,但未來作為 microservice 相當令人期待。

餘下的部份如 ** Throw an HTTP status code** Using session** Set HTTP headers or HTTP statusDatabase* *之後在找時間補上。

Posted by Lloyd Huang in on December 25, 2017

December 19, 2017

Lisp Web - Parenscript *ps-lisp-library* - HowTo

Parenscript 是 Lisp 的子集,簡單說就是可以用 Lisp 語法撰寫 javascript,但本質上還是在撰寫 JS 。

建議閱讀 Vladimir Sedach 在 Slideshre 上的簡報,可以直接暸解 PS (Parenscript) 它的目的及能力範圍。

建立 JSON

用 create / array 來建立 Object 輸出 Json 範例如下:

;; Json target sample
;; { dataInfo : [{ msg : 'apple' },
;;               { msg : 'banana' },
;;               { msg : 'coconut' }] };
;;
;; { el : '#app',
;;   data : { items : [{ message : 'Apple' },
;;                     { message : 'Banana' },
;;                     { message : 'Coconut' }] },
;;   methods : { clickme : function () { return ++this.count; }}
;;   data1 : dataInfo };
(ps
(create data-info (array (create msg "apple")
                 (create msg "banana")
                 (create msg "coconut")))
(Create el "#app"
    data (create items
             (array (create message "Apple")
                    (create message "Banana")
                    (create message "Coconut")))
    methods (create clickme
                (lambda () (incf (@ this count))))
    data1 data-info))

直覺想用 cl-json 來建立 json 。於是找到 Stack overflow 有篇 is there a way to insert raw javascript in parenscript code? 在討論如何置入 js code,其中 lisp-raw 這自建 function 滿好用的。實際上也可以用 JSON.parse 將 string 轉成 json, **誤區範例** 如下:

;; Json target sample
;; { el : '#app',
;;   data : { items : [{ message : 'Apple' },
;;                     { message : 'Banana' },
;;                     { message : 'Coconut' }] } }

(ql:quickload :parenscript)
(in-package :parenscript)
(ql:quickload :cl-json)

(ps
  (defvar data ((@ *json* parse)
        (lisp (json:encode-json-alist-to-string
               '((:el . "#app")
                 (:data . (((:message . "Apple"))
                           ((:message . "Banana"))
                           ((:message . "Coconut"))))))))))
;; var data = JSON.parse('{"el":"#app",
;;                         "data":[{"message":"Apple"},
;;                                 {"message":"Banana"},
;;                                 {"message":"Coconut"}]}');

(define-expression-operator lisp-raw (lisp-form)
  `(ps-js:escape
    ,lisp-form))
(defun lisp-raw (x) x)

(ps
  (defvar data (lisp-raw (json:encode-json-alist-to-string
                    '((:el . "#app")
                      (:data . (((:message . "Apple"))
                                ((:message . "Banana"))
                                ((:message . "Coconut")))))))))
;; var data = {"el":"#app",
;;             "data":[{"message":"Apple"},
;;                     {"message":"Banana"},
;;                     {"message":"Coconut"}]};
;;

嘗試後有三個缺點,因為是 alist 轉成 string 後直接操作,所以…

  1. 沒辦法直接在 json 裏 link 到另一個 json
  2. 沒辦法直接在 json 裏放 lambda function
  3. 語法相較於 create / array 并沒有比較簡單明暸

變數函數名稱大小寫

由於 common lisp 預設 symbol variable function 都是大寫,所以轉成 JS 需要作 Symbol conversion 範例如下:

bla-foo-bar = blaFooBar;
*array = Array;
*global-array* = GLOBAL-ARRAY;

why not (boo.bar.baz foobar)?

簡報裏有說明為什麼不要直接使用 dot 。請改用 getprop / @ / chain 來串

  • (GETPROP object {slot-specifier}*)
  • (@ {slot-specifier}*)
  • (CHAIN {slot-specifier | function-call}*)
(funcall (getprop document 'write) "hello world")
(funcall (@ document write) "hello world")
(chain document (write "hello world"))
((@ document write) "hello world")

;; 相同的輸出結果
;; document.write('hello world');

Runtime library *ps-lisp-library*

  • (MAPCAR function {array}*)
  • (MAP-INTO function array)
  • (MAP function array)
  • (MEMBER object array)
  • (SET-DIFFERENCE array1 array2)
  • (REDUCE function array object)
  • (NCONC {array}*)

*ps-lisp-library* 有許多有用的 library , 於是寫了些 samples 說明如下:

以下範例請先將 ps 轉成 js ,後用滑鼠複製到 chrome 的 web console 下執行。

;; testing *ps-lisp-library* mapcar map-into map member set-difference reduce nconc
(ps
  ;; 引入 *ps-lisp-library* 當成 runtime library
  (lisp *ps-lisp-library*)

  (mapcar #'(lambda (i j k) (+ i j k)) '(1 2 3) '(2 3 4) '(3 4 5))
  ;; 執行結果 [6, 9, 12]

  (let ((a '(1 2 3)))
    (map-into #'(lambda(i) (+ i 5)) a) a)
  ;; 執行結果 a = [6, 7, 8]

  (map #'(lambda (i) (+ i 5)) '(1 2 3))
  ;; 執行結果 [6, 7, 8]

  (member 5 '(1 2 3 4))
  ;; 執行結果 false
  (member 3 '(1 2 3 4))
  ;; 執行結果 true

  (set-difference '(1 2 5 1 3 4 5) '(2 7 3 4))
  ;; 執行結果 [1, 5, 1, 5]

  (reduce #'(lambda (i j) (* i j)) '(1 2 3 4 5))
  ;; 執行結果 120 (1 * 2 * 3 * 4 * 5 = 120)
  (reduce #'(lambda (i j) (+ i "-" j)) '("hi" "lloyd" "huang" "hello" "world"))
  ;; 執行結果 "hi-lloyd-huang-hello-world"

  (nconc '(1 2 3) '(3 4 5 6 7) '(7 8 9))
  ;; 執行結果 [1, 2, 3, 3, 4, 5, 6, 7, 7, 8, 9]
  )

引入 *ps-lisp-library* 後終於有像在寫 Lisp 了。XD

SLIME integration

lisp 開發搭配建議使用 slime 與 editor 串接,有興趣的人請參考 lisp 入門介紹 How I got started with Common Lisp in 2017

在 slime-mode 下有二個 command 可以方便預覽 JS code。

  • C-c C-j or M-x slime-eval-last-expression-in-repl
  • C-c M-m or M-x slime-macroexpand-all

或是搭配 Emacs - Trident-mode 也非常方便。

Posted by Lloyd Huang in on December 19, 2017

December 12, 2017

Lisp Web - Parenscript Vue.js Hunchentoot - Hello World

想用 Lisp 寫 Web 所以有了這篇,以下內容主要參考下列文件並加上其它資料匯整而來。

本篇適合入門快速體驗、開發測試。實務上線建議搭其它 web framework 如 caveman2 node.js 或 Npm Browserify 等工具使用。

Parenscript Tutorial

(ql:quickload "hunchentoot")
(ql:quickload "cl-who")
(ql:quickload "parenscript")
(ql:quickload "css-lite")

(defpackage "PS-TUTORIAL"
  (:use "COMMON-LISP" "HUNCHENTOOT" "CL-WHO" "PARENSCRIPT"))
(in-package "PS-TUTORIAL")

;; web server 開啟於 127.0.0.1 8080 port 上
(defparameter *server*
  (start (make-instance 'easy-acceptor :address "localhost" :port 8080)))

;; 關閉/重啟 server
;; (stop *server*)
;; (start *server*)

(setf *js-string-delimiter* #\')
;; (setf *js-string-delimiter* #\")

(setf *prologue* "<!DOCTYPE html>")
(setf (html-mode) :HTML5)

;; 排版設定
(setq *PS-Print-pretty* t)
(setq *INdent-num-spaces* 4)

(define-easy-handler (tutorial1 :uri "/tutorial1") ()
  (with-html-output-to-string (s nil :prologue t :indent t) ;; prologue/indent 設定 cl-who 輸出排版後 HTML
    (:html :lang "en"
     (:head (:title "Parenscript tutorial: 1st example"))
     (:body (:h2 "Parenscript tutorial: 1st example")
        "Please click the link below." :br
        (:a :href "#" :onclick (ps-inline (alert "Hello World"))
            "Hello World")))))

(define-easy-handler (tutorial2 :uri "/tutorial2") ()
  (with-html-output-to-string (s nil :prologue t :indent t)
    (:html :lang "en"
     (:head
      (:title "Parenscript tutorial: 2nd example")
      (:script :type "text/javascript"
           (str (ps
                  (defun greeting-callback ()
                    (alert "Hello World"))))))
     (:body
      (:h2 "Parenscript tutorial: 2nd example")
      (:a :href "#" :onclick (ps (greeting-callback))
      "Hello World")))))

(define-easy-handler (tutorial3 :uri "/tutorial3") ()
  (with-html-output-to-string (s nil :prologue t :indent t)
    (:html
     (:head
      (:title "Parenscript tutorial: 3nd example")
      (:script :src "/tutorial3.js"))
     (:body
      (:h2 "Parenscript tutorial: 3nd example")
      (:a :href "#" :onclick (ps (greeting-callback))
      "Hello World")))))

(define-easy-handler (tutorial3-javascript :uri "/tutorial3.js") ()
  (setf (content-type*) "text/javascript")
  (ps
    (defun greeting-callback ()
      (alert "Hello World"))))

以上引用 Parenscript Tutorial 的程式來說明,其中幾個套件說明如下:

  • hunchentoot : 高效能 web server
  • cl-who:with-heml-output-to-string : html code 翻譯/產生器
  • parenscript:ps : javascript code 翻譯/產生器

測試可用 curl/wget/firefox/chrome

http://127.0.0.1:8080/tutorial1
http://127.0.0.1:8080/tutorial2
http://127.0.0.1:8080/tutorial3

Parenscript Vue.js - Json & Symbol Coversion

接續 Parenscript Tutorial,以下引入 Vue.js 作為前端開發 framework,其內容參考下列文件改寫並加上其它資料匯整。

;; https://nowills.blogspot.tw/2016/08/vue-js.html
(define-easy-handler (vue-hello :uri "/vue-hello") ()
  (with-html-output-to-string (s nil :prologue t :indent t)
    (:html
     (:head (:meta :charset "UTF-8")(:title "0 Hello World"))
     (:body
      (:div :id "app" "{{ message }}")      ;; div tag id="app"
      (:script :src "https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.1/vue.min.js")
      (:script
       :type "text/javascript"
       (str (ps
          (new (*vue (create el "#app"  ;; 建立 Vue function 把 json 資料傳進去
                             data (create message "Hello World!")))))))))))


;; https://nowills.blogspot.tw/2016/08/vue-js-real-time.html
(define-easy-handler (vue-hello-input :uri "/vue-hello-input") ()
  (with-html-output-to-string (s nil :prologue t :indent t)
    (:html
     (:head (:meta :charset "UTF-8") (:title "0 Hello World"))
     (:body
      (:div :id "app" (:h1 "{{ message }}")  ;; div tag id="app"
        (:input :type "text" :v-model "message")
        (:div "{{ $data | json }}"))     ;; {{ $data | json }}
      (:script :src "https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.1/vue.min.js")
      (:script
       :type "text/javascript"
       (str (ps
          (let ((data (create message "Hello World!"))) ;; 建立名為 data 的 json 資料
            (new (*vue (create el "#app"
                               data data)))))))))))     ;; 把 data 引入 data


;; https://nowills.blogspot.tw/2016/08/vue-js-if-elsev-show.html
(define-easy-handler (vue-v-show :uri "/vue-v-show") ()
  (with-html-output-to-string (s nil :prologue t :indent t)
    (:html
     (:head (:meta :charset "UTF-8") (:title "2 v-show"))
     (:body
      (:div :id "app"
        (:h1 :class "error" "{{ score }}分")
        (:div :v-show "score"
              (:p :v-if "score >= 6" "Vue.js so easy")
              (:p :v-else t "still learning Vue.js"))
        (:p "你覺得 vue.js 簡單嗎?請輸入 1-10 分")
        (:input :type "text" :v-model "score"))
      (:script :src "https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.1/vue.min.js")
      (:script
       :type "text/javascript"
       (str (ps
          (new (*vue (create el "#app"
                             data (create score "0")))))))))))


;; https://nowills.blogspot.tw/2016/08/vue-js-vuejsv-onclick.html
(define-easy-handler (vue-click :uri "/vue-click") ()
  (with-html-output-to-string (s nil :prologue t :indent t)
    (:html
     (:head (:meta :charset "UTF-8") (:title "3 click"))
     (:body
      (:div :id "app"
        (:p "目前以點擊:{{count}}次")
        (:input :type "submit" :value "立即送出" :v-on\:click "clickme"))
      (:script :src "https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.1/vue.min.js")
      (:script
       :type "text/javascript"
       (str (ps
          (new (*vue (create el "#app"
                             data (create count 0)
                             methods (create clickme
                                             (lambda () (incf (@ this count))))))))))))))

(define-easy-handler (vue-click-1 :uri "/vue-click-1") ()
  (with-html-output-to-string (s nil :prologue t :indent t)
    (:html
     (:head (:meta :charset "UTF-8") (:title "3 click"))
     (:body
      (:div :id "app"
        (:button :v-on\:click "handleIt(\"uh\")" "I say uh")
        (:button :v-on\:click "handleIt(\"ah\")" "you say ah"))
      (:script :src "https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.1/vue.min.js")
      (:script
       :type "text/javascript"
       (str (ps
          (new (*vue (create el "#app"
                             data (create count 0)
                             methods (create handle-it
                                             (lambda (msg) (alert msg)))))))))))))

幾個教學範例後,您首先會面臨的是如何用 PS (Parenscript) 來建立 json - javascript object。另外會困擾人的是 JS function name 大小寫的問題。 由於 CL (common lisp) 語言預設 function/變數 全部大寫。故請詳閱 Symbol conversion 這個章節,用 「-/*」 來區分 JS function/變數 大小寫的問題。

Parenscript Vue.js - css-lite v-for v-bind

餘下的範例引入 css-lite:css

;;https://nowills.blogspot.tw/2016/10/vue-js-for-loop.html
(define-easy-handler (9x9 :uri "/9x9") ()
  (with-html-output-to-string (s)
    (:html (:head (:meta :charset "UTF-8") (:title "9 x 9"))
     (:body (:div :id "app"
                  (:div :v-for "i in 9"
                        (:h3 "{{ i }}")
                        (:div :v-for "j in 9" "{{ i }} x {{ j }} = {{ i * j }}")))
            (:script :src "https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.1/vue.min.js")
            (:script
             :type "text/javascript"
             (str (ps
                    (new (*vue (create el "#app"))))))))))

(define-easy-handler (fruit-list :uri "/fruit-list") ()
  (with-html-output-to-string (s)
    (:html
     (:head (:meta :charset "UTF-8") (:title "fruit list"))
     (:body
    (:div :id "app"
      (:ul
       (:li :v-for "item in items" "{{ item.message }}")))
    (:script :src "https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.1/vue.min.js")
    (:script
     :type "text/javascript"
     (str (ps
        (new (*vue (create el "#app"
                           data (create items
                                        (array (create message "Apple")
                                               (create message "Banana")
                                               (create message "Coconut")))))))))))))


(define-easy-handler (index-items :uri "/index-items") ()
  (with-html-output-to-string (s)
    (:html
     (:head (:meta :charset "UTF-8") (:title "index items"))
     (:body
    (:div :id "app"
      (:ul
       (:li :v-for "(item, index) in items" "{{ parentMessage }} - {{ index }} - {{ item.message }}")))

    (:script :src "https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.1/vue.min.js")
    (:script
     :type "text/javascript"
     (str (ps
        (new (*vue (create el "#app"
                           data (create parent-message "台北市"
                                        items
                                        (array (create message "中正區")
                                               (create message "中山區")
                                               (create message "大同區")))))))))))))

;;https://nowills.blogspot.tw/2016/12/vue-js-v-bind.html
(define-easy-handler (red-green-123 :uri "/red-green-123") ()
  (with-html-output-to-string (s nil :prologue t :indent t)
    (:html (:head (:meta :charset "UTF-8") (:title "red green 123")
            (:style (str (css-lite:css ((".active")(:color "red" :font-size "30px")) ;; add css-lite:css
                                       ((".normal")(:color "green" :font-size "30px"))))))
     (:body (:div :id "app"
                  (:div :v-bind\:class "{ active: myCheck , normal: myCheck2 }" "123")

            (:script :src "https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.1/vue.min.js")
            (:script
             :type "text/javascript"
             (str (ps
                    (new (*vue (create el "#app"
                                       data (create my-check nil
                                                    my-check2 t))))))))))))

Posted by Lloyd Huang in on December 12, 2017

March 17, 2017

Org-mode list of features

Org-mode 功能概廓介紹

Org-mode 是 Emacs 上的殺手級應用,slogan 「Your Life in Plain Text」 - 「與文字共舞」, 是的你沒看錯 org mode 就是一個以純文字展現的應用。

Org mode 是個高效率的文字編輯系統,可以用來作筆記,維護 TODO Lists,規劃計畫,文件草稿。它常與 GTD 放在一起討論, 作為 GTD 工作與時間管理上的實踐工具。但其功能遠不止於此,它可以當簡報工具、心智圖、專案管理、繪製表格、試算表、資料庫、 文件加密、文學編程 (Literate programming)、Redmine bugs/issues report 工具、出版、寫 blog (網誌出版系統)。 也正因為它有這麼多的面向,所以初學者非常容易混謠或是只看到片面的現像。

以下以 http://orgmode.org/features.html 內容為主,加上個人觀點的說明:

編輯 - Editing

Org 引用 outline.el 為實作基礎,可便利的在各個標題快速移動,收折標題與內文,方便撰文時以筆記標題式呈現全貌利於思考佈局, 展開看細部的角度來寫作。這功能不僅在寫 code 同時在寫文件上也很方便。

todo folding

圖片來源 Mishadoff Blog - Emacs org-mode use cases, for non-programmers. 這同時是一篇介紹 org-mode 編輯功能的好文章, 推薦初學者服用。

計劃 - Planning

Org 提供 TODO lists 可用來作工作管理及時程計劃。預設有幾個 keyword 如 TODO | DONE。狀態 keywords 可以自行擴充設定。

* 購物清單
** DONE 買午奶
   CLOSED: [2017-03-15 Wed 15:59]
** TODO 買洗衣機
planning

Tasks 可以加標籤 tags 埋屬性 properties 加上時間標籤(期限/結束時間/排程),搭配 GTD 對時間及工作管理上非常好用。 GTD 可以大大改善工作效率,真心推薦。實作可參考以下文章。

todo statistics

計時 - Clocking

我並沒有使用這個功能,主因是沒有計算工時的需求。它主要的概念是針對各別的 Task 作 clocking in 及 clocking out, 由 in/out 的時間差來計算工時,其關鍵點在於累加統計以及產生漂亮的報表。

clocking

行程表 - Agendas

當有數千個工作項 tasks 時,只要將工作即期的時間排定,Agendas 能讓使用者很輕易的專注在未來即將發生的事件而能預先準備, 作到提醒的功能。預設的 Agendas 可以將 scheduled 及 deadline 的工作以週間的方式呈現。Agenda 可依個人喜好客制化, 以每天/每月/針對 TODO 的關鍵字/針對 tag 等等呈現。這個特色可以讓 Agenda 無痕的融入您的工作流程中。

agenda

個人的實際經驗,工作上鎖碎的雜事很多,如果同時間也在籌辦大型活動,每日超過百封的信件及同時間上千個待辦事項在排隊等著處理時, 這個情況十分可怕,身處在其間的人很有可能快要精神錯亂。這個時候就非常需要導入一個工作流程如 GTD。 Agenda 在這個流程中可以妥善的幫忙當守門人,防止掉球的事情發生。非常有用真心推薦。

Capturing - 快照?? 速記?? 速寫??

沒能找到較好的中文翻譯。Capturing 的動作主要事先建立個人專屬的 TODO 模版 (templates),當有好的點子或新的工作項目, 快速喚起 Capture 把事情先寫下來,然後回神到目前的工作上。避免臨時出現的雜事/念,干擾工作,而快速的把事情記下來等 待工作告一段落後有空時在整理,依分類、優先權重、緊急來分批處理。

capture

表格與試算表 - Tables

Org 的表格功能超級直覺好用,有一陣子我還特別會用 Orgtbl-mode 在 reStructuredText 上繪製表格, 後來乾脆就直接用 org-mode 匯出成 reST 使用。如要用一分鐘來介紹 org-mode ,我會只介紹 tables 及 outline 這兩個神奇的功能。

此外表格可由 .csv .tsv 匯入產生,表格還可以加上試算表的功能,計算的語法可用 Calc Emacs Package 或是 Emacs lisp code。

tables

匯出 - Exporting

匯出格式官方支援的有 HTML, LATEX, ODT(OpenDocument 格式),也提供 backends 的接口方便其它開發轉換成其它格式。 例如 Org2OPML (mindmaps 心智圖),列入官方的 Markdown exportOx-Rst (reStructuredText),Org-Reveal (Reveal.js), org-ioslide (Google I/O HTML5 slide) 等等。

export

文件內嵌程式碼 - Working with source code

Org 讓文學編程 (literate programming) 變的很容易且自然。程式片段可以很輕易置入於文件中, 編輯程式時又可以在一區隔開的 Org buffer 進行,語法高亮、縮排、字串補齊等方便 coding 的能力完完全全延用您 Emacs 的設定, 執行的結果也可附註於檔案內。

整體呈現的觀感與 jupyter/ipython notebook 很相近,較大的差異是 jupyter 呈現在 browser 上,而 org-mode 則呈現在 Emacs 裏。

babel

Posted by Lloyd Huang in on March 17, 2017

March 08, 2017

Emacs Org-mode format export to reStructuredText

Emacs Org-mode format export to reStructuredText

reStructuredText 使用經驗

工作上寫技術文件的比重就越來越高了,文件是很重要的溝通工具。依時間演進下所使用的工具如下:

LinuxSGML -> CJK-LaTex ChiLaTex -> DocBook -> OO.o -> CJK-LaTex -> reStructuredText

reST 我由 2009 年開始使用直到現在,投資報酬率很高,累積的工具也越來越多。其間也試用過 Markdown , 個人主觀經驗作為文件排版格式 reST 還是比較合適。

Markdown 原始設計,易讀的純文字檔,便於轉換成 xhtml html。就設計的概念及推廣實作它是非常成功的,但作為文件還是少了些特性,如表格、 目錄章節、索引、註腳、列印、出版等功能。但以網路流通性易學及多人協作上考量,它是目前首選。

Org-mode - Emacs 上的殺手級應用,作為 GTD 實踐工具,我由 2010 年開始使用。它大大的改善工作習慣及效率,以及時間掌握與應用。 隨著時間對 Org-mode 使用也走出了單純的 GTD 工具往簡報、Mind Map、Project Management、簡易試算表、簡易資料庫、文件加密、Literate programming 等使用面向。但文件排版與個人筆記我還是交給 reST + Latex + Sphinx。

最近我開始使用 org-mode + ox-rst 來產出 reST 文件。原因無他,就是 Emacs org-mode 實在是太方便及好用了。

Ox-Rst exports your Org documents to reStructuredText. Ox-Rst relies on the Org-mode 9.0 export framework.

一般來說,文件格式的轉換會有失真的現象。例如 html 轉 text,hyperlink 字型及顏色的特性就會失去。但 ox-rst 很巧妙的在 org-mode 語法上作了合宜的擴充,幾乎可以一對一轉換,強烈推薦給 reST 及 org-mode 的愛用者。

問題筆記

在使用上遇到了一個比較困擾的問題,就是 ^ 插入符號 Caret 及 _ 底線 Underscore 的使用,這兩個符號在 org-mode 上會特別解釋。情況如下:

model^rgb_led = modelrgbled
L^A T_E X = LATEX

如果要避免這個問題,在 org-mode 把 ^ 設定為不解釋即可,作法如下:

#+OPTIONS: ^:nil

reStructuredText 與 ox-rst 使用入門

ox-rst 是個 Emacs Org-mode 的延伸使用套件 GitHub ox-rst 官網 已經有很詳細的使用說明文件。但使用的關鍵還是在 reST 的語法熟悉上, 畢竟實際還是在寫 reST。以下是 OpenFoundry 上的兩篇介紹文,提供給有興趣的人參考。

Posted by Lloyd Huang in on March 08, 2017

February 26, 2017

ffmpeg - edit video by ffmpeg

Edit video by ffmpeg

Date:<2017-11-30 Thu 17:43>

演講被要求 Demo 採錄影的方式呈現。最後用 audacity 錄音,ffmpeg 剪輯及混音,留個記錄給未來的自已查詢。

  • ffmpeg - video converter
  • avidemux3 - GUI 影片剪輯軟體
  • audacity - wave/mp3 聲音編輯軟體
  • gneve - GNU Emacs Video Editor mode
# 影片切段 Split/Trim video using FFmpeg
$> ffmpeg -ss 00:00 -t 00:18.7 -i input.mp4 -vcodec copy -acode copy output.mp4

# 多影片合併 Merge video files and convert to desired formats
$> cat flist.txt
file './s1.mp4'
file './s2.mp4'
file './s3.mp4'
$> ffmpeg -f concat -safe 0 -i flist.txt -c copy output.mp4

# 除去影片聲音
$> ffmpeg -i input.mp4 -vcodec copy -an output.mp4

# 取出影片聲音 | aac 或 mp3 由影片原始檔案決定
$> ffmpeg -i input.mp4 -acodec copy -vn output.aac
$> ffmpeg -i input.mp4 -acodec copy -vn output.mp3

# 匯入音軌
$> ffmpeg -i input.mp4 -i in.aac -map 0:v -map 1:a -bsf:a aac_adtstoasc -c copy output.mp4
$> ffmpeg -i input.mp4 -i in.mp3 -map 0:v -map 1:a -c copy output.mp4

# 重新壓製 (encode)
$> ffmpeg -i input.mp4 -c:v mpeg4 -b:v 20000K -acodec copy output.mp4

# other 影片旋轉
$> ffmpeg -i input.mp4 -vf vflip output.mp4
$> ffmpeg -i input.mp4 -vf hflip output.mp4

# PIP Picture in Picture with FFMPEG
$> ffmpeg -y -i main.mp4 -i overlay.mp4 -filter_complex \
"[1]scale=iw/4:ih/4 [pip]; [0][pip] overlay=main_w-overlay_w:main_h-overlay_h" \
-c:a aac -c:v mpeg4 -b:v 20000k -r 30 output.mp4

發現 2 秒的影片接上 120 秒的聲音,影片會停在最後的畫面直到聲音撥完。猜測應該可以用圖檔配 mp3。

各段影片中的聲音格式不可以混用,例如前面用 mp3 之後接 aac,這會造成後面接的聲音無法解析。

Posted by Lloyd Huang in on February 26, 2017

February 13, 2017

Emacs - fixed problem ansi-term work with top/less/more

Emacs - fixed problem ansi-term work with top/less/more

一直以來我在 Emacs 的 ansi-term 模式下使用 top/less/more 時一直有頂端少了幾行的問題。如下圖所示:

ansi-term problem

今天有空追查了 term.el 後才終於搞懂問題。由於之前設訂了行距,致使 term 的行數計算錯誤導致,其設定如下:

(setq-default line-spacing 5) ;; 修改行距

解決的方式有二個可供選擇,(1) 設定 term-mode-hook 在啟動 term-mode 時把當時 term buffer 行距設為 0 ,這應該是最簡便的解法,在 init.el 加入以下設定:

(add-hook 'term-mode-hook (function (lambda () (setq line-spacing 0))))

(2) 覆寫 term-check-size function 協助校正終端機高度 (term-height)。這樣作可以保有修改行距的權力, 但校正值的選擇我目前用嘗試錯誤的方法自行測試校正,高度校正值我是用 window-text-height 與行距的比例來作調整,事實上這個算式需包含字高的設定才行。

(defun term-check-size (process)
  (let ((height-adjust (/ (window-text-height) 5))) ;; Modify 5, range is 5~40, Please try and error.
    (when (or (/= term-height (window-text-height))
          (/= term-width (term-window-width)))
      (term-reset-size (- (window-text-height)  height-adjust) (term-window-width))
      (set-process-window-size process term-height term-width))))

方法各有優缺點,不熟 emacs-lisp 建議用方法(1)。

ansi-term ok

Posted by Lloyd Huang in on February 13, 2017

July 25, 2015

Emacs Copy/Paste work with clipboard of X Server.

Emacs Copy/Paste work with clipboard of X Server

工作上常需要 Emacs 與 X Server 相互 Copy/Paste 文字,為了暸解這功能搞了幾天。

xsel man page

The X server maintains three selections, called PRIMARY, SECONDARY and CLIPBOARD. The PRIMARY selection is conventionally used to implement copying and pasting via the middle mouse button. The SECONDARY and CLIPBOARD selections are less frequently used by application programs. This program operates on the PRIMARY selection unless otherwise specified.

By default, this program outputs the selection without modification if both standard input and standard output are terminals (ttys). Otherwise, the current selection is output if standard output is not a terminal (tty), and the selection is set from standard input if standard input is not a terminal (tty). If any input or output options are given then the program behaves only in the requested mode.

If both input and output is required then the previous selection is output before being replaced by the contents of standard input.

實際上 X server 有三個 selections, 名稱為 PRIMARY, SECONDRY, CLIPBOARD. 用在滑鼠中鍵的稱為 PRIMARY,應用程式用多為 CLIPBOARD 與 SECONDARY。

Copy from X-window, Paste to Emacs

Emacs paste text from Clipboard

透過 X-window 的剪貼簿貼文字:

(x-clipboard-yank)
or
(insert (x-get-selection 'CLIPBOARD))

綁定組合鍵:

(global-set-key (kbd "C-x C-y 1") 'x-clipboard-yank)

Emacs paste text from PRIMARY

透過滑鼠標記後貼文字:

(mouse-yank-primary 1)
or
(insert (x-get-selection 'PRIMARY))

綁定組合鍵:

(global-set-key (kbd "C-x C-y 2") (lambda () (interactive) (mouse-yank-primary 1)))

Copy from Emacs, Paste to X-window

Emacs copy text to Clipboard

請參考以下兩個 URL

xclip-mode 用了二年多快,近來常用 TRAMP mode (Transparent Remote Access, Multiple Protocols) 遠端 edit/coding。 但 xclip-mode 與 TRAMP mode 是互斥的,後改用 yank-to-x-clipboard 在有需要時在 copy 到 Clipboard 後使用。

Emacs copy text to PRIMARY

X-window 下的 emacs 只要 mark 標註一段文字,就會自動複製到 PRIMARY。其中的文字之後用滑鼠中鍵就可以貼上。