[{"content":"什么是Syncthing? 简而言之, Syncthing 是一款专门用于文件同步的开源软件. 它自主设计了一套名为 BEP (Block Exchange Protocol) 的协议, 并基于此搭建了 P2P 架构, 每一端都能独立进行文件的发送与接收.\n需要说明的是, 文件内容本身是点对点直连传输的. 但两台设备要\u0026quot;找到彼此\u0026quot;, 默认还是依赖官方维护的 discovery server (设备发现) 和 relay server (NAT 穿透失败时的中继). 这两个服务器不经手你的文件内容, 只负责牵线搭桥. 如果你对隐私要求极高, 也可以自建这两个服务, 但对大多数人来说没有必要.\n为什么要使用 Syncthing? 日常跨设备同步文件, 常见的做法无非是丢云盘或插移动硬盘倒来倒去. 但这两种方式各有各的问题: 云盘要经过第三方服务器, 大文件同步慢, 免费额度有限; 移动硬盘完全依赖人工操作, 谈不上\u0026quot;同步\u0026quot;.\nSyncthing 的优势在于:\n去中心化: 不经过任何第三方存储, 文件只在你自己的设备之间流转. 增量同步: 只传输变化的部分, 不用每次全量拷贝. 跨平台: Windows, Linux, macOS, Android 都有对应客户端. 开源免费: 没有容量限制, 也不用担心某天服务商跑路. 如果你有多台常驻设备 (比如台式机 + 笔记本 + NAS), 且不想依赖云盘, Syncthing 是一个相当稳妥的选择.\n步骤 下载: 前往 Syncthing 下载适合的架构版本, 解压或安装运行. 如果你的发行版软件源收录了 Syncthing, 也可以用包管理器安装 (比如 Ubuntu/Debian: sudo apt install syncthing). 好处是系统自带了 systemd 服务模板, 后面开机启动省事很多. 坏处是版本会落后一些. Windows 用户直接从官网下载安装包, 双击安装即可, 后续的 Web UI 操作和 Linux 完全一致, 不再赘述.\n打开Web UI: 启动后, 进入浏览器, 访问http://localhost:8384.\n添加设备: 点击\u0026quot;添加远程设备\u0026quot;, 填入另一台设备的 Device ID. Device ID 可以在对方的 Web UI 里点击右上角\u0026quot;操作 → 显示 ID\u0026quot;查到, 是一长串字符或者一个二维码. 添加之后, 需要在两台设备上分别完成这个操作 (互相添加对方), 并在对方的界面上手动接受连接请求, 双方才算配对成功.\n添加并共享文件夹: 点击\u0026quot;添加文件夹\u0026quot;, 填写文件夹路径和文件夹标签 (Folder ID). 标签是两台设备之间识别同步关系的唯一标识, 两边必须一致. 保存后, 记得在文件夹的\u0026quot;共享\u0026quot;选项卡中勾选刚才配对好的目标设备. 对方会收到一条共享邀请, 接受后选择本地存放路径, 同步才会真正开始. 只加设备不共享文件夹, 或者加了文件夹不勾目标设备, 都不会触发任何同步.\n在另一台电脑上重复上述操作即可.\n设置开机启动 Linux 通过 systemd 管理系统后台服务, 控制程序何时启动、以什么身份运行、异常退出后如何处理. Windows 则通过系统服务和任务计划程序管理. 下面按平台和安装方式分别说明.\n通过包管理器安装 (Linux) 如果你是用 apt / dnf 等包管理器安装的, 系统已经自带了 syncthing@\u0026lt;user\u0026gt;.service 模板服务, 一条命令搞定:\nsudo systemctl enable --now syncthing@\u0026lt;user\u0026gt;.service 不需要手写 unit 文件, 手动安装的读者请继续往下看.\n手动安装 (Linux) 如果你是手动下载的二进制文件, 才需要走完下面完整的流程:\n创建软链接: 有些教程在 /usr/bin/ 下创建软链接, 但那是发行版包管理器管辖的目录, 自己放文件容易和系统更新冲突. 更推荐放在 /usr/local/bin/ 下 sudo ln -s \u0026lt;你的Syncthing可执行文件的绝对路径\u0026gt; /usr/local/bin/syncthing 编辑 service 服务文件: sudo vim /etc/systemd/system/syncthing.service 粘贴如下内容: [Unit] Description=Syncthing - Open Source Continuous File Synchronization After=network.target [Service] User=\u0026lt;user\u0026gt; ExecStart=/usr/local/bin/syncthing Restart=on-failure SuccessExitStatus=3 4 RestartForceExitStatus=3 4 [Install] WantedBy=multi-user.target Restart=on-failure: 服务异常退出时自动重启.\nSuccessExitStatus=3 4 + RestartForceExitStatus=3 4: Syncthing 自动升级或手动重启时以退出码 3 或 4 退出. 不加这两行, systemd 会把正常重启当成崩溃, 触发不必要的报错.\nWantedBy=multi-user.target: 在多用户模式下启动.\n记得把 User 换成你自己的用户名, 用 root 跑 Syncthing 没有必要, 也不安全.\n重新加载 daemon: sudo systemctl daemon-reload 设置开机启动: sudo systemctl enable syncthing 手动启动: sudo systemctl start syncthing 如果是包管理器安装的, 则用 sudo systemctl start syncthing@\u0026lt;user\u0026gt;.service 启动.\n完成后, 用 sudo systemctl status syncthing (或 syncthing@\u0026lt;user\u0026gt;) 确认服务为 active (running) 状态即可.\n安装版 (Windows) Windows 安装包默认将 Syncthing 注册为系统服务. 打开 Win+R 输入 services.msc, 找到 Syncthing 服务, 确认启动类型为\u0026quot;自动\u0026quot;即可.\n便携版 (Windows) 便携版不会注册系统服务. 可以在任务计划程序中创建一条基本任务: 触发器选\u0026quot;当前用户登录时\u0026quot;, 操作选\u0026quot;启动程序\u0026quot;, 填入 syncthing.exe 的完整路径. 一般情况用安装版就够了.\n补充说明 忽略不需要同步的文件 有些文件夹或文件类型不需要也不应该被同步. 在同步文件夹根目录创建 .stignore, 按行写入忽略模式, 语法和 .gitignore 类似. 一个常见的配置如下:\n# 版本控制 .git/ # 依赖目录 node_modules/ __pycache__/ # 编译产物 *.o *.class # 临时文件 *.tmp *.swp ~* 配置后 Syncthing 会自动跳过这些路径, 不再传输和扫描.\n文件版本控制 日常使用中误删或误覆盖的情况难以完全避免. 在 Web UI 中点击对应文件夹 → 编辑 → File Versioning, 可以启用版本控制. 推荐使用 Simple File Versioning, 设置保留最近 N 个旧版本. 当文件被修改或删除时, 旧版本会被保留下来, 而不是被彻底丢弃.\n冲突处理 两台设备同时在同一个文件上做了修改, Syncthing 不会静默覆盖任意一方. 它会保留最后一次同步的版本作为基础, 另一端的修改另存为冲突副本. 副本命名格式为 文件名.sync-conflict-YYYYMMDD-HHMMSS.ext. 你发现冲突后手动核对两个版本的内容, 保留正确的, 删除多余的那个即可.\n纯局域网模式 如果所有设备都在同一个局域网内, 不需要经过公网的 discovery 和 relay 服务器. 进入 操作 → 高级 → 连接, 取消勾选 Global Discovery 和 Enable Relaying, 只保留 LAN Discovery. 这样 Syncthing 只会在局域网内广播和连接, 不走公网, 速度更快, 隐私性也更高.\n","date":"2026-07-14T14:18:28+08:00","permalink":"/mowmark_blog/computer/application/sync/000_syncthing/","title":"使用Syncthing进行文件同步"},{"content":"前言 在写好 Linux 双系统安装指南 后, 我原本想按照底层-\u0026gt;中间件-\u0026gt;高层的顺序来规划文章, 结果在写Bash, Csh 和 Zsh的时候, 发现寸步难行, 因为读者没法科学上网, 很多必要的软件都下载不了, 因此还是先把科学上网的相关内容写好.\n什么是代理? 顾名思义, 代理就是代替一个人去完成某个事情, 而在计算机领域中, 代理也是类似的道理: 代理服务器代替客户去访问某个特定域名, 以此规避某些限制, 或者获取某些资格.\n在这个过程中, 代理协议就是代理服务器和客户机的通信协议, 而代理软件则是基于这些协议开发出来的软件.\n代理协议 目前的代理协议主要分为传统代理协议和现代隧道协议. 后者相当于在增加了加密和混淆的隧道中跑传统代理.\n传统代理协议 SOCKS5 在 OSI七层模型 中, 这个协议工作在会话层 (第五层), 不关心上层数据的内容. 客户端告诉代理服务器\u0026quot;我要连接某个 IP 的某个端口\u0026quot;, 服务器建立连接后双向转发, 仅此而已. 它的特点是握手简单, 无加密, 无混淆. 但是所有现代代理软件都支持 SOCKS5 入站和出站, 是名副其实的通用接口.\nHTTP Proxy 这个协议工作在应用层, 只能代理 HTTP/HTTPS 流量. 处理 HTTPS 时用 CONNECT 方法建立隧道, 所以也叫做 HTTP Tunnel, 同样无加密.\n这两个协议只实现了基础的代理逻辑, 但是并没有增强流量转发过程中的安全性, 存在被劫持和数据泄露的风险. 因此才有后续隧道协议的诞生.\n现代隧道协议 这类协议基本都是在 SOCKS5 和 HTTP Proxy 外面包了一层加密和伪装. 它们具有两个核心设计目标:\n加密: 防止内容被识别. 混淆: 伪装流量特征, 防止被识别和封锁. 主要协议包括:\nShadowsocks (SS) 特点是极简和高效. 客户端把原始数据用预共享密钥加密后直接发给服务端, 服务端解密后转发, 没有复杂的握手过程. 加密方式经历了多次迭代, 主流使用 AEAD 加密 (如 aes-256-gcm). 后来又出了 Shadowsocks 2022 版本, 引入了基于 PSK 的会话密钥, 进一步解决了重放攻击和主动探测的问题.\nVMess (V2Ray 原生协议) V2Ray 项目的原生协议. 相比 Shadowsocks, 核心改进在于在数据传输前增加了认证环节: 客户端用一个基于时间的 UUID 生成请求头, 服务端能够验证请求的合法性, 不符合的直接丢弃. 支持 AEAD 加密和多路复用.\nVLESS 可以理解为去掉了内层加密的 VMess. VLESS 本身不负责加密, 而是把加密交给底层传输层 (TLS / XTLS / Reality) 来做, 避免双重加密带来的性能开销.\nTrojan 设计思路是拥抱标准 TLS, 把代理流量伪装成普通的 HTTPS 访问. 服务端看起来就是一个正常的 HTTPS 网站, 客户端用标准 TLS 连接, 在加密通道内传输代理数据. 外部无法区分这是代理流量还是正常的网页访问, 以此实现混淆.\nReality Trojan 的伪装依赖你自己的域名和 TLS 证书, 如果域名暴露了, 伪装就破了. Reality 更进一步: 在 TLS 握手时借用一个真实存在的知名网站 (比如微软、苹果) 的域名作为 SNI, 从外部看来这就是一个访问大厂网站的正常连接. 不需要自己购买域名和证书, 伪装程度更高.\nHysteria / Hysteria2 基于 QUIC (UDP) 协议的代理. 核心优势在于弱网环境下的表现: 利用 QUIC 的多路复用和 0-RTT 特性, 在丢包严重的网络中比基于 TCP 的协议快很多. 代价是 UDP/QUIC 在某些网络环境下可能被限速或阻断.\nTUIC 同样基于 QUIC, 设计上比 Hysteria 更轻量. 已经停止独立维护, 其实现被吸收进了 sing-box.\n当然, 如果你只是正常使用, 不打算自建节点或者深入研究, 其实没有什么必要了解每个协议的细节──现在的代理软件基本都支持多种协议, 选哪个协议更多取决于你的节点提供商支持什么.\n协议演化的主线可以概括为:\n无加密 \u0026ndash;\u0026gt; 对称加密 \u0026ndash;\u0026gt; 认证 + 加密 \u0026ndash;\u0026gt; TLS 伪装 \u0026ndash;\u0026gt; 借用 TLS \u0026ndash;\u0026gt; QUIC 优化弱网\n代理软件 核心架构 现代代理软件几乎都遵循同一个数据流模型:\ngraph LR A[\"应用程序\"] --\u003e B[\"入站 Inbound\"] B --\u003e C{\"路由引擎\"} C --\u003e|\"规则 A\"| D1[\"出站: 代理节点\"] C --\u003e|\"规则 B\"| D2[\"出站: 直连\"] C --\u003e|\"规则 C\"| D3[\"出站: 拒绝\"] D1 --\u003e E[\"远端代理服务器\"] D2 --\u003e F[\"目标网站\"]其中三个核心组件:\n入站: 定义软件以什么方式接收流量. 常见的有本地 SOCKS/HTTP 监听 (需要应用主动设置代理) 和 TUN 虚拟网卡 (接管整个系统的流量, 无需逐个应用配置). 路由引擎: 根据域名, IP, 进程名和端口等信息判断流量走哪个出站, 是实现国内直连, 国外走代理的关键. 出站: 定义流量的出口与发送协议, 可以直连, 代理或者拒绝. 不同软件的区别主要体现在: 配置格式, 支持的协议范围, 路由规则的表达能力, 以及是否支持策略组 (自动选择延迟最低的节点, 故障自动切换等).\n代理软件 代理软件不计其数, 但追溯源头, 主要是三个分支和若干个独立项目:\ngraph TD subgraph \"V2Ray 系\" direction TB V1[\"2015 · V2Ray CoreVictoria Raymond 创建\"] --\u003e V2[\"2020 · Xray Core社区 fork, 新增 XTLS/Reality\"] V1 --\u003e V3[\"v2rayN (PC客户端)\"] V1 --\u003e V4[\"v2rayA (Web 界面)\"] V2 --\u003e V3 end subgraph \"Clash 系\" direction TB C1[\"2018 · ClashGo 语言, 规则驱动\"] --\u003e C2[\"2020 · Clash Premium闭源增强版, 已停更\"] C1 --\u003e C3[\"2022 · Clash Meta开源 fork, 支持更多协议\"] C3 --\u003e C4[\"2023 · mihomoClash Meta 更名\"] C1 --\u003e C5[\"Clash for Windows已删库\"] C4 --\u003e C6[\"Clash Verge Rev\"] end subgraph \"sing-box 系\" direction TB S1[\"2022 · sing-boxSagerNet 作者新项目\"] --\u003e S2[\"Hiddify Next\"] S1 --\u003e S3[\"NekoBox\"] end subgraph \"独立项目\" I1[\"Surge (macOS/iOS)\"] I2[\"Shadowrocket (iOS)\"] I3[\"dae / daed (eBPF)\"] I4[\"NaïveProxy\"] I5[\"Brook\"] end简单说明各系的特点:\nV2Ray 系 (Xray Core)\n配置格式为 JSON, 采用多入站/多出站/路由的架构. Xray 是 V2Ray 的活跃 fork, 目前事实上已取代原版, 是 VLESS + Reality 的主要实现者. 适合需要精细控制流量走向的场景, 但配置复杂, 手写 JSON 容易出错. Clash 系 (mihomo)\n配置格式为 YAML, 核心概念是\u0026quot;策略组 + 规则\u0026quot;. 策略组可以自动测速选最快节点, 也可以手动切换. 原版 Clash 和 Clash Premium 已停更, 目前活跃的是 mihomo (原 Clash Meta). 配置直观, 社区生态成熟, 有大量现成的规则集可以直接用. sing-box 系\n配置格式为 JSON, 设计目标是融合 V2Ray 的灵活性和 Clash 的易用性. 协议支持最全面: Shadowsocks, VMess, VLESS, Trojan, Hysteria, TUIC, WireGuard, Reality 全都原生支持. 相对较新, 文档和中文教程还不如前两者丰富. 独立项目\nSurge / Shadowrocket: Apple 生态下的成熟商业客户端. dae: 基于 eBPF 的 Linux 透明代理, 性能极高, 适合路由器场景. NaïveProxy: 复用 Chromium 网络栈, TLS 指纹和 Chrome 浏览器完全一致, 抗识别能力极强但部署复杂. 内核能力对比 特性 Xray Core mihomo sing-box 配置格式 JSON YAML JSON 策略组 不原生支持 原生支持 支持 VLESS + Reality 支持 支持 支持 Hysteria 2 不支持 支持 支持 WireGuard 出站 支持 支持 支持 TUN 模式 需外部工具 原生支持 原生支持 GUI 客户端与内核的关系 很多人容易把 GUI 客户端和内核混为一谈. 实际上 GUI 客户端只是一层壳, 负责提供界面、解析订阅、管理配置, 真正干活的是底层内核.\nGUI 客户端 底层内核 平台 v2rayN Xray Windows / MacOS / Linux v2rayA Xray 全平台 (Web) v2rayNG Xray Android Clash Verge Rev mihomo Windows / MacOS / Linux Hiddify Next sing-box 全平台 NekoBox Xray 或 sing-box (可切换) Windows / Linux Shadowrocket 自有实现 iOS Stash mihomo 兼容 iOS 小结 这篇文章只做了一个总体的概括. 协议决定了数据怎么加密和伪装, 软件决定了流量怎么分流和管理, 而 GUI 客户端决定了你日常的使用体验.\n我目前主要在 Linux 和 Windows 上使用 v2rayN 和 Clash Verge Rev, Android 上主要使用 v2rayNG 和 Clash Meta For Android, 相关的内容后续会逐步更新.\n","date":"2026-06-08T00:00:00+08:00","permalink":"/mowmark_blog/computer/application/vpn/000_protocol/","title":"代理协议与代理软件"},{"content":"背景 由于工作原因, 目前使用一台笔记本进行户外部署与测试, 而台式机则留在工位上进行仿真与开发. 工位上有两台显示器, 于是产生了一个需求: 两台计算机共享显示器, 并且能在键鼠和文件传输上无缝切换.\n这篇文章记录我最终的解决方案, 包括三个部分:\n键鼠共享: 一套键盘鼠标同时控制两台计算机. 显示器输入源切换: 通过命令行或脚本切换显示器的视频输入, 不需要伸手按物理按键. 文件共享: 通过 RJ45 接口的网线在两台计算机之间共享文件. 上述三个部分都支持 Windows 和 Ubuntu 双系统, 对于其他的 Linux 发行版也有一定的参考价值.\n硬件情况 我的硬件配置如下:\n设备 CPU / 显卡 视频输出接口 台式机 AMD R7 9700X / RTX 5060 Ti 16GB DP×3 + HDMI×1 ThinkBook 14 G7+ AHP AMD Ryzen 7 H 255 / Radeon 780M Type-C×2（DP Alt Mode）+ HDMI 显示器 接口 SANC G72 Max（27 寸） DP×2 + HDMI×2 HKC S24 Pro（24 寸） HDMI×1 + VGA×1 连接方式如下:\ngraph LR A0[台式机] \u003c--DP--\u003e B0[27 寸] A0 \u003c--HDMI--\u003e B1[24 寸] A1[笔记本] \u003c--Type-C 转 DP--\u003e B0 A1 \u003c--HDMI 转 VGA--\u003e B1 A0\u003c--RJ45--\u003eA1 键鼠共享 显示协议区别 键鼠共享软件与你的桌面环境使用的显示协议有关. Wayland 对键鼠焦点捕捉的限制比 X11 严格得多, 很多在 X11 下能用的软件在 Wayland 下失灵.\n对于 Barrier, Input Leap 和 Deskflow 这几个软件, 我尝试后最终的结论是:\nX11 环境: 三个软件都能使用. Wayland 环境: 只有 Deskflow 能够正常工作. 顺带一提, 这几个软件其实是一脉相承的:\ngraph TD A[1996 · CosmoSynergySGI 内部工具] --\u003e B[2001 · SynergyChris Schoeneman 重写, GPLv2 开源] B --\u003e C[2009 · Synergy+社区 fork, 后合并回主线] C --\u003e B B --\u003e D[2018 · Barrier因 Synergy 2 商业化, 社区 fork] D --\u003e E[2021 · Input LeapBarrier 维护停滞后社区 fork] B --\u003e F[2024 · DeskflowSymless 重新以 GPL 开源 Synergy 核心,作为统一的上游项目]Deskflow 更接近原始 Synergy, 再加上是 Symless 官方维护, Wayland 适配走在了前面. 而 Barrier 和 Input Leap 已经有相当长一段时间没有更新.\n如果你不知道自己当前用的是哪种协议, 在终端中执行:\necho $XDG_SESSION_TYPE 查看结果是 wayland 或 x11 即可.\n安装和配置 安装: 只需要在 GitHub release 上下载安装包, 安装即可. 配置: 服务端: 新建虚拟屏幕, 配置相对关系, 记录右上角的建议IP, 点击启动. 客户端: 在连接到输入框中填入建议IP, 点击连接. 如果客户端连不上, 检查服务端的防火墙是否放行了 24800 端口 (Ubuntu: sudo ufw allow 24800/tcp, Windows 会有自动弹窗提示.)\n显示器输入源切换 DDC/CI 协议 前面已经接好了视频信号线, 已经能够正常使用, 只是每次切换主机时都需要伸手按物理按键, 比较麻烦. 我希望我的大部分操作可以不离开键鼠.\nDDC/CI (Display Data Channel / Command Interface) 协议能够解决这个问题. 简单来说, 这是一个让计算机通过视频接口中的 I2C 通信针脚直接控制显示器的协议, 让亮度, 对比度和输入源等信息都能从系统层面读写. 这是一个行业通用协议， 大多数显示器都支持, 不一定默认启用, 可以在显示器的 OSD 菜单中找一下\u0026quot;DDC/CI\u0026quot;开关.\n这里需要注意一个坑:\nDDC/CI 依赖 I2C 信号透传. HDMI, DP, Type-C 都能正常透传 I2C, 唯独 VGA 转接线不行, 这是因为, VGA 接口里有 DDC 针脚，但廉价转接芯片普遍不会保留它. 所以在我的方案里，24 寸显示器走 VGA 接笔记本时，无法用 DDC/CI 控制, 只能在台式机这边用 DP 控制它.\n扩展坞、HDMI 一分二转接线之类的设备同样普遍不透传 I2C.\n说完了原理, 下面分别说 Ubuntu 和 Windows 下的操作.\nUbuntu 系统 安装与权限配置 # 安装 ddcutil sudo apt update \u0026amp;\u0026amp; sudo apt install ddcutil # 加载 I2C 模块 sudo modprobe i2c-dev # 将当前用户加入 I2C 用户组 sudo usermod -aG i2c $USER 执行完注销重新登录, 让用户组生效.\n切换输入源 在 Ubuntu 系统上, 最终要执行的命令格式为:\nddcutil --bus=\u0026lt;I2C 总线号\u0026gt; setvcp 60 \u0026lt;输入源代码\u0026gt; 需要的两个参数都可以从 ddcutil 自身查到.\n第一步: 查询总线号:\nddcutil detect 输出类似:\nDisplay 1 I2C bus: /dev/i2c-7 ... Mfg id: HKC - HKC OVERSEAS LIMITED Model: S24 PRO Display 2 I2C bus: /dev/i2c-10 ... Mfg id: SAC - UNK Model: G72 Max 每个显示器的 I2C bus 后面带着的数字 (这里是 7 和 10) 就是总线号.\n第二步: 查询输入源代号\n在终端中执行:\nddcutil --bus=\u0026lt;上一步的总线号\u0026gt; capabilities 在输出结果中寻找 Feature: 60 (Input Source)这一部分:\nFeature: 60 (Input Source) Values: 11: HDMI-1 12: HDMI-2 0f: DisplayPort-1 10: DisplayPort-2 下面列出来的就是这台显示器支持的输入源代号.\n第三步: 拼接完整命令\n举个例子, 假设现在我要把 27寸显示器 (总线 10) 切到 DisplayPort-1:\nddcutil --bus=10 setvcp 60 0x0f 自动化脚本 每次手敲命令也很烦, 所以我 让 Gemini 3.1 Pro 写了个交互式脚本 SmartSwitch, 可以扫描所有显示器, 用颜色标注当前活跃的输入源, 记住每个端口接的是哪台主机, 并且一键切换.\n#!/bin/bash # ========================================== # SmartSwitch v3.0 — 带颜色状态标注的显示器输入源管理 # ========================================== # 功能： # 1. 🟢 绿色标注当前活跃输入源（自动检测） # 2. 🟡 黄色标注已连接但未活跃的输入源（手动标记） # 3. ⚪ 灰色标注未标记/未知状态的输入源 # 4. 一键切换输入源 # 5. 切换后自动验证 # # 前置要求：sudo ddcutil detect 能扫描到显示器 # ========================================== set -euo pipefail # -------------------- 配置 -------------------- CONFIG_DIR=\u0026#34;${HOME}/.config/smart-switch\u0026#34; DEVICE_FILE=\u0026#34;${CONFIG_DIR}/devices.conf\u0026#34; # 记录哪些端口有设备 # 预定义输入源名称映射 declare -A SOURCE_MAP=( [\u0026#34;0x01\u0026#34;]=\u0026#34;VGA-1\u0026#34; [\u0026#34;0x03\u0026#34;]=\u0026#34;DVI-1\u0026#34; [\u0026#34;0x04\u0026#34;]=\u0026#34;DVI-2\u0026#34; [\u0026#34;0x0f\u0026#34;]=\u0026#34;DisplayPort-1\u0026#34; [\u0026#34;0x10\u0026#34;]=\u0026#34;DisplayPort-2\u0026#34; [\u0026#34;0x11\u0026#34;]=\u0026#34;HDMI-1\u0026#34; [\u0026#34;0x12\u0026#34;]=\u0026#34;HDMI-2\u0026#34; [\u0026#34;0x1b\u0026#34;]=\u0026#34;USB Type-C\u0026#34; ) # -------------------- 颜色定义 -------------------- # 检测终端是否支持颜色（非管道/重定向时启用） if [[ -t 1 ]]; then COLOR_GREEN=\u0026#39;\\033[0;32m\u0026#39; COLOR_YELLOW=\u0026#39;\\033[0;33m\u0026#39; COLOR_GRAY=\u0026#39;\\033[0;90m\u0026#39; COLOR_RED=\u0026#39;\\033[0;31m\u0026#39; # 仅用于真正的错误/警告 COLOR_BOLD=\u0026#39;\\033[1m\u0026#39; COLOR_RESET=\u0026#39;\\033[0m\u0026#39; else # 输出到文件或管道时禁用颜色 COLOR_GREEN=\u0026#39;\u0026#39; COLOR_YELLOW=\u0026#39;\u0026#39; COLOR_GRAY=\u0026#39;\u0026#39; COLOR_RED=\u0026#39;\u0026#39; COLOR_BOLD=\u0026#39;\u0026#39; COLOR_RESET=\u0026#39;\u0026#39; fi # 图标（如果终端不支持 Unicode，回退到 ASCII） if [[ \u0026#34;${LC_ALL:-${LANG:-C}}\u0026#34; == *\u0026#34;UTF-8\u0026#34;* || \u0026#34;${LC_ALL:-${LANG:-C}}\u0026#34; == *\u0026#34;utf-8\u0026#34;* ]]; then ICON_ACTIVE=\u0026#34;●\u0026#34; # 活跃（实心圆） ICON_STANDBY=\u0026#34;○\u0026#34; # 待命（空心圆） ICON_OFF=\u0026#34;·\u0026#34; # 未连接（小点） else ICON_ACTIVE=\u0026#34;*\u0026#34; ICON_STANDBY=\u0026#34;o\u0026#34; ICON_OFF=\u0026#34;.\u0026#34; fi # -------------------- 工具函数 -------------------- to_lower() { echo \u0026#34;$1\u0026#34; | tr \u0026#39;[:upper:]\u0026#39; \u0026#39;[:lower:]\u0026#39; } ensure_config_dir() { if [[ ! -d \u0026#34;$CONFIG_DIR\u0026#34; ]]; then mkdir -p \u0026#34;$CONFIG_DIR\u0026#34; fi if [[ ! -f \u0026#34;$DEVICE_FILE\u0026#34; ]]; then cat \u0026gt; \u0026#34;$DEVICE_FILE\u0026#34; \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; # =========================================== # SmartSwitch — 已连接设备标记 # =========================================== # 格式: BUS|HEX_CODE=设备名称 # # 示例: # 7|0x0f=我的台式机 # 7|0x11=Surface Laptop # 10|0x11=Surface Laptop # # 标记后的端口在非活跃时会显示为黄色（待命状态） # 删除某行即可取消标记 # =========================================== EOF fi } # 获取设备标记 get_device_label() { local bus=\u0026#34;$1\u0026#34; local vcp=\u0026#34;$2\u0026#34; local key=\u0026#34;${bus}|$(to_lower \u0026#34;$vcp\u0026#34;)\u0026#34; local result=\u0026#34;\u0026#34; while IFS=\u0026#39;=\u0026#39; read -r k v; do [[ \u0026#34;$k\u0026#34; =~ ^[[:space:]]*# ]] \u0026amp;\u0026amp; continue k=$(echo \u0026#34;$k\u0026#34; | xargs) [[ -z \u0026#34;$k\u0026#34; ]] \u0026amp;\u0026amp; continue if [[ \u0026#34;$k\u0026#34; == \u0026#34;$key\u0026#34; ]]; then result=\u0026#34;$v\u0026#34; break fi done \u0026lt; \u0026#34;$DEVICE_FILE\u0026#34; 2\u0026gt;/dev/null echo \u0026#34;$result\u0026#34; [[ -n \u0026#34;$result\u0026#34; ]] || true } # 检查端口是否已被标记（返回 0=已标记, 1=未标记） is_port_marked() { local bus=\u0026#34;$1\u0026#34; local vcp=\u0026#34;$2\u0026#34; local key=\u0026#34;${bus}|$(to_lower \u0026#34;$vcp\u0026#34;)\u0026#34; local escaped_key=\u0026#34;${key//./\\\\.}\u0026#34; if grep -q \u0026#34;^${escaped_key}=\u0026#34; \u0026#34;$DEVICE_FILE\u0026#34; 2\u0026gt;/dev/null; then return 0 else return 1 fi } # 读取当前活跃输入源（返回 16 进制字符串如 0x0f） get_current_source() { local bus=\u0026#34;$1\u0026#34; local terse_output terse_output=$(ddcutil --bus=\u0026#34;$bus\u0026#34; getvcp 60 --terse 2\u0026gt;/dev/null || true) if [[ -z \u0026#34;$terse_output\u0026#34; ]]; then echo \u0026#34;\u0026#34; return fi # terse 格式: \u0026#34;VCP 60 \u0026lt;flags\u0026gt; \u0026lt;value\u0026gt;\u0026#34; value 以 x 开头表示16进制 (如 x0f) local hex_val hex_val=$(echo \u0026#34;$terse_output\u0026#34; | awk \u0026#39;/^VCP 60 / {print $NF}\u0026#39;) if [[ -n \u0026#34;$hex_val\u0026#34; \u0026amp;\u0026amp; \u0026#34;$hex_val\u0026#34; =~ ^x[0-9a-fA-F]+$ ]]; then echo \u0026#34;0x${hex_val#x}\u0026#34; else echo \u0026#34;\u0026#34; fi } # 标记/取消标记设备 toggle_device_mark() { local bus=\u0026#34;$1\u0026#34; local vcp=\u0026#34;$2\u0026#34; local key=\u0026#34;${bus}|$(to_lower \u0026#34;$vcp\u0026#34;)\u0026#34; local escaped_key=\u0026#34;${key//./\\\\.}\u0026#34; local current_label current_label=$(get_device_label \u0026#34;$bus\u0026#34; \u0026#34;$vcp\u0026#34;) local port_name=${SOURCE_MAP[$(to_lower \u0026#34;$vcp\u0026#34;)]:-\u0026#34;未知接口\u0026#34;} echo \u0026#34;\u0026#34; echo \u0026#34;━━━ 标记设备 ━━━\u0026#34; echo \u0026#34;端口: ${port_name} (${vcp})\u0026#34; if [[ -n \u0026#34;$current_label\u0026#34; ]]; then echo \u0026#34;当前标记: [${current_label}]\u0026#34; echo \u0026#34;\u0026#34; echo \u0026#34; 1. 修改标记\u0026#34; echo \u0026#34; 2. 删除标记（端口将变为灰色）\u0026#34; echo \u0026#34; 0. 取消\u0026#34; read -rp \u0026#34;👉 选择: \u0026#34; action case \u0026#34;$action\u0026#34; in 1) read -rp \u0026#34;输入新名称: \u0026#34; new_label sed -i \u0026#34;/^${escaped_key}=/d\u0026#34; \u0026#34;$DEVICE_FILE\u0026#34; if [[ -n \u0026#34;$new_label\u0026#34; ]]; then echo \u0026#34;${key}=${new_label}\u0026#34; \u0026gt;\u0026gt; \u0026#34;$DEVICE_FILE\u0026#34; echo \u0026#34;✅ 已更新: ${new_label}\u0026#34; fi ;; 2) sed -i \u0026#34;/^${escaped_key}=/d\u0026#34; \u0026#34;$DEVICE_FILE\u0026#34; echo \u0026#34;✅ 已删除标记（端口状态变为灰色）\u0026#34; ;; *) ;; esac else echo \u0026#34;当前状态: ⚪ 未标记（灰色）\u0026#34; echo \u0026#34;\u0026#34; read -rp \u0026#34;输入设备名称以标记此端口（如: 台式机）: \u0026#34; new_label if [[ -n \u0026#34;$new_label\u0026#34; ]]; then echo \u0026#34;${key}=${new_label}\u0026#34; \u0026gt;\u0026gt; \u0026#34;$DEVICE_FILE\u0026#34; echo \u0026#34;✅ 已标记: ${new_label}（端口状态变为黄色）\u0026#34; fi fi } # 打印带颜色的输入源条目 print_source_entry() { local index=\u0026#34;$1\u0026#34; local vcp=\u0026#34;$2\u0026#34; local bus=\u0026#34;$3\u0026#34; local current_source=\u0026#34;$4\u0026#34; local port_name=${SOURCE_MAP[$(to_lower \u0026#34;$vcp\u0026#34;)]:-\u0026#34;未知接口\u0026#34;} local color=\u0026#34;\u0026#34; local icon=\u0026#34;\u0026#34; local suffix=\u0026#34;\u0026#34; if [[ \u0026#34;$vcp\u0026#34; == \u0026#34;$current_source\u0026#34; ]]; then color=\u0026#34;$COLOR_GREEN\u0026#34; icon=\u0026#34;$ICON_ACTIVE\u0026#34; suffix=\u0026#34; ← 当前活跃\u0026#34; elif is_port_marked \u0026#34;$bus\u0026#34; \u0026#34;$vcp\u0026#34;; then color=\u0026#34;$COLOR_YELLOW\u0026#34; icon=\u0026#34;$ICON_STANDBY\u0026#34; local device_label device_label=$(get_device_label \u0026#34;$bus\u0026#34; \u0026#34;$vcp\u0026#34;) suffix=\u0026#34; [${device_label}]\u0026#34; else color=\u0026#34;$COLOR_GRAY\u0026#34; icon=\u0026#34;$ICON_OFF\u0026#34; fi printf \u0026#34;${color}%s. %s %s (%s)%s${COLOR_RESET}\\n\u0026#34; \\ \u0026#34;$index\u0026#34; \u0026#34;$icon\u0026#34; \u0026#34;$port_name\u0026#34; \u0026#34;$vcp\u0026#34; \u0026#34;$suffix\u0026#34; } # -------------------- 主循环 -------------------- ensure_config_dir # 首次运行提示 if [[ ! -s \u0026#34;$DEVICE_FILE\u0026#34; ]] || ! grep -v \u0026#39;^#\u0026#39; \u0026#34;$DEVICE_FILE\u0026#34; | grep -q \u0026#39;[^[:space:]]\u0026#39;; then echo \u0026#34;\u0026#34; echo -e \u0026#34;${COLOR_BOLD}💡 提示:${COLOR_RESET} 这是首次运行。\u0026#34; echo \u0026#34; 脚本可以自动检测当前活跃的输入源（绿色），\u0026#34; echo \u0026#34; 但需要你手动标记哪些端口连接了设备（黄色）。\u0026#34; echo \u0026#34; 进入菜单后选择「🏷️ 标记设备」即可。\u0026#34; echo \u0026#34;\u0026#34; read -rp \u0026#34;按\u0026lt;Enter\u0026gt;键继续...\u0026#34; fi while true; do clear echo \u0026#34;==========================================\u0026#34; echo \u0026#34; SmartSwitch v3.0 — 输入源状态管理\u0026#34; echo \u0026#34;==========================================\u0026#34; echo \u0026#34;🟢 活跃 🟡 待命 ⚪ 未连接\u0026#34; echo \u0026#34;==========================================\u0026#34; echo \u0026#34;正在扫描 I2C 总线...\u0026#34; # 扫描显示器 scan_result=$(ddcutil detect -t 2\u0026gt;/dev/null || true) declare -a buses=() declare -a models=() in_valid_display=false while IFS= read -r line; do line=\u0026#34;${line#\u0026#34;${line%%[![:space:]]*}\u0026#34;}\u0026#34; if [[ \u0026#34;$line\u0026#34; == \u0026#34;Display \u0026#34;* \u0026amp;\u0026amp; \u0026#34;$line\u0026#34; != \u0026#34;Invalid display\u0026#34; ]]; then in_valid_display=true continue elif [[ \u0026#34;$line\u0026#34; == \u0026#34;Invalid display\u0026#34; ]]; then in_valid_display=false continue fi [[ \u0026#34;$in_valid_display\u0026#34; != true ]] \u0026amp;\u0026amp; continue if [[ \u0026#34;$line\u0026#34; == \u0026#34;I2C bus:\u0026#34;* ]]; then bus_path=\u0026#34;${line##* }\u0026#34; bus_num=\u0026#34;${bus_path##*-}\u0026#34; buses+=(\u0026#34;$bus_num\u0026#34;) elif [[ \u0026#34;$line\u0026#34; == \u0026#34;Monitor:\u0026#34;* ]]; then mon_str=$(echo \u0026#34;$line\u0026#34; | sed -E \u0026#39;s/^Monitor:[[:space:]]+//\u0026#39;) mon_model=$(echo \u0026#34;$mon_str\u0026#34; | cut -d\u0026#39;:\u0026#39; -f2) models+=(\u0026#34;$mon_model\u0026#34;) fi done \u0026lt;\u0026lt;\u0026lt; \u0026#34;$scan_result\u0026#34; if [[ ${#buses[@]} -eq 0 ]]; then echo \u0026#34;\u0026#34; echo -e \u0026#34;${COLOR_RED}❌ 未检测到支持 DDC/CI 的显示器。${COLOR_RESET}\u0026#34; echo \u0026#34;\u0026#34; echo \u0026#34;排查建议:\u0026#34; echo \u0026#34; 1. 执行: sudo ddcutil detect\u0026#34; echo \u0026#34; 2. 检查显示器 OSD 菜单中 DDC/CI 是否开启\u0026#34; echo \u0026#34; 3. 是否使用了 DisplayLink 扩展坞？\u0026#34; exit 1 fi # --- 显示器选择 --- while true; do echo \u0026#34;\u0026#34; echo \u0026#34;==== 可用的显示器 ====\u0026#34; for i in \u0026#34;${!buses[@]}\u0026#34;; do echo \u0026#34;$((i + 1)). ${models[$i]} (Bus: ${buses[$i]})\u0026#34; done echo \u0026#34;0. 退出脚本\u0026#34; echo \u0026#34;------------------------\u0026#34; read -rp \u0026#34;👉 选择显示器 (0-${#buses[@]}): \u0026#34; monitor_choice if [[ \u0026#34;$monitor_choice\u0026#34; == \u0026#34;0\u0026#34; ]]; then echo \u0026#34;👋 再见！\u0026#34; exit 0 elif ! [[ \u0026#34;$monitor_choice\u0026#34; =~ ^[0-9]+$ ]] || \\ [[ \u0026#34;$monitor_choice\u0026#34; -lt 0 ]] || \\ [[ \u0026#34;$monitor_choice\u0026#34; -gt \u0026#34;${#buses[@]}\u0026#34; ]]; then echo \u0026#34;⚠️ 输入错误！\u0026#34; else break fi done selected_bus=${buses[$((monitor_choice - 1))]} selected_model=${models[$((monitor_choice - 1))]} # --- 获取能力列表 --- echo \u0026#34;\u0026#34; echo \u0026#34;🔍 正在读取 [${selected_model}] 的能力信息...\u0026#34; raw_caps=$(ddcutil --bus=\u0026#34;$selected_bus\u0026#34; capabilities 2\u0026gt;/dev/null || true) # 提取 Feature 60 区块 feature_60_block=$(echo \u0026#34;$raw_caps\u0026#34; | awk \u0026#39; /Feature: 60/ { flag=1; print; next } /Feature:/ { if(flag) exit } flag \u0026#39;) declare -a valid_vcps=() if [[ -n \u0026#34;$feature_60_block\u0026#34; ]]; then while read -r hex_code; do if [[ -n \u0026#34;$hex_code\u0026#34; \u0026amp;\u0026amp; \u0026#34;$hex_code\u0026#34; != \u0026#34;0x00\u0026#34; ]]; then valid_vcps+=(\u0026#34;$(to_lower \u0026#34;$hex_code\u0026#34;)\u0026#34;) fi done \u0026lt; \u0026lt;(echo \u0026#34;$feature_60_block\u0026#34; | awk -F: \u0026#39; /^[[:space:]]+[0-9a-fA-F]{2}:/ { gsub(/^[ \\t]+/, \u0026#34;\u0026#34;, $1) print \u0026#34;0x\u0026#34;$1 } \u0026#39;) fi # --- 获取当前活跃输入源 --- current_source=$(get_current_source \u0026#34;$selected_bus\u0026#34;) # --- 输入源操作循环 --- while true; do echo \u0026#34;\u0026#34; echo \u0026#34;==== [${selected_model}] 输入源状态 ====\u0026#34; echo \u0026#34;\u0026#34; # 打印图例 echo -e \u0026#34; ${COLOR_GREEN}${ICON_ACTIVE} 绿色${COLOR_RESET} = 当前活跃 ${COLOR_YELLOW}${ICON_STANDBY} 黄色${COLOR_RESET} = 已连接待命 ${COLOR_GRAY}${ICON_OFF} 灰色${COLOR_RESET} = 未标记\u0026#34; echo \u0026#34; ─────────────────────────────────────\u0026#34; # 打印每个输入源 option_index=1 if [[ ${#valid_vcps[@]} -gt 0 ]]; then for vcp in \u0026#34;${valid_vcps[@]}\u0026#34;; do print_source_entry \u0026#34;$option_index\u0026#34; \u0026#34;$vcp\u0026#34; \u0026#34;$selected_bus\u0026#34; \u0026#34;$current_source\u0026#34; ((option_index++)) done else echo -e \u0026#34; ${COLOR_RED}⚠️ 无法获取此显示器的输入源列表${COLOR_RESET}\u0026#34; fi echo \u0026#34; ─────────────────────────────────────\u0026#34; switch_opt=$option_index echo \u0026#34;${switch_opt}. 🔄 切换输入源\u0026#34; mark_opt=$((option_index + 1)) echo \u0026#34;${mark_opt}. 🏷️ 标记/取消标记设备\u0026#34; refresh_opt=$((option_index + 2)) echo \u0026#34;${refresh_opt}. 🔃 刷新状态\u0026#34; return_opt=$((option_index + 3)) echo \u0026#34;${return_opt}. 🔙 返回显示器列表\u0026#34; echo \u0026#34;0. ❌ 退出脚本\u0026#34; echo \u0026#34;------------------------\u0026#34; read -rp \u0026#34;👉 选择操作 (0-${return_opt}): \u0026#34; action_choice # --- 处理选择 --- if [[ \u0026#34;$action_choice\u0026#34; == \u0026#34;0\u0026#34; ]]; then echo \u0026#34;👋 再见！\u0026#34; exit 0 elif [[ \u0026#34;$action_choice\u0026#34; == \u0026#34;$return_opt\u0026#34; ]]; then break elif [[ \u0026#34;$action_choice\u0026#34; == \u0026#34;$refresh_opt\u0026#34; ]]; then # 刷新：重新获取当前活跃源 current_source=$(get_current_source \u0026#34;$selected_bus\u0026#34;) echo \u0026#34;✅ 状态已刷新\u0026#34; continue elif [[ \u0026#34;$action_choice\u0026#34; == \u0026#34;$mark_opt\u0026#34; ]]; then # 标记设备子菜单 echo \u0026#34;\u0026#34; echo \u0026#34;--- 选择要标记的端口 ---\u0026#34; for i in \u0026#34;${!valid_vcps[@]}\u0026#34;; do vcp=${valid_vcps[$i]} name=${SOURCE_MAP[$(to_lower \u0026#34;$vcp\u0026#34;)]:-\u0026#34;未知接口\u0026#34;} label=$(get_device_label \u0026#34;$selected_bus\u0026#34; \u0026#34;$vcp\u0026#34;) if [[ -n \u0026#34;$label\u0026#34; ]]; then echo \u0026#34;$((i + 1)). ${name} (${vcp}) → 已标记: ${label}\u0026#34; else echo \u0026#34;$((i + 1)). ${name} (${vcp}) → 未标记\u0026#34; fi done echo \u0026#34;0. 返回\u0026#34; read -rp \u0026#34;选择: \u0026#34; mark_choice if [[ \u0026#34;$mark_choice\u0026#34; =~ ^[0-9]+$ ]] \u0026amp;\u0026amp; \\ [[ \u0026#34;$mark_choice\u0026#34; -ge 1 ]] \u0026amp;\u0026amp; \\ [[ \u0026#34;$mark_choice\u0026#34; -le \u0026#34;${#valid_vcps[@]}\u0026#34; ]]; then toggle_device_mark \u0026#34;$selected_bus\u0026#34; \u0026#34;${valid_vcps[$((mark_choice - 1))]}\u0026#34; current_source=$(get_current_source \u0026#34;$selected_bus\u0026#34;) fi continue elif [[ \u0026#34;$action_choice\u0026#34; == \u0026#34;$switch_opt\u0026#34; ]]; then # 切换输入源子菜单 echo \u0026#34;\u0026#34; echo \u0026#34;--- 切换到哪个输入源？---\u0026#34; for i in \u0026#34;${!valid_vcps[@]}\u0026#34;; do vcp=${valid_vcps[$i]} name=${SOURCE_MAP[$(to_lower \u0026#34;$vcp\u0026#34;)]:-\u0026#34;未知接口\u0026#34;} label=$(get_device_label \u0026#34;$selected_bus\u0026#34; \u0026#34;$vcp\u0026#34;) if [[ \u0026#34;$vcp\u0026#34; == \u0026#34;$current_source\u0026#34; ]]; then echo \u0026#34;$((i + 1)). ${name} (${vcp}) ← 当前活跃\u0026#34; elif [[ -n \u0026#34;$label\u0026#34; ]]; then echo \u0026#34;$((i + 1)). ${name} (${vcp}) [${label}]\u0026#34; else echo \u0026#34;$((i + 1)). ${name} (${vcp})\u0026#34; fi done echo \u0026#34;$((${#valid_vcps[@]} + 1)). ✍️ 手动输入代码\u0026#34; echo \u0026#34;0. 取消\u0026#34; read -rp \u0026#34;选择: \u0026#34; switch_choice target_vcp=\u0026#34;\u0026#34; if [[ \u0026#34;$switch_choice\u0026#34; == \u0026#34;0\u0026#34; ]]; then continue elif [[ \u0026#34;$switch_choice\u0026#34; =~ ^[0-9]+$ ]] \u0026amp;\u0026amp; \\ [[ \u0026#34;$switch_choice\u0026#34; -ge 1 ]] \u0026amp;\u0026amp; \\ [[ \u0026#34;$switch_choice\u0026#34; -le \u0026#34;${#valid_vcps[@]}\u0026#34; ]]; then target_vcp=${valid_vcps[$((switch_choice - 1))]} elif [[ \u0026#34;$switch_choice\u0026#34; -eq \u0026#34;$((${#valid_vcps[@]} + 1))\u0026#34; ]]; then read -rp \u0026#34;输入 16 进制代码 (如 0x11): \u0026#34; target_vcp target_vcp=$(to_lower \u0026#34;$target_vcp\u0026#34;) else echo \u0026#34;⚠️ 无效选择\u0026#34; continue fi # 执行切换 if [[ \u0026#34;$target_vcp\u0026#34; == \u0026#34;$current_source\u0026#34; ]]; then echo \u0026#34;⚠️ 该输入源已是当前活跃状态，无需切换。\u0026#34; read -rp \u0026#34;按\u0026lt;Enter\u0026gt;键继续...\u0026#34; -n 1 -s continue fi target_name=${SOURCE_MAP[$(to_lower \u0026#34;$target_vcp\u0026#34;)]:-\u0026#34;未知接口\u0026#34;} target_label=$(get_device_label \u0026#34;$selected_bus\u0026#34; \u0026#34;$target_vcp\u0026#34;) if [[ -n \u0026#34;$target_label\u0026#34; ]]; then target_display=\u0026#34;${target_label} (${target_name})\u0026#34; else target_display=\u0026#34;${target_name}\u0026#34; fi echo \u0026#34;\u0026#34; echo \u0026#34;🚀 正在切换到: ${target_display} (${target_vcp})...\u0026#34; echo \u0026#34; 执行: ddcutil --bus=${selected_bus} setvcp 60 ${target_vcp}\u0026#34; if ddcutil --bus=\u0026#34;$selected_bus\u0026#34; setvcp 60 \u0026#34;$target_vcp\u0026#34; \u0026gt; /dev/null 2\u0026gt;\u0026amp;1; then sleep 0.5 new_source=$(get_current_source \u0026#34;$selected_bus\u0026#34;) if [[ \u0026#34;$new_source\u0026#34; == \u0026#34;$target_vcp\u0026#34; ]]; then echo -e \u0026#34;${COLOR_GREEN}✅ 切换成功！${COLOR_RESET}\u0026#34; current_source=\u0026#34;$new_source\u0026#34; else echo \u0026#34;⚠️ 指令已发送，验证读取为 ${new_source}（预期 ${target_vcp}）\u0026#34; echo \u0026#34; 如果画面已切换，可忽略（可能是响应延迟）。\u0026#34; current_source=$(get_current_source \u0026#34;$selected_bus\u0026#34;) fi else echo -e \u0026#34;${COLOR_RED}❌ 切换失败！${COLOR_RESET}\u0026#34; echo \u0026#34; 请检查：权限、DDC/CI 开关、显示器休眠状态\u0026#34; fi echo \u0026#34;\u0026#34; read -rp \u0026#34;按\u0026lt;Enter\u0026gt;键继续...\u0026#34; -n 1 -s else echo \u0026#34;⚠️ 输入错误，请重新选择！\u0026#34; fi done done Windows系统 在 Windows 系统下, 可以使用 NirSoft 出品的 ControlMyMonitor 软件进行检测与切换. 这个软件是免安装的图形界面 (GUI) 程序.\n打开之后会列出当前显示器的所有 VCP 控制项, 找到 VCP Code = 60 的一行, 双击修改当前值, 输入对应的输入源代码 (和 Ubuntu 下查到的代码一致, 没有 0x 十六进制前缀) 即可切换.\n由于这个软件本身就是图形界面, 操作直观, 我没有给它编写脚本.\n文件共享 两台计算机的位置非常接近, 直接用一根网线对连传文件, 比走无线快得多, 也不依赖路由器.\n网络配置 直连之后两台计算机不在一个由 DHCP 服务器管理的网段里, 需要手动设置静态 IP. 我的方案是把两台计算机放到 192.168.10.0/24 这个段里:\n计算机 IP 子网掩码 台式机 192.168.10.1 255.255.255.0 笔记本 192.168.10.2 255.255.255.0 笔记本如果没有 RJ45 接口, 可以使用 Type-C 转 RJ45 的转接器.\nUbuntu Gnome 桌面环境下打开 设置 → 网络 → 有线, 点击齿轮按钮, 在 IPv4 选项卡里把方法改为\u0026quot;手动\u0026quot;, 填入对应的地址和子网掩码即可. KDE 也是类似操作.\nWindows 设置 → 网络和 Internet → 以太网 → IP 分配 → 编辑, 选择\u0026quot;手动\u0026quot;, 启用 IPv4 后填入对应的地址和子网掩码.\n配置完成后, 在两台计算机上互相 ping 一下对方的 IP, 确认能通.\n可能情况 由于两台计算机是 Windows + Ubuntu 双系统, 需要兼容性比较强的共享协议── SMB (Windows 原生支持, Linux 通过 Samba 支持). 下面分几种情况说明:\n情况一: Windows 共享文件夹. 情况二: Ubuntu 共享文件夹. 情况三: Windows 访问共享文件夹. 情况四: Ubuntu 访问共享文件夹. 按需配置即可.\n情况一: Windows 共享 右键想共享的文件夹 → 属性 → 共享 → 高级共享, 勾选\u0026quot;共享此文件夹\u0026quot;, 点击\u0026quot;权限\u0026quot;按钮设置访问权限. 打开 控制面板 → 网络和共享中心 → 高级共享设置, 在当前网络配置文件下启用\u0026quot;网络发现\u0026quot;和\u0026quot;文件和打印机共享\u0026quot;. 防火墙放行 SMB: 进入 Windows 安全中心 → 防火墙和网络保护 → 允许应用通过防火墙, 确认\u0026quot;文件和打印机共享\u0026quot;在当前网络下被勾选. 情况二: Ubuntu 共享 安装Samba sudo apt update \u0026amp;\u0026amp; sudo apt install samba 修改配置文件 编辑 /etc/samba/smb.conf, 在文件末尾追加:\n[share] # 需要保证共享文件夹存在 path = /home/\u0026lt;你的用户名\u0026gt;/\u0026lt;共享文件夹\u0026gt; browseable = yes read only = no guest ok = no valid users = \u0026lt;你的用户名\u0026gt; 为该用户设置 Samba 密码 (和系统密码独立): sudo smbpasswd -a \u0026lt;你的用户名\u0026gt; 重启服务 sudo systemctl restart smbd 开放防火墙端口 如果系统启用了 ufw, 放行 Samba:\nsudo ufw allow samba 情况三: Windows 访问 按 Win + R 输入:\n\\\\192.168.10.2\\share 弹窗中输入 Ubuntu 的用户名和刚才设置的 Samba 密码即可. 也可以右键\u0026quot;映射网络驱动器\u0026quot;, 把它挂成本地盘符方便日常使用.\n情况四: Ubuntu 访问 打开文件管理器, 在地址栏输入:\nsmb://192.168.10.1/共享文件夹名 首次访问会要求输入 Windows 这一端的用户名和密码, 输入即可. 勾选\u0026quot;永久记住\u0026quot;可以避免下次重复输入.\n如果想从命令行访问, 可以安装 cifs-utils 后挂载:\nsudo apt install cifs-utils sudo mkdir -p /mnt/win-share sudo mount -t cifs //192.168.10.1/共享文件夹名 /mnt/win-share \\ -o username=\u0026lt;Windows 用户名\u0026gt;,uid=$(id -u),gid=$(id -g) 小结 到这里, 键鼠, 显示器, 文件三个部分都打通了. 尽管还存在或多或少的体验问题, 但这确实是我目前能够找到的最优方案.\n","date":"2026-06-06T00:00:00+08:00","permalink":"/mowmark_blog/computer/hardware/000_multiplecomputer/","title":"双系统计算机的键鼠共享, 外接显示器切换与文件共享方案分享"},{"content":"最近在新电脑上安装了 Windows + Ubuntu 双系统, 顺手把流程整理成文章. 这篇文章假设你已经决定安装 Linux, 关于\u0026quot;该不该用 Linux\u0026quot;以及\u0026quot;选哪个发行版\u0026quot;的问题, 可以先看 Linux 发行版选型经验分享. 本文以 Ubuntu 为例, 其他主流发行版的安装流程大同小异, 可以作为参考.\n准备工作 你需要准备什么 一台已经安装 Windows 10+ 的电脑 一个 16GB 以上的 U 盘 (制作启动盘会清空 U 盘内所有数据, 提前备份) 系统镜像: 从 Ubuntu 官网 下载对应版本的 ISO 文件 烧录软件: 推荐 Rufus (Windows 专用, 免安装, 操作直观) 或 Ventoy (支持一个 U 盘存放多个镜像，适合经常折腾的用户). MacOS 或 Linux 用户可以选跨平台的 Etcher 确定版本 版本号的选择取决于硬件兼容性和项目需求, 前者优先级更高——新硬件 (尤其是新显卡) 需要较新的内核才能正常驱动, 装一个版本过老的系统往往会在驱动上碰壁. 没有特殊需求的话, 选最新的 LTS(长期支持)版本是最稳妥的选择.\n桌面环境方面, Gnome 和 KDE 是最常见的两个选择, 前者简洁, 后者可定制性更强. 如果有特定偏好, 可以在 Ubuntu Flavors 里选择对应的风味版本下载, 不需要另外安装.\n为 Linux 准备硬盘空间 这是双系统安装中最容易出问题的一步, 建议在开始之前想清楚用哪种方案.\n方案一 (推荐): 安装到另一块物理硬盘\n如果电脑有两块硬盘, 直接把 Ubuntu 装到第二块上, 不需要事先进行任何操作. 这种方案下, 两个系统物理隔离, 引导程序互不干扰, Ubuntu 的 GRUB 不容易被 Windows 更新覆盖. 这也是我目前采用的方案.\n方案二：在同一块硬盘上划出未分配空间\n如果只有一块硬盘, 需要提前在 Windows 下为 Linux 划出空间: 右键\u0026quot;此电脑\u0026quot; -\u0026gt; 管理 -\u0026gt; 磁盘管理, 找到容量充裕的分区, 右键选择\u0026quot;压缩卷\u0026quot;, 划出至少 50GB. 完成后会出现一块灰色的\u0026quot;未分配\u0026quot;区域, 保持这个状态即可, 不要新建卷.\n后文中, 方案一简称双硬盘方案, 方案二简称单硬盘方案.\n制作启动 U 盘 不管是 Rufus 还是 Etcher，原理都一样，步骤大同小异。Ventoy 的使用方式略有不同, 我会在后续文章中单独说明. 下面以 Rufus 为例.\n插入 U 盘, 打开 Rufus. 设备: 确认下拉菜单中选中的是 U 盘, 不是电脑的内置硬盘. 引导类型: 点击右侧\u0026quot;选择\u0026quot;, 加载下载好的 ISO 文件. 分区类型: 现代电脑选 GPT, 目标系统选 UEFI (非 CSM). 电脑较老, 只支持传统 BIOS 的话选 MBR. 其余保持默认, 点击\u0026quot;开始\u0026quot;. 弹窗提示写入模式时, 保持默认的\u0026quot;以 ISO 镜像模式写入\u0026quot;即可. 等待进度条完成. 安装步骤 从 U 盘启动 重启电脑, 开机亮屏的瞬间连续按下启动热键, 进入 Boot Menu（启动菜单）或 BIOS 界面. 不同主板/笔记本的热键不同, 常见的有 F2, F12, Del, Esc, 可以在开机画面上留意提示, 或提前查一下主板型号.\n进入 Boot Menu 后, 选择带有 UEFI 前缀的 U 盘选项，回车确认.\n安装向导 从 U 盘启动后会进入 GRUB 菜单, 选择 Try or Install Ubuntu, 稍等片刻进入图形化安装界面. 大部分步骤跟着向导走即可, 有两个地方需要特别注意:\n语言\n建议在安装阶段选择 English. 如果选了中文, 系统可能会把\u0026quot;桌面\u0026quot;\u0026ldquo;下载\u0026quot;等目录以中文命名, 终端里输入路径时很不方便. 安装完成后可以在系统设置里把界面语言改回中文, 同时选择保留英文目录名.\n分区\n这是整个安装过程中最关键的一步, 双硬盘和单硬盘的处理方式不同.\n双硬盘方案\n注意选中空硬盘, 而不是 Windows 系统所在的那块 可以自动分区, 也可以手动分区, 手动分区的推荐方案如下: 挂载点 分区格式 大小 说明 /boot/efi FAT32 500MB ESP 引导分区, 存储 GRUB 等引导文件 /boot ext4 1GB 存储内核 (vmlinuz) 和初始化镜像 (initramfs) swap swap 内存的1~2倍 内存不足时的交换空间 / ext4 剩余全部空间 根目录, 存储系统和用户数据 /home 和 /var 也可以单独划分, 但对于个人用户来说意义不大, 反而容易因某个分区塞满而需要花时间调整. 非服务器场景下, 以上四个分区足够用了.\n分区格式补充:\nFAT32: 只能识别 4GB 以下的文件, 现在几乎只用于 /boot/efi, 属于历史遗留. swap: 非独立文件系统, 专门换页空间. 内存不足时, 系统会把不活跃的内存页换出到 swap, 避免 OOM (Out Of Memory) 崩溃. 有休眠需求的话, swap 大小建议不低于物理内存. ext4 / btrfs / xfs: 目前主流的 Linux 文件系统, 日常使用选用 ext4 最省心, btrfs 支持快照但稳定性相对差一些. LVM (逻辑卷): 在物理分区上抽象出一层逻辑卷管理, 方便后续扩容和迁移, 也支持快照. 对有多块硬盘或后续可能扩容的用户来说值得考虑, 普通场景不必折腾. 单硬盘方案:\n省心方式: 选择 \u0026ldquo;Install Ubuntu alongside Windows Boot Manager\u0026rdquo;, 安装程序会自动识别并使用之前划分出来的未分配空间 手动分区: 选择 \u0026ldquo;Something else\u0026rdquo;, 手动挂载 / (根目录, 建议 30GB 以上). EFI 分区直接选用 Windows 已有的那个, 不要格式化. 安装之后 关于双系统引导 安装完成后, 开机会出现 GRUB 菜单, 可以选择进入 Ubuntu 或 Windows.\n有一点需要提前知道: Windows 大版本更新有时会把 EFI 分区的默认启动项强制改回 Windows Boot Manager, 导致 GRUB 菜单消失, 开机直接进入 Windows. 这不意味着 Ubuntu 被删了——你的文件完好无损, 只是引导入口被覆盖了.\n遇到这种情况, 进入 BIOS, 把启动顺序改回 ubuntu 或 GRUB 就可以恢复. 如果引导损坏比较严重, 可以用 Ubuntu 启动盘进入 Live 环境, 用 boot-repair 工具修复. 如果你用的是双硬盘方案, 这个问题基本不会出现.\n关于后续配置 装好系统只是开始. 换国内软件源, 安装显卡驱动, 配置开发环境等内容, 我会在后续文章里分别展开.\n","date":"2026-06-04T00:00:00+08:00","permalink":"/mowmark_blog/computer/linux/001_linuxinstallation/","title":"Linux 双系统安装指南"},{"content":"不少人接触 Linux 的第一道门槛不是安装, 而是选择──面对成千上百个发行版, 不知道应该选择哪个. 这篇文章把我对主流发行版的理解梳理一遍, 帮你在开始之前少走一点弯路.\n为什么要用 Linux Windows 已经足够成熟, 日常使用完全没有问题. 但\u0026quot;傻瓜式\u0026quot;的另一面是用户对底层的掌控权有限. 对于追求系统可控性的人来说, 走向 Linux 是自然而然的事.\n在专业场景下这一点更明显——无论是运维, 开发, 还是折腾 NAS 和软路由, Linux 往往能提供比 Windows 更好的原生支持和生态适配.\n选发行版之前, 先想清楚三件事 你的使用场景是什么 不同发行版有各自的定位, 选错了之后迁移的成本不低:\nGentoo: 提倡用户自己完成编译, 难度高, 适合硬核玩家. 折腾的过程本身就是乐趣的一部分. Arch Linux: 简洁自由, 组件自选, 滚动更新, 适合想深度掌控系统的玩家. Fedora: 更新节奏快, 软件版本新, 由 Red Hat 社区主导, 定位是\u0026quot;前沿但可靠\u0026quot;, 适合开发者. Ubuntu: 开箱即用, 适用面广, 是目前使用最广泛的 Linux 发行版, 对大多数开发工作都有良好的支持. Rocky Linux / AlmaLinux: 以稳定性为第一优先级, 是旧时代 CentOS 的社区平替, 适合运维人员或对稳定性要求高的服务器场景. 你需要的软件生态是否齐全 主流发行版有各自的包管理体系:\nDebian / Ubuntu 系: dpkg + apt Fedora / Red Hat 系: rpm + dnf Arch 和 Gentoo 各自有一套独立的包管理器 此外, Snap 和 Flatpak 作为跨发行版的通用方案, 正在被越来越多的发行版支持. 选型前确认常用软件在目标发行版上是否容易获取, 可以省掉不少后续麻烦.\n遇到问题时能否快速找到答案 社区文档的质量直接影响你的使用体验. Ubuntu 的中文资料最丰富, 上手阻力小; Arch Wiki 则是公认的 Linux 界最全面的参考文档之一——即使不用 Arch, 遇到问题时也值得去查一查.\n各发行版的整体情况和社区热度, 可以在 DistroWatch 上浏览.\n新手从哪里开始 如果暂时没有特定的使用场景, 从 Ubuntu 或其衍生版 (如 Linux Mint) 开始是门槛最低的选择. 软件生态完善, 中文资料丰富, 遇到问题基本都能搜到答案.\n确定具体镜像 版本号怎么定 选完发行版后, 还需要确定具体的版本号. 这取决于硬件兼容性和项目需求, 两者冲突时, 硬件优先.\n举个例子: 你有一个基于 Ubuntu 22.04 的项目需要复现, 按说应该装 22.04; 但如果你的电脑是新硬件, 22.04 默认的内核版本可能无法正常驱动对应设备. 一个折中的方案是装最新的 LTS 版本 (如 26.04), 然后用 Docker 拉一个 22.04 的容器来跑旧项目. 这也是我目前采用的方案.\n没有特殊需求的话, 选最新的 LTS(长期支持) 版本就好, 维护周期长, 社区资料也更完整.\n桌面环境怎么选 桌面环境主要影响视觉风格和资源占用, 不同发行版通常都会提供预装不同桌面环境的镜像:\nGnome (Ubuntu 默认): 现代化, 简洁, 逻辑清晰, 上手门槛低. KDE Plasma (Kubuntu): 功能丰富, 高度自定义, 动效细腻. Xfce / LXQt (Xubuntu / Lubuntu): 轻量, 适合老旧设备. 平铺式窗口管理器 (i3wm、Sway 等): 键盘驱动工作流, 学习曲线较陡, 适合进阶用户, 本文不展开. 新手选 Gnome 或 KDE 都不会出错, 具体选哪个看个人口味.\n小结 选型的核心逻辑其实并不复杂: 先看场景, 再看生态, 最后看社区. 如果你非常纠结, 不如先选一个装上, 遇到问题再说. 试过一个后, 你对\u0026quot;自己到底需要什么\u0026quot;的判断会清晰得多.\n如果你是第一次接触 Linux, Ubuntu 是最不容易踩坑的起点. 选好之后, 下一步是把它装进电脑里──如果你打算保留 Windows, 可以参考我的另一篇文章: Linux 双系统安装指南, 内容以 Ubuntu 为主, 但安装流程对其他主流发行版也有一定参考价值.\n","date":"2026-06-03T00:00:00+08:00","permalink":"/mowmark_blog/computer/linux/000_linuxdistributionacquaintance/","title":"Linux 发行版选型经验分享"},{"content":"承接上一篇文章: 使用 GitHub Pages 部署博客, 这篇文章我将分享使用 Terminal 主题美化博客的个人经验. 由于个人精力有限, 可能有一些地方并没有涉及到, 欢迎讨论交流.\nHugo 配置文件的基本要求 在正式开始之前, 我们需要先梳理 Hugo 主题的基本结构, 而这又绕不开 Hugo 项目的核心机制: 配置文件的覆写机制\n配置文件的覆写机制 简单来说, 在新版Hugo中, config/_default/目录下的配置文件优先级大于根目录下的配置文件大于主题目录(themes/)下的配置文件, 此外, 主题目录如果随 Git 自动更新, 我们在里面做的任何修改都会被直接覆盖. 基于这两点, 如果我们要修改主题的配置, 就不能在主题目录下进行, 而要在根目录或者 config/ 目录下操作.\n配置文件的基本要求 除此之外, 配置文件的格式, 内容和作用域也需要厘清一下:\nHugo 早期要求配置文件名字应该为 config, 后来变成了 hugo, 目前版本是两种名字都可以接受, 而格式则可以为 yaml, toml 或者 json 当中的任意一种; Hugo 支持将所有配置信息都写在同一个文件中, 也可以拆分开来, 作为不同的配置文件, 但名称必须为 params, menu 或者 languages 已经规定好的一级目录; Hugo 项目的全局配置控制整个网站的样式, 如网站主标题, 导航菜单, 默认语言等, 这些信息都存储在 hugo.toml 或者从中拆分出来的配置文件里, 可能存放在根目录或者 config/ 目录下; 写在每篇文章头部的 Front Matter 控制这篇文章的元数据, 如标题, 日期, 标签等, 其优先级高于全局配置. 在使用 hugo new ... 命令新建文章时, Hugo 会根据存放在 archetypes/ 目录下的文章模板, 自动创建新文章的 Front Matter. 基于上述几个理由, 读者在使用不同主题时, 有可能会在根目录或者 config/ 目录下看到不同类型的配置文件\nTerminal 主题的配置 Hugo 官方对主题结构没有严格规定, 所以每个主题的组织方式都不太一样, 以下内容基于 Hugo Extended v0.161.1 和 Terminal 2024-11-22, 仅供参考.\n复制初始文件 使用 git submodule ... 命令安装好主题后, 将 themes/terminal/exampleSite/ 目录下的文件复制到根目录, 作为配置的初始模板. 作者给出的默认配置相当长, 但实际上大部分参数保持默认即可, 只有少数几个参数需要留意.\n修改配置文件 打开根目录的 hugo.toml:\nbaseurl = \u0026#34;https://\u0026lt;Your-UserName\u0026gt;.github.io/\u0026#34; # 改成你的实际域名 languageCode = \u0026#34;zh-cn\u0026#34; # 当前 Hugo 规定的语言代码, 中文用户记得改这里 theme = \u0026#34;terminal\u0026#34; paginate = 5 [params] # 这个参数对应 content/ 目录下存放文章的文件夹名称 # 如果你的文章放在 content/blog/ 下，这里就要改成 \u0026#34;blog\u0026#34; # 不改的话首页会加载不出任何文章，是比较容易踩的坑 contentTypeName = \u0026#34;posts\u0026#34; # 首页显示的菜单项数量，超出的部分会折叠进右上角 # 如果你想让页面干净一点，设成 0 也没问题 showMenuItems = 2 实际修改主题文件 前面提到不能在主题目录里改文件, 那如果我们确实需要修改主题的某个模板, 应该怎么做?\n以修改页脚为例. 假设你想在底部加上备案号, 或者就是单纯不喜欢默认的版权格式, 直觉上可能会去 themes/terminal/layouts/partials/footer.html 里改, 但这样做下次主题更新就会丢失.\n正确的做法是:\n在根目录下创建相同的路径 layouts/partials/; 把 themes/terminal/layouts/partials/footer.html 复制到这个目录下; 修改根目录下的这个副本. Hugo 在构建时会优先读取根目录下的 layouts/ 目录, 主题里的原版文件不会被动, 下次更新主题也不会影响你的改动. 其他模板文件的修改方式都是一样的道理.\n小结 这篇文章到这里基本就结束了. Terminal 主题本身并不复杂, 配置文件改几个参数就能跑起来, 真正需要花时间理解的反而是 Hugo 本身的复写机制, 这个搞清楚了, 后续不管换什么主题, 修改起来都会顺手很多.\n如果读者想要使用 Stack 主题, 后面我会更新相关文章, Stack 主题的结构比 Terminal 复杂不少, 需要花费比较多的时间进行调试.\n","date":"2026-05-22T09:12:24+08:00","permalink":"/mowmark_blog/website/hugo/002_hugothemeterminal/","title":"使用 Terminal 主题美化博客"},{"content":"承接上一篇文章: 使用Hugo创建个人博客, 在本地搭建好环境后, 本文将告诉你如何将博客手动以及自动化发布到公网.\n网络发布基础配置 想要将网站发布到公网, 需要满足三个条件: 代码托管平台, 服务器(托管服务)以及域名.\n出于性价比与便利性, 本文采用 GitHub 仓库 + GitHub Pages (免费托管) 的一站式方案, 并使用 GitHub 提供的免费域名.\n补充信息: 上面三个条件中, 代码托管平台有GitHub, GitLab和Gitee等平台, 域名可以从 GitHub, 腾讯云, 阿里云等平台获取, 而托管服务则可以由 GitHub Pages, Cloudflare Pages, 腾讯云和阿里云提供, 后两者用的人不多, 喜欢挑战自己的伙伴可以尝试部署一下.\n创建 GitHub 仓库 首先, 登录你的 GitHub 官网, 新建一个仓库.\n这里有一个相当重要的命名规则:\n如果你希望博客最终的访问域名是简单的https://\u0026lt;Your-UserName\u0026gt;.github.io, 那么仓库的名称必须命名为\u0026lt;Your-UserName\u0026gt;.github.io. 如果你随便起了个名字 (例如my-blog), 那后续的访问域名就会是https://\u0026lt;Your-UserName\u0026gt;.github.io/my-blog. 权限请务必选择 Public(公开), 否则 GitHub Pages 无法免费提供服务. 更多信息可以查看 GitHub Pages 说明.\n将本地代码推送到 GitHub 在上一篇文章中, 我们已经在本地项目目录执行过了 git init. 现在, 我们需要将本地内容推送到刚刚创建的 GitHub 远程仓库.\n首先, 配置 Git 用户信息 (如果之前没配置好全局账号):\ngit config user.email \u0026lt;Your-EmailAddress\u0026gt; git config user.name \u0026lt;Your-UserName\u0026gt; 提示: 加上 --global 参数可以进行全局配置, 配置信息存放在 ~/.gitconfig 文件里. 局部配置则存放在项目隐藏文件 .git/config 中.\n接下来, 将本地文件提交并推送到远程仓库:\n# 在项目根目录下: # 将所有文件加入暂存区并提交 git add . git commit -m \u0026#34;Feat: 博客首次初始化\u0026#34; # 关联远程仓库 (请将下面的链接替换为你的仓库地址) git remote add origin https://github.com/\u0026lt;Your-UserName\u0026gt;/\u0026lt;Your-Repository\u0026gt;.git # 将本地代码推送到远程仓库的 main 分支 git push -u origin main 提示: 关于这个部分, 有非常多的教程会倾向于先在本地运行 hugo 命令生成静态网页 (放在 public/ 目录下), 再把 public 文件夹 push 到 GitHub 仓库. 这种做法相当繁琐, 每次发布文章都需要本地编译. 所以这篇文章我们采用了另外一种做法: 将包含 .md 原稿的整个项目源码推送上去, 让 GitHub Actions 帮我们在云端自动部署并发布.\n部署博客网站 配置 GitHub Pages 进入 GitHub 仓库, 点击顶部的 Settings, 在左侧栏找到 Pages.\n在 Build and deployment（构建和部署）板块下, 将 Source 的选项从默认的 Deploy from a branch 改为 GitHub Actions.\n创建自动化部署工作流 我们需要在项目根目录下创建一个特定的文件, 告诉 GitHub 每当有代码推送时该怎么做.\n在项目根目录下执行:\nvim .github/workflows/hugo.yaml 将以下 官方推荐的配置代码 粘贴到你的文件中, 根据需要更改分支名称和 Hugo 版本:\n# 自动将 Hugo 站点部署到 GitHub Pages 的示例工作流 name: Deploy Hugo site to Pages on: push: branches: - main # 推送到 main 分支时出发 workflow_dispatch: # 允许在 Actions 页面手动触发 permissions: contents: read pages: write id-token: write concurrency: group: \u0026#34;pages\u0026#34; cancel-in-progress: false defaults: run: shell: bash jobs: build: runs-on: ubuntu-latest env: HUGO_VERSION: 0.141.0 # 建议填入本地使用的 Hugo 版本 steps: - name: Install Hugo CLI run: | wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \\ \u0026amp;\u0026amp; sudo dpkg -i ${{ runner.temp }}/hugo.deb - name: Install Dart Sass run: sudo snap install dart-sass - name: Checkout uses: actions/checkout@v4 with: submodules: recursive # 确保拉取主题子模块 fetch-depth: 0 - name: Setup Pages id: pages uses: actions/configure-pages@v5 - name: Install Node.js dependencies run: \u0026#34;[[ -f package-lock.json || -f npm-shrinkwrap.json ]] \u0026amp;\u0026amp; npm ci || true\u0026#34; - name: Build with Hugo env: HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache HUGO_ENVIRONMENT: production TZ: America/Los_Angeles run: | hugo \\ --gc \\ --minify \\ --baseURL \u0026#34;${{ steps.pages.outputs.base_url }}/\u0026#34; - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: ./public # Deployment job deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 保存后, 将这个文件推送给 GitHub:\ngit add . git commit -m \u0026#34;添加 GitHub Actions 部署脚本\u0026#34; git push 推送完成后, 点击 GitHub 仓库顶部的 Actions 标签, 会看到一个正在转圈的任务, 等状态变成绿色的勾✅, 你的网站就已经成功发布到公网上了!\n访问你的域名(https://\u0026lt;Your-UserName\u0026gt;.github.io 或者 https://\u0026lt;Your-UserName\u0026gt;.github.io/\u0026lt;Your-Repository\u0026gt;)看看吧!\n到目前为止, Hugo 搭建并初始化个人博客的基本步骤就已经写完了, 如果读者对网站的样式没有特别要求, 其实现在已经完全可以开始写文章了.\n如果对 Terminal 主题不是特别满意的, 欢迎移步我的下一篇文章: 使用 Terminal 主题美化博客.\n如果想要使用 Stack 主题美化博客的, 我后续会更新相关文章, 敬请关注!\n","date":"2026-05-19T22:01:19+08:00","permalink":"/mowmark_blog/website/hugo/001_githubpages/","title":"使用 GitHub Pages 发布个人博客"},{"content":"前言 由于学业原因, 最近一直在Ubuntu下学习SLAM算法以及开发, 有相当多操作学了又忘, 忘了再学. 为了打破这个循环, 我决定写一个个人博客进行技术沉淀与知识巩固, 顺便作为一个经验分享与交流的地方.\n第一篇文章, 就从如何搭建个人博客开始!\n博客搭建 本质上, 搭建博客就是部署一个轻量化的个人网站, 如果要求不高, 完全可以使用博客园、CSDN 等现成的内容平台, 但作为一个喜欢折腾的人, 我更希望能够拥有一个自己的独立站点.\n技术方案对比 目前主流的独立博客方案大概分为两派：传统动态 CMS（如 WordPress、Halo）和 静态站点生成器（如 Hugo、Hexo）。由于静态站点不需要维护数据库，部署成本极低（甚至免费），我最终锁定了后者。\n常见的静态框架对比如下：\n框架 底层语言 编译速度 特点与优势 Hugo Go 极快 🚀 上手快，零依赖，适合喜欢专注内容写作的人 Hexo Node.js 中等 中文社区庞大，插件丰富，但环境配置略繁琐 Astro Astro.js 快 现代前端新宠，组件化强，适合有一定前端基础的玩家 VitePress Vue/Vite 快 Vue 生态，文档风格利落，非常适合做技术文档 由于我对复杂的前端开发不太感冒, 且相对看重编译速度和本地环境的纯净度, Hugo 成为了我的首选。\n环境准备与依赖安装 Hugo依赖于Go语言, 但从v0.121.0起, Hugo已内置了Go工具链, 且包管理器能够自动处理依赖, 所以不管是手动安装二进制包还是使用包管理器安装的用户, 都可以跳过 Go 语言的安装部分 (除非需要安装其他依赖 Go 语言的软件)\n但是后续的代码托管与发布一定离不开Git, 因此, 在安装 Hugo 之前, 我们必须先准备好Git, 已经安装好了的读者则可以跳过这个部分.\n提示: 以下部分网站可能需要科学上网环境, 请自行准备所需工具.\n安装Go语言 Windows系统: 打开Go 官网, 下载*.msi安装包, 一路 Next 即可. 如果习惯使用包管理器Scoop或者Chocolatey, 也可以通过执行以下命令来安装 scoop install go # 或者 choco install golang Linux系统: 官方推荐使用snap安装, 会自动处理依赖, 该步骤可跳过. 安装Git Windows系统: 前往Git 官网, 根据系统架构下载合适的安装包, 运行即可. 或使用包管理器: winget install Git.Git # 或者 scoop install git # 或者 choco install git Linux系统: 根据你的发行版执行对应命令: # 基于Debian sudo apt update \u0026amp;\u0026amp; sudo apt install git # 基于Fedora sudo dnf update \u0026amp;\u0026amp; sudo dnf install git # 基于Arch sudo pacman -Syu git 搭建 Hugo 博客环境 安装 Hugo 本体 与上面类似, 我们依然可以通过安装包或者包管理器安装 Hugo, 需要注意一点, Hugo本身不带样式, 需要安装主题, 而部分主题依赖于Extended版的Hugo, 所以在这一步中, 推荐安装Extended版本的Hugo\nWindows环境: 前往Hugo的Git仓库, 下载Extended版本的Hugo即可. 或使用包管理器快速安装: winget install Hugo.Hugo.Extended # 或者 scoop install hugo-extended # 或者 choco install hugo-extended Linux环境: 使用snap安装 sudo snap install hugo --channel=extended 安装完成后, 在终端里运行hugo version, 只要输出版本号信息, 就说明大功告成!\n初始化项目 找一个合适的目录, 进入终端执行以下命令:\nhugo new site my-blog cd my-blog git init 此时, Hugo 已经生成了标准的目录结构:\nmy-blog/ ├── archetypes # 文章默认模板 │ └── default.md ├── assets/ # 资源目录 ├── content/ # 博客文章存放目录 ← 最重要 ├── data/ ├── hugo.toml # 主配置文件 ← 最重要 ├── i18n/ # 语言翻译 ├── layouts/ # 自定义布局, 优先级高于主题 ├── static/ # 静态资源(图片, 字体等) └── themes/ # 主题目录 9 directories, 2 files 安装与配置主题 Hugo 本身不带样式, 需要安装主题. 这里以极简风格的Terminal主题为例:\n将主题作为 Git 子模块引入(强烈推荐这种方式, 方便后续更新):\ngit submodule add https://github.com/panr/hugo-theme-terminal.git themes/terminal 接着, 打开根目录下的hugo.toml配置文件, 指定我们刚刚安装的主题, 并顺便修改一下基础配置:\nbaseURL = \u0026#34;http://localhost:1313\u0026#34; # 沿用默认的本地调试地址 defaultContentLanguage = \u0026#34;zh-cn\u0026#34; # 博客语言 title = \u0026#34;MowMark 的技术博客\u0026#34; # 你的博客名称 theme = \u0026#34;terminal\u0026#34; # 必须与 themes 目录下的文件夹名称一致 到这一步, 博客的底层搭建已经完成了99%\n本地预览 在项目根目录下执行:\nhugo server -D # -D 参数表示渲染草稿文章(draft: true) -D 参数表示渲染草稿文章(draft: true)\nhugo server 对应的常用参数还有 --port 端口号, --baseURL 域名等, 具体参数可以查看hugo server\n打开浏览器, 访问http://localhost:1313, 就能看到个人博客已经在本地成功运行了!\n当然, 在本地运行是不够的, 如何将它部署到公网上, 让所有人都能访问?\n这就需要用到GitHub Pages 和自动化的构建工具了. 由于篇幅原因, 这部分内容我会在使用GitHubPages部署博客中讲解.\n","date":"2026-05-13T20:53:02+08:00","permalink":"/mowmark_blog/website/hugo/000_hugoinit/","title":"使用 Hugo 搭建个人博客"}]