拓冰建站拓冰建站
首页 / 资讯中心 / 正文

BrewUI:用SwiftUI为Homebrew打造原生可视化图形界面,破解命令行工具新手困境

1. 为什么做BrewUI命令行工具的新手困境1.1 Homebrew是好东西但它的交互真的算不上友好先说说背景。做 macOS 开发的人几乎没有不用 Homebrew 的。无论是装 git、node、python 这类基础环境还是装 nginx、redis、postgres 这类服务端组件一条brew install基本都能搞定。它这么多年稳坐 macOS 包管理第一把交椅靠的就是简单直接的命令设计和庞大的 formula 生态这一点没什么争议。但问题也很明显。Homebrew 的日常操作长这样打开终端敲brew search拼公式名记不住再打开浏览器去 formulae.brew.sh 查装完了想看看装了哪些东西敲brew list输出是一长串密密麻麻的纯文本版本信息挤在一起扫一眼根本分不清谁是谁想升级某几个包得先brew outdated看列表再手动拼brew upgrade加包名有一次把 redis 和 redis-cli 搞混了差点把不相关的包一起升了。更要命的是依赖关系。Homebrew 每次安装都会顺带拉一串依赖装完你根本不知道这个包到底带了哪些东西。等哪天想清理系统brew autoremove一执行屏幕上滚过一堆日志你只能等着,完全不知道它在干嘛也不敢中断怕把包管理器搞坏。这个过程对懂命令行的人来说只是有点烦对刚接触 macOS 生态的新手来说那就是劝退级别的体验。我做了几年后端开发用 Homebrew 至少四五年身边同事和朋友被它这套纯命令行操作劝退的例子太多了。不是包管理器本身不行是它的信息呈现方式太原始——它输出的是给机器看的文本而不是给人看的内容。BrewUI 这个项目就是冲着这个痛点去的把 Homebrew 的完整能力变成图形界面让用户不用背命令、不用盯着终端猜进度也能完成几乎所有常见操作。1.2 做GUI不是要取代命令行而是补上可视化这块拼图在动工之前我需要把定位说清楚。BrewUI 不是要做一个更漂亮的终端也不是想替代 Homebrew 本身。Homebrew 的命令行能力是它的核心价值GUI 无法也不该取代它两者是互补关系命令行适合脚本化、批处理、远程操作GUI 适合日常浏览、状态检查、依赖分析和误操作防护。举个例子。用户想知道本地装了哪些包、哪些可以升级、哪些体积特别大在终端里得先执行几条命令再自己拼信息而 GUI 可以用表格和分组一次呈现还能按大小排序、按类别过滤,这是纯文本很难做到的体验。再比如卸载一个包brew uninstall会列出它会移除的依赖但对新手来说这串依赖名毫无意义他们根本不知道哪些是别的包还在用的哪些删了无所谓。GUI 可以把整棵依赖树画出来让用户一眼看明白这个包连接了哪些东西。所以 BrewUI 的定位是一个可视化管理层底层仍然老老实实调用 brew 命令不做任何绕过包管理器的操作。这既保证了数据来源可靠也天然继承了 Homebrew 的所有能力更新今天某个新 formula 发布了明天 brew 支持了新参数BrewUI 不需要改一行代码就能用上。这个薄封装的思路是整个项目最重要的一项设计决策后面所有技术方案都围绕它展开。2. 整体设计先想清楚边界再动手写代码2.1 功能范围怎么划哪些进GUI哪些留在命令行第一版功能清单我是按使用频率来排的。日常最高频的无非几件事搜索并安装新包、查看已装列表、批量升级、卸载并清理依赖。这些必须做好、做顺是核心路径。第二梯队是信息类功能查看某个包的详情版本、依赖、安装路径、检查 outdated 列表、统计分析本地安装的包占比。第三梯队才是高阶操作比如管理 services 服务、操作 tap 仓库、处理 doctor 诊断信息这些可以放到菜单栏和设置页做成高级入口不进主页。同时我明确砍掉了一些东西。比如brew edit、brew create这类涉及修改 formula 的开发功能不是普通用户的场景留命令行就够了。还有brew cat这种直接打印文本的操作做进 GUI 意义不大。做一些取舍很重要如果什么功能都想塞进去界面就会变成一个命令行模拟器反而丢了图形化的意义。好 GUI 的标准不是功能最多而是该点三下的操作绝不让用户点五下。关于哪些留在命令行我的判断标准很简单需要管道、变量、条件判断才能完成的复杂操作留在终端看状态、做选择、触发安装这类单次确认型操作交给 GUI。比如我要批量升级并清理缓存一行brew upgrade brew cleanup搞定我没必要在 GUI 里做一个按钮。但如果我要从 30 个可升级包里挑出 5 个来升GUI 里的勾选框就比手写命令行舒服得多。2.2 技术选型为什么用SwiftUI而不是Electron技术栈这块我纠结过一阵子。BrewUI 的目标平台是 macOS从一开始就锁定原生主要原因是性能和系统整合度。Homebrew 安装在本地进程调用、文件监听、权限交互都绕不开操作系统层面的能力原生框架处理这些是最顺的。Electron 当然开发效率高、跨平台好说但一个包管理工具要常驻菜单栏、频繁刷新列表、解析大量文本日志用 WebView 包一层总觉得有些重内存占用和响应速度都不占优。而且 macOS 用户对原生交互已经形成了肌肉记忆比如 CommandW 关闭窗口、拖放文件到图标上、系统级通知弹权限提示这些原生框架做几乎零成本跨平台方案还要手动适配。最终确定用 SwiftUI配合 Swift Concurrency 的 async/await 处理异步任务。SwiftUI 的声明式布局写界面效率很高列表、表单、导航这些元素都能直接绑定数据模型窗口和菜单栏支持开箱即用。状态管理用 ObservableObject 和 Published 来驱动 UI 刷新整套模型和 Homebrew 的操作流读状态、执行命令、更新状态刚好对得上。整个工程的目录结构大约是这样BrewUI/ ├── App/ // App入口、全局状态 ├── Models/ // Formula、Dependency、OutdatedItem等数据结构 ├── Services/ │ ├── BrewService.swift // 命令执行封装 │ ├── BrewOutputParser.swift // 文本/JSON输出解析 │ ├── BrewLockMonitor.swift // 检测brew是否在执行 │ └── AppSettings.swift // 缓存与偏好设置 ├── Views/ │ ├── DashboardView.swift // 总览仪表盘 │ ├── SearchView.swift // 搜索与详情 │ ├── InstalledListView.swift // 已安装列表 │ ├── UpgradeCenterView.swift // 升级管理 │ └── DependencyGraphView.swift // 依赖关系图 └── Support/ // 扩展、工具函数按这个划分Services 层是纯逻辑、不涉及 UI以后想加命令行版本或者变成菜单栏小组件都能复用Views 层只负责渲染和用户交互职责清楚调试也好下手。2.3 数据模型怎么定义紧跟Homebrew的JSON输出这里必须先重点说一个关键选择解析 Homebrew 的输出时能用 JSON 就用 JSON千万不要依赖解析终端文本。Homebrew 从 1.7 开始就提供了完整的 JSON 输出支持比如brew info --jsonv2 --formula name返回的就是结构化的 formula 元数据包括名称、版本号、依赖列表、依赖关系、安装路径、许可证、描述等等。用它做数据源写一个 Swift 的 Decodable 模型就能直接映射干净又稳定。我定义的核心模型大致长这样省略部分字段struct FormulaInfo: Decodable, Identifiable { let name: String let fullName: String? let desc: String? let versions: Versions let dependencies: [String] let buildDependencies: [String] let runtimeDependencies: [RuntimeDependency]? let installed: [InstalledVersion]? let kegOnly: Bool let license: String? let conflictsWith: [String]? let caveats: String? var id: String { name } } struct Versions: Decodable { let stable: String? let head: String? let bottle: Bool } struct InstalledVersion: Decodable { let version: String let installedAsDependency: Bool let installedOnRequest: Bool let runtimeDependencies: [RuntimeDependency]? } struct RuntimeDependency: Decodable { let fullName: String let version: String let declaredDirectly: Bool? }拿这个结构体去做列表和详情页所有字段都清清楚楚。搜索也简单brew search --formula keyword配合--json参数可以拿结构化结果或者直接跑一次brew info --jsonv2拿全量数据后在内存里过滤。这里有个性能和实时性的平衡问题下面专门聊。3. 核心功能实现从brew命令到GUI视图3.1 搜索与详情把原始输出整理成信息卡片搜索功能看起来简单但体验要想做好关键在于最快路径设计。用户打开搜索页输入关键字选择分类formula 还是 cask点搜索得到结果列表。点进去就是详情页展示描述、版本、依赖、安装状态、体积等信息每一段都明确标注这个信息告诉我们什么而不是把原始输出原样搬上去。这里我踩过的坑是第一版直接尝试把brew search的文本输出塞进 UI。结果发现搜索返回的是一堆以 formua 名空格分隔的纯字符串根本没法解析哪个是规则名、哪个是已安装标记、哪个描述对应哪个包只能用正则硬抠还要处理颜色转义符丑得不行。后来我改用brew search --formula --jsonv2 keyword一次调用就能拿到公式名、描述、版本号的结构化数据解析逻辑瞬间清爽了。安装操作的信息展示我也没有用默认的开始安装按钮了事。用户点击安装前我会先显示一行这个包将安装 X 个依赖其中 Y 个是编译依赖所需空间约为 Z MB的摘要依赖详情可以展开看。这个设计让用户对结果有预期也减少了误安装的风险。很多不懂包管理的用户装完一个工具发现系统里莫名其妙多了一堆库第一反应是中毒了而不是这是依赖提前把信息讲清楚能省掉很多困惑。关于安装时模拟也就是brew install --dry-run我建议 GUI 做前置校验时可以直接调用它能列出将要安装的依赖但不会真正执行非常适合做刚才说的安装前摘要成本低收益高。3.2 安装/卸载任务进程管理、输出解析与进度反馈安装和卸载是耗时的长任务这部分是整个 App 工程复杂度最高的环节。SwiftUI 里发起一个异步任务很简单但真正难的是三件事启动进程、实时拿输出、在主线程更新 UI。先看作一个核心的执行服务封装用 Swift 的 Process 直接调 brew 可执行文件final class BrewService { static let shared BrewService() private var brewPath: String { // 先判断Apple Silicon/opt/homebrew还是Intel/usr/local let appleSiliconPath /opt/homebrew/bin/brew if FileManager.default.fileExists(atPath: appleSiliconPath) { return appleSiliconPath } return /usr/local/bin/brew } func run(_ arguments: [String], outputHandler: escaping (String) - Void) async throws { return try await withCheckedThrowingContinuation { continuation in let process Process() let pipe Pipe() let errorPipe Pipe() process.executableURL URL(fileURLWithPath: brewPath) process.arguments arguments process.standardOutput pipe process.standardError errorPipe pipe.fileHandleForReading.readabilityHandler { handle in let data handle.availableData if let output String(data: data, encoding: .utf8) { outputHandler(output) } } process.terminationHandler { proc in pipe.fileHandleForReading.readabilityHandler nil if proc.terminationStatus 0 { continuation.resume() } else { continuation.resume(throwing: BrewError.installFailed(code: proc.terminationStatus)) } } do { try process.run() } catch { continuation.resume(throwing: error) } } } }这里三个细节值得展开。一是 brew 路径不能写死Apple Silicon 和 Intel Mac 的安装路径不同要用文件是否存在来判断否则新机型直接调不到命令。二是 stdout 和 stderr 要分开接管brew 经常把进度条写在 stderr 里如果只读 stdout界面会一直干等着没反馈。三是 installations 的进度条是\r回车符同一行刷新的直接拆分行会导致界面不断刷新闪来闪去需要先按\r做段切分再按换行符做行切分。进度反馈我做了一个简单方案监听输出里的Downloading、Pouring、Linking等关键字把当前阶段展示在界面上再配合系统日志的最后一行原文。真正的精确百分比不太好做因为每个包编译时间差异太大但阶段提示配合活动指示器已经足够让用户安心了。卸载那边要额外处理一个情况brew uninstall会询问是否删除依赖。命令行里用户自己回答 y/nGUI 里不能弹终端。我的方案是先跑一次brew uninstall --dry-run formula拿到它将要移除的依赖列表然后在 GUI 里做一个二次确认弹窗告诉用户该公式还关联这些依赖是否继续移除?用户点确认后才真正执行卸载。这一步看似只是多问了一句实际能挽回很多手滑造成的损失。3.3 升级管理区分升级和清理状态同步要做好brew upgrade全家桶升级是最简单粗暴的但 GUI 的价值在于精细控制。升级页我会默认把brew update作为前置步骤先把本地 formula 索引刷新到最新然后拉取 outdated 列表用表格列出所有可升级的包每一行都展示当前版本、目标版本、所属仓库并标注这个包最近一次升级时间。用户勾选要升级的包后再执行brew upgrade加包名列表。这里有个隐藏问题brew upgrade支持只升级指定包但得一个一个传参数如果用户选了 50 个包命令字符串就会特别长。实测下来没什么大问题只是完成后的输出是全部混在一起的我做了按包名拆分日志的处理每个包一个小分组方便对照结果。另外一个细节是升级完成后要自动重新查询一次 installed 列表和 outdated 列表才能保证界面数据和实际情况一致——如果只靠操作前的快照升级完页面还显示旧版本会非常误导人。清理这块我用brew cleanup --dry-run先获取可以清理的缓存文件列表用户可以预览将释放多少空间再决定是否执行。这样比直接运行brew cleanup稳妥毕竟用户对自己的磁盘空间有知情权。brew autoremove --dry-run也可以做同样的预览列出未使用但还在系统里的旧依赖让用户决定是不是要清掉。3.4 依赖关系可视化把brew deps --tree变成看得懂的图依赖图是 BrewUI 里最受好评的一个功能也是实现起来最需要耐心的。brew deps --tree --formula name的原始输出是树形文本长这样wget ├── ca-certificates ├── irzcx ├── libidn2 │ ├── gettext │ ├── libunistring │ └── libxcrypt ├── openssl3 │ └── ca-certificates └── zlib解析这个树形文本是个经典问题。我的做法是逐行扫描计算节点的缩进层级用一个栈来维护当前路径遇到├──表明是当前层级的兄弟节点遇到└──表明是最后一个兄弟节点同时是新的嵌套层级开始。因为 Homebrew 的树形输出没有终结符标志遇到└──开始的新节点它下面的│前缀子节点都属于它解析时需要用当前行是父还是子的启发式规则来判断我命名为末节点栈回退法。实际实现如下struct DependencyNode: Identifiable { let id: String // 用路径拼接保证唯一 let name: String let version: String? let children: [DependencyNode] } func parseDependencyTree(_ text: String) - DependencyNode { let lines text.components(separatedBy: .newlines) let rootName lines.first?.trimmingCharacters(in: .whitespaces) ?? root var root DependencyNode(id: rootName, name: rootName, version: nil, children: []) var stack: [DependencyNode] [root] let prefixRegex try! NSRegularExpression(pattern: ^(│?\\s*)([├└]──\\s*)?(.)$) for line in lines.dropFirst() { guard !line.isEmpty else { continue } let nsRange NSRange(line.startIndex..., in: line) guard let match prefixRegex.firstMatch(in: line, range: nsRange), let nameRange Range(match.range(at: 3), in: line) else { continue } let name String(line[nameRange]) let prefix match.range(at: 1).length match.range(at: 2).length let level prefix / 2 // 每个层级的缩进占2个字符位 let node DependencyNode(id: stack.prefix(level 1).map(\\.name).joined(separator: -) - name, name: name, version: nil, children: []) while stack.count level 1 { stack.removeLast() } stack[level].children.append(node) stack.append(node) } return root }解析完就是一颗标准树结构配合 SwiftUI 的自定义递归视图可以画成缩进树或者力导向图。这个功能的价值在于用户安装一个大包之前可以先看依赖树评估风险升级某个底层库时也能直观看到哪个顶层包会受影响提前知道可能带来的连锁反应。比如有一次用户想升级 openssl3先看了依赖图发现有三个 python 包都依赖它升级前心里就有了底后面完全没慌。4. 工程里那些坑排查实录与处理方案4.1 brew自身的锁与并发冲突BrewUI 刚发第一个测试版的时候有几个用户反馈点击安装后按钮一直转圈终端里也没反应。排查后发现是我没有处理 brew 自身的文件锁。Homebrew 为了并发安全在安装、更新等操作时会创建锁文件如果同时跑两个 brew 命令后一个会一直等待前一个释放锁表现就是进程挂起。解决思路是启动任何命令前先检查锁目录。锁文件通常在/opt/homebrew/var/homebrew/locks或者/usr/local/var/homebrew/locks下不同版本略有区别但大体一致。我在 BrewService 里加了一个锁检查逻辑如果检测到锁文件存在就先弹提示另一个 brew 操作正在进行请稍后再试而不是直接启动一个新的 brew 进程去排队。同时把这个检查逻辑做成定时轮询锁释放后自动把当前等待中的操作继续跑完体验上更顺。这个检查也帮我避免了一个更隐蔽的 bug用户在 GUI 里点了安装同时自己又在终端里执行了brew update两边都在锁定 brew 状态。如果不检测GUI 会一直卡到天荒地老用户以为是 App 死了其实是在等另一个进程。现在流程是点击安装 - 检查锁 - 如果有锁提示并等待 - 锁释放后自动重试。4.2 权限弹窗与sudo处理的取舍Homebrew 本身不需要 sudo这是它的一个设计特点大多数操作普通用户权限就够了。但有几个场景还是会有权限问题典型的是/usr/local目录下Intel 机器的写入权限因为某些旧版本的 Homebrew 使用系统目录作为安装前缀用户对那部分目录没有写权限。BrewUI 的处理原则是不主动提权不自己调 sudo。因为这会给 App 带来完全不必要的安全风险。做法是提前检测目标目录的可写性写不进去就明确告知用户当前目录不可写请先修复 Homebrew 目录权限再试并给出修复命令让用户复制到终端里跑。这里有个很容易踩的坑如果直接把 sudo 相关命令内嵌到 App 里Mac App Store 审核那关直接过不去而且用户安全方面也是大忌。Homebrew 官方对目录权限问题给出的标准修复方式就是重新chown目录把这个步骤引导好就足够了。4.3 中英文输出混杂、日志解析不稳定早期版本我试图用正则去解析brew install的终端输出然后从中提取安装进度。后来发现完全不可行因为 Homebrew 的输出会根据系统语言变化中文系统下输出的提示语和英文系统不一样而且版本更新后提示语还经常微调。今天写着 Pouring you know it明天就可能变成 正在注入你懂的正则怎么写都脆。后来我完全放弃了从人类可读文本里抓数据的思路转向统一走 JSON 接口。需要机器判断的状态全部走--json参数比如brew info --jsonv2 --formula name拿单个包的详细信息brew list --formula --jsonv2拿全部已安装包的名称、版本和依赖信息brew outdated --jsonv2拿可升级列表brew search --formula --jsonv2 keyword拿搜索结果只有无法用 JSON 替代的场景才解析文本比如brew deps --tree的树形结构以及安装过程中的实时日志。实时日志我给用户看原文即可不追求机器解析这样既解决了显示问题又不需要在解析正则上死磕。这是一条特别重要的经验GUI 包管理器的数据解析永远优先选择结构化数据只有结构化数据拿不到的时候才退回到文本解析。4.4 源慢、下载中断与镜像配置的处理Homebrew 默认的下载源在国外国内用户经常卡在 Downloading 阶段进度条一动不动。这个问题不解决GUI 做得再好也没用。BrewUI 的做法分两层。第一层是检测。每次启动时记录 brew 命令的执行耗时如果连续多次 install 命令都超时或者下载速度为 0就提示用户当前下载源响应较慢是否考虑配置镜像源。这里的措辞要注意我不能替用户做决定只是提供选项因为修改源属于系统级配置用户需要知情并主动确认。第二层是引导配置。用户的~/.zshrc或者~/.bash_profile里可能已经设置了HOMEBREW_API_DOMAIN、HOMEBREW_BOTTLE_DOMAIN等环境变量。GUI 没法直接读取 shell 环境变量因为 App 启动时不是从 shell 里继承的所以我在设置页里加了两个输入框允许用户显式填入这些环境变量的值在每次调用 brew 前注入到 Process 环境里。这样切换源只需要在 GUI 里填一次地址不需要打开终端改配置文件对不熟悉 shell 的用户友好得多。同时我会附上对应镜像源的官方文档链接让用户自主判断该用什么地址。关于中断和重试brew 自带的下载机制在中断后重新执行会续传所以 GUI 里我做得比较简单安装中途用户点击停止时直接终止进程然后提示部分下载已缓存下次再试会自动续传。实测确实能续因为没有清理缓存目录这是 Homebrew 自己的机制我们的代码只需要保证不误删~/Library/Caches/Homebrew就行。5. 性能优化与体验打磨5.1 列表渲染与数据缓存的平衡BrewUI 的已安装列表可能包含几百个包搜索索引也可能有上万条 formula 记录。直接每次打开页面都跑一遍brew info --jsonv2会非常慢实测全量索引数据有 4~6 MB解析也要一两秒用户等待的体验就很差了。我的方案是引入两级缓存。第一级是搜索结果缓存把每次搜索的关键字和 JSON 响应存到本地文件过期时间为 5 分钟。用户连续搜索同一个关键词时直接就命中本地缓存。第二级是 installed 状态缓存安装列表变动的场景其实不多我把brew list --formula --jsonv2的结果缓存在 UserDefaults 里同时监听几个会改变状态的页面操作安装成功、卸载成功、升级成功操作完成后主动刷新缓存。这样每次启动 App 都不用重新跑brew list只有真正有变化时才重新拉取页面打开速度几乎是无感的。SwiftUI 侧列表性能也要注意。几百行的 List 其实不大但如果每行都在绑一个复杂的 ObservableObject滚动就会掉帧。我的处理是把数据模型设计成纯值类型Struct列表只绑最终要显示的那些字段比如名称、版本、描述需要展开详情时才去加载额外的依赖和体积信息。体积这个数据可以用brew info --jsonv2里带的有效体积字段不需要实时计算磁盘占用省掉了很多文件系统 IO。5.2 后台任务与主线程的隔离Process 的回调线程和主线程不是一回事这是个天然的坑。pipe.fileHandleForReading.readabilityHandler是在后台线程调用的如果直接在里面更新 Published 属性SwiftUI 会在主线程渲染时找到不一致的状态轻则日志警告重则画面闪烁崩溃。我的处理是所有的 UI 状态更新都通过DispatchQueue.main.async包一层或者更干脆在 Service 层里只发异步事件View 通过.task或者onReceive来接收事件后统一更新。后台任务的取消机制也要设计好。用户可能装到一半点取消或者是切到另一个页面发起新的安装。我的做法是给每个任务生成一个 UUID 作为标识并维护一个正在运行的任务字典。取消时向字典里的 Process 发送terminate()然后从字典移除。SwiftUI 侧要注意不能简单地用.task的取消因为 Task 取消后如果进程还在跑后台还是会执行一定要真正调用process.terminate()而不是只取消 Swift 协程。5.3 版本迭代与用户反馈闭环BrewUI 从第一版到现在功能基本稳定后我花了大量精力在错误反馈和崩溃日志收集上。macOS 上有个简单的方案直接监听NSSavePanel和文本输出收集器把用户操作的关键路径记录成本地日志文件包含每次执行的命令、退出码和耗时。用户遇到问题时可以从 App 菜单直接导出日志不用技术背景也能反馈反馈里信息就够了。很多用户提的需求也很有意思。有人希望加一键部署开发环境功能我解释这不是 CASK 能解决的问题那属于配置管理和环境编排的范畴不在包管理器 GUI 的边界内。也有人希望加包体积排行这个我做了很简单从 JSON 里读体积字段排序就行。还有呼吁加触摸栏支持的我评估后没有做原因是 Homebrew 操作都是低频长任务触摸栏更适合快速动作如搜索和安装确认而不是完整列表操作投入产出比不高。产品边界就是靠这些拒绝和取舍一步一步清晰起来的。6. 最终交付与开源情况BrewUI 目前的版本已经支持 Homebrew formula 的完整生命周期管理搜索、查看详情、安装、卸载、升级、清理、依赖分析、services 启停以及缓存和镜像源配置。已安装列表支持按名称、大小、依赖数量、最后安装时间排序可以一键查看某个包的影响范围。整套交互流程我用下来最大的感受是排查问题时不用再开一个终端窗口了GUI 里有全部信息点几下就能定位到问题包。这个项目是开源方式发布的代码托管在公开仓库里遵循 MIT 协议。发布几周后陆续有用户提交 PR有人补了 cask 支持有人优化了中文搜索结果的分词也有人修了 Intel Mac 上的路径兼容问题。开源给我带来的最大价值不是代码本身而是各种真实设备暴露出的兼容性坑——用户的机器配置五花八门有些问题我自己根本测不出来。最后再分享一个我做了很久才想通的设计取舍。BrewUI 的自动刷新特性一开始是紧跟在操作结束后的后来发现频繁的刷新会导致用户正在浏览的列表跳来跳去体验很糟。后来改成了操作结束后展示结果差量提示比如该升级包列表自动刷新但用户正在查看的详情页保持原有内容不变只有用户主动刷新或者再次进入时更新。表面上这只是一个小交互改动实际效果是用户对整个 App 的信任感一下子提升了——它不会突然把正在看的页面变掉永远知道自己在什么位置。命令行和 GUI 的边界我以后还会不断调整但核心原则不会变BrewUI 是 Homebrew 的可视化窗口不是包管理器的替代者。如果你也常被 brew 的长篇日志搞到眼花或者想让不熟命令行的朋友也能放心管理开发环境这个工具应该刚好合适。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门