单输入框双模式:这款 3MB 浏览器的地址栏设计哲学拆解
【免费下载链接】SearchA small, fast WebKit browser for macOS, by Office Commun.项目地址: https://gitcode.com/gh_mirrors/search59/Search
浏览器地址栏在过去二十年里经历了"合一—堆料—再回归"的三段式演化。如今主流浏览器的输入框上方,往往叠加着站点 Logo、AI 摘要、新闻流、起始页、账户头像——输入框本身只是入口之一。而 Search(Office Commun 出品、基于 WebKit 的 macOS 浏览器,磁盘占用约 3MB)给出的答案是:整个浏览器只有一个输入框,且它同时且仅同时回答两个问题——"你要去哪"和"你想搜什么"。
这不是功能缺失,而是一套被代码严格约束的设计哲学。本文从仓库源码出发,拆解这个单输入框背后的双模式判定链、键盘流设计,以及砍掉 UI 的取舍清单。
一、单输入框的交互史:从 OmniBox 到极简主义的回归
2006 年 Firefox 2 引入"智能地址栏"(Smart Location Bar),开始把历史记录补全塞进地址栏;2008 年 Chrome 用 OmniBox 把"输入地址"和"输入搜索词"合并进同一个字段,成为此后所有浏览器的默认形态。但合一之后是不断的堆料:Chrome 后续把应用启动器、账户、站点建议塞进同一区域;Arc 等新浏览器则把地址栏升级为"命令栏",让输入框承担站点搜索、快捷指令甚至 AI 提问。
Search 的回归不是回到 Firefox 1 的"地址栏和搜索框分列两端",而是把合一的输入框重新做成"唯一的输入框"。仓库 README.md 的描述很直白:
One field.Type an address and you go there; type words and you search. It finishes addresses from your own history and never sends what you type anywhere until you press Return.
与之配套的是:没有工具栏、没有起始页、没有侧边栏建议、没有账号。整个窗口只有一行标签页和页面本身。在 Omnibox.swift 的头部注释里,这个设计被写得近乎执拗:
One field, in the middle, and the few places it thinks you mean. It takes addresses and only addresses: type something that isn't a place and it shivers and says so, rather than quietly handing your keystrokes to a search engine.
注意后半句——"它只接受地址"。这与 Chrome 的哲学截然相反:Chrome 会把你打的任何内容模糊处理成搜索请求,而 Search 要求输入内容必须能判定为一个地址,否则它宁可"颤抖着拒绝",也不肯悄悄把你的按键交给搜索引擎。
二、双模式判定逻辑:一条 5 步决策链
单输入框最核心的工程问题只有一个:按下 Return 时,怎么知道你打的是地址还是搜索词?大多数浏览器用"看起来像域名就当作地址,否则就搜索"的启发式。Search 的答案在 Browser.swift 的destination(for:)中,是一段严格有序的决策链:
func destination(for typed: String) -> URL? { if let site = siteChip { return site.url(for: typed) } // 1. 字段里已有站点芯片 → 站内搜索 if let url = Address.url(from: typed) { return url } // 2. 输入本身是合法地址 → 直接前往 if let (keyword, rest) = Keyword.match(typed, in: prefs.keywords), let url = Engine.url(for: rest, template: keyword.template) { // 3. 命中关键字快捷词("yt cats")→ 定向搜索 return url } return searchURL(for: typed) // 4. 兜底:交给默认搜索引擎 }判定核心:Address.url 的严格语法
第二步的Address.url(from:)(Address.swift)是整条链的守门员,它的严格程度决定了双模式的边界:
- 含空格的输入直接判为"不是地址"——"hello world" 不可能是一个网站;
- scheme 白名单只有
http/https/file/about/data,mailto:、自定义协议一律拒绝; - 主机名必须通过
looksLikeHost校验:至少两个点分标签、TLD 全部为字母("todo" 不是域名,因为它只有一个标签;以数字结尾的"1.2.3.4.5"是版本号也不是地址)、四个数字段视为内网 IP、IPv6 的[::1]单独处理; - 协议选择按目标推断:
localhost、.local域名、192.168.*、10.*、Docker 常用的172.16–31.*走http://,其余一律https://——因为"本地服务器几乎没有证书,强行 https 只会得到连接失败"。
建议列表:搜索永远排在地址之后
同一个判定还被用于构建输入框下方的建议列表(guess(),Browser.swift):
var list = history.suggestions(for: typed, limit: 3) // 最多 3 条来自你自己历史记录的建议 if !typed.isEmpty, Address.url(from: typed) == nil { // 只有当它"不可能是个地址"时 list.append(Suggestion(key: typed, …, kind: .search)) // 才会追加一条搜索建议 } if let command { list.insert(.command(command), at: 0) } // 命令栏词条置顶注意两个细节:历史建议上限是 3 条("列表长到阅读成本超过输入地址本身");搜索建议只在输入被判定为不可能是个地址时才出现,而且排在列表末尾。灰色补全(ending)同样来自你的历史记录——Search 不会把输入发送给任何远程补全服务,直到你按下 Return。这就是 README 里"finishes addresses from your own history"的源码落点。
提交优先级与"拒绝"反馈
submit()的优先级链同样严格:箭头键选中的行 > 灰色补全的地址 > 你实际打的内容。如果三者都无法构成 URL,输入框会拒绝而不是静默转搜索——refusals += 1触发 Omnibox 的抖动动画(Omnibox.swift 中Shake修饰符),边框短暂变红,明说"这不是一个地方"。
三、键盘流设计:一个字段,整条键盘
单输入框要成立,必须把键盘操作做得比任何可视化控件都快。Search 的字段交互基本可以用一句话概括:全程键盘,无鼠标依赖。
| 按键 | 行为 |
|---|---|
⌘L | 唤起输入框,当前地址整段选中,直接输入即替换 |
Tab | 接受灰色补全的剩余部分 |
↑/↓ | 在建议列表中行走,走出列表顶端则放下列表 |
Return | 按优先级链提交 |
Esc | 收回输入框(先清掉站点芯片,再回到页面) |
为什么用 NSTextField 而不是 SwiftUI TextField
Omnibox.swift 中AddressField的注释点出了一个只有实现者才懂的细节:SwiftUI 的 TextField 只能持有你打的那段字符串,而这里必须持有"你没打的那部分"——灰色补全的地址后缀要真实存在于字段里且处于选中状态,这样继续输入会替换它、Return 会接受它。这需要一个真正的NSTextField和它的 delegate。Coordinator里甚至专门处理了"删除键必须真的删得掉"的问题:如果不加保护,灰色补全会在每次退格后被原样写回,形成一个"永远缩短不了的输入框"。
从"地址模式"切换到"搜索模式":站点芯片
这是双模式设计里最优雅的一环,源码注释写得很清楚:"在地址栏搜索一个站点,如 Arc(设置 › 通用)"(SiteSearch.swift)。你输入"red",列表顶部会出现"Search Reddit",按Tab后站点以一个小芯片(SiteChip)的形式进入输入框:
func lockSiteOffer() -> Bool { guard prefs.searchesSites, siteChip == nil, let site = siteOffer else { return false } siteOffer = nil typed = "" siteChip = site // 之后输入的任何词,都进入该站点的搜索 return true }此后destination(for:)的第一分支接管:输入框内的所有词都通过该站点的模板构造站内搜索 URL。内置了 15 个站点(Reddit、YouTube、X、ChatGPT、Claude、Perplexity、Wikipedia、GitHub、Stack Overflow、MDN、Amazon、Google Maps、IMDb、Spotify、Figma),且你去过的、声明了 OpenSearch 的站点会自动加入。字段里已经有芯片时按⌫可以把它拿走。这个特性默认关闭(Prefs.swift 中searchesSites注释:"Off unless asked for")——符合项目"功能默认最小化"的一贯原则。
第三种模式:⌘K 切换器与命令词
严格说这不止双模式。summoning状态让同一个字段变成标签切换器:⌘K时列表只回答"我已经打开了哪些页",guess()直接短路到openPages(matching:),picked预设为最新页面——"⌘K 然后 Return 就是整个手势"。另有可选的命令栏(AddressCommands.swift):输入settings、history、downloads、passwords等词,回车直接打开应用内对应面板而不是去 Google 搜这个词。同样默认关闭,注释解释得很实在:"没人指望在浏览器里输入 settings 会打开应用自己的设置面板"。
四、砍掉 UI 的背后:为隐私与速度让路的取舍清单
单输入框不是孤立的 UI 决定,它依附于一整份"不做什么"清单。README 的 "What it doesn't do" 部分几乎和 "What it does" 一样长:
速度的取舍
- 3MB 的根源是复用系统已有的 WebKit——"没有第二份 Chromium 要下载、更新、常驻内存",这也是启动和新建标签近乎即时的原因;
- 约 42,000 行 Swift,零第三方依赖,一个文件一个关注点,
Vault.swift是钥匙串、Shield.swift是广告拦截、Session.swift是会话恢复; - 广告拦截是编译一次的
WKContentRuleList,在 WebKit 网络层拦截,运行时零开销,而非 JS 注入式拦截; - 会话恢复的 Tab 是懒构建的——"带着二十个标签启动依然即时",因为不点开就不占进程。这个优化在代码里甚至细到动画层面:
Breath(输入框下方的呼吸微光)从 SwiftUI 动画改为 Core Animation 图层后,空标签页的 CPU 占用从"18% 的一个核心"降到了渲染服务器托管(Omnibox.swift 中的实测注释)。
隐私的取舍
- 没有同步、没有账号、没有云。README 的原文是:"离开你 Mac 的只有你请求的页面、它们的图标、以及每天一次检查是否有新版本的小请求";
- 没有遥测、没有分析、不上报崩溃;
- 密码只进 macOS 系统钥匙串,历史书签是本地 JSON 文件;唯一窗口、无多窗口模式——"标签页是唯一的新开方式"。
结语
Search 的地址栏证明了"单输入框"并不等于"功能少",而是一种更严格的输入契约:它能接受的东西被精确界定,不能接受的会明确拒绝。双模式之所以没有退化成模糊匹配,是因为判定逻辑(Address.url的语法门禁)先于建议列表(搜索建议只在"不可能是个地址"时才出现)先于提交动作(拒绝时抖动提示)形成了一条完整的链路;键盘流则把这条链路的每一步都映射成一个键。在浏览器产品普遍把输入框做成"增长入口"的今天,这种把 UI 砍到只剩一个字段、把逻辑做进判定链的做法,反而显出一种工程上的克制与自信——工具应该安静,这是它最响亮的主张。
【免费下载链接】SearchA small, fast WebKit browser for macOS, by Office Commun.项目地址: https://gitcode.com/gh_mirrors/search59/Search
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考