75% 鍵盤心得

話說前陣子眼看從 2012 使用至今的 KBtalKing Race 75% 日漸年華老去(其實還很好用, 外觀也新,只是 Micro USB 插孔有些舊,根本就是藉口),加上家人用的 iMac 前方桌面 有限,配上的全尺寸 Cherry G80-3484 實在太佔地方又沒支援 Mac,就開始起心動念肖想 新鍵盤,打算至少進個兩把。這幾天在網路上四處翻找,試圖補足這幾年沒什麼注意的鍵盤 新發展,最終看上的卻跟一開始沒什麼兩樣:補入一把二手的 Race 一代,以及全新 Keychron K2v2. 雖說看似簡單,過程卻是千迴百轉,簡記如下。

首先,為何專挑 75% ? 這是個相當關鍵的決定,大大限制了選擇數目。100% 也就是一般常 見的右邊九宮數字鍵、中間編輯鍵與方向鍵、左方字母鍵盤的配置,好處是標準,要換鍵帽 什麼的都容易,但我習慣打字時 FGHJ 鍵是對準螢幕中間,保持雙手置中,結果整個鍵盤就 變成偏右,滑鼠被迫到更遠處。偶爾不得已要用上滑鼠時(日常操作都以熱鍵為主,但仍然 沒法完全避免),右手簡直要翻越千山萬水。注意到這問題後購入 Filco Majestouch Tenkeyless, 就再也回不去了。所謂 tenkeyless (TKL), 就是少掉右邊數字鍵,這又稱 80% 鍵盤,在電競也常用,好處是數字九宮格相對少用,拿掉之後滑鼠更接近,遊戲時也有 更大活動空間。

Cherry G80-3484, 100%

Filco Majestouch TKL, 80%

比 80% 再縮小,常見的是 60% 及 6x%,主要就是從 80% 再拿掉右邊六個及方向鍵以及上 方 F1 ~ F12, 只留主要字母區,就叫 60%. 有些配置會加回來幾個,例如方向鍵、 PgUp/PgDn 之類的,但 F 功能鍵還是沒有,這就叫 6x%, 例如 65% 之類。這類配置通常都 會多個 Fn 鍵,碰到真的需要的場合時,例如要按 F1, 那就是 Fn 加上方數字鍵 1 之類。 有些電競不需要少掉的鍵,也是很適合,可以空出更多空間,而且容易攜帶南征北討。另外, 著名的的號稱 Unix programmer 用鍵盤 Happy Hacking Keyboard (HHKB) 也屬此類。HHKB 的 layout 很有趣, 目前我的 CapsLock 設定成 Ctrl, 以及 Backspace 與 Backslash 互換,就都是跟 HHKB 一樣。

照片取自 HHKB 官網

夾在中間的 75% 可說是選擇最少的。這種配置,簡單講就是類似一般筆電鍵盤,大多數 80% 的鍵都還在,改排在字母區上方跟右方多了一圈。那為何不去上面的 80% 或是往下去 60% 呢?首先考慮這些按鍵的常用程度。最主要是注音輸入法,時常要選字,就需要方向鍵。 另外即便裝了 Vimium 之類東西, Home/End/PgUp/PgDn 仍然時不時會需要,還有我的 F1 ~ F12 大多數在編輯器 (Emacs) 裡 面都有設定功能。此時 80% 需要移動手掌離開字母區才能按到,這是極力避免的。那 60% 及 6x% 搭配 Fn 手掌不用動都可以按,但 75% 也是不用動手掌,且不需組合鍵就直接按到, 相比起來我更喜歡一些,這就單純是個人喜好問題了。

KBtalKing Race, 75%

一旦決定 75% 配置,選項就沒剩幾個。加上對中國品牌有排斥感,中國製造勉強接受,刪 掉後又更少。基本就剩寥寥幾個 Vortex Tab 75 / Race 3, Keychron K2 或是新的 Q1, 還 有 Akko x Ducky 3084 雖是中國品牌但聯名的勉強給過,這幾個。Race 3 很不錯,問題鍵 帽是 DSA 這不行,個人最愛 Cherry 原廠高度,OEM / Filco 也行,但 DSA 全等高的就沒 辦法。換鍵帽的話,Race 3 幾個特規大小的鍵很難換,Tab 75 的配置比較常見,好換,但 慣用的紅軸款停產了,也沒有補貨計畫。聯名款的 3084 不好找,蝦皮有,但都是假裝台灣 賣家其實是中國人代購,且價錢都是坑人。至於掏寶,我知道有,但堅持不註冊。再上去, 還有些客製化鍵盤,一把光是殼跟板子大概就 150 美金以上,太貴又太花功夫,還不想去 那邊。

對 Keychron 品質期待不高,但家中需要一隻堪用的機械鍵盤,它支援 Mac 且是少數公認 在 Mac 下藍芽依舊穩定的,因此入選。但工作用的一把還是找不著,正在煩惱,剛好看到 拍賣釋出跟手上一樣的 Race 品相很新, 就直接收了。

今天 Keychron K2v2 到貨,質感真的不好,這點一如預期,但其他包括無線穩定度、與 Mac 相容程度都中規中矩,這也一如預期。有一點沒料到的是,原來功能鍵列的高度影響這 麼大。一般這種 Cherry/OEM 鍵帽,從下方數上來到第五列數字,型態分別稱為 B B C D E, 最下面 Ctrl - Win - Alt 跟 Z - X - C 這兩列是相同的,那第六列,也就是功能列, 常見的是做 E, 也就是跟數字列一樣。但在老鍵盤上,會有另一種型態 F, 高度比 E 更高, 以作為區分。在全尺寸或是 TKL 情況下,由於功能列跟數字列中間會留空,所以沒什麼問 題。但在 K2 這種緊湊布局上,如果是 E E 的型態,那要按功能鍵時手指就會「撞上」數 字鍵,相當困擾。

K2 側面,可看出最左一排與第二排高度相仿

Race 側面,最左一排與第二排有明顯高度差

這是為何當年奕之華規劃 Race 時,選擇以 F 鍵高來做功能列。Race 打習慣了,換到 K2 這問題就變得相當明顯。我想這可能也是後來 Vortex 的 75% 都採等高鍵帽的原因之一。 許多較高價位的 75% 鍵盤,會如同 TKL 一樣,在數字與功能列間留白,今天算是了解這重 要性了。

那麼話說回來,鍵盤追尋之路就到此為止了嗎?當然不。慣用紅軸是因為喜歡線性手感,此 類鍵盤軸中,應用 hall effect 為原理 的磁力軸很有趣,內部無接觸,所以極耐用(遠高於人類壽命)且摩擦力低,可達成極好的 手感。另外基於電磁原理,能感測按下深度,所以還可調整觸發深度配合打字習慣。這就是 所謂的「類比」鍵盤,在電競也有一堆應用,比如全按跟半按可以觸發不同動作,或是需要 極快反應時可以把觸發深度調到很淺等等。目前用這種軸的有 SteelSeries 賽睿的 Apex Pro 還買得到,Wooting two 賣完了要等 HE 版本, 還有 Input Club 的 Keystone 在預 購。以前有間中國的創業公司叫 Ace Pad Tech (APT) 也有做,但似乎已消失了。這非常的 生火,但最接近我需求的只有 TKL 版本可買,且是 2019 年產品,PChome 賣 5990 大洋, 現在下手可能沒多久就出新款了,所以還能先忍著。

以下 Keychron K2 開箱圖。

包裝

開蓋

上方

lsp-mode 與 Python virtualenv 的整合

Emacs 的 lsp-mode 是利用 Language Server Protocol (LSP) 來支援程式語言的編輯,LSP 一開始是為了 VS Code 開發的,可以把對語言的編輯支援與 IDE/editor 脫勾,常常是孤兒的 Emacs 就比較可以跟上時代的發展。但,慣用的 palantir/python-language-server (pyls) 在搭配 Python 幾乎一定會使用的 virtualenv 時,還是會有認不出虛擬環境裡模組的問題,如果再加上 pyenv 之類的,就還需要考慮不同 Python 版本,事情又更麻煩。以下是幾種解決方案:

每個虛擬環境都裝一套

這需要搭配 add-dir-local-variable 使用,在專案目錄底下新增一個 dir local variable pyvenv-activate 指向虛擬環境的絕對路徑,然後只要在這個環境下安裝 pyls 就可以了。這是個非常穩當的方法,所有基於虛擬環境的應用例如 pipenv 之類的也都適用。

缺點是,整套 pyls 也佔不少空間,每個專案底下都做一次很麻煩,要升級還得各自升級。

自己寫個 pyls wrapper

這算是前面方法的變體。先在自己環境裡裝一套 pyls,例如我習慣裝在 homedir 底下,那就是

1
python3 -m pip install --user 'python-language-server[all]'

這樣只要在設定環境時,用 virtualenv --system-site-packages 或者像我是用 pipenv 那就是 pipenv install --site-packages 就可以一併帶入到虛擬環境裡面。但這方法有個問題:帶入的模組是不會在虛擬環境裡新增執行檔的,也就是說 Emacs 執行 pyls 這命令時,會用到虛擬環境「外面」的執行檔,這就會出錯了。解決方法是要繞一圈,在虛擬環境裡以 python -m pyls 的方式執行。

但問題又來了:如果直接把 lsp-pyls-server-command 這變數改掉,lsp-pyls.el 的寫法是把整個值當成命令名稱來用,但系統裡面當然不該有個名稱叫做 python -m pyls 的執行檔。所以可以自己寫個 pyls.sh 裡面就寫 python -m pyls 然後把 lsp-pyls-server-command 改成 pyls.sh 就行了。這方法一樣要仰賴在每個專案正確設定 pyvenv-activate. 而我是慣用 pipenv,所以只要寫成這樣

1
2
3
4
5
6
7
8
9
#!/bin/bash

set -e

if pipenv --venv &> /dev/null; then
exec pipenv run python -m pylsp "$@"
else
exec python3 -m pylsp "$@"
fi

如此不特別設定 pyvenv-activate 也一樣會動。

pipenv 與 virtualenv 的關係

pipenv 用來管理 python 的 app 是不錯(注意不是模組,模組還是該用 distutils),不過基於 python 社群的一貫風格還是要搞點問題出來才行。先前碰到 issue 2364,搞了好久才全部弄清楚。背景知識要先知道 pyvenv, pyenv, virtualenv, pip, pipenv 這一堆東西是幹嘛的(stackoverflow 上的說明),然後要知道 virtualenv 會綁初始 pip, setuptools, wheel 的版本,就比較容易弄清楚了。

狀況是:在 Ubuntu 19.04 上面用 pipenv 設定開發的專案,Python 版本綁在系統預設的 3.6. 後來移到 20.04,由於系統版本升到 3.8,所以先裝 pyenv 然後再 pipenv sync,理論上這樣 pipenv 會先用 pyenv 裝好 3.6、產生執行 3.6 版的 virtualenv、然後裝好所有需要的模組。但最後在安裝模組階段噴錯。研究後發現裡面用到的模組需要較新的 setuptools 版本才能正確安裝,而版本是由 virtualenv 產生環境時決定的,但 pipenv 的開發者認為 virtualenv 屬於使用者環境的一部分,他們不應該去修改,使用者一番爭論後答應看一看,就又沒下文了。

當安裝 pipenv 時 (python3 -m pip install pipenv),由於 virtualenv 是一個 dependency,所以會先檢查有沒有符合的版本,沒有的話會自動帶入。這就是在 20.04 那邊出錯的原因,因為 19.04 的系統沒裝 python3-virtualenv 但 20.04 的系統有,所以最早在 19.04 那台安裝 pipenv 時帶入了新版的 virtualenv,比 20.04 系統裡面預設安裝的新,所以一直沒問題。移到 20.04 時,pip 檢查到 virtualenv 已經存在,就用現有的,所以產生的環境裡面 setuptools 比較舊,安裝模組時就失敗了。

這邊要注意的是: pyenv 在這邊的作用,並不及於 pipenv 以及 virtualenv. 也就是說,在預設為 3.8 的系統上跑 pipenv install 時,是用 3.8 搭配的 pipenv,自然也執行到了 3.8 搭配的 virtualenv. 直到以 pipenv shell 或者 pipenv run 的時候,被執行的程式才會真正用到虛擬環境裡的 python 版本,也就是 3.6.

知道以上資訊後,解法也很簡單。不管系統裡面有沒有 virtualenv,反正都用 python3 -m pip install --user virtualenv 在 home 底下裝一份新的就好。這也表示在重建 build 環境時,除了 Pipfile 裡面已經綁死的東西,也要注意用到的 virtualenv 版本是否相容。

Ubuntu 20.04 更新

平日自用的 Ubuntu 機器有三台,一台準系統接電視、另外兩台筆電分別是家用、工作用。雙十連假手癢想升級到 20.04,電視那台是標準安裝,很順利就升級完畢。筆電配置就比較奇怪,ubuntu-desktop 沒裝齊(差很多),要用 .xsession 進桌面,環境混合了 xfce, 一點點 gnome 以及 awesome 這個 tiling window manager. 考慮到這是非典型的配置,懷疑直接跑圖形界面的升級會有問題,就用命令列的 do-release-upgrade 先拿裡面沒什麼東西的家用那台試試。結果才換完 repo 就跳出 X,用 Ctrl-Alt-F1 無法換 console,用 Alt-SysRq- 那幾個鍵竟然也沒反應,最後 cold reset, 慘。重開當然進不了 X,於是就在 runlevel 3 之下把套件裝一裝,大部分都可以自動,有些 ecryptfs-utils (這個該換了)之類的要手動裝,裝完重開就大致正常了。

有了這經驗,第二台工作用筆電就直接進 runlevel 3 升級,跑完微調一下套件,很順利就完成。這樣膽子就長出來了,下次來遠端升級那一堆古董伺服器,有幾台還在 12.04 呢…

ThreadPoolExecutor 就是個坑

主要問題可參考這個 issue,裡面有以下片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import concurrent.futures as cf

bucket = range(30_000_000)

def _dns_query(target):
from time import sleep
sleep(0.1)

def run():
with cf.ThreadPoolExecutor(3) as executor:
future_to_element = dict()

for element in bucket:
future = executor.submit(_dns_query, element)
future_to_element[future] = element

for future in cf.as_completed(future_to_element):
elt = future_to_element[future]
print(elt)

run()

可發現這是照著文件例子寫的,只是數量增加一下,記憶體就爆了。問題乍看出在 future_to_element 長太大,但就算不把 submit() 的結果放在容器中,只要在 with clause 中就一樣會佔用記憶體。也就是說在工作數量大的情況下,ThreadPoolExecutor 並不實用。Issue 中建議是另外加個 loop 把總工作切成一批批的處理,每次處理一批就要等到全部跑完,然後換下一批。若為了節省時間把批次處理量加大,記憶體又消耗很多,不是很理想。

這其實是不必要的:future 可用以得知是否出錯、以及工作完成的結果,但要知道結果有其他機制可以利用,最典型的例如送到 queue,問題就只剩錯誤處理。先前在「出錯就盡快停止」的條件下,寫過類似下面的程式片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import threading
import typing


class ThreadExecutor:

def __init__(self, max_workers: int):
self.max_workers = max_workers
self.semaphore = threading.Semaphore(max_workers)
self.last_error = None

def _wrapper(self, fn: typing.Callable, args, kwargs):
try:
fn(*args, **kwargs)
except Exception as e:
self.last_error = e
raise e
finally:
self.semaphore.release()

def submit(self, fn: typing.Callable, *args, **kwargs) -> bool:
if self.last_error:
return False
self.semaphore.acquire()
threading.Thread(
target=self._wrapper, args=(fn, args, kwargs)
).start()
return True

def join(self):
for _ in range(self.max_workers):
self.semaphore.acquire()
if self.last_error:
raise self.last_error


def task(x):
import random
import time
if random.random() < 0.001:
raise RuntimeError(x)
print(x)
time.sleep(1)
return


def main():
excr = ThreadExecutor(100)
for x in range(500):
if not excr.submit(task, x):
break
excr.join()


if __name__ == '__main__':
main()

如此不用批次也沒有消耗記憶體問題。

但以上提的還算是表面。最終是會需要 Scala concurrent.Map 那樣的機制,而且必須是 lazy,或是 Go 用 channel 也能處理得很漂亮。只能說 Python 在 concurrency 領域還差得很遠,這也限制了它作為 backend, infrastructure 等等應用的普及度。

用虛擬 webcam 作為線上開會視訊 aka v4l2loopback on Ubuntu 18.04

會找這個來用是因為想把線上開會時的視訊整個替換掉。v4l2loopback 會在系統上做出 一個假的 webcam device,可以當成真的 webcam 來用,播放餵給它的東西。套件名稱是 v4l2loopback-utils 會依賴 v4l2loopback-dkms 一起裝進來,但版本太舊,跑起來問 題一堆,所以直接去 github 那邊抓了最 新 release v0.12.4 來用。

安裝就直接照 README.mdmake 然後 insmod v4l2loopback.ko 下去就行了。講 究一點可以照裡面說的跑 depmod -a 之類的讓它自動帶 dependencies. 先設定:

1
2
v4l2loopback-ctl set-caps 'video/x-raw,format=UYVY,width=640,height=640' \
/dev/video2

/dev/video2 是系統上 insmod 以後產生出的 device name, 然後 640x640 可以改成你 想要的解析度。

1
2
3
gst-launch-1.0 -v filesrc location="fakeportrait.jpg" \
! decodebin ! imagefreeze ! videoconvert ! videoscale \
! identity drop-allocation=1 ! v4l2sink device=/dev/video2

這例子是把一張靜態圖片做背景。用 zoom 可以看到沒問題,用 cheese 碰到問題就是不讓 我換預設 device,已經找過不用 cheese -d /dev/video2 改用 cheese --device="Dummy video device (0x0000)" 還是有問題,乾脆砍了。

zoom screenshot

awesome window manager: choose default layout

awesome window manager 也許多年了,一向都蠻順手,這 陣子單獨用筆電作業的時間比較多,也就是沒外接螢幕跟鍵盤,總覺得卡卡的。原因是筆電 螢幕解析度高,但相對應的 DPI 也調到正確值 210,一行大約 140 個字母。由於習慣一行 最多 80 個字母,所以如果螢幕寬度無法容納 160 個字,視窗就不適合用左右並列的方式。 在 awesome 的 rc.lua 裡原本是這樣寫:

1
2
3
4
5
if s.geometry.width > 1600 then
layout = awful.layout.layouts[1] -- 左右並列
else
layout = awful.layout.layouts[2] -- 上下並列
end

這樣會按照螢幕橫向解析度決定要不要並列,但現在不適用了,因為雖然筆電螢幕寬度是 2560 點,但並不希望是左右並列。解決方法是實際去計算能放幾個字。慣用的 adobe source code pro 10pt, 72pt 是一英吋,然後這字型是設計成寬度大約是字體大小的 60%, 最後加入 0.25pt 當成字距用以微調。

1
2
3
4
5
if (s.geometry.width / s.dpi) / ((10 * 0.6 + 0.25) / 72 )) >= 160 then
layout = awful.layout.layouts[1]
else
layout = awful.layout.layouts[2]
end

實際用幾組不同螢幕規格試算,結果都還符合預期。

讓 pmount 支援 exfat

一直覺得 pmount 沒支援 exfat 很麻煩,剛弄了個一行的 patch.

抓下來以後可以這樣做一個自己的 package 裝起來。

1
2
3
4
5
6
apt-get source pmount
cd pmount-0.9.23
quilt import ~/Downloads/exfat.patch
debuild -i -us -uc -b
cd ..
sudo dpkg -i pmount_0.9.23-3build1_amd64.deb

2020/09/23 更新: 不知有什麼改變了,總之現在要多個 nonempty 才會動。patch 已更新。

12 pt 的字,到底是多大

(重刊 2017/12/18 在 MMDays 的文章)

這幾天筆者加購了一台配備高解析螢幕的筆電,由於平時使用的並不是 Apple 系統,且筆電算算也有六年沒換,雖說蹭別人的 Mac Book Pro 玩過幾次,以高解析螢幕為主的使用經驗,在此之前還真沒有過。筆電來了,自然要把附帶的 Windows 10 縮一縮,裝上慣用的 Ubuntu 才是正辦。過程中,又碰上老問題:字太小。

這問題快十年前就碰過,大約知道是 DPI 設定不正確。雖說本行不是幹這個,但為了顧眼睛還是得處理一下,還可寫篇文章談談不專業的豆知識交差。先從字型大小說起。

字型尺寸當然是從電腦出現之前就存在,pt 是 point 縮寫,雖在歷史上幾經變遷,最終在數位出版時代訂立 desktop publishing point,長度為 1/72 英寸,大約 0.353 公釐。說到這似乎已說完了:那麼,12 pt 字,當然就是 12/72,也就是 1/6 英寸(又稱 1 pica),會有什麼問題呢?

然而對電腦使用者而言,卻往往不是如此。我們知道 12 pt 的字,用印表機印出時,大小是固定的。但在一般解析度螢幕上,字比較大,而在高解析度螢幕,字就變小了。這似乎可以自圓其說:高解析度下,一個螢幕上的「點」比較小,所以如果螢幕上的字是由固定點數所組成的,那麼每一點越小,字當然就比較小了。早期的字型的確是這樣的,叫做點陣字,也的確會有在不同解析度大小不一致的問題。但後來向量字型出現,可以平順的縮小放大,也開始用 pt 為標示單位,為何還是有大小不一致的情況呢?

要說明這個,就必須先說明 DPI 是什麼1。DPI 是 dots per inch 的縮寫,表示每英寸點數。數字越大,解析度越高。例如印刷常用的 300 dpi,看起來已經非常清晰,一般 24” 的 1080p 螢幕是 92 dpi,Apple 的 Retina 筆電多半落在 200 多,而 iPhone 由於觀看距離更近,可高達 300 多,iPhone X 更高達 458 dpi (註 1)。有了 DPI 概念之後,再回去想想 pt 的問題,就比較清楚了:假定某印表機以 300 dpi 列印,那麼 12 pt,也就是 1/6 寸的字型,其大小就是 50 點 (dots),而若是在解析度比較低的螢幕,例如 90 dpi,就是 15 點。如此,同樣的字型尺寸,顯示在不同解析度的螢幕上,就是一樣大,只是精細程度不同。

這麼說來,只要知道螢幕 DPI,字型大小的問題照理說就可以得到解決,但為何沒有呢?這又是另外一個故事。1980 年代,Apple Macintosh(麥金塔)電腦設定的解析度是 72 dpi,讀者可發現這跟 point 單位一致,也就是 1 point 剛好對應到 1 dot,12 pt 在螢幕上就用 12 dots 表示。當時搭配的螢幕是 512 x 342,換算下來寬度大約是 7.1 in,而北美慣用的 letter 大小紙張寬度 8.5 in,兩邊扣掉 0.7 in 的留白,恰好是 7.1 in,所以若螢幕不設定兩邊留白,字型大小就會完全一致。這乍看是非常合理的設定,但也產生一些問題。例如 12 pt 的字,在 300 dpi 下,每個字可以用  50 點表示,相當足夠,但在螢幕上,就只剩 12 點,加上英文字母上下有突出,主要的部份例如字母 x,只剩下 6 點可用,看起來就比較粗糙。再加上螢幕跟眼睛之間還有鍵盤,一般觀看距離比紙張來得遠,字看起來就更小了。

這時,微軟想出了一個聰明 (?) 的方法來改善這問題:相對於 Apple 的 72 dpi,他們增加 1/3,在所有軟體中將解析度設定為 96 dpi. 短期內這有幾個效果:若需要 10 pt 的字,解析度是 72 dpi,就只能用 10 點來設計字型,看起來比較粗糙;而若解析度是 96 dpi,就有 13 點,設計上會更容易些。而在顯示時,由於當時螢幕一般都還在 72 dpi 的水準,96 dpi 會導致螢幕使用比所需更多的點數來顯示,讓字母高度變成原先的 4/3 倍,看起來比較清楚。顯而易見的缺點是,這背離了原本 DPI 與 pt 的關係,而隨著 Windows 3.1 大紅,到 Windows XP 長達十多年的統治,這也造成相當深遠的影響。在 Windows 7 之前,預設的 96 dpi 並不會隨著不同螢幕而改變,這也造成前面所提到的,高解析螢幕字比較小的印象。

為了解決這問題,後來微軟發展出 device independent pixel (DIP) 這單位,其定義就是每 DIP 為一寸的 1/96。沒錯,就是 96 dpi 這個魔術數字。舊的 Windows 應用程式,其基本假設就是畫 96 個點是一寸,那麼在高解析螢幕上,只要將其使用的「點」定義為 DIP,就可以保持這程式的界面仍然可用。這也是為何一些比較古早的應用程式,看起來有點糊糊的,因為 Windows 自動以 DIP 放大了原先界面的緣故。

讓這筆糊塗帳更加糊塗的,還有另一個當初看起來很有道理的設計:網頁的 CSS 規範。CSS 全名為 Cascade Style Sheet,就是一般網頁使用的排版格式,其內有個常見的長度單位,px, pixel unit. 以 1998 年公佈的 CSS level 2 來說,CSS 的長度單位分成兩種,絕對與相對。絕對單位包括了 in, cm, mm, pt, pc (pica) 等等,可注意到也有熟悉的 pt,而相對單位則有 em, ex, px,其中 em, ex 是相對於設定的字型大小,例如 1 em 就等於設定的字型大小,所以若設定字型大小為 12 px,那 1 em 就是 12 px. 而 px 則是相對於「顯示裝置的解析度」,也就是螢幕上的一點。由於各個螢幕一點的大小不見得一致,px 就成了相對單位。在 CSS 2 裡面,針對解析度不同以及觀看距離不同的情況,還建議了參考的改變方式,這容後再論。

在 199x 的年代,一般螢幕的解析度不是那麼高,設計上差了幾點,看起來就差很多。例如一條線寬度是一點或兩點,看起來會有相當明顯的差異。所以,設計上會講究 pixel perfect,也就是仔細控制網頁上各個元件長度是幾「點」,而不使用絕對單位。在指定字型時,往往是類似 font-size: 12px 這樣的形式,而其他諸如欄位寬度等等,也一律使用 px 來設計,以確保顯示出的結果符合預期。但這造成一個問題:網頁上的長度單位,包括字型,又再度隨著螢幕解析度而改變了。在 72 dpi 的螢幕上, 12 px 就是 12 pt,沒什麼問題,但螢幕的 dpi 一直在提昇,12 px 的字也就越變越小,只能創造出各式各樣觀念去修正。

在這裡就要提到,前面 CSS 2 針對不同解析度及不同觀看距離,所做的建議。其內容是,1 px 應等於在一般觀看距離,即平均手臂長度,定義為 28 寸,觀看 90 dpi 的螢幕,螢幕上 1 點的大小。也就是說,1 px 從「長度」變成了「角度」,約略等於 0.0227 度,投射出的距離,是會隨著觀看距離而改變的。底下這張圖,直接來自 CSS2:

Reference pixel

這裡顯示出,在 28 寸的距離,1 px 約等於 0.28 mm,而在 140 寸(例如投影情況),1 px 約等於 1.4 mm,是原先的 5 倍。複雜歸複雜,至少這裡對於 1 px 提出一個還不錯的標準,希望在不同的觀看條件下,都能得到一致的經驗,只是事與願違,後來大部分實作都還是採用 1 px 對應螢幕上一點的設計,且 96 dpi 隨著 Windows 四處普及,大家都把 96 px 當成實作上需要 1 寸的約略單位,不但應用程式與網頁如此,連螢幕也是,以避免使用者由於 dpi 差距過大而難以使用。在實作日漸偏離標準的情況下,CSS 2.1 這修正版裡,針對這部份有幾個修改,都相當有趣:

  1. 參考用的 px,從原先定義的 28 寸,90 dpi,改為 96 dpi。也就是說,W3C 肯認大部分螢幕都設計成接近 96 dpi,所以在預設的閱讀距離,也就是 28 寸時,參考用的 px 約略就等於螢幕上的一點。

  2. 將 px 從相對單位,改為絕對單位,長度是 0.75 pt. 如此一來,在 96 dpi 的裝置上,96 px = 72 pt = 1 in,就一致了。

  3. 讓 px 有雙重定義:在接近 96 dpi 的螢幕上,px 保持在 0.75 pt,也就是約略仍等於螢幕上的一點。而在其他情況,則採用角度的定義,而讓其他單位如 pt 等「相對於 px」定義出來。亦即,假設現有一 200 dpi 螢幕,在 28 寸位置,若採用角度定義,px 就不再表示螢幕上的一點,而是約略等於兩點,而 pt 由於相對於 px,就仍然可以約略等於 1/72 寸。而由於是角度,所以只有在 28 寸的觀看距離,pt 才會約略等於物理上的 pt 單位。若在不同距離,例如預設的觀看距離比較近的 iPad mini 與 iPhone 等,網頁上的 pt 與物理上的,就不會相等了。

繞了這麼一大圈,回到一開始的題目:新筆電的 DPI 設定。以下是設定前與設定後比較,這也可以看出,Apple 當時提倡 Retina 螢幕,打破 96 dpi 魔咒,還要維持各類程式、網站等仍然可用,實在是相當了不起的突破,也推動其他廠商跟進,最終造福使用者。

Before

After

[1]: DPI / PPI 的問題,本文略過不表。

govendor vs dep

我的 ~/.gitconfig 裡面有這樣的設定

1
2
3
4
[url "git@github.com:"]
insteadOf = https://github.com/
[url "git@bitbucket.com:"]
insteadOf = https://bitbucket.org/

作用是存取 private repository 的時候,會自動換成 git+ssh. 本來不需要設定這個, 因為 git clone 的時候都會特別注意網址,只是為了方便使用 go get,因為它對這兩 個站預設只會用 https://,碰到 private repo 的時候就沒法自動了。

但剛才抓別人的 project 下來以後跑 govendor sync 卻發現它針對 bitbucket.org/private_team/private_repo 這樣的情況沒法處理:

1
2
3
$ govendor sync
Error: Remotes failed for:
Failed for "bitbucket.org/private_team/private_repo" (failed to ping remote repo): https://api.bitbucket.org/1.0/repositories/pilot_seals/quividisdk: 403 Forbidden

改為試著執行 golang 官方做的 dep,竟然會自己讀 govendor 用的 vendor/vendor.json 檔案來轉換,而且順利的下載了 private repo 執行完畢。

找了一下原因,發現 govendor 裡面用了官方建議的 golang.org/x/tools/go/vcs 來 處理 bitbucket 的 repo 類型判斷,然後 vcs 裡面會呼叫上面的 API 網址;但如果是 private 的,就連那網址都會 403,導致噴錯。奇怪的是,dep 就沒有這個問題。看了看 程式碼,發現它用的不是官方那份 vcs,而是來自 github.com/Masterminds/vcs 的另 外一份,說明寫說是基於 vcs 擴充而成的。看一下 bitbucket 相關程式碼,發現多了這 段:

1
2
3
4
5
6
7
8
9
10
// Fast path for ssh urls where we may not even be able to
// anonymously get details from the API.
if ul.User != nil {
un := ul.User.Username()
if un == "git" {
return Git, nil
} else if un == "hg" {
return Hg, nil
}
}

這也太黑心了,dep 自己沒用官方那份,這樣 govendor 豈不是死得不明不白…

2018/07/24 補充

golang.org/x/tools/go/vcs 是從 cmd/go/internal/get copy 出來的,而 upstream 已解掉此問題:#5375,程式碼在 vcs.gogovendor 本身的 issue 在 #151.