- 桌面应用
【免费下载链接】Mos
一个用于在 macOS 上平滑你的鼠标滚动效果或单独设置滚动方向的小工具, 让你的滚轮爽如触控板 | A lightweight tool used to smooth scrolling and set scroll direction independently for your mouse on macOS
Mos 是一款免费、轻量、用 Swift 编写的 macOS 菜单栏工具,核心目标只有一个:让你的鼠标滚轮滚动得像触控板一样平滑自然,同时允许用户独立翻转鼠标滚轮的滚动方向、按应用定制滚动行为。本文以仓库根目录下的 README.enUS.md 为主线,完整继承其特性清单与安装指南,并结合 Mos/ScrollCore 目录下的源码,从事件拦截、插值计算、相位模拟到参数配置逐层拆解其实现原理。读完本文,你既能完成 Mos 的安装与基本配置,也能理解"平滑滚动"在系统底层究竟是如何被伪造出来的。
项目概览:一个菜单栏常驻的滚动魔法师
Mos 定位于"免费且简单"(free & simple):它不修改系统文件、不注入任何第三方库到目标应用,而是通过 macOS 的 Quartz Event Services 在系统事件流层面截取滚动事件,经过插值计算后重新注入平滑的滚动事件,从而让传统机械鼠标/滚轮鼠标获得接近触控板的滚动手感。
从 README.enUS.md 列出的特性看,Mos 的核心能力包括:
- 平滑滚动:让鼠标滚轮滚动像触控板一样顺滑;
- 可定制滚动行为:支持调整滚动速度与加速度(曲线)等参数;
- 触控板 / 鼠标分离处理:可以单独为鼠标滚轮设置滚动方向,不影响触控板;
- 图形化监控窗口:实时展示滚动事件的可视化图表;
- 应用级白名单/黑名单:内置按应用处理的例外列表;
- 纯 Swift 构建:要求 Xcode 9.0+ 与 Swift 4.0+(见 README 徽章与 Mos.xcodeproj 工程)。
这些特性在仓库中都有对应的源码模块:滚动核心 ScrollCore.swift、事件模型 ScrollEvent.swift、插值器 Interpolator.swift、事件发送器 ScrollPoster.swift、相位状态机 ScrollPhase.swift 以及曲线滤波 ScrollFilter.swift。
下载与安装:Homebrew 一键安装与手动安装
README 提供了两种安装途径,以下是完整可复制的操作步骤。
通过 Homebrew 安装
如果你的 Mac 已安装 Homebrew,安装 Mos 只需一条命令:
$ brew install mos安装完成后,应用会被放置在系统应用目录:
/Applications/Mos.app当发布新版本时,通过以下命令升级:
$ brew update $ brew reinstall mos升级完成后,退出并重新启动应用即可生效(README 明确要求 "Quit then relaunch the app")。需要说明的是,当前仓库并未提供自制(homebrew)配方文件,brew install mos的可用性取决于官方 cask/formula 仓库的维护状态。
手动安装
也可以从 GitHub Releases 页面下载最新版本的 dmg 镜像手动安装。仓库 dmg 目录中提供了构建 DMG 安装包所需的资源:
- create-dmg.command:DMG 制作脚本;
- dmg-bg.png:安装器背景图(1400x800);
- dmg-icon.png:安装器图标(1024x1024)。
下载后将Mos.app拖入/Applications目录即可。首次运行 Mos 时,macOS 会要求授予**辅助功能(Accessibility)**权限,因为 Mos 需要通过 CGEventTap 读取并注入全局滚动事件,这是其工作的前提。
核心机制:一次滚轮滚动在 Mos 内部经历了什么
要理解 Mos 的价值,需要先理解它的工作链路。从源码看,Mos 的滚动处理由ScrollCore(ScrollCore.swift)统筹,其startHandlingScroll()方法在应用启动后创建了三个 CGEventTap 事件拦截器(见 ScrollCore.swift):
| 拦截器 | 监听事件掩码 | Tap 类型 | 用途 |
|---|---|---|---|
scrollEventInterceptor | scrollWheel(滚动) | .defaultTap(可修改) | 截获滚动事件并执行平滑/翻转 |
hotkeyEventInterceptor | flagsChanged(修饰键) | .listenOnly(只读) | 监听 Control/Option/Command/Shift 热键 |
mouseEventInterceptor | leftMouseDown(左键按下) | .listenOnly(只读) | 点击左键立即停止滚动 |
三个拦截器均挂在.cgAnnotatedSessionEventTap的.tailAppendEventTap位置,即事件流的尾部。scrollEventInterceptor使用.defaultTap意味着它可以修改或吞掉事件,这是实现平滑滚动的关键;另外两个只监听不修改。
一条滚动事件进入scrollEventCallBack(ScrollCore.swift)后,处理流程如下:
- 触控板判定与放行:调用
ScrollEvent.isTrackpad(with:)判断事件来源。若判定为触控板,直接原样放行事件(return Unmanaged.passUnretained(event)),这正是"独立处理触控板与鼠标"的底层实现。判定依据是事件的滚动相位字段(scrollWheelEventMomentumPhase/scrollWheelEventScrollPhase)与累计加速度(scrollWheelEventScrollCount)是否非零,详见 ScrollEvent.swift。此外源码注释特别指出:黑苹果的触控板驱动直接模拟鼠标输入、Magic Mouse 的滚动特征与内置触控板一致,这两类设备无法被可靠区分。 - 滚动阶段介入:
ScrollPhase.shared.kickIn()更新滚动相位状态机。 - 目标应用解析:通过
ScrollUtils.shared.getRunningApplication(from:)获取滚动事件的目标应用,进而查询该应用是否在例外列表中(getExceptionalApplication)。 - 平滑/翻转开关决策:根据例外应用配置或全局配置决定
enableSmooth与enableReverse。若 Launchpad 处于激活状态则强制关闭平滑(防止桌面图标抖动)。 - 数据解析:
ScrollEvent(with:)从事件中提取 Y/X 双轴的滚动数据,分别读取scrollWheelEventDeltaAxis1/2(整数增量)、scrollWheelEventPointDeltaAxis1/2(点增量)与scrollWheelEventFixedPtDeltaAxis1/2(固定点增量),按优先级选取"可用值",详见 ScrollEvent.swift。 - 方向翻转:若启用翻转,则对事件的三个增量字段全部取反并更新可用值(
reverseX/reverseY,见 ScrollEvent.swift)。 - 归一化:若启用平滑且输入数据为非 Fixed 类型(即来自
pointDelta的动态增量),则使用 Step 作为门限值将过小的滚动增量归一化,保证插值有足够的输入量(normalizeY/normalizeX,见 ScrollEvent.swift)。 - 注入平滑事件:若启用平滑,则吞掉原始事件(返回
nil),将归一化后的增量交给ScrollPoster异步插值发送;否则原样放行事件。
平滑滚动的核心:CVDisplayLink 驱动的插值发送器
平滑滚动的"平滑感"来自ScrollPoster(ScrollPoster.swift)。它使用CVDisplayLinkCreateWithActiveCGDisplays创建与显示器刷新率同步的回调循环(见 ScrollPoster.swift),每个刷新帧执行一次processing():
// 计算插值 let frame = ( y: Interpolator.lerp(src: current.y, dest: buffer.y, trans: duration), x: Interpolator.lerp(src: current.x, dest: buffer.x, trans: duration) ) // 更新滚动位置 current = (y: current.y + frame.y, x: current.x + frame.x) // 平滑滚动结果 let filledValue = filter.fill(with: frame) // 变换滚动结果(Shift 横向滚动归一化) let shiftedValue = shift(with: filledValue) // 发送滚动结果 post(ref, shiftedValue)这里Interpolator.lerp(线性插值,见 Interpolator.swift)每帧从当前位置向目标缓冲位置逼近duration比例的位移;duration越小,逼近越快、滚动越跟手;越大则越柔和绵长。滚动手感即由此参数控制。
ScrollFilter(ScrollFilter.swift)则对插值结果做曲线峰值滤波:用定长非线性数列[first, first+0.23*diff, first+0.5*diff, first+0.77*diff, nextValue]填充,用于去除滚动起始时的抖动,使曲线更平滑。
post(_:_:)方法(ScrollPoster.swift)复制原始事件、写入插值后的增量数据(scrollWheelEventPointDeltaAxis1/2),将fixedPtDelta清零、isContinuous置 1,并通过tapPostEvent(proxy)将事件发布到事件流中——源码注释指出,使用tapPostEvent发布事件可以避免事件被回调本身重复接收。
滚动结束后,当插值帧的位移小于precision精度门限时自动暂停(stop(Phase.PauseAuto)),避免无意义的持续事件。
滚动相位模拟:让第三方应用"以为"是触控板
为什么平滑滚动事件能被 Chrome、Safari 等应用接受并表现顺滑?关键在于 Mos 模拟了触控板滚动的相位字段。ScrollPhase(ScrollPhase.swift)维护一个状态机:
| Phase 状态 | 含义 | Scroll/Momentum 取值 |
|---|---|---|
| Idle | 空闲 | 0.0 / 0.0 |
| Contact | 手指触碰(开始) | 128.0 / 0.0 |
| Tracing | 跟随滚动 | 2.0 / 0.0 |
| Momentum | 缓动(惯性滚动) | 0.0 / 2.0 |
| PauseAuto | 自动停止 | 0.0 / 3.0 |
| PauseManual | 手动停止 | 4.0 / 0.0 |
状态在kickIn()(新滚动事件到达)与transfrom()(发送时)之间按映射表流转,其中applyMomentum()使用 300ms 防抖将状态推进到 Momentum,模拟触控板的惯性阶段(见 ScrollPhase.swift)。特别地,stop()中针对 Chrome 的处理(ScrollPoster.swift):当目标应用是 Chrome 时,发送一个带PauseManual(Scroll=4.0)的零事件来重置 Chrome 的滚动缓冲区,否则 Chrome 会残留惯性状态。
热键与鼠标干预
ScrollCore还监听修饰键按下/抬起(ScrollCore.swift),支持三类临时行为:
- Dash(冲刺):按住配置的修饰键时,
dashAmplification变为 5.0,滚动增益放大 5 倍,用于快速翻页; - Toggle(切换方向):按住时临时翻转滚动方向;
- Block(屏蔽平滑):按住时临时禁用平滑滚动,并调用
ScrollPoster.brake()刹车。
这些修饰键的配置保存在每个应用例外项中(dash/toggle/block三个字段,见 Constants.swift),左右键均可触发。同时,左键按下事件会立即停止正在进行的平滑滚动(mouseLeftEventCallBack,见 ScrollCore.swift),避免在滚动中点击产生漂移。
配置参数:默认值与存储方式
Mos 的全部设置通过Options单例(Options.swift)读写UserDefaults。以下是 Constants.swift 中定义的默认参数与对应存储键:
基础设置(General)
| 配置项 | 默认值 | UserDefaults 键 | 说明 |
|---|---|---|---|
| 平滑滚动 | true | smooth | 是否启用鼠标平滑滚动 |
| 翻转方向 | true | reverse | 是否翻转鼠标滚动方向(不影响触控板) |
| 登录自启 | false | 系统 LoginItems | 通过 LoginServiceKit 管理 |
| 隐藏菜单栏图标 | false | hideStatusItem | 隐藏状态栏图标 |
| 白名单模式 | false | allowlist | false=黑名单,true=白名单 |
| 应用列表 | 空 | applications | 例外应用(JSON 编码) |
对应界面代码见 PreferencesGeneralViewController.swift:四个复选框(平滑、翻转、自启、隐藏图标)直接绑定上述开关。
高级设置(Advanced / 滚动曲线)
| 配置项 | 默认值 | UserDefaults 键 | 说明 |
|---|---|---|---|
| Step(步进) | 35.0 | step | 非 Fixed 事件的归一化门限值 |
| Speed(速度) | 3.00 | speed | 插值增益系数,越大滚动越快 |
| Duration(时长) | 3.90 | duration | 插值缓动时长(0~5),越大越柔和 |
| Precision(精度) | 1.00 | precision | 位移小于该值时自动停止插值 |
| Dash 热键 | 0 | dash | 冲刺热键(修饰键枚举值) |
| Toggle 热键 | 0 | toggle | 方向切换热键 |
| Block 热键 | 0 | block | 平滑屏蔽热键 |
其中duration不会直接参与插值,而是通过generateDurationTransition(with:)(Constants.swift)转换为过渡系数durationTransition:
let upperLimit = 5.0 + 0.2 let val = 1-(duration/upperLimit).squareRoot() return Double(round(1000 * val)/1000)即durationTransition = 1 - sqrt(duration / 5.2),默认duration=3.90对应约0.134。这个转换让滑块取值与手感呈非线性关系,小数值区间更精细。读取配置时(Options.swift)会同步计算durationTransition并保存到scrollAdvanced。
所有参数在写入时通过didSet观察器自动持久化(saveOptions()),并设置optionsExist标志;读取时若标志不存在则先以默认值写入一次。配置采用 JSON 编码存于UserDefaults.standard,其中应用列表使用EnhanceArray(EnhanceArray.swift)按path去重并监听变更自动保存。
按应用定制:例外应用与黑白名单
Mos 允许为特定应用单独配置滚动行为。ExceptionalApplication(ExceptionalApplication.swift)是 Codable 模型,字段包括:
path:应用可执行路径或 Bundle 路径(唯一标识,Equatable 按此比较);displayName:显示名称(可为空,为空时从路径解析);inherit:是否继承全局高级参数(仅影响 Advanced 部分),默认true;scrollBasic/scrollAdvanced:该应用自己的滚动基础与高级配置。
运行时决策逻辑在 ScrollCore.swift:若事件目标应用在例外列表中,则使用该应用的isSmooth/isReverse/getStep/getSpeed/getDuration;否则在非白名单模式(allowlist == false,即黑名单语义)下使用全局配置。注意isSmooth与isReverse的判定是双重 AND:应用级开关与全局开关同时为真才生效(ExceptionalApplication.swift)。
当allowlist为true时则转为白名单语义:只有例外列表中的应用才会应用平滑/翻转,其他应用一律放行原始事件。这就是 README 所称的"built-in per app block/allow list support"的完整实现。
图形化监控与窗口管理
README 提到的"informative graph that shows the scroll events"对应仓库的 Monitor 模块。从 Constants.swift 可看到应用维护了四类窗口控制器:
introductionWindowController:首次启动引导;welcomeWindowController:欢迎窗口;monitorWindowController:滚动事件监控窗口(图形化呈现);preferencesWindowController:偏好设置窗口。
监控窗口的实现位于 Windows/MonitorWindow,偏好设置则拆分为 General / Advanced / Exception / Donate / About 等多个面板(见 Windows/PreferencesWindow),其中例外应用管理界面支持添加、编辑与输入(PreferencesExceptionViewController.swift 与 PreferencesExceptionInputViewController.swift)。
贡献与许可证
Mos 欢迎任何形式的贡献:你可以通过提交 Issue 报告问题或提出改进,也可以直接提交 PR 完善代码或本地化翻译(仓库 Localization 目录已包含 en、zh-Hans、zh-Hant、ja、ko、ru、de、tr、uk、el 等多语言Localizable.strings)。提交 Issue 与 PR 前可参考 ISSUE_TEMPLATE.md、CONTRIBUTING.md 与 CODE_OF_CONDUCT.md。
许可证方面,README 明确:Mos 采用CC Attribution-NonCommercial(署名-非商业性使用)许可(LICENSE),版权归 Caldis 所有,且禁止将 Mos 上传至 App Store。在使用、二次开发或分发时请务必遵守该非商业限制。
总结:从 README 到源码的完整闭环
回顾 README.enUS.md 的五项核心特性,每一项目前都能在仓库中找到对应实现:平滑滚动由ScrollCore+ScrollPoster+Interpolator协作完成;方向独立翻转由ScrollEvent.reverse与事件分类保证(触控板事件被直接放行);速度与加速度曲线由step/speed/duration参数控制;监控窗口由 Monitor 模块提供;黑白名单由ExceptionalApplication列表与allowlist标志实现。
Mos 的巧妙之处在于:它不在任何应用内部做手脚,而是站在系统事件流这一"更高维度",用 CGEventTap 截流、用 CVDisplayLink 逐帧插值、用相位字段伪装触控板滚动语义——这套"事件级平滑"方案正是它保持轻量、免费且广泛兼容的底层原因。安装、配置与源码结合阅读后,你既可以用brew install mos快速上手,也能在需要时深入 Mos/ScrollCore 定制属于自己的滚动曲线。
- 桌面应用
【免费下载链接】Mos
一个用于在 macOS 上平滑你的鼠标滚动效果或单独设置滚动方向的小工具, 让你的滚轮爽如触控板 | A lightweight tool used to smooth scrolling and set scroll direction independently for your mouse on macOS
相关推荐
Mos 使用指南与源码原理全解:在 macOS 上实现鼠标滚轮平滑滚动与方向独立设置
Mos 使用指南与源码原理全解:在 macOS 上实现鼠标滚轮平滑滚动与方向独立设置 Mos 是一款免费、开源的 macOS 状态栏小工具,核心目标是用软件手段
桌面应用macOS滚动方向设置完全指南:3步解锁触控板与鼠标独立控制
macOS滚动方向设置完全指南:3步解锁触控板与鼠标独立控制 Scroll Reverser是一款专为macOS设计的开源工具,通过创新的独立滚动控制技术,彻底
桌面应用Mac滚动革命:Mos工具实现鼠标触控板独立控制终极方案
Mac滚动革命:Mos工具实现鼠标触控板独立控制终极方案 在Mac使用过程中,你是否经常遇到这样的困扰:习惯了触控板的自然滚动方向,但外接鼠标却总是反向操作?在
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考