2026/03/02

zsh

在 RockyLinux 測試 zsh。目前比較常見的,還是使用 bash,如果要測試 zsh,需要另外安裝。

安裝

dnf -y install zsh

修改預設的 shell

chsh -s /bin/zsh

基本的設定檔

可直接跳到下面的 Oh My Zsh

# 1. 基本環境
export LANG=en_US.UTF-8
export EDITOR=vim
export PATH="/usr/local/bin:$PATH"

# 2. Prompt 設定
PROMPT='[%*]%n@%m %~$ '

# 3. Alias 常用指令
alias ll='ls -lh'
alias la='ls -la'
alias ..='cd ..'
alias gs='git status'

# 4. 歷史設定
HISTSIZE=5000
SAVEHIST=5000
HISTFILE=~/.zsh_history
setopt share_history      # 多個 zsh session 共享歷史

# 5. 自動補全 & 修正
autoload -Uz compinit && compinit
setopt correct            # 拼字錯誤自動建議
setopt autocd             # 輸入資料夾名稱自動 cd
setopt nocaseglob         # 補全時忽略大小寫

# 6. 補全行為微調
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'

功能有

  • 乾淨的 prompt[時間]使用者@主機 當前路徑$

  • 常用 aliasll, la, gs, ..

  • 歷史 → 儲存 5000 筆並共享不同視窗

  • 自動補全 → tab 補全,支援大小寫不敏感

  • 錯字修正 & auto cd → 輸入資料夾名稱會自動進去

Oh My Zsh

Oh My Zsh 是 zsh 常見的 plugin 管理工具,可安裝多個 plugin 擴充 zsh 的功能,但也要注意,載入越多 plugin 會讓 zsh 啟動變慢。

安裝

sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

常見的 theme 工具是 Powerlevel10k,但這邊不使用,直接用基本的 Prompt

修改 ~/.zshrc 增加以下這個部分,然後就能將一些不同用途的設定檔分開

for file in .zshrc_*;
do source $file;
done

增加常用 plugins

安裝

cd $ZSH/custom/plugins
# 安裝 zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-autosuggestions.git

# 安裝 zsh-syntax-highlighting
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git

# zsh-completions
git clone https://github.com/zsh-users/zsh-completions

設定檔

~/.zshrc_basic

# 1. 指定 Oh My Zsh 安裝路徑
export ZSH="$HOME/.oh-my-zsh"

# 3. Plugins
# git           → git alias & 補全
# zsh-autosuggestions → 歷史自動建議
# zsh-syntax-highlighting → 命令語法高亮
# history-substring-search → 部分字串歷史搜索
# extract       → 快速解壓縮
plugins=(
  git
  zsh-autosuggestions
  zsh-syntax-highlighting
  zsh-completions
  history-substring-search
  extract
  colored-man-pages
  z
)

# 4. 啟動 Oh My Zsh
source $ZSH/oh-my-zsh.sh

# 5. PATH & 編輯器
export PATH="/usr/local/bin:$PATH"
export EDITOR=vim
export LANG=en_US.UTF-8

# 6. Alias 常用指令
alias ll='ls -lh'
#alias la='ls -A'
alias la='ls -la'
alias ..='cd ..'
alias gs='git status'
alias gp='git push'
alias gd='git diff'

# 7. 歷史設定
HISTSIZE=5000
SAVEHIST=5000
HISTFILE=~/.zsh_history
setopt share_history      # 多個 zsh session 共享歷史

# 8. 自動補全 & 行為優化
autoload -Uz compinit && compinit
setopt correct            # 拼字錯誤自動建議
setopt autocd             # 輸入資料夾名稱自動 cd
setopt nocaseglob         # 補全大小寫不敏感
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'

另外做一個 ~/.zshrc_prompt

#PROMPT='%F{green}[%D{%H:%M}]%f%F{yellow}%n@%m%f %F{blue}%~%f$ '
#PROMPT='%F{green}[%D{%H:%M}]%f%F{yellow}%n@%m%f %F{blue}%~%f${git_prompt_info} $ '
autoload -Uz vcs_info
precmd() { vcs_info }
zstyle ':vcs_info:git:*' formats '(%b)'

PROMPT='%F{blue}[%D{%H:%M}]%f%F{green}%n@%m%f %F{magenta}%~%f${vcs_info_msg_0_} $ '

增加功能,區分本地跟遠端的 ssh

Zsh / Bash 都可以靠環境變數來判斷:

  • 本地登入 -> SSH_CONNECTION / SSH_TTY 不存在

  • 遠端登入 (SSH) -> SSH_CONNECTIONSSH_TTY 存在

if [[ -n "$SSH_CONNECTION" ]]; then
  HOST_STYLE="%F{red}@%m%f(ssh)"
else
  HOST_STYLE="%F{yellow}@%m%f"
fi

PROMPT='%F{blue}[%D{%H:%M}]%f%F{green}%n%f'"$HOST_STYLE"' %F{magenta}%~%f${vcs_info_msg_0_} $ '

重新登入後,就可以使用 zsh

extract plugin

extract 定義了一個名為 extract 的函數,用於解壓縮你傳遞給它的檔案,並支援多種檔案類型。

使用方式為 extract 檔案名稱

不需要知道具體的解壓縮命令,只需執行 extract 就可以解壓大部分常見檔案,直接輸入 x 檔案名稱 也可以

z plugin

Usage: z [OPTION]... [ARGUMENT]
Jump to a directory that you have visited frequently or recently, or a bit of both, based on the partial
string ARGUMENT.

With no ARGUMENT, list the directory history in ascending rank.

  --add Add a directory to the database
  -c    Only match subdirectories of the current directory
  -e    Echo the best match without going to it
  -h    Display this help and exit
  -l    List all matches without going to them
  -r    Match by rank
  -t    Match by recent access
  -x    Remove a directory from the database (by default, the current directory)
  -xR   Remove a directory and its subdirectories from the database (by default, the current directory)

autojump

z 是用 shell script 實作,autojump 是 python,z 的速度比較快,且內建於 zsh。一般建議就直接使用 z,如果要使用 autojump,需要做另外的設定。

安裝

cd $ZSH/custom/plugins

# 安裝 autojump
git clone https://github.com/wting/autojump.git

注意 autojump 需要另外安裝套件

dnf -y install autojump

因為 autojump 只有提供 bash 版本的初始化 script

/usr/share/autojump/autojump.bash

現在要使用 zsh,故要另外產生一個 zsh 版本的初始化

到 autojump 原始的 github 網頁取得 autojump.zsh

把 autojump.zsh 放到 /usr/share/autojump/autojump.zsh

修改設定檔

plugins=(
  autojump
)

# Autojump 初始化
# 確認 autojump 安裝後再 source
if [ -f /usr/share/autojump/autojump.zsh ]; then
  source /usr/share/autojump/autojump.zsh
fi
# 任意切換目錄
cd ~/download/dir1
cd ~/download/dir2

# 檢查 autojump
j -s

# autojump
j dir1

2026/02/09

Cytoscape.js

Cytoscape.js 是一個處理資料視覺化的 javascript library,當我們要對資料關係進行可視化顯示時,例如社交網路關係或網路拓樸圖時,Cytoscape.js 是個不錯的選擇。

Cytoscape 和 Cytoscape.js 是兩個完全獨立不同的軟體

  • Cytoscape

    • 使用 Java 語言編寫的用於網絡可視化的桌面應用程序

    • 需要安裝 Java SDK 才能使用

    • 用於大型網絡分析和可視化的高性能應用程序

  • Cytoscape.js

    • 用於網絡可視化的 javascript library,本身不是一個完整的 Web Application

    • 可以在大多數瀏覽器上使用

    • 不需要 plugin 即可運行

    • 需要編寫程式來建構 Web Application

    • 支援 Extensions

    • 基於 CSS 將資料映射到元件屬性

sample1

建立一個 圓形排列 的 4 個節點 (A, B, C, D),節點之間有箭頭連線,點擊節點會有事件

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>test1</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
            display: block;
        }
    </style>
</head>
<body>
    <h2>test1</h2>
    <div id="cy"></div>

    <script>
        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'a', label: '節點 A' } },
                { data: { id: 'b', label: '節點 B' } },
                { data: { id: 'c', label: '節點 C' } },
                { data: { id: 'd', label: '節點 D' } },
                { data: { id: 'ab', source: 'a', target: 'b' } },
                { data: { id: 'bc', source: 'b', target: 'c' } },
                { data: { id: 'cd', source: 'c', target: 'd' } },
                { data: { id: 'da', source: 'd', target: 'a' } }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'background-color': '#0074D9',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#0074D9'
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 3,
                        'line-color': '#AAAAAA',
                        'target-arrow-color': '#AAAAAA',
                        'target-arrow-shape': 'triangle',
                        'curve-style': 'bezier',
                    }
                }
            ],

            layout: {
                name: 'circle'
            }
        });

        // 點擊事件
        cy.on('tap', 'node', function(evt) {
            let node = evt.target;
            console.log('你點了節點: ' + node.id());
        });
    </script>
</body>
</html>

sample2

流程圖,layout 調整為 dagre extension。

使用時要引用 dagre library 及 Cytoscape.js 的 extension

dagre 正是 Cytoscape.js 常用來畫flowchart或 directed graph 的 layout。適合做 flowchart, network topology, workflow

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>Cytoscape.js Flowchart</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <script src="https://unpkg.com/dagre/dist/dagre.min.js"></script>
    <script src="https://unpkg.com/cytoscape-dagre/cytoscape-dagre.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h2>flowchart</h2>
    <div id="cy"></div>

    <script>
        cytoscape.use(cytoscapeDagre);

        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'start', label: '開始' } },
                { data: { id: 'step1', label: '步驟 1' } },
                { data: { id: 'step2', label: '步驟 2' } },
                { data: { id: 'decision', label: '判斷 ?' } },
                { data: { id: 'end', label: '結束' } },
                { data: { id: 's1', source: 'start', target: 'step1' } },
                { data: { id: 's2', source: 'step1', target: 'step2' } },
                { data: { id: 's3', source: 'step2', target: 'decision' } },
                { data: { id: 's4', source: 'decision', target: 'end' } },
                { data: { id: 's5', source: 'decision', target: 'step1' } }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'shape': 'round-rectangle',
                        'background-color': '#28a745',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#28a745'
                    }
                },
                {
                    selector: 'node[id="decision"]',
                    style: {
                        'shape': 'diamond',
                        'background-color': '#ffc107',
                        'text-outline-color': '#ffc107'
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 2,
                        'line-color': '#555',
                        'target-arrow-color': '#555',
                        'target-arrow-shape': 'triangle',
                        'curve-style': 'bezier',
                    }
                }
            ],

            layout: {
                name: 'dagre',
                // rankDir: 'TB'  // top-to-bottom
                rankDir: 'LR' // 由左到右 排列
            }
        });
    </script>
</body>
</html>

sample3

鐵路模擬,增加火車在鐵軌上移動的動畫

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>Cytoscape.js Railway</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h2>Railway</h2>
    <div id="cy"></div>

    <script>
        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'station1', label: '車站 1' } },
                { data: { id: 'station2', label: '車站 2' } },
                { data: { id: 'station3', label: '車站 3' } },
                { data: { id: 'checkpoint4', label: '檢查點 4' } },
                { data: { id: 'checkpoint5', label: '檢查點 5' } },
                { data: { id: 'checkpoint6', label: '檢查點 6' } },
                { data: { id: 's1', source: 'station1', target: 'station2' } },
                { data: { id: 's2', source: 'station2', target: 'station3' } },
                { data: { id: 's3', source: 'station2', target: 'checkpoint4' } },
                { data: { id: 's4', source: 'checkpoint4', target: 'checkpoint5' } },
                { data: { id: 's5', source: 'checkpoint5', target: 'station3' } },
                { data: { id: 's6', source: 'checkpoint5', target: 'checkpoint6' } },

                // 列車節點
                { data: { id: 'train1', label: '🚆' }, classes: 'train' }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'shape': 'ellipse',
                        'background-color': '#0074D9',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#0074D9'
                    }
                },
                {
                    selector: 'node[id^="station"]',
                    style: {
                        'shape': 'round-rectangle',
                        'background-color': '#17a2b8',
                        'text-outline-color': '#17a2b8'
                    }
                },
                {
                    selector: 'node.train',
                    style: {
                        'background-color': 'red',
                        'shape': 'ellipse',
                        'label': 'data(label)',
                        'font-size': 24,
                        'width': 30,
                        'height': 30
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 2,
                        'line-color': '#555',
                        'target-arrow-color': '#555',
                        'target-arrow-shape': 'triangle'
                    }
                }
            ],

            layout: {
                name: 'breadthfirst',
                directed: true,
                padding: 20
            }
        });

        function moveAlongEdge(train, fromNode, toNode, duration, callback) {
            const start = fromNode.position();
            const end = toNode.position();
            const startTime = performance.now();

            function animate(now) {
                const elapsed = now - startTime;
                const t = Math.min(elapsed / duration, 1); // 0~1
                const x = start.x + (end.x - start.x) * t;
                const y = start.y + (end.y - start.y) * t;
                train.position({ x, y });

                if (t < 1) {
                    requestAnimationFrame(animate);
                } else if (callback) {
                    callback();
                }
            }

            requestAnimationFrame(animate);
        }

        function moveTrain(path) {
            let i = 0;
            const train = cy.getElementById('train1');

            function step() {
                if (i >= path.length - 1) return;
                const fromNode = cy.getElementById(path[i]);
                const toNode = cy.getElementById(path[i + 1]);

                moveAlongEdge(train, fromNode, toNode, 2000, () => {
                    i++;
                    step();
                });
            }

            step();
        }

        // 定義路徑
        const route = ['station1', 'station2', 'checkpoint4', 'checkpoint5', 'station3'];

        // 初始化列車位置
        cy.getElementById('train1').position(cy.getElementById(route[0]).position());

        // 2 秒後啟動列車
        setTimeout(() => moveTrain(route), 2000);
    </script>
</body>
</html>

2026/02/02

systemd template unit service

systemd template unit 是一種樣板服務 (service template),可以用同一份 unit 檔去啟動多個獨立的 service instance。當我們需要用同一個 service daemon 啟動多個 service instance 時,就可以透過這個方式,讓 service 對應到不同的設定檔,同時並存於一台機器中。

httpd

在 /usr/lib/systemd/system 目錄,除了 httpd.service,還有 httpd@.service

  • @ 代表這個 unit 是一個「模板」。

  • %i 代表實例名稱 (instance name),會在啟動的時候被替換。

systemd template 支援一些 specifier,常見的有:

  • %i → instance name (例如 site1 / site2)

  • %I → instance name,保持大小寫

  • %n → 完整的 unit name (httpd@site1.service)

  • %p → prefix name (httpd)

httpd@service 的內容是這樣

httpd@.service
# This is a template for httpd instances.
# See httpd@.service(8) for more information.

[Unit]
Description=The Apache HTTP Server
After=network.target remote-fs.target nss-lookup.target
Documentation=man:httpd@.service(8)

[Service]
Type=notify
Environment=LANG=C
Environment=HTTPD_INSTANCE=%i
ExecStartPre=/bin/mkdir -m 710 -p /run/httpd/instance-%i
ExecStartPre=/bin/chown root.apache /run/httpd/instance-%i
ExecStart=/usr/sbin/httpd $OPTIONS -DFOREGROUND -f conf/%i.conf
ExecReload=/usr/sbin/httpd $OPTIONS -k graceful -f conf/%i.conf
# Send SIGWINCH for graceful stop
KillSignal=SIGWINCH
KillMode=mixed
PrivateTmp=true

service 會讀取 /etc/httpd/conf/%i.conf 設定檔,並將 pid 放在 /run/httpd/instance-%i

所以要產生兩個 httpd unit service 設定檔

cp /etc/httpd/conf/httpd.conf /etc/httpd/conf/site1.conf
cp /etc/httpd/conf/httpd.conf /etc/httpd/conf/site2.conf

修改 site1.conf 以下這些設定。site2.conf 就改另一個 Listen 8001,site1 改為 site2,去掉其他 Directory 的部分

Listen 8000
PidFile /run/httpd-site1.pid

DocumentRoot "/var/www/site1"

<Directory "/var/www/site1">
    Options Indexes FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>

ErrorLog "/var/log/httpd/site1_error.log"

CustomLog "/var/log/httpd/site1_access.log" combined

啟動

systemctl start httpd@site1
systemctl start httpd@site2

systemctl enable httpd@site1
systemctl enable httpd@site2

haproxy

如果是 haproxy,因為套件裡面沒有 unit service,我們需要自己製作一個

首先產生 /usr/lib/systemd/system/haproxy@.service 檔案

[Unit]
Description=HAProxy Load Balancer %i instance
After=network-online.target
Wants=network-online.target

[Service]
Environment="CONFIG=/etc/haproxy/%i.cfg" "PIDFILE=/run/haproxy-%i.pid" "CFGDIR=/etc/haproxy/conf.d.%i"
EnvironmentFile=/etc/sysconfig/haproxy.%i
ExecStartPre=/usr/sbin/haproxy -f $CONFIG -f $CFGDIR -c -q $OPTIONS
ExecStart=/usr/sbin/haproxy -Ws -f $CONFIG -f $CFGDIR -p $PIDFILE $OPTIONS
ExecReload=/usr/sbin/haproxy -f $CONFIG -f $CFGDIR -c -q $OPTIONS
ExecReload=/bin/kill -USR2 $MAINPID
SuccessExitStatus=143
KillMode=mixed
Type=notify

[Install]
WantedBy=multi-user.target

製作設定檔

cp /etc/sysconfig/haproxy /etc/sysconfig/haproxy.site1
cp /etc/sysconfig/haproxy /etc/sysconfig/haproxy.site2

製作 /etc/haproxy/sit1.cfg

global
    log 127.0.0.1 local2
    chroot /var/lib/haproxy
    pidfile /var/run/haproxy-site1.pid
    stats socket /var/run/haproxy.admin.sock mode 660 level admin

    maxconn     50000
    maxconnrate 100000
    maxsessrate 100000
    user        haproxy
    group       haproxy
    daemon
    nbproc  1
    ca-base     /etc/pki/site1
    crt-base    /etc/pki/site1
    tune.ssl.default-dh-param   2048
    # turn on stats unix socket
    stats socket /var/lib/haproxy/stats-site1

    ssl-default-bind-options no-sslv3
    ssl-default-bind-options no-sslv3 no-tlsv11 no-tlsv10

defaults
    log global
    mode    http
    option  httplog clf
    option  forwardfor
    option  dontlognull
    option  httpchk
    option  http-keep-alive
    retries 3
    maxconn 50000
    rate-limit sessions 20000
    option  http-server-close
    timeout connect 1h
    timeout client  1h
    timeout server  1h
    #timeout connect 5000
    #timeout client  50000
    #timeout server  50000
    timeout tunnel  1h

frontend http_redirect
    bind    *:80
    mode    http
    acl kill_it method TRACE
    http-request deny if kill_it
    redirect   scheme https code 301 if !{ ssl_fc }
    default_backend web_server

frontend https_switch
    bind    *:443 ssl crt server.pem ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384
    mode    http
    option  forwardfor
    reqadd  X-Forwarded-Proto:\ https

    default_backend web_server

backend web_server
    mode    http
    fullconn    50000
    balance leastconn
    option      forwardfor
    #cookie      SERVERID insert indirect nocache
    #cookie SESSIONID prefix indirect nocache
    cookie  SESSIONID prefix nocache
    http-request        set-header X-Forwarded-Port %[dst_port]
    http-request        add-header X-Forwarded-Proto https if { ssl_fc }
    #option      httpchk GET /
    option  httpchk *
    server  W01 localhost:8000 weight 10 check cookie W01 inter 5s rise 2 fall 3

製作另一個設定檔 /etc/haproxy/site2.cfg,注意要修改 bind port

然後注意,申請兩個 ssl 憑證,放到 /etc/pki/site1 跟 /etc/pki/site2

啟動

systemctl start haproxy@site1
systemctl start haproxy@site2

systemctl enable haproxy@site1
systemctl enable haproxy@site2