9
7
2022
17

让离线软件真正离线

去年我做了个索引 Telegram 群组的软件——落絮,终于可以搜索到群里的中文消息了。然而后来发现,好多消息群友都是通过截图发送的,落絮就索引不到了。也不能不让人截图嘛,毕竟很多人描述能力有限,甚至让复制粘贴都能粘出错,截图就相对客观真实可靠多了。

所以落絮想要 OCR。我知道百度有 OCR 服务,但是我显然不会在落絮上使用。我平常使用的 OCR 工具是 tesseract,不少开源软件也用的它。它对英文的识别能力还可以,尤其是可自定义字符集所以识别 IP 地址的效果非常好,但是对中文的识别能力不怎么样,图片稍有不清晰(比如被 Telegram JPEG 压缩)、变形(比如拍照),它就乱得一塌糊涂,就不说它给汉字之间加空格是啥奇怪行为了。

后来听群友说 PaddleOCR 的中文识别效果非常好。我实际测试了一下,确实相当不错,而且完全离线工作还开源。但是,开源是开源了,我又没能力审查它所有的代码,用户量太小也不能指望「有足够多的眼睛」。作为基于机器学习的软件,它也继承了该领域十分复杂难解的构建过程,甚至依赖了个叫「opencv-contrib-python」的自带了 ffmpeg、Qt5、OpenSSL、XCB 各种库的、不知道干什么的组件,试图编译某个旧版 numpy 结果由于太旧不支持 Python 3.10 而失败。所以我决定在 Debian chroot 里安装,那边有 Python 3.9 可以直接使用预编译包。所以问题来了:这么一大堆来源不明的二进制库,用起来真的安全吗?

我不知道。但是我知道,如果它联不上网的话,那还是相对安全的。毕竟我最关心的就是隐私安全——一定不能把群友发的图片泄漏给未知的第三方。而且联不上网的话,不管你是要 DDoS 别人、还是想挖矿,收不到指令、传不出数据,都行不通了嘛。我只要它能从外界读取图片,然后把识别的结果返回给我就好了。

于是一个简单的办法是,拿 bwrap 给它个只能访问自己的独立网络空间它不就访问不了互联网了吗?不过说起来简单,做起来还真不容易。首先,debootstrap 需要使用 root 执行,执行完之后再 chown。为了进一步限制权限,我使用了 subuid,但这也使得事情复杂了起来——我自己都难以访问到它了。几经摸索,我找到了让我进入这个 chroot 环境的方法:

#!/bin/bash -e

user="$(id -un)"
group="$(id -gn)"

# Create a new user namespace in the background with a dummy process just to
# keep it alive.
unshare -U sh -c "sleep 30" &
child_pid=$!

# Set {uid,gid}_map in new user namespace to max allowed range.
# Need to have appropriate entries for user in /etc/subuid and /etc/subgid.
# shellcheck disable=SC2046
newuidmap $child_pid 0 $(grep "^${user}:" /etc/subuid | cut -d : -f 2- | tr : ' ')
# shellcheck disable=SC2046
newgidmap $child_pid 0 $(grep "^${group}:" /etc/subgid | cut -d : -f 2- | tr : ' ')

# Tell Bubblewrap to use our user namespace through fd 5.
5< /proc/$child_pid/ns/user bwrap \
  --userns 5 \
  --cap-add ALL \
  --uid 0 \
  --gid 0 \
  --unshare-ipc --unshare-pid --unshare-uts --unshare-cgroup --share-net \
  --die-with-parent --bind ~/rootfs-debian / --tmpfs /sys --tmpfs /tmp --tmpfs /run --proc /proc --dev /dev \
  -- \
  /bin/bash -l

这里给了联网权限,是因为我需要安装 PaddleOCR。没有在创建好 chroot 之后、chown 之前安装,是因为我觉得拿着虽然在 chroot 里但依旧真实的 root 权限装不信任的软件实在是风险太大了。装好之后,再随便找个图,每种语言都识别一遍,让它下载好各种语言的模型,接下来它就再也上不了网啦(为避免恶意代码储存数据在有网的时候再发送):

#!/bin/bash -e

dir="$(dirname $2)"
file="$(basename $2)"

user="$(id -un)"
group="$(id -gn)"

# Create a new user namespace in the background with a dummy process just to
# keep it alive.
unshare -U sh -c "sleep 30" &
child_pid=$!

# Set {uid,gid}_map in new user namespace to max allowed range.
# Need to have appropriate entries for user in /etc/subuid and /etc/subgid.
# shellcheck disable=SC2046
newuidmap $child_pid 0 $(grep "^${user}:" /etc/subuid | cut -d : -f 2- | tr : ' ')
# shellcheck disable=SC2046
newgidmap $child_pid 0 $(grep "^${group}:" /etc/subgid | cut -d : -f 2- | tr : ' ')

# Tell Bubblewrap to use our user namespace through fd 5.
5< /proc/$child_pid/ns/user bwrap \
  --userns 5 \
  --uid 1000 \
  --gid 1000 \
  --unshare-ipc --unshare-pid --unshare-uts --unshare-cgroup --unshare-net \
  --die-with-parent --bind ~/rootfs-debian / --tmpfs /sys --tmpfs /tmp --tmpfs /run --proc /proc --dev /dev \
  --ro-bind "$dir" /workspace --chdir /workspace \
  --setenv PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
  --setenv HOME /home/worker \
  -- \
  /home/worker/paddleocr/ocr.py "$1" "$file"

kill $child_pid

这个脚本会把指定文件所在的目录挂载到 chroot 内部,然后对着这个文件调用 PaddleOCR 来识别并通过返回结果。这个调用 PaddleOCR 的 ocr.py 脚本位于我的 paddleocr-web 项目

不过这也太复杂了。后来我又使用 systemd 做了个服务,简单多了:

[Unit]
Description=PaddleOCR HTTP service

[Service]
Type=exec
RootDirectory=/var/lib/machines/lxc-debian/
ExecStart=/home/lilydjwg/PaddleOCR/paddleocr-http --loglevel=warn -j 2
Restart=on-failure
RestartSec=5s

User=1000
NoNewPrivileges=true
PrivateTmp=true
CapabilityBoundingSet=
IPAddressAllow=localhost
IPAddressDeny=any
SocketBindAllow=tcp:端口号
SocketBindDeny=any
SystemCallArchitectures=native
SystemCallFilter=~connect

[Install]
WantedBy=multi-user.target

这里的「paddleocr-http」脚本就是 paddleocr-web 里那个「server.py」。

但它的防护力也差了一些。首先这里只限制了它只能访问本地网络,TCP 方面只允许它绑定指定的端口、不允许调用 connect 系统调用,但是它依旧能向本地发送 UDP 包。其次运行这个进程的用户就是我自己的用户,虽然被 chroot 到了容器里应该出不来。嗯,我大概应该给它换个用户,比如 uid 1500,应该能起到跟 subuid 差不多的效果。

顺便提一句,这个 PaddleOCR 说的是支持那么多种语言,但实际上只有简体中文等少数语言支持得好(繁体都不怎么样),别的语言甚至连语言名和缩写都弄错,越南语识别出来附加符号几乎全军覆没。

Category: Linux | Tags: linux 安全 隐私 网络
8
18
2022
15

我所讨厌的网页行为

有些网页的行为通常不被视为 bug,甚至是故意为之,但很令人讨厌。这里记录一些我所讨厌的网页「特性」。它们被归为两类,要么导致某些场景下用不了,或者用着很不方便,要么很打扰人。

可访问性问题

忽视系统、浏览器设置,在浏览器使用浅色主题的情况下默认使用深色主题,或者在浅色主题下代码部分使用深色主题。反过来问题不大,因为我有 DarkReader

主体文本不支持选择和复制。选择和复制之后,用户能做很多事情,比如查生字、翻译、搜索相关主题。

已访问链接与未访问链接显示没有差别。

消除可交互元素(链接、按钮)的 outline。这个 outline 以前是虚线框,现在火狐改成了蓝色框,用于标识当前键盘交互的对象。

搜索框不支持回车确认,必须换鼠标点击。

位于文本框后的按钮不支持使用 Tab 键切换过去,并且 Tab 键在此文本框中也没有任何显著的作用。必须换鼠标点击。

需要交互的元素不能被 vimium 插件识别为可点击。这大概是使用非交互元素来处理交互事件,甚至事件监听器都不在元素本身上。

使用 JavaScript 实现原本可以直接用链接实现的内容(链接目标是某个 JavaScript 函数调用)。这导致我无法使用中键来在新标签页中打开。

显著不同的内容没有独立的 URL。尤其见于一些单页应用(SPA)。要到达特定内容(比如加书签或者分享给别人),就只能记录先点哪里、再点哪里等。

预设用户的屏幕大小,导致浏览器窗口过小的时候部分关键内容(如登录按钮)看不到、无法操作。

交互元素没有无障碍标签。成堆的「未加标签 按钮」。

通过 User-Agent 判断浏览器,并拒绝为某些 User-Agent 服务(但实际上去除这个限制之后,功能是完全没有问题的)。

当没有带声音自动播放权限时,无声播放主体内容(而非等待用户操作使其获得权限)。说的就是 Bilibili。

为大屏幕用户(如桌面用户)展示为手机屏幕设计的页面。这些页面中字体特别巨大,并且不能被浏览器缩放影响。交互元素上鼠标指针不改变为手状,甚至只支持触摸操作而不支持鼠标点击。

悬浮于主内容之上的「在App中打开」。点名批评 imgur。它的按钮不光挡住图片,而且用户放大图片的时候它也被放大,挡住更多图片内容。

不能禁用的图片懒加载,或者视频内容被移出画面、切换到后台就停止加载。点名批评 Telegram、维基百科。我等你加载呢,你非要我盯着看你加载浪费时间?现在网好,你赶紧给我加载好,进电梯或者地铁或者山洞了,我再慢慢看你的内容啊。

视频内容被移出画面就停止播放。点名批评知乎。我让你播放你就给我播放。我不看视频,是因为视频画面没啥可看的,可是我听音频部分呀。

覆盖浏览器的 Ctrl-F 查找快捷键,并不提供方案来避免覆盖。我就搜索当前页面,不要你的站内搜索功能。

注册前请务必先阅读用户条款和规则,用户条款和规则页面需登录后才可访问。

简体中文内容指定繁体中文的字体,或者添加繁体中文的标签。或者反过来。

打扰用户

在内容页面,任何会动的非主体内容,包括但不限于广告、内容推荐。形式可以是动态 GIF、滚动动画、视频等。用于首页渲染效果的背景动画和视频不算,作为主体内容者也不算。

针对非音视频网站,自动或者非用户明确表达地(比如在用户点击不相关内容时)播放带音频的内容。

消耗 CPU 的背景特效。如 canvas-nest。会让 CPU 很吵,也会浪费能源、加剧气候变化。

Category: 用户体验 | Tags: 网页 accessibility
8
10
2022
2

tmux 状态栏优化

在 tmux 的状态栏里,通常会显示当前时间。配置起来也非常简单,%Y-%m-%d %H:%M:%S这样的时间格式化字符串扔过去就可以了。然而这样做有个小问题:这个时间只能精确到秒。我的意思不是说我想让它显示毫秒,而是希望它像电视台和广播电台的时间一样,显示(播报)「12:00:00」的时候,就刚好是这一秒的开始。

一般来说,这么延迟个一秒以内的随机数问题不大,除了你有多个这种时间戳的时候——

tmux inside tmux inside tmux

这些时间戳哪个先更新、哪个后更新可完全说不准的,你可能看到明明在地球另一边的服务器上先到某一秒,本地才跟上。甚至同一个 tmux 的不同客户端里,这个时间戳的更新时间都可能会有差异。

我想优化这个的另一个原因是,我经常使用 extrace 来查看程序调用另一程序使用的命令行参数,然而我本地连了多少个 tmux,每秒便会有多少个 sh + awk 进程出来读系统负载。尤其是我从 Awesome 换到 Wayfire 之后,顶栏改用 waybar 了,很多指示器都是内建或者自己写的外部脚本,不再需要每隔几秒跑个子进程去获取信息,这样 tmux 调用子进程来刷新状态造成的干扰就突显了出来。

于是就有了 accurate-time 程序。它每个整秒会去读系统负载,然后和当前时间一起送给 tmux 来显示。每秒一个进程,已经少了很多啦。

既然是我的程序自己来读负载,也就方便做更多事情了,比如根据负载情况使用不同的文字颜色:绿色表示低负载,灰白是稍微有点活干,蓝色和 cyan 是比较忙碌,黄色、品红表示已经忙不过来啦,红色就是要累趴下啦。之前偶然间发现 qemu-git 这个包使用 ninja、但是链接的时候又套了一层 make,造成系统负载冲到了两百多。但是无论高低,tmux 的负载显示都是红色,所以我可能之前已经视而不见许多次了。加上颜色之后,这类异常就更容易被注意到了。以前我本地每次风扇呼呼地转才发现系统负载高,但是我要是用耳机的话就听不到了,现在也多了个高负载的指示。

安装和配置很简单,cargo build --release 编译,然后把编译出来的 target/release/accurate-time 扔到 $PATH 里,再如下配置 tmux 状态栏右边即可:

if-shell "accurate-time tmux" {
  set -g status-interval 0
} {
  set -g status-interval 1
  set -g status-right "#[fg=red]#(awk '{print $1, $2, $3}' /proc/loadavg) #[fg=colour15]%Y-%m-%d %H:%M:%S"
}

本来我还打算给 waybar 上的时间也这么做一下的,不过程序写好了才发现 waybar 自己已经把时间对齐到更新间隔了。

Category: Linux | Tags: tmux Rust linux
8
8
2022
5

Google Chrome 中的字体设置

Google Chrome 是我的备用浏览器,主要用于对比和检查网页的渲染效果,以及某些网页在火狐上可能不太正常,就用 Google Chrome 试试。

但 Google Chrome 有个非常令人恼火的问题:默认字体实在太难受了!

如果没有指明字体,那么 Google Chrome 默认使用 Times New Roman 字体。这是我十年前从 Windows 那边拷过来的字体,看上去细细软软的,很复古,不太适合屏幕显示。屏幕显示一般使用无衬线字体,但 Google Chrome 默认是衬线字体。好吧,那网页要是指明要 sans-serif 字体呢?Google Chrome 这次看上了 Arial,同样是一款古老的、来自于 Windows 的字体。

Google Chrome 默认使用 Windows 字体

行吧……反正我也不是多喜欢这两字体,就全部删掉好了!结果,呃,「自定义」??

Google Chrome 固执己见

实际上它们分别是「Liberation Serif」和「Liberation Sans」字体。这是我的系统上 fc-match「Times New Roman」和「Arial」给出的字体,由 ttf-liberation 包提供,google-chrome 包依赖(看 AUR 上的评论,这是因为 Google Chrome 的 PDF 渲染需要这个字体)。

Google Chrome 就是这么喜欢 Times New Roman 和 Arial,系统上不安装它也要找个替代品来用,就是不听用户通过 fontconfig 设置的默认字体。用户要想 Google Chrome 听点话,需要在「设置」->「外观」->「自定义字体」里像这样设置一下:

Google Chrome 使用 fontconfig 的设置方法

而火狐的话,用户不需要在这种犄角旮旯里设置:

火狐默认使用 fontconfig 的设置

可惜大部分用户都在用 Google Chrome 或其变种,所以制作网页上还是得手动指定一些现代点的字体。

Category: Linux | Tags: 字体 Google Chrome
4
1
2022
9

从 getmail6 到 offlineimap

起因

上个月收到这样一封邮件:

自 5 月 30 日起,您可能会无法再访问那些采用安全性较低的登录技术的应用

意思就是说,Google 觉得把密码直接交给邮件客户端,权限太大,不够安全。所以要用户改用基于 OAuth2 的认证方式,只给程序邮件相关的权限。哦,你说应用专属密码?要用那个必须得启用两步验证——也就是意味着遇到灾难的话,我无法从一无所有的状态开始恢复。

从 POP 到 IMAP

getmail6 只支持使用 XOAUTH2 认证的 IMAP 协议,并不在 POP 协议上支持这个(不知道是否有可能)。所以我得换 IMAP 协议了。

具体操作步骤在 getmail6 的示例配置中有写。简单来说就是自己去申请个桌面软件的 app 信息,然后给自己的用户添加试用权限,再通过 OAuth2 获取 refresh token 和 access token,就能登录了。getmail6 自带了个 getmail-gmail-xoauth-tokens 程序用来走 OAuth2 流程,不需要另外安装程序来处理的同时也可以给其它程序使用。

所以我的 msmtp 配置就不用麻烦了,改两行配置就好:

auth oauthbearer
passwordeval getmail-gmail-xoauth-tokens ~/.getmail/gmail/lilydjwg@gmail.com.json

但是呢,虽然邮件是收回来了,IMAP 和 POP 还是挺不一样的。POP 没有「文件夹」的概念,所有收到的邮件,不管我有没有在 Gmail 网页或者客户端上阅读、归档,不管它进了哪个标签(文件夹)(「垃圾邮件」除外),我都会收到,并且把收过的邮件标记为已读。

而通过 getmail6 使用 IMAP 收取,我能做的选择就是,要不要把收过的邮件标记为已读或者删掉(可在 Gmail 中设置为归档)。不管如何,getmail6 只会收到它运行时位于收件箱中的邮件。如果我选择标记为已读的话,那么已读邮件也不会被 getmail6 收到。所以标已读的话,我在别的地方看过的邮件不会被收到。删除的话会好点,收过的邮件归档了,还省得我手动去归档,但是在别的地方,收过的邮件和已处理的邮件没了区分。

所以不如上 offlineimap,完全同步好了。

从 getmail6 到 offlineimap

offlineimap 的配置就比较复杂了,一是要对文件夹名进行转码,二是我要设定只同步指定的文件夹:收件箱、Maillist 和垃圾邮件。要同步垃圾邮件的原因是,Gmail 经常把有用的邮件往里边扔。

[general]
accounts = gmail
maxsyncaccounts = 10
socktimeout = 60
pythonfile = ~/.offlineimap/offlineimap.py

[Account gmail]
localrepository = gmail-local
remoterepository = gmail-remote

[Repository gmail-local]
type = GmailMaildir
localfolders = ~/.Maildir
filename_use_mail_timestamp = no
nametrans = gmail_nametrans_local

[Repository gmail-remote]
type = Gmail
remoteuser = lilydjwg@gmail.com

sslcacertfile = /etc/ssl/cert.pem
ssl = yes
starttls = no

oauth2_client_id_eval = get_client_id("lilydjwg@gmail.com")
oauth2_client_secret_eval = get_client_secret("lilydjwg@gmail.com")
oauth2_access_token_eval = get_access_token("lilydjwg@gmail.com")

nametrans = gmail_nametrans_remote
folderfilter = gmail_folderfilter
import os
import json
import subprocess

_LOADED_DATA = {}

def _load_data(account):
  with open(os.path.expanduser(f'~/.getmail/gmail/{account}.json')) as f:
    _LOADED_DATA[account] = json.load(f)

def get_client_id(account):
  if account not in _LOADED_DATA:
    _load_data(account)
  return _LOADED_DATA[account]['client_id']

def get_client_secret(account):
  if account not in _LOADED_DATA:
    _load_data(account)
  return _LOADED_DATA[account]['client_secret']

def get_access_token(account):
  cmd = [
    'getmail-gmail-xoauth-tokens',
    os.path.expanduser(f'~/.getmail/gmail/{account}.json'),
  ]
  out = subprocess.check_output(cmd, text=True)
  return out

def gmail_nametrans_remote(foldername):
  foldername = foldername.removeprefix('[Gmail]/').encode('ascii').decode('imap4-utf-7')
  if foldername == '垃圾邮件':
    foldername = 'Spam'
  elif foldername == '草稿':
    foldername = 'Drafts'
  return foldername

def gmail_nametrans_local(foldername):
  if foldername == 'Spam':
    foldername = '[Gmail]/垃圾邮件'
  elif foldername == 'Drafts':
    foldername = '[Gmail]/草稿'
  return foldername.encode('imap4-utf-7').decode('ascii')

def gmail_folderfilter(foldername):
  foldername = foldername.encode('ascii').decode('imap4-utf-7')
  return foldername in [
    'INBOX', '[Gmail]/垃圾邮件', '[Gmail]/草稿',
    'Maillist',
  ]

然后在 Gmail 那边创建个过滤器,把来自邮件列表的邮件扔到「Maillist」文件夹里去。搜索「 (to:@googlegroups.com OR from:vim-dev-github@256bit.org OR to:@zsh.org)」并创建过滤器,选择操作「跳过收件箱、 应用标签“Maillist”」即可。注意以后在修改的时候直接修改「包含字词」字段即可,并且记得「OR」「AND」「NOT」之类的操作符需要改回大写。

这样做完之后还有个问题:一封邮件同步到 offlineimap 后,我在 mutt 里阅读并删掉了它。offlineimap 一看,哟,邮件没了,得在服务器上删掉。Gmail 根据我的设置,把从 IMAP 删除的邮件归档,但是它并没有选项来标记为已读。所以这封邮件最终会以未读的状态躺在「所有邮件」里。

于是我去 App Script 里写了个脚本,把这些邮件标记为已读:

function mark_as_read() {
  const threads = GmailApp.search('is:unread AND NOT (label:Maillist OR in:inbox)', 0, 30)
  for(const thread of threads) {
    Logger.log('Marking as read: %s', thread.getFirstMessageSubject())
    thread.markRead()
  }
}

手动运行一遍之后,就可以在左侧栏里给它设置个触发器定时跑啦。

新邮件提示

使用 offlineimap 之后,最大的问题变成了邮件散落在不同的账号下的不同文件夹,一个个过去翻看太低效了。所以我就给 zsh 设置了提醒:

mailpath=(
  ~/.Maildir/INBOX/new'?GMail has a new message.'
  ~/.Maildir/Spam/new'?GMail has a new spam.'
  ~/.Mail/inbox'?New local mails.'
)

问号前边是邮箱的路径,后边是提示信息。之前那个 mbox 格式的邮箱我还留着,用来收取来自本地 cron 的邮件。

一个小问题是,procmail 用不成了。不过现在各种无用的网站消息也少了,所以不需要通过 procmail 处理垃圾邮件了(新浪微博我没有使用邮件注册、LinkedIn 和 Twitter 消停了、网易和QQ邮箱不用了)。现在中文邮件列表也几乎没人用了,我也不用让程序去重写「回复:RE:回复:」这类糟糕的邮件标题和过滤掉自动回复了。

Category: Linux | Tags: linux 电子邮件 IMAP
3
26
2022
12

微信消息通知的困扰

一直以来,不得不用的微信以其糟糕的通知体验让我十分不爽。

在手机上,我使用的是 Google Play 商店里的微信。在电脑上,我使用的是通过 Wine 运行的 Windows 版本微信([archlinuxcn] 仓库里的 wine-wechat-setup 脚本可用于安装)。

消息通知不及时

这个问题是最近我的手机日渐陈旧之后我才注意到的。表现是,在一段时间(比如一两天)不使用微信之后,收到新的微信消息或者视频通话,可能会延迟几个小时收到通知。在 Android 通知日志中可以确认,收到通知的时间和消息在微信中展示的时间有数小时之差,并不是因为我没有及时看手机。

我的 Telegram 从来不这样丢消息,即使因为后台进程过多 Telegram 被杀之后,通知只会不能在其它端阅读之后被清除,而不会延迟那么久。而微信,即使它还在后台运行着,却经常占着资源不干活,何况消息通知本应走 FCM。

打开微信即清除所有通知

我有一条微信消息,但是我现在不方便立即回复(比如需要使用电脑而我正出门在外),所以我会让那条通知一直留着。其实以前版本的 Android 系统更方便,可以将通知延后一段时间,只是不能自动指定延后的时间比较遗憾,后来被移除真的太可惜了。

然后呢,比如我要进个超市,或者测个核酸,付个钱啥的,只好打开微信扫码呗。结果所有还未处理的通知全部不见了!等忙完当时的事情,回到家里的时候,我就不一定还记得我还有几条微信消息还没处理了。

多端不同步容易错过

我在用电脑的时候,如果不登录电脑版微信的话,那么我将不会注意到手机上的微信有新消息。那就登录电脑版吧,然后我去上个厕所吃个饭,不用电脑的时候又会错过消息。在电脑端登录的时候让手机上也显示消息通知?那样所有消息都要看两遍,而且消息多的时候还得仔细回想某条消息到底是不是已经处理了。

Telegram 的消息同步做得真好啊。你在哪端用,哪端先给你发通知。其它端的消息会晚几秒出现。一旦在任意端读取了消息,另外的端上的相应消息全部都会被取消掉,不会有消息重复的问题(不过最近好像 Android 上的通知取消变得不那么可靠了)。

手机不可离远

现在电脑版微信终于不需要天天在手机上确认登录了。只要我每两三天登录一次,就可以避免把手机扔去充电了、来到电脑前、又去找手机的麻烦事。——我一开始是这么以为的。直到我发现,我刚刚看到通知、正要处理的消息,并没有在电脑版微信里同步出来。

不知道为什么,微信跟电脑用户有仇似的。明明我有电脑了,不需要凑合于手机的小屏幕和戳戳戳的屏幕键盘了,微信还非得把手机给拉过来。

语音通话不支持耳机接听

某天,我因为沉迷于放置型游戏,把手机扔桌子上充上电让它自个儿玩,自己去睡觉了。第二天早上睡正酣的时候,来了个电话,我拿耳机给接了。然后需要通话的另一方不知道怎么想的,没有商量就发起了微信语音通话,这个耳机根本接不到……

我也尝试过让 Google 助理回拨电话,不过使用不熟练,并没有成功。你说我为什么不起床去接?我睡着被电话惊醒了,还没回过神来啊 QAQ。

3
8
2022
8

Qt 的字体渲染问题

GUI 程序我现在依然倾向于 GTK,因为虽然 Qt 拥有良好的跨平台性,但可能是太注重跨平台性了,在 Linux 平台上反而有一些水土不服的问题。

字体太多,支持太少

你可能觉得,系统上字体太少,所以经常会遇到不常见的字符无法显示的情况。然而对于 Qt 来说,字体越多,反而越容易遇到个别字符不能显示的情况。

这是我的 /etc/fonts/conf.d/66-qt.conf 中的一段。因为顺序的原因,我只能放到 /etc 下。除了针对 sans-serif 配置外,我也有同样的配置应用于 serif 和 monospace。

<fontconfig>
  <!-- Adjust font order for Qt applications -->
  <alias>
    <family>sans-serif</family>
    <prefer>
      <!-- 格拉哥里字母:Ⰽⱁⱀⱄⱅⰰⱀⱅⰹⱀ Ⰹⱍⰹⰳⱁⰲ -->
      <family>Noto Sans Glagolitic</family>
      <!-- 爪哇文:꧁   ꧂ -->
      <family>Noto Sans Javanese</family>
      <!-- 西夏文:𗷲𗒅 -->
      <family>Noto Serif Tangut</family>
      <!-- 埃及象形文字:𓁹 -->
      <family>Noto Sans Egyptian Hieroglyphs</family>
      <!-- 苏美尔楔形文字:𒆠𒂗𒂠 -->
      <family>Noto Sans Cuneiform</family>
      <!-- 中日韩统一表意文字扩展 C:𫚥 -->
      <family>HanaMinB</family>
      <!-- 拉让文:ꥃ -->
      <family>Noto Sans Rejang</family>
      <!-- 越南傣文:ꪀꪑ -->
      <family>Noto Sans Tai Viet</family>
      <!-- 切罗基文:ꮳꮧꮢ ᨣ -->
      <family>Noto Sans Cherokee</family>
      <!-- 老傣仂文:ᨣ -->
      <family>Noto Sans Tai Tham</family>
      <!-- 安纳托利亚象形文字:𔘓 -->
      <family>Noto Sans Anatolian Hieroglyphs</family>
      <!-- 马姆穆文补充:𖤍  -->
      <family>Noto Sans Bamum</family>
      <!-- 图标字体(PUA): -->
      <family>OperatorMonoSSmLig Nerd Font</family>
      <!-- 巴塔克文:ᯤ -->
      <family>Noto Sans Batak</family>
      <!-- 古北欧文:ᛋᛖᚱᚣᚨᛚᚳᚨᚾᛞᛚᛖ -->
      <family>Noto Sans Runic</family>
    </prefer>
  </alias>
</fontconfig>

这个配置的意思是,把这些字体的优先级提高一些。当使用 fontconfig 的程序要显示字符的时候,它会指定一个模式,匹配到一个字体列表。渲染文字的时候,就可以遍历这个列表,直到找到可以显示这个字符的字体,所以一般来说,只要系统上装了对应字符的字体,它就能显示出来。

但是 Qt 额外地需要这个配置,因为 Qt 只会检查列表中的前255项。而世界上的不同文字那么多,所以想要能够显示它们,就得有一堆字体。比如 noto-fonts 这个包里就有614个字体文件,远超 Qt 支持的数量。总有些奇奇怪怪的文字被网友用来当颜文字,或者挂在名字上彰显个性。不这么调整一下,Qt 遇到了就只能「吃豆腐」了。

空心豆腐

当一个字符显示不出来的时候,那么怎么显示好呢?一般会显示成某种方框。Pango火狐会将该字符的 Unicode 码点以十六进制的形式显示在方框里边,这样虽然不知道这个字符长什么样子,但至少知道它是哪个字符,也知道多块豆腐是不是同一字符,在不能复制字符本身的时候很有用。比如当它出现在求助者的截图里的时候,比如当它出现在不能复制的地方的时候。

然而 Qt 不这样做。管你什么字符,Qt 统一显示为空心方框。从视觉上完全无法知晓它到底是什么字符,要是复制不到的话,就别想弄明白你缺什么字体了。

PS: Matrix 客户端 fluffychat 的 Web 版,使用的是 Fluffy 图形界面库,即使在 Web 版,文字渲染依然完全是自己做的。不管浏览器的设置不管系统的设置,豆腐块是带叉号的方框,还不能选中,十分讨厌。

非 BMP 字符

所有使用 UTF-16 的平台(Java、JavaScript、Windows、Qt),外加 MySQL 容易遇到的一个问题:非 BMP 字符(也就是那些 U+FFFF 之后的字符)会被当作是两个字符处理。随着 emoji 的流行,大家应该都修了不少。然而,Qt 在展示非 BMP 字符的时候,你可以选中半个字符。如果不小心漏掉半个的话,复制出来的半个字符就会变成问号(还好不是 GBK 时代那样弄乱后续所有字符)。

font features

一些字体可以通过 fontconfig 设置 fontfeatures 属性来启用(或者禁用)一些特性,比如连字,带斜杠的 0,小型大写字母,居中的中文标点,等等。Pango 很早就支持了,火狐最近也支持了,但 Qt 那边依旧没啥动静。(感谢 Coelacanthus 的评论。)

Category: Linux | Tags: linux 字体 Qt
2
2
2022
19

Wayfire 迁移进展(四):不那么 high 的 DPI

使用24寸4k屏幕作为主屏的时候很简单,设置 scale 为 2 就好了。但是,当 2 嫌太大、1 嫌太小的时候,问题就来了。比如我希望使用 120dpi,把 scale 设置为 1.25 可好?

scale=1.25 text

而这才是理想的效果:

120dpi text

看不出来差别?放大八倍,你看差别多明显:

8x compare

正常 120dpi 渲染出来的文字边缘清晰犀利,次像素平滑左红右蓝。再看看 scale=1.25 的文字,线条经常糊掉,次像素平滑效果几乎完全被抹掉。实际看上去的效果就是跟透明麿沙玻璃看屏幕似的,线条边缘总是有点糊糊的感觉,1080p 的屏幕被降级成了 720p 似的。

之所以出现这样的情况,是因为 Wayland 只支持整数倍缩放。因为,Wayland 混成器不能告诉客户端你得把窗口给画成 1.25 倍的,而客户端也无法告诉混成器我这个图像画的是 1.25 倍。所以,混成器只好告诉客户端你给我画个 2 倍的图像吧。混成器拿到图像之后再缩小 0.625 倍,自然有些逻辑像素就不能对应到单个的物理像素上去了。

所以,我还是设置 scale=1,不要混成器帮我去缩放。我自己通过另外的办法告诉客户端把字写大点儿。图标之类的就顾不上啦,反而大点小点都还能看。比如我要 1.25 倍大小的文字,就这样做:

  • GTK 3:在 dconf 里设置org.gnome.desktop.interface.text-scaling-factor=1.25就好了。最开始的截图就是 dconf-editor 里这一项配置。
  • Qt:设置环境变量 QT_WAYLAND_FORCE_DPI=120
  • Telegram:除了上边这个环境变量外,额外地在它自己的设置里设置 150% 的缩放(Telegram 的字偏小所以要设置得大一些)。设置环境变量是为了 fcitx5。
  • waybar:config 文件中设置 heightstyle.css 中设置 font-size
  • Xwayland:和 X11 下的 HiDPI 设置差不多的。比如 GTK 2 设置 Xresources Xft.dpi: 120 就好了。

我遇到的差不多就这些了。没办法,Linux 就是这么乱 QAQ。不过虽然 Wayland 协议不支持,好歹还有绕过的办法。

Category: Linux | Tags: Wayland screen 显示器 linux
12
4
2021
5

Wayfire 迁移进展(三):taskmaid, waybar 以及 mako 等

我又来更新我的 Wayfire 迁移进展啦~

我写了一个 taskmaid 工具,使用 wlr foreign toplevel management 扩展来提供窗口管理相关功能。程序自己作为 daemon 随 wayfire 启动运行,通过 D-Bus 提供接口供别的程序使用。你问我为什么不直接每个需要的程序直接使用 Wayland 协议?因为用起来麻烦呀。Wayland 提供信息的方式是一组一组的事件,也并没有高层次的库,处理起来只能一堆回调怼上去,相当不顺手。

它最主要的功能就是在 waybar 上标题当前窗口的标题啦。顺便加上了中键关闭和显示 app-id 的功能。应用程序图标因为没有办法准确匹配(比如火狐 nightly 版本的 app-id 也是 firefox),所以没有做。

其次是获取当前活动窗口所在的显示器接口名称。我使用这个名称来判断我是不是位于 E-ink 屏幕上,并为终端、Vim、skim、mutt 等工具使用专门适配过的亮色主题。这个方案比之前在 X11 下使用当前鼠标坐标来判断要灵活一些。当然更灵活的方案是匹配显示器的名称啦,这个数据 Wayfire 的 Wayland 协议里是有提供的,有需要的话我再做。Wayland 并没有一个协议来获取当前鼠标或者键盘焦点所在的显示器信息,所以我只好用窗口管理协议来跟踪活动的窗口了。

顺便还做了个 lswin 工具,列出打开的窗口信息。工作区太多啦,我又喜欢最大化,有时候会有窗口忘了关。甚至偶尔我还会不小心把窗口最小化了,然后没有办法给恢复回来……如果以后还经常出现这种情况,我再给 taskmaid 加一个恢复最小化窗口的功能好了。

除了使用 taskmaid 显示标题之外,我还加了个显示 AQI 的小脚本。以及之前忘记加网速指示器了,现在也加回来了。不得不说 waybar 比 Awesome 的那个顶栏要好配置得多。不仅不限语言,而且不是非得用定时器,可以在信息变动的时候及时更新信息,没变动就不浪费资源获取重复的信息了。我的 waybar 配置都在这里

我给 Wayfire 发了一个 pull request,添加了最基础的快捷键禁制器的支持,可以在 spicy 等软件中屏蔽 Wayfire 自己的快捷键了,大大方便了我对 Wayfire 和 Sway 的测试。当然也可以用于 VNC 啦。不过并没有像 Sway 那样加选项、支持主动禁用快捷键等功能,短期内我也不太可能会去加这个。原本我是想着在 Wayfire 里再跑一个 Wayfire 或者 Sway 啥的,但考虑到配置方面的问题,还是拿虚拟机隔离了比较好。

最新的 Wayfire 版本已经支持切换到之前的工作区啦~

桌面通知程序 mako,之前遇到两个问题。一是通知经常是糊的,没有适配 HiDPI 屏幕,而鼠标指针则一直都是又糊又大。我给修了,虽然我其实有点不知道我是怎么修好的……总之是整理了一下代码,为了减少调试日志而减少了与 Wayland 混成器的通讯,也变得更高效了。会记住上次显示使用的缩放倍率,所以不会像之前那样一出来是糊的、下一刻才会变清晰了。在 Wayland 协议里,客户端可以获知有哪些显示器、大小和缩放倍率如何,但是客户端并不能提前知道自己将会显示在哪个显示器上(倒是能够指定显示在哪个显示器上),只有显示出来之后才知道,然后做调整后再更新一下……

另一个问题是,在 Wayfire 下 mako 在全屏时不会显示通知,会被盖住。设置 layer=overlay 之后倒是能在全屏时显示通知了,但是它也会在 swaylock 锁屏界面上显示……后来了解到 mako 有模式这么个特性,我就在锁屏的时候切换到专为锁屏设置的模式下,解锁之后再切回来。反正锁屏是一句命令,自己拿脚本包一下就好了。

最近新的桌面环境稳定下来了,倒是没有再遇到更多的 bug 了。Spicy 会在里边的虚拟机跑特效动画时经常报个「Gdk-Message: Error flushing display: 资源暂时不可用」错误然后退出,我给它用 try_until_success 包了一下倒是问题不大。我也发现了不光是 Wireshark,也有 GTK 程序弹出菜单会显示在错误的位置。

那么就酱~

Category: Linux | Tags: Wayland wayfire
11
20
2021
14

Wayfire 迁移进展(二):Xwayland HiDPI 以及 waybar

这几天完成了一个很重要的功能:我让 Xwayland 支持 HiDPI 了!

实际上让 Xwayland 支持 HiDPI 的补丁早就有了,但是我当时尝试的时候补丁并不能很好地应用,我手动修了修,不明不白地应用上之后,并没有能够正常使用。现象是,DPI 是对了,但是窗口大小会不断地缩为原来的四分之一(长宽都减半),全屏时也只占左上角的四分之一。

这几天我给 xorg-xwayland 打上了新版补丁,然后在一番理解之后,给 wlroots 写好了相应的补丁,现在 Xwayland 终于也可以看清晰了!

这两个补丁,xwayland 那边是通过一个 X 窗口属性来设置缩放倍数,然后 xwayland 会告诉混成器自己的窗口使用了对应的缩放倍数,这样混成器就不会当它不支持缩放、强行给拉伸一下了。当然还有输入坐标的转换之类的。缩放倍数为 2 时,X 客户端会看到之前两倍的显示大小,并且 X 使用的坐标是 Wayland 这边的两倍,所以 Wayland 的输入事件从 X 服务器传给 X 客户端的时候需要乘以 2。

混成器这边,需要了解客户端传过来的 X 坐标和 Wayland 坐标不再相同,需要进行相应的转换。没有进行转换的结果就是,客户端告诉混成器说自己是 1024x1024 的窗口大小,然后实际上创建出来是 512x512 的。混成器再告诉客户端你现在只有这么大,然后客户端说好,我调整一下。于是又变小了……

两个补丁打上之后,由于头文件有变化,混成器可能需要重新编译一遍。然后按 X 的方式设置 HiDPI,比如设置 Xft.dpi: 192 或者 winecfg 里设置 dpi 为 192。如果有运行于 Xwayland 的 GTK 3 程序,也要设置 GDK_SCALE=2 GDK_DPI_SCALE=0.5。然后执行以下命令设置 X 属性,让 Xwayland 做相应调整:

xprop -root -format _XWAYLAND_GLOBAL_OUTPUT_SCALE 32c -set _XWAYLAND_GLOBAL_OUTPUT_SCALE 2

接下来就能愉快地 wine 和 gimp 啦~

这几天做的另一个比较大的动作是配置好了 waybar。我也不知道这个组件叫什么好,很多地方都叫面板(panel),i3 / sway 那边直接叫 bar。它现在经常出现在屏幕顶部所以我也有时叫它顶栏。总之就是显示窗口信息、系统托盘和状态指示器啥的那一条。

我是从它自带的配置文件修改的,但是风格给完全改掉了。原本的风格是一块一块的彩色背景的字,我嫌太过显眼,经过一番调整之后,给改成了黑底上的彩色字,跟我原来的 Awesome 差不多,也和我显示器的黑边挺配的。指示器的放置也是差不多的。十分遗憾的是并没有合适的窗口列表小部件可用。它自带的那个 wlr/taskbar 会把所有工作区的窗口全部显示出来,很挤。而且不知道为什么,一旦加上它之后就最小要占用 34px 的高度,太占空间了,所以作罢。我打算以后自己实现一个,现在就拿正在播放的媒体凑一下吧。它现在长这样:

我的 waybar

左边就是使用 playerctl 做的媒体信息。左键可暂停播放,滚轮切歌。字的颜色是和窗口边框匹配的。

中边留着给窗口标题。

右边依次是:

  • idle 禁制器。点一下眼睛亮起来,禁用无活动时自动休眠。
  • CPU 和 load 信息。
  • CPU 温度。太高了会变红。
  • 内存使用率。
  • 电池信息。充满电又插着电源线,它就隐藏起来了。预期充电或者使用的时候图标会出现,并且可以看到剩余时间。如果在放电并且电量低,应该会变红并且闪烁。这设定跟我 Awesome 的那个一样,不知道等到什么时候才能用上,到时候才能看到效果了。
  • 音量。左键单击是切换静音,滚轮调节音量。图标会显示设备类型(我这里有内建、HDMI(实际上是走的 DP)、蓝牙三种)。麦克风静音的时候也会显示图标。
  • 系统托盘。它终于可以在多个屏幕上同时显示啦~
  • 时钟。

我最终还是用上了那个包名为「otf-font-awesome」的图标字体。

waybar 比 Awesome 的 bar 好配置多了。很方便使用外部程序来定义。也不一定要用定时器。程序可以一直跑着,一行一条状态更新,就不需要在「更新不及时」和「更新太频繁消耗资源、干扰调试」之间抉择了。

我把 wayfire、waybar 以及其它一些东西的配置文件上传到 GitHub dotconfig 仓库了。XDG 标准路径挺好的。

还有一些小的更新——

壁纸我使用了 swaybg,因为它支持不同显示器使用不同的壁纸,这样我的 e-ink 墨水屏就可以独享纯白壁纸了~或者什么时候我专门找张合适的黑白壁纸也挺好的。

fcitx5 输入条的文字 padding 太大。鉴于我现在日常使用 Wayland 了,我改了我这个主题,适配 Wayland。X 那边也没有太难看。

fcitx5-paste-primary 已经添加了 Wayland 支持,虽然实现很不优雅……

看图软件,使用 imv(同时支持 Wayland 和 X)取代了 sxiv。许多图片要预览的话,thunar 或者 geeqie 也挺好的。

以及一些已经报告的 bug:

还有一些未报告和未调查清楚的 bug,等事情明了之后我再更新啦~

Category: Linux | Tags: Wayland wayfire

| Theme: Aeros 2.0 by TheBuckmaker.com