Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cotabby.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@
F4CFF323CDA02530ED0EBE57 /* MPL-2.0.txt in Resources */ = {isa = PBXBuildFile; fileRef = 4E283DF8948B10268B46811F /* MPL-2.0.txt */; };
F4EEE6291095B0BF2D3FBA21 /* GhostTextColorPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E44E393DD58B978B1EAB6CF /* GhostTextColorPreset.swift */; };
F596D7438459A7A4246A39CE /* GhostTextPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A594D02B0EF3C2DD62EFE69A /* GhostTextPreview.swift */; };
F71FD79FAC8B59C1CBD9E2E0 /* SettingsIndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2FFC2055C52FB837DEEB8F /* SettingsIndexTests.swift */; };
F7237FDB0665465F1C7EDCDE /* CustomRulesEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82F7F7355967725162DF2D1B /* CustomRulesEditor.swift */; };
F77DB394E9D6C6C482131BF9 /* VisualContextModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE97A8169438D593C6C23412 /* VisualContextModels.swift */; };
F78F594F77C26C233377E71F /* KeyCodeLabels.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAAE6B395FAB604DF059280A /* KeyCodeLabels.swift */; };
Expand Down Expand Up @@ -685,6 +686,7 @@
5976600F428C1265121D4C0C /* SystemSettingsWindowLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemSettingsWindowLocator.swift; sourceTree = "<group>"; };
59E299BE2E9D42A33D5D2F5D /* ScreenTextExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTextExtractor.swift; sourceTree = "<group>"; };
5A03E565A11581FD2150B142 /* CompletionRenderMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionRenderMode.swift; sourceTree = "<group>"; };
5A2FFC2055C52FB837DEEB8F /* SettingsIndexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsIndexTests.swift; sourceTree = "<group>"; };
5A3B81B92E0743C6152ED8DD /* EmojiPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerController.swift; sourceTree = "<group>"; };
5A567677424A82D9EEF47495 /* KeyRecorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyRecorderView.swift; sourceTree = "<group>"; };
5AD3F4F9FBE82007E4E15F58 /* GhostSuggestionLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostSuggestionLayoutTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1263,6 +1265,7 @@
D5A5591BEB9EE7B6E9064412 /* SelfCaptureGateTests.swift */,
2D7360A6D4261989A66658ED /* SentenceBoundaryClassifierTests.swift */,
2BC293F6125E2B14DCF05AD9 /* SettingsAttentionEvaluatorTests.swift */,
5A2FFC2055C52FB837DEEB8F /* SettingsIndexTests.swift */,
0850B07CCDBA67C756C6EC59 /* ShortcutConflictTests.swift */,
D562A73C7C680F2AA65F9F7F /* SpellingDictionaryResourceTests.swift */,
E0871985CB1F877EC422E18C /* SpellingLanguageResolverTests.swift */,
Expand Down Expand Up @@ -2177,6 +2180,7 @@
AF26E77871200BB1FAAEBE79 /* SelfCaptureGateTests.swift in Sources */,
1D1C6FF0B8F50AC14A1000F4 /* SentenceBoundaryClassifierTests.swift in Sources */,
C618C5595DA9C57C806A3E03 /* SettingsAttentionEvaluatorTests.swift in Sources */,
F71FD79FAC8B59C1CBD9E2E0 /* SettingsIndexTests.swift in Sources */,
8441299082E6B68F7F88911B /* ShortcutConflictTests.swift in Sources */,
303652F15C0FE55595669D81 /* SpellingDictionaryResourceTests.swift in Sources */,
66D0D9F605AF462F569A5CFD /* SpellingLanguageResolverTests.swift in Sources */,
Expand Down
4 changes: 3 additions & 1 deletion Cotabby/UI/Settings/Panes/AboutPaneView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ struct AboutPaneView: View {

Spacer(minLength: 12)

Button("Check for Updates") {
Button {
appUpdateManager.checkForUpdates()
} label: {
Label("Check for Updates", systemImage: "arrow.triangle.2.circlepath")
}
}
.padding(.vertical, 4)
Expand Down
8 changes: 4 additions & 4 deletions Cotabby/UI/Settings/Panes/AppsPaneView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ struct AppsPaneView: View {
Toggle(isOn: suggestInIntegratedTerminalsBinding) {
SettingsRowLabel(
title: "Suggest in Integrated Terminals",
description: "Show ghost text in the VS Code and Cursor integrated terminal. "
+ "Off by default so suggestions don't overlap shell prompts and command "
+ "output — the code editor and Copilot chat in the same window keep "
+ "suggesting either way."
description: "Show ghost text in VS Code and Cursor integrated terminals. "
+ "Off by default so suggestions stay out of shell prompts; the editor "
+ "and chat in the same window keep suggesting either way.",
systemImage: "terminal"
)
}
}
Expand Down
23 changes: 18 additions & 5 deletions Cotabby/UI/Settings/Panes/EngineAndModelPaneView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ struct EngineAndModelPaneView: View {
)
) {
SettingsRowLabel(
title: "Switch based on power source",
title: "Switch Based on Power Source",
description: "Use a different engine or model on battery vs. while plugged in. " +
"For example, Apple Intelligence on battery to save power and a larger local " +
"model while charging.",
Expand Down Expand Up @@ -173,11 +173,18 @@ struct EngineAndModelPaneView: View {
@ViewBuilder
private var appleIntelligenceSections: some View {
Section("Apple Intelligence") {
LabeledContent("Availability") {
LabeledContent {
Text(foundationModelAvailabilityService.userVisibleMessage)
.foregroundStyle(foundationModelAvailabilityService.isAvailable ? .green : .orange)
.multilineTextAlignment(.trailing)
.fixedSize(horizontal: false, vertical: true)
} label: {
SettingsRowLabel(
title: "Availability",
description: "Whether this Mac can run Apple Intelligence. Requires a supported " +
"Apple Silicon Mac with Apple Intelligence turned on in System Settings.",
systemImage: "apple.logo"
)
}
}
}
Expand All @@ -194,7 +201,7 @@ struct EngineAndModelPaneView: View {
.fixedSize(horizontal: false, vertical: true)
} label: {
SettingsRowLabel(
title: "Status",
title: "Model Status",
description: "Whether the local model is loaded and ready to generate. " +
"Loading takes a few seconds the first time.",
systemImage: "info.circle"
Expand Down Expand Up @@ -245,7 +252,7 @@ struct EngineAndModelPaneView: View {
}

Section("Folder") {
LabeledContent("Path") {
LabeledContent {
VStack(alignment: .trailing, spacing: 8) {
Text(modelDownloadManager.modelsDirectoryPath)
.font(.callout.monospaced())
Expand All @@ -262,11 +269,17 @@ struct EngineAndModelPaneView: View {
}
}
}
} label: {
SettingsRowLabel(
title: "Models Folder",
description: "Where downloaded model files are stored on this Mac.",
systemImage: "folder"
)
}

Toggle(isOn: $lmStudioSourceEnabled) {
SettingsRowLabel(
title: "Also use LM Studio models",
title: "Also Use LM Studio Models",
description: lmStudioModelsURL == nil
? "Install LM Studio to load models from its library here."
: "Add models from your LM Studio library (~/.lmstudio/models) to the picker " +
Expand Down
2 changes: 1 addition & 1 deletion Cotabby/UI/Settings/Panes/ShortcutsPaneView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ struct ShortcutsPaneView: View {
)
} label: {
SettingsRowLabel(
title: "Toggle Tabby",
title: "Toggle Cotabby",
description: "Turn Cotabby on or off globally without opening the menu bar.",
systemImage: "power.circle"
)
Expand Down
9 changes: 6 additions & 3 deletions Cotabby/UI/Settings/Panes/WritingPaneView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,25 @@ struct WritingPaneView: View {
Toggle(isOn: suppressCompletionsOnTypoBinding) {
SettingsRowLabel(
title: "Hide Suggestions on Typo",
description: "Stops normal completions while the current word appears misspelled."
description: "Stops normal completions while the current word appears misspelled.",
systemImage: "eye.slash"
)
}

Toggle(isOn: offerTypoCorrectionsBinding) {
SettingsRowLabel(
title: "Offer Corrections on Typo",
description: "Shows a green replacement you can apply with your accept key."
description: "Shows a green replacement you can apply with your accept key.",
systemImage: "checkmark.bubble"
)
}
.disabled(!suggestionSettings.suppressCompletionsOnTypo)

Toggle(isOn: automaticallyFixTyposBinding) {
SettingsRowLabel(
title: "Automatically Fix Typos",
description: "After you press Space, replaces a misspelled word without requiring your accept key."
description: "After you press Space, replaces a misspelled word without requiring your accept key.",
systemImage: "checkmark.circle"
)
}
.disabled(!suggestionSettings.suppressCompletionsOnTypo)
Expand Down
39 changes: 29 additions & 10 deletions Cotabby/UI/Settings/SettingsIndex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum SettingsItem: String, CaseIterable, Identifiable {
case showKeyHint
case ghostTextColor
case ghostTextOpacity
case ghostTextSize
// Emoji
case emojiPicker
case emojiSkinTone
Expand All @@ -45,6 +46,7 @@ enum SettingsItem: String, CaseIterable, Identifiable {
// Engine & Model
case engine
case appleIntelligenceAvailability
case modelStatus
case selectedModel
case powerBasedModelSwitching
case batteryModel
Expand All @@ -60,6 +62,7 @@ enum SettingsItem: String, CaseIterable, Identifiable {
case toggleTabby
// Apps
case disabledApps
case suggestInIntegratedTerminals
// Permissions
case accessibility
case inputMonitoring
Expand Down Expand Up @@ -94,6 +97,7 @@ enum SettingsItem: String, CaseIterable, Identifiable {
case .showKeyHint: return "Show Accept-Key Hint"
case .ghostTextColor: return "Ghost Text Color"
case .ghostTextOpacity: return "Ghost Text Opacity"
case .ghostTextSize: return "Ghost Text Size"
case .emojiPicker: return "Inline Emoji Picker"
case .emojiSkinTone: return "Skin Tone"
case .emojiPeopleStyle: return "People Emoji Style"
Expand All @@ -110,23 +114,25 @@ enum SettingsItem: String, CaseIterable, Identifiable {
case .contextLivePreview: return "Live Preview"
case .engine: return "Engine"
case .appleIntelligenceAvailability: return "Apple Intelligence Availability"
case .modelStatus: return "Model Status"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
case .selectedModel: return "Selected Model"
case .powerBasedModelSwitching: return "Switch Models Based on Power Source"
case .batteryModel: return "Battery Model"
case .pluggedInModel: return "Plugged-in Model"
case .powerBasedModelSwitching: return "Switch Based on Power Source"
case .batteryModel: return "On Battery"
case .pluggedInModel: return "Plugged In"
case .downloadModels: return "Download Models"
case .huggingFaceBrowser: return "Hugging Face Model Browser"
case .modelsFolder: return "Models Folder"
case .lmStudio: return "LM Studio Models"
case .acceptanceMode: return "Acceptance Mode"
case .acceptWord: return "Accept Word"
case .acceptEntireSuggestion: return "Accept Entire Suggestion"
case .toggleTabby: return "Toggle Tabby"
case .toggleTabby: return "Toggle Cotabby"
case .disabledApps: return "Disabled Apps"
case .suggestInIntegratedTerminals: return "Suggest in Integrated Terminals"
case .accessibility: return "Accessibility"
case .inputMonitoring: return "Input Monitoring"
case .screenRecording: return "Screen Recording"
case .performanceTracking: return "Performance Tracking"
case .performanceTracking: return "Enable Performance Tracking"
case .resourceUsage: return "Live Resource Usage"
case .recentRequests: return "Recent Requests"
case .checkForUpdates: return "Check for Updates"
Expand Down Expand Up @@ -154,6 +160,7 @@ enum SettingsItem: String, CaseIterable, Identifiable {
case .showKeyHint: return "keyboard"
case .ghostTextColor: return "paintpalette"
case .ghostTextOpacity: return "circle.lefthalf.filled"
case .ghostTextSize: return "textformat.size"
case .emojiPicker: return "face.smiling"
case .emojiSkinTone: return "hand.raised.fingers.spread"
case .emojiPeopleStyle: return "person.2"
Expand All @@ -170,9 +177,10 @@ enum SettingsItem: String, CaseIterable, Identifiable {
case .contextLivePreview: return "text.cursor"
case .engine: return "cpu"
case .appleIntelligenceAvailability: return "apple.logo"
case .modelStatus: return "info.circle"
case .selectedModel: return "shippingbox"
case .powerBasedModelSwitching: return "battery.100"
case .batteryModel: return "battery.50"
case .powerBasedModelSwitching: return "battery.100.bolt"
case .batteryModel: return "battery.25"
case .pluggedInModel: return "powerplug"
case .downloadModels: return "arrow.down.circle"
case .huggingFaceBrowser: return "magnifyingglass"
Expand All @@ -183,6 +191,7 @@ enum SettingsItem: String, CaseIterable, Identifiable {
case .acceptEntireSuggestion: return "text.insert"
case .toggleTabby: return "power.circle"
case .disabledApps: return "nosign"
case .suggestInIntegratedTerminals: return "terminal"
case .accessibility: return "accessibility"
case .inputMonitoring: return "keyboard"
case .screenRecording: return "camera.viewfinder"
Expand All @@ -204,7 +213,7 @@ enum SettingsItem: String, CaseIterable, Identifiable {
.allowMultiLine, .acceptPunctuation, .inlineMacros, .onboarding:
return .general
case .suggestionDisplay, .showFieldIndicator, .showWordCount, .showKeyHint,
.ghostTextColor, .ghostTextOpacity:
.ghostTextColor, .ghostTextOpacity, .ghostTextSize:
return .appearance
case .emojiPicker, .emojiSkinTone, .emojiPeopleStyle, .emojiHistory:
return .emoji
Expand All @@ -213,13 +222,13 @@ enum SettingsItem: String, CaseIterable, Identifiable {
return .writing
case .extendedContext, .contextLivePreview:
return .context
case .engine, .appleIntelligenceAvailability, .selectedModel,
case .engine, .appleIntelligenceAvailability, .modelStatus, .selectedModel,
.powerBasedModelSwitching, .batteryModel, .pluggedInModel,
.downloadModels, .huggingFaceBrowser, .modelsFolder, .lmStudio:
return .engineAndModel
case .acceptanceMode, .acceptWord, .acceptEntireSuggestion, .toggleTabby:
return .shortcuts
case .disabledApps:
case .disabledApps, .suggestInIntegratedTerminals:
return .apps
case .accessibility, .inputMonitoring, .screenRecording:
return .permissions
Expand Down Expand Up @@ -277,6 +286,9 @@ enum SettingsItem: String, CaseIterable, Identifiable {
case .ghostTextOpacity:
return ["opacity", "transparency", "fade", "alpha", "translucent", "dim",
"brightness", "visibility"]
case .ghostTextSize:
return ["size", "font size", "scale", "bigger", "smaller", "larger", "text size",
"zoom", "multiplier", "too big", "too small"]
case .emojiPicker:
return ["emoji", "smile", "picker", "inline", "colon", "emoticon", "face",
"symbol"]
Expand Down Expand Up @@ -326,6 +338,9 @@ enum SettingsItem: String, CaseIterable, Identifiable {
case .appleIntelligenceAvailability:
return ["apple intelligence", "availability", "available", "supported",
"compatibility", "status", "macos", "device support"]
case .modelStatus:
return ["status", "loaded", "ready", "runtime", "running", "health",
"model loaded", "loading"]
case .selectedModel:
return ["model", "gguf", "pick", "selected", "active model", "choose model",
"current model", "default model"]
Expand Down Expand Up @@ -366,6 +381,10 @@ enum SettingsItem: String, CaseIterable, Identifiable {
case .disabledApps:
return ["apps", "disable", "exclude", "block", "ignore", "blacklist",
"deny list", "exception", "app exclusion", "skip", "off in"]
case .suggestInIntegratedTerminals:
return ["terminal", "terminals", "integrated terminal", "vscode", "vs code",
"cursor", "shell", "xterm", "command line", "cli", "console",
"ghost text in terminal"]
case .accessibility:
return ["accessibility", "ax", "permission", "access", "system settings",
"privacy", "grant", "allow"]
Expand Down
46 changes: 46 additions & 0 deletions CotabbyTests/SettingsIndexTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import XCTest
@testable import Cotabby

/// Pins the hygiene rules of the Settings search index. The index drifts silently when a new
/// setting ships without an entry (it simply never appears in search), so these tests make the
/// cheap invariants loud: every item must carry a non-empty title, symbol, and keyword set, and
/// the queries users actually type for recently shipped settings must land on them.
final class SettingsIndexTests: XCTestCase {
func test_everyItemHasTitleSymbolAndKeywords() {
for item in SettingsItem.allCases {
XCTAssertFalse(item.title.isEmpty, "\(item) needs a title")
XCTAssertFalse(item.systemImage.isEmpty, "\(item) needs an SF Symbol")
XCTAssertFalse(item.keywords.isEmpty, "\(item) needs search keywords")
}
}

func test_itemIdsAreUnique() {
let ids = SettingsItem.allCases.map(\.id)
XCTAssertEqual(ids.count, Set(ids).count, "duplicate SettingsItem ids break Identifiable lists")
}

func test_searchFindsRecentlyShippedSettings() {
// Each pair pins one real query for a setting that previously shipped without an index
// entry. If one of these fails, a rename or removal broke search for that setting.
let expectations: [(query: String, item: SettingsItem)] = [
("ghost text size", .ghostTextSize),
("terminal", .suggestInIntegratedTerminals),
("vscode", .suggestInIntegratedTerminals),
("typo", .automaticallyFixTypos),
("model status", .modelStatus),
("battery", .batteryModel),
("plugged", .pluggedInModel)
]
for expectation in expectations {
XCTAssertTrue(
SettingsItem.results(for: expectation.query).contains(expectation.item),
"query \"\(expectation.query)\" should surface \(expectation.item)"
)
}
}

func test_blankQueryReturnsNothing() {
XCTAssertTrue(SettingsItem.results(for: " ").isEmpty)
XCTAssertTrue(SettingsItem.results(for: "").isEmpty)
}
}