实用Linux桌面系统定制化方案

 技术  Linux 󰈭 5473字

前前后后折腾自己使用的这套Linux桌面系统也有一年多的时间了, 但是由于其中涉及的事务纷繁细琐, 因而一直没有专门整理过一篇内容对其做系统的介绍. 不过由于这个周末又重构了之前的一套账单处理工具和一些常用脚本, 一下子便感觉到目前系统和功能服务已经愈发复杂起来了, 可以考虑对其做一个简单的介绍以便更好的传教(不是)

桌面系统和Vim的配置文件可以在 rqdmap/dotfiles: Those make my Linux unique. 看到.

桌面系统

桌面系统是最开始折腾的环节, 也是我目前依赖最深的功能组件之一. 我的需求是尽量使用纯键盘的方式操作桌面组件, 这并不是开历史的倒车, 只是因为对于编码人员来说使用纯键盘在不同的桌面窗口之间移动的舒适感和幸福感要远远大于鼠标+键盘结合的方式.

可以想象, 一个窗口开着tex或markdown写笔记, 一个窗口开着pdf/markdown阅读器用于实时查看笔记效果, 再一个窗口开着chrome查阅资料, 可能写几句就需要腾出右手去摸索鼠标的位置找到光标再去拖动chrome网页或者阅读器的位置, 然后再将手移动回键盘的正确位置.. 这个场景是我最开始遇到的痛点, 因而痛定思痛, 转向了平铺式窗口管理器的怀抱.

平铺式桌面总览

如果使用过macos上的yabai平铺桌面管理器 + 相关的 status bar + 快捷键工具就知道Linux上的这套系统到底有多爽快了

目前桌面系统的配置为:

  • WM: bspwm, 平铺式窗口管理器

    • 配置文件.config/bspwm/bspwmrc作为启动整个系统的入口文件, 其中塞了一些希望开机启动的指令等.
  • Hotkey: sxhkd, 轻量级快捷键工具

    • 快捷键工具, 不仅可以用于处理 bspc 桌面移动指令, 还加了一些诸如启动终端, 截屏录屏等快捷键.
  • Status bar: polybar, 状态栏

    • 非常好的状态栏, 不仅对bspwm的多窗口支持效果好, 还能自己塞一些脚本(比如电池预期耗尽时间等)进去; 目前 polybar 的每一个显示组件的样式配置都是一个个审查后配置的.
  • Launcher: rofi, 应用启动器

    • 非常好的启动管理器, 不仅支持应用启动, 还魔改了一下支持自定义的脚本功能, 使用Ctrl + Enter以及 Ctrl + Alt + Enter 即可召出, 十分方便
    平铺式桌面总览
  • Compositor: picom, 混成器

    • 可以用来渲染一些平滑过度, 阴影效果, 透明度等.. 这个组件对我来说的主要用处还是透明度, 因为bspc原本的预选功能是纯色方块, 非常的丑, 使用混成器才能变成透明的; 至于平滑过度这种功能试用了一下还是删掉了, 总有一种迟钝感, 甚至看久了要晕3D了..

这一部分大概是去年定制化ArchLinux的初步工作, 大概耗时一两个月.. 原因在于其中配置文件居多, 比较需要反复尝试来符合审美, 另外bspwm配置一旦失误系统就会直接起不来, 此时则可能需要回滚到kde环境上做修复.. 另外这段时间还有nvidia显卡驱动周期性抽风, 加上我的显示屏也周期性抽风, 使得调试工作变得困难重重扑朔迷离..

参考阅读:

Neovim 编辑器

三月份的时候印象里和师兄忙毕设做了一会, 做完后的一个多月左右尽情折腾了自己的 Neovim 配置, 虽然本质上只增加了补全功能, 但是这其实是一个非常深的科技, 在此之前的诸如lsp, snippet等等前置科技必须全部研究完才能点. 因而在配置这块功能的时候真的是惨淡经营, 不仅对原本的 neovim 的配置文件全部做了 lua 化, 而且将配置文件的结构也模块化, 并几乎对于每一个插件都去看了他的相关文档并配置为自己喜欢的模样..

以至于画了张模块示意图后就实在懒得去对每个插件的内容和配置做解说了: Neovim插件管理与配置 - rqdmap | blog

在此期间可以说是熟练 lua 编程了, 并读了很多 lua 代码 (因为文档质量良莠不齐甚至严重过期), 以至于那段时间写别的代码都有点语法熟悉不过来..

总的来说目前的 Neovim 基本满足我的需求, 其可以做到:

  • 优美统一的 Gruvbox 外观

  • snippet 片段支持

  • 文件管理器支持

  • LSP支持:

    • 代码补全

    • 代码跳转

    • tagbar功能

    • 静态检查

    • 针对不同语言的独特功能, 如LaTeX的自动编译, markdown的实时渲染等

  • 自定义的统一快捷键体验

  • 各类自己实现的小的lua脚本工具等

LGTM! 只要是文字输入型的工作, 我已经完全离不开Neovim了:)

实用工具

trash 软删除功能

参考: trash:取代危险的rm - rqdmap | blog

这个指令比想象中的用的要多, 目前大部分的删除都是使用的trash(因为内嵌到了joshuto的快捷键中), 虽然在实现了这个功能后还没有出现trash立功的情况, 但是不管怎么说让人更加安心了一些.

不过这个工具的问题还是比较多的, 一个是兼容性问题, 不能开箱即用, 因为.trash目录不一定存在(maybe加一个自动 mkdir 会好一点); 另一个是效率问题, 特别是跨磁盘分区时的trash指令将进行一次缓慢的mv.. 嘛总的来说凑活用(

GIF录屏功能

最开始希望有个简单的能直接录gif的工具, 但是调查了几个常用的都不太好用, 要么太重了要么效果不好.

后来发现 phisch/giph 这个项目很轻量级同时很满足我的需求, 就拿来用了, 不过发现其在多录屏同时进行时有点问题.. 就提出了人生第一个pr: Make giph stop all previous recordings properly by rqdmap · Pull Request #26 · phisch/giph

因为一直到最后也没有merge.. 就一直也用的自己fork的版本了: rqdmap/giph: simple video (gif, webm, mp4) recorder

同时还额外做了一层wrap, 加入了 screenkey 可以同时显示键盘敲击的字符功能. screenkey需要自己指定展示窗口的大小, 因而写了简单的脚本动态取当前窗口的位置, 生成一个位于底部的screenkey展示窗口, 相关代码如下:

  1#!/bin/bash
  2
  3regex='bash.+\/record\s*'
  4
  5if [ $(pgrep -c -f $regex) -gt 2 ]; then
  6    exit
  7fi
  8
  9for pid in $(pgrep -f $regex); do
 10    if [ $pid -eq $$ ]; then
 11      continue
 12    fi
 13
 14    kill -USR1 $pid
 15    exit 0
 16done
 17
 18
 19handled=false
 20function handle_usr1() {
 21    echo "Received SIGUSR1"
 22    if [[ $handled == true ]]; then
 23        return
 24    fi
 25    handled=true
 26
 27    if [[ $screenkey_pid ]]; then
 28        kill $screenkey_pid
 29    fi
 30    if [[ $giph_pid ]]; then
 31        kill -USR1 $giph_pid
 32    fi
 33    wait
 34    exit
 35}
 36
 37trap handle_usr1 SIGUSR1
 38
 39
 40usage() {
 41    cat << EOF
 42SYNOPSIS:
 43    record [OPTIONS] 
 44
 45OPTIONS:
 46    -h, --help          Show help and exit
 47    -r, --record        Record current window
 48    -f, --full          Record full screen
 49    -k, --key           Display key strokes
 50    -o, --output        Output file
 51EOF
 52}
 53
 54while [[ "$1" == -* ]];
 55    do case "$1" in
 56        -h|--help)
 57            usage
 58            exit 0
 59            ;;
 60        -r|--record)
 61            record="-r"
 62            ;;
 63        -f|--full)
 64            FULL="-f"
 65            ;;
 66        -k|--key)
 67            KEYSTROKE="-k"
 68            ;;
 69        -o|--output) OUTPUT="$2"
 70            shift
 71            ;;
 72        *)
 73            echo "Invalid option: $1"
 74            usage
 75            exit 1
 76            ;;
 77    esac
 78    shift
 79done
 80
 81if [[ -z "$OUTPUT" ]]; then
 82    OUTPUT=~/Videos/giph/`date +"%s"`.gif
 83fi
 84
 85
 86if [[ -z "$FULL" ]]; then
 87    x=$(xdotool getwindowgeometry $(xdotool getwindowfocus) | grep Position | awk '{print $2}' | cut -d ',' -f 1)
 88    y=$(xdotool getwindowgeometry $(xdotool getwindowfocus) | grep Position | awk '{print $2}' | cut -d ',' -f 2)
 89    width=$(xdotool getwindowgeometry $(xdotool getwindowfocus) | grep Geometry | awk '{print $2}' | cut -d 'x' -f 1)
 90    height=$(xdotool getwindowgeometry $(xdotool getwindowfocus) | grep Geometry | awk '{print $2}' | cut -d 'x' -f 2)
 91else
 92    x=0
 93    y=0
 94    width=$(xdotool getdisplaygeometry | cut -d ' ' -f 1)
 95    height=$(xdotool getdisplaygeometry | cut -d ' ' -f 2)
 96fi
 97
 98border=10
 99width=$(echo "$width+6" | bc)
100height=$(echo "$height+6" | bc)
101
102geometry=${width}x${height}+${x}+${y}
103
104# Use bash command to extract position and size of the current window.
105if [[ $KEYSTROKE ]]; then
106    margin=0.05
107
108    _x=$(printf "%.0f" $(echo "$x+$width*$margin" | bc))
109    _width=$(printf "%.0f" $(echo "$width*(1-2*$margin)" | bc))
110
111    _height=60
112
113    if [[ -z "$FULL" ]]; then
114        _y=$(printf "%.0f" $(echo "$y+$height-$_height-10" | bc))
115    else
116        _y=$(printf "%.0f" $(echo "$y+$height-$_height-50" | bc))
117    fi
118
119    # echo "x: $_x, y: $_y, width: $_width, height: $_height"
120
121    screenkey -p fixed -g ${_width}x${_height}+${_x}+${_y} --opacity 0.5 --bak-mode \
122    baked --persist --key-mode composed --bak-mode normal --mods-mode tux -f    \
123    "Iosevka Term Heavy 6" --compr-cnt 3 --font-color white --bg-color black & 
124
125    screenkey_pid="$!"
126fi
127
128sleep 0.3
129
130if [[ $FULL ]]; then
131    giph -y $OUTPUT &
132    giph_pid="$!"
133elif [[ $record ]]; then
134    giph -g $geometry -y $OUTPUT &
135    giph_pid="$!"
136fi
137
138wait

录制功能绑定了一个快捷键, 按下后即可直接录制当前窗口大小的gif并自动生成.

禁用笔记本键盘

一个简单且使用的小脚本, 用于禁用/启用笔记本内置键盘.

当外接键盘后想把他架在笔记本原本的键盘上时可以使用, 防止内置键盘的误触.

1#!/bin/zsh
2
3id="AT Translated Set 2 keyboard"
4shit=$(xinput list-props "$id")
5enabled=$(echo $shit  | grep 'Device Enabled'  | cut -d ':' -f 2)
6enabled=$(echo "1-$enabled" | bc)
7xinput set-prop $id "Device Enabled" $enabled

独显禁用

使用bbswitch模块, 向/proc/acpi/bbswitch写入off即可. 需要root权限.

不能直接使用shell脚本, 可以考虑使用C语言的fopen等文件操作执行写入, 编译成可执行程序后再加上suid即可以非root权限执行

参考:

btrfs系统快照

通过btrfs的系统定期快照功能; 但是如何定期send到远端还没处理好, 因为frp好像有连接不稳定的问题, 可能需要提issue/pr看下源码.. 其实是稳定出现btrfs send & recv 不稳定的情况, 很怪

参考: Mac(x86派)的废物再利用 - rqdmap | blog

frp 自动代理功能

frp自动代理分为两部分, 一部分是实时上传本机的局域网ip, 第二部分是修改路由表做端口聚合.

  • 实时上传本机的ip并启动frp server; 不使用ddns是因为他的传播更新有点慢, 所以考虑直接用自己服务器nginx做个代理给出ip地址就行了:

    1#!/bin/zsh
    2
    3set -e
    4$(dirname $0)/upload_ip/main.sh &
    5frps -c /data/rqdmap/Applications/frp/frps.ini

    处理ip上传子任务的脚本:

    1#!/bin/zsh
    2# main.sh
    3here=$(dirname $0)
    4ip=$(python3 $here/main.py)
    5
    6if [[ $ip =~ ^10\. ]]; then
    7	echo $ip > $here/ip
    8	scp $here/ip root@ssh.rqdmap.top:/root/docker/utils/ip
    9fi

    局域网ip探测脚本:

     1# main.py
     2import socket
     3
     4def get_inner_ip():
     5    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
     6    s.connect(('8.8.8.8', 80))
     7    ip = s.getsockname()[0]
     8    s.close()
     9    return ip
    10
    11print(get_inner_ip())
  • 端口聚合, 由于并不能保证自己随时处在校园网环境下, 因而写了个简单的端口聚合脚本, 具体参考: Mac(x86派)的废物再利用 - rqdmap | blog

clash 更新脚本

为了防止由于代理本身的问题导致连接错误而无法更新代理, 需要把对https的代理先取消掉.

随后从远端拉取配置文件, 并修改其对应的端口内容, 与 frp 端口聚合所默认的本地端口一致.

1#!/bin/zsh
2
3set -e
4unset https_proxy
5target='/home/rqdmap/.config/clash/config.yaml'
6curl "https://sub.xxx.xxxx/xxxxxxxxx" -o $target
7sed -i 's/^\(port: \).*/\18901/' $target
8sed -i 's/^\(external-controller: \).*/\1:9091/' $target

fcitx 词库迁移工具

手机上我使用 gboard 输入法, 但是其原生词库非常的狗屎, 因而考虑将本机的 fcitx 词库迁移进去, 本机不仅有个性化词库, 还有arch官网下载的萌娘百科词库, 让人高兴.

不过问题是fcitx词库中存的是全拼方案, 但是 gboard 好像不能直接在双拼方案中识别这些, 必须转成双拼编码才能识别… 因而提出将全拼编码转为双拼编码的需求..

具体的词库转换代码就不放了, 写的比较随意, 主要复杂的工作是编码方案, 贴在这里传之其人.

  • 编码方案是自己在wikipedia上扒了一张汉语拼音组合表 与 [a-z]*[a-z] 字符集取交集后再人工筛一遍跑一遍自己的词库得到的, 因而可能漏一些我从来没打过的发音.

activity-watch 活动监视器

很好的开源监控工具, 其组织架构是一个server+多个watcher组成, 我这里感觉官方给的几个就足够用了(afk, window, vim), 就没有折腾一些别的定制化watcher

可以详细地看到自己各项活动, 数据保存在本地. good

不过有几个缺点:

  • 几个watcher会检查自己的parent是否被杀死(通过查看自己进程的父亲的是1来判断), 这个设计的初衷应该是带ui界面的一体化aw工具套件的设计, 不过对于我这种纯cli的就很狗屎了

  • 一些前端提示关不掉, api接口好像有问题

  • Xlib本身有问题, 需要patch

优点是aw完全开源, 可以自己动手丰衣足食:

 1#!/bin/zsh
 2
 3source ./venv/bin/activate
 4
 5git clone --recursive https://github.com/ActivityWatch/activitywatch.git
 6pushd activitywatch
 7
 8# 更新工具链
 9rustup update
10pushd aw-server-rust
11cargo update
12popd
13
14# 使用node18
15source /usr/share/nvm/init-nvm.sh
16nvm use v18
17for subdir in $(ls -d */) ; do
18	pushd $subdir
19	poetry env use 3.9
20	popd
21done
22
23make clean_all
24make uninstall
25
26rm -r aw-server aw-qt
27make build
28
29popd
30
31# 修改xlib的BUG:
32# 0.32 throws AttributeError: 'BadRRModeError' object has no attribute 'sequence_number' · Issue #241 · python-xlib/python-xlib
33# https://github.com/python-xlib/python-xlib/issues/241
34patch -p1 < ./xlib.patch
35patch -p1 < ./aw_watcher_afk.patch
36patch -p1 < ./aw_watcher_window.patch
37
38pushd activitywatch
39
40# 注释掉未分类提示
41sed -i 's/^/<!-- /; s/$/ -->/' aw-server-rust/aw-webui/src/components/UncategorizedNotification.vue
42
43make build
44
45make package
46
47rm -rf ~/Applications/activitywatch
48mv dist/activitywatch ~/Applications/
49
50popd
51deactivate

几个patch在: aw_watcher_afk.patch aw_watcher_window.patch xlib.patch

joshuto 文件管理器

joshuto 是更快的 ranger 系列的文件管理器, ranger有些场景真的太慢了, 不过 joshuto 的问题也有不少, 好在owner维护的很及时, 我还有幸合并过几次pr:

原生的joshuto不支持图片预览的, 作者说原因是没有优雅的办法兼容多系统.. 好吧.. 不过有人提了pr用于提供前后的hook函数, 可以让使用者根据自己的系统自己配置, Linux上有ueberzug工具可以处理, 也算是有了解决办法:

  • joshuto.toml配置中加入hook的位置:
 1$ grep on_preview
 2joshuto.toml
 332:preview_shown_hook_script = "~/.config/joshuto/on_preview_shown"
 433:preview_removed_hook_script = "~/.config/joshuto/on_preview_removed"
 5
 6$ cat on_preview_removed
 7#!/usr/bin/env bash
 8test -z "$joshuto_wrap_id" && exit 1;
 9remove_image
10
11$ cat on_preview_shown
12#!/usr/bin/env bash
13test -z "$joshuto_wrap_id" && exit 1;
14
15path="$1"       # Full path of the previewed file
16x="$2"          # x coordinate of upper left cell of preview area
17y="$3"          # y coordinate of upper left cell of preview area
18width="$4"      # Width of the preview pane (number of fitting characters)
19height="$5"     # Height of the preview pane (number of fitting characters)
20
21
22# Find out mimetype and extension
23mimetype=$(file --mime-type -Lb "$path")
24extension=$(/bin/echo "${path##*.}" | awk '{print tolower($0)}')
25
26case "$mimetype" in
27    image/png | image/jpeg)
28        show_image "$path" $x $(echo 1+$y | bc) $width $height
29        ;;
30    *)
31        remove_image
32esac
  • 在zsh配置中再加一些alias, 这里还支持终端直接cd到joshuto退出的位置, 这个小功能很好用:
 1function j {
 2    local IFS=$'\t\n'
 3    local tempfile="$(mktemp -t tmp.XXXXXX)"
 4    local cmd=(
 5		$HOME/.zsh/joshuto.sh
 6		--output-file $tempfile
 7	)
 8    ${cmd[@]} "$@"
 9    if [[ -f "$tempfile" ]] && [[ "$(cat -- "$tempfile")" != "$(echo -n `pwd`)" ]]; then
10        cd -- "$(cat "$tempfile")" || return
11    fi
12    command rm -f -- "$tempfile" 2>/dev/null
13}
  • $HOME/.zsh/joshuto.sh 长这样:
 1#!/usr/bin/env bash
 2
 3if [ -n "$DISPLAY" ] && command -v ueberzug > /dev/null; then
 4    export joshuto_wrap_id="$$"
 5    export joshuto_wrap_tmp="$(mktemp -d -t joshuto-wrap-$joshuto_wrap_id-XXXXXX)"
 6    export joshuto_wrap_ueber_fifo="$joshuto_wrap_tmp/fifo"
 7    export joshuto_wrap_pid_file="$joshuto_wrap_tmp/pid"
 8    export joshuto_wrap_preview_meta="$joshuto_wrap_tmp/preview-meta"
 9    export joshuto_wrap_ueber_identifier="preview"
10
11    function start_ueberzug {
12	mkfifo "${joshuto_wrap_ueber_fifo}"
13	tail --follow "$joshuto_wrap_ueber_fifo" | ueberzug layer  --parser bash &
14	echo "$!" > "$joshuto_wrap_pid_file"
15	mkdir -p "$joshuto_wrap_preview_meta"
16    }
17
18    function stop_ueberzug {
19	ueberzug_pid=`cat "$joshuto_wrap_pid_file"`
20	kill "$ueberzug_pid"
21	rm -rf "$joshuto_wrap_tmp"
22    }
23
24    function show_image {
25	>"${joshuto_wrap_ueber_fifo}" declare -A -p cmd=( \
26		[action]=add [identifier]="${joshuto_wrap_ueber_identifier}" \
27		[x]="${2}" [y]="${3}" \
28		[width]="${4}" [height]="${5}" \
29		[path]="${1}")
30    }
31
32    function remove_image {
33	>"${joshuto_wrap_ueber_fifo}" declare -A -p cmd=( \
34	    [action]=remove [identifier]="${joshuto_wrap_ueber_identifier}")
35    }
36
37    function get_preview_meta_file {
38	echo "$joshuto_wrap_preview_meta/$(echo "$1" | md5sum | sed 's/ //g')"
39    }
40
41    export -f get_preview_meta_file
42    export -f show_image
43    export -f remove_image
44
45    trap stop_ueberzug EXIT QUIT INT TERM
46    start_ueberzug
47    # echo "ueberzug started"
48fi
49
50command joshuto "$@"

Vim化的工具套件

zathura + Vimium(Chrome插件) 结合 bspwm + Neovim 真的是绝好的一致性体验

博客实用工具

其实主要用处是创建一篇新的博客, 因为使用了page-bundle, 每次都手动输入一堆实在很让人受不了, 所以就wrap了一层hugo指令:

 1#!/bin/zsh
 2
 3function help(){
 4	cat <<EOF
 5usage:
 6	blog [COMMAND] [FILE]
 7
 8Commands:
 9  new				Create a new page-bundle.
10  remove			Move page-bundle folder to trash.
11  ls				List all posts ordered by time.
12  edit				Edit post content.
13  server			Start hugo server.
14  stop				Kill all hugo server.
15  help				Display help message.
16EOF
17	exit 0
18}
19
20HUGO=$HOME/hugo-blog
21pushd $HUGO
22if [ $# -eq 0 ]; then
23	help
24	return 1
25fi
26
27while [ $# -gt 0 ]; do
28	case $1 in
29		new)
30			name=${2?"Usage: blog new <name>"}
31			shift
32			if [[ $name == 'anime/'* ]]; then
33				hugo new -k page-bundle-anime $name
34			else
35				hugo new -k page-bundle $name
36			fi
37
38			echo -ne "\033[32m[Open it?]\033[0m (y/n) "
39			read answer
40			if [ -z "$answer" -o "${answer:0:1}x" = 'yx' -o "${answer:0:1}" = 'Yx' ]; then
41				$EDITOR $HUGO/content/$name/index.md
42			fi
43			;;
44		remove)
45			name=${2?"Usage: blog remove <name>"}
46			shift
47			trash $HUGO/content/$name
48			;;
49		ls)
50			ls -tlr --color=always content/posts
51			;;
52		edit)
53			name=${2?"Usage: blog edit <name>"}
54			shift
55			$EDITOR content/$name/index.md
56			;;
57		stop)
58			pkill hugo
59			;;
60		server)
61			hugo server --buildDrafts --disableFastRender --enableGitInfo
62			;;
63		help)
64			help
65			;;
66		*)
67			help
68			;;
69	esac
70	shift
71done
72popd

nginx 展示

nginx前端看板反代本机的一些本地服务, 比如 hugo 默认的 1313, aw 的 5600 等等..

这样可以直接通过域名访问这些特殊的端口服务, 比较方便

 1...
 2	server {
 3		listen 80;
 4		server_name aw.localhost;
 5
 6		location / {
 7			proxy_pass http://localhost:5600;
 8			proxy_set_header Host localhost:5600;
 9			proxy_set_header Origin  http://localhost:5600;
10			proxy_set_header Referer http://localhost:5600/;
11
12		}
13		allow 127.0.0.1; deny all;
14	}
15...

lightdm 主题配置

dm的选择在sddm(kde默认), gdm和lightdm之间反复了很久, 期间还用过一段时间的纯cli dm, 最后还是选用了lightdm, 因为是图形化的dm所以能更好地与我的nvidia显示屏脚本配合, 另外其支持webkit, 主题也蛮好看的, 稍微魔改了一些lightdm的css元素属性后: rqdmap/lightdm-webkit-theme-litarvan: Litarvan’s LightDM HTML Theme

嗨! 这里是 rqdmap 的个人博客, 我正关注 GNU/Linux 桌面系统, Linux 内核, 后端开发, Python, Rust 以及一切有趣的计算机技术! 希望我的内容能对你有所帮助~
如果你遇到了任何问题, 包括但不限于: 博客内容说明不清楚或错误; 样式版面混乱; 加密博客密码太难了猜不出来等问题, 请通过邮箱 rqdmap@gmail.com 联系我!
修改记录:
  • 2023-12-04 02:16:08实用Linux桌面系统定制化方案
实用Linux桌面系统定制化方案