Skip to content
Open
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
11 changes: 11 additions & 0 deletions Sources/CodexBar/CodexbarApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate {

@objc private func handleSessionLimitResetNotification(_ notification: Notification) {
guard let event = notification.object as? SessionLimitResetEvent else { return }
// Emit the hook regardless of the confetti preference; hooks have their own switch.
self.store?.emitQuotaResetHook(
provider: event.provider,
window: .session,
usedPercent: event.usedPercent,
accountLabel: event.accountLabel)
guard self.settings?.confettiOnSessionLimitResetsEnabled == true else { return }
self.playLimitResetConfetti(
provider: event.provider,
Expand All @@ -437,6 +443,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {

@objc private func handleWeeklyLimitResetNotification(_ notification: Notification) {
guard let event = notification.object as? WeeklyLimitResetEvent else { return }
self.store?.emitQuotaResetHook(
provider: event.provider,
window: .weekly,
usedPercent: event.usedPercent,
accountLabel: event.accountLabel)
guard self.settings?.confettiOnWeeklyLimitResetsEnabled == true else { return }
self.playLimitResetConfetti(
provider: event.provider,
Expand Down
138 changes: 138 additions & 0 deletions Sources/CodexBar/PreferencesHooksPane.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import CodexBarCore
import SwiftUI

@MainActor
struct HooksPane: View {
@Bindable var settings: SettingsStore

var body: some View {
Form {
Section {
Toggle(isOn: self.enabledBinding) {
SettingsRowLabel(L("hooks_enable_title"), subtitle: L("hooks_enable_subtitle"))
}
Label(L("hooks_trust_warning"), systemImage: "exclamationmark.triangle.fill")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
} header: {
Text(L("tab_hooks"))
}

Section {
if self.settings.hookRules.isEmpty {
Text(L("hooks_empty"))
.font(.caption)
.foregroundStyle(.secondary)
} else {
ForEach(self.settings.hookRules) { rule in
HookRuleRow(
rule: self.binding(for: rule),
onDelete: { self.settings.removeHookRule(id: rule.id) })
}
}

Button {
self.settings.addHookRule(HookRule(event: .quotaReached, executable: ""))
} label: {
Label(L("hooks_add_rule"), systemImage: "plus")
}
} header: {
Text(L("hooks_rules_header"))
}
}
.formStyle(.grouped)
}

private var enabledBinding: Binding<Bool> {
Binding(
get: { self.settings.hooksEnabled },
set: { self.settings.setHooksEnabled($0) })
}

private func binding(for rule: HookRule) -> Binding<HookRule> {
Binding(
get: { self.settings.hookRules.first(where: { $0.id == rule.id }) ?? rule },
set: { self.settings.updateHookRule($0) })
}
}

@MainActor
private struct HookRuleRow: View {
@Binding var rule: HookRule
let onDelete: () -> Void

var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Toggle(L("hooks_rule_enabled"), isOn: self.$rule.enabled)
.labelsHidden()
.toggleStyle(.switch)
.controlSize(.mini)

Picker(L("hooks_event"), selection: self.$rule.event) {
ForEach(HookEventType.allCases, id: \.self) { event in
Text(event.rawValue).tag(event)
}
}
.labelsHidden()

Picker(L("hooks_provider"), selection: self.providerBinding) {
Text(L("hooks_any_provider")).tag(String?.none)
ForEach(UsageProvider.allCases, id: \.self) { provider in
Text(ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName)
.tag(String?.some(provider.rawValue))
}
}
.labelsHidden()

Spacer()

Button(role: .destructive, action: self.onDelete) {
Image(systemName: "trash")
}
.buttonStyle(.borderless)
.accessibilityLabel(L("hooks_delete_rule"))
}

if self.rule.event == .quotaLow {
HStack {
Text(L("hooks_threshold"))
.foregroundStyle(.secondary)
TextField(L("hooks_threshold_placeholder"), value: self.thresholdPercentBinding, format: .number)
.frame(width: 60)
Text(verbatim: "%")
.foregroundStyle(.secondary)
}
.font(.caption)
}

TextField(L("hooks_executable_placeholder"), text: self.$rule.executable)
.textFieldStyle(.roundedBorder)
.font(.system(.caption, design: .monospaced))

TextField(L("hooks_arguments_placeholder"), text: self.argumentsBinding)
.textFieldStyle(.roundedBorder)
.font(.system(.caption, design: .monospaced))
}
.padding(.vertical, 4)
}

private var providerBinding: Binding<String?> {
Binding(get: { self.rule.provider }, set: { self.rule.provider = $0 })
}

/// Threshold stored as a 0...1 fraction, edited as a 0...100 percentage.
private var thresholdPercentBinding: Binding<Double?> {
Binding(
get: { self.rule.threshold.map { $0 * 100 } },
set: { self.rule.threshold = $0.map { min(max($0, 0), 100) / 100 } })
}

/// Whitespace-joined arguments. Simple split by spaces; adequate for v1.
private var argumentsBinding: Binding<String> {
Binding(
get: { self.rule.arguments.joined(separator: " ") },
set: { self.rule.arguments = $0.split(separator: " ").map(String.init) })
}
}
2 changes: 2 additions & 0 deletions Sources/CodexBar/PreferencesSelection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ extension SettingsPane {
case .general: "general"
case .display: "display"
case .advanced: "advanced"
case .hooks: "hooks"
case .about: "about"
case .debug: "debug"
case let .provider(provider): "provider:\(provider.rawValue)"
Expand All @@ -20,6 +21,7 @@ extension SettingsPane {
case "general": self = .general
case "display": self = .display
case "advanced": self = .advanced
case "hooks": self = .hooks
case "about": self = .about
case "debug": self = .debug
default:
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/PreferencesSidebar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ struct SettingsSidebarView: View {
SettingsSidebarPaneRow(pane: .general, systemImage: "gearshape.fill", color: .gray)
SettingsSidebarPaneRow(pane: .display, systemImage: "menubar.rectangle", color: .blue)
SettingsSidebarPaneRow(pane: .advanced, systemImage: "slider.horizontal.3", color: .purple)
SettingsSidebarPaneRow(pane: .hooks, systemImage: "bolt.horizontal.circle.fill", color: .orange)
SettingsSidebarAboutRow()
if self.settings.debugMenuEnabled {
SettingsSidebarPaneRow(pane: .debug, systemImage: "ladybug.fill", color: .red)
Expand Down
4 changes: 4 additions & 0 deletions Sources/CodexBar/PreferencesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ enum SettingsPane: Hashable {
case general
case display
case advanced
case hooks
case about
case debug
case provider(UsageProvider)
Expand All @@ -23,6 +24,7 @@ enum SettingsPane: Hashable {
case .general: L("tab_general")
case .display: L("tab_display")
case .advanced: L("tab_advanced")
case .hooks: L("tab_hooks")
case .about: L("tab_about")
case .debug: L("tab_debug")
case let .provider(provider):
Expand Down Expand Up @@ -126,6 +128,8 @@ struct PreferencesView: View {
DisplayPane(settings: self.settings, store: self.store)
case .advanced:
AdvancedPane(settings: self.settings, store: self.store)
case .hooks:
HooksPane(settings: self.settings)
case .about:
AboutPane(updater: self.updater)
case .debug:
Expand Down
18 changes: 18 additions & 0 deletions Sources/CodexBar/Resources/ar.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,25 @@
"tab_providers" = "مقدمو الخدمات";
"tab_display" = "العرض";
"tab_advanced" = "متقدمة";
"tab_hooks" = "الخطافات";
"tab_about" = "حول";

/* Hooks Pane */
"hooks_enable_title" = "تفعيل الخطافات";
"hooks_enable_subtitle" = "تشغيل أوامر خارجية عند وقوع أحداث الحصة أو المزود.";
"hooks_trust_warning" = "يمكن للخطافات تنفيذ أوامر محلية على جهاز Mac. اضبط فقط الأوامر التي تثق بها.";
"hooks_rules_header" = "القواعد";
"hooks_empty" = "لا توجد خطافات مُهيأة.";
"hooks_add_rule" = "إضافة قاعدة";
"hooks_delete_rule" = "حذف القاعدة";
"hooks_rule_enabled" = "مُفعّل";
"hooks_event" = "الحدث";
"hooks_provider" = "المزود";
"hooks_any_provider" = "أي مزود";
"hooks_threshold" = "التشغيل عند الاستخدام ≥";
"hooks_threshold_placeholder" = "90";
"hooks_executable_placeholder" = "/usr/local/bin/my-command";
"hooks_arguments_placeholder" = "الوسائط (مفصولة بمسافات)";
"tab_debug" = "تصحيح الأخطاء";

/* Providers Pane */
Expand Down
18 changes: 18 additions & 0 deletions Sources/CodexBar/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -485,9 +485,27 @@
"tab_providers" = "Providers";
"tab_display" = "Display";
"tab_advanced" = "Advanced";
"tab_hooks" = "Hooks";
"tab_about" = "About";
"tab_debug" = "Debug";

/* Hooks Pane */
"hooks_enable_title" = "Enable hooks";
"hooks_enable_subtitle" = "Run external commands when quota or provider events occur.";
"hooks_trust_warning" = "Hooks can execute local commands on your Mac. Only configure commands you trust.";
"hooks_rules_header" = "Rules";
"hooks_empty" = "No hooks configured.";
"hooks_add_rule" = "Add rule";
"hooks_delete_rule" = "Delete rule";
"hooks_rule_enabled" = "Enabled";
"hooks_event" = "Event";
"hooks_provider" = "Provider";
"hooks_any_provider" = "Any provider";
"hooks_threshold" = "Fire at usage ≥";
"hooks_threshold_placeholder" = "90";
"hooks_executable_placeholder" = "/usr/local/bin/my-command";
"hooks_arguments_placeholder" = "Arguments (space-separated)";

/* Providers Pane */
"select_a_provider" = "Select a provider";
"cancel" = "Cancel";
Expand Down
18 changes: 18 additions & 0 deletions Sources/CodexBar/Resources/fa.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,25 @@
"tab_providers" = "ارائه دهندگان";
"tab_display" = "نمایش";
"tab_advanced" = "پیشرفته";
"tab_hooks" = "قلاب‌ها";
"tab_about" = "درباره";

/* Hooks Pane */
"hooks_enable_title" = "فعال‌سازی قلاب‌ها";
"hooks_enable_subtitle" = "اجرای دستورهای خارجی هنگام رخ‌دادن رویدادهای سهمیه یا ارائه‌دهنده.";
"hooks_trust_warning" = "قلاب‌ها می‌توانند دستورهای محلی را روی مک شما اجرا کنند. فقط دستورهایی را تنظیم کنید که به آن‌ها اعتماد دارید.";
"hooks_rules_header" = "قوانین";
"hooks_empty" = "هیچ قلابی پیکربندی نشده است.";
"hooks_add_rule" = "افزودن قانون";
"hooks_delete_rule" = "حذف قانون";
"hooks_rule_enabled" = "فعال";
"hooks_event" = "رویداد";
"hooks_provider" = "ارائه‌دهنده";
"hooks_any_provider" = "هر ارائه‌دهنده";
"hooks_threshold" = "اجرا در مصرف ≥";
"hooks_threshold_placeholder" = "90";
"hooks_executable_placeholder" = "/usr/local/bin/my-command";
"hooks_arguments_placeholder" = "آرگومان‌ها (جدا شده با فاصله)";
"tab_debug" = "اشکال زدایی";

/* Providers Pane */
Expand Down
18 changes: 18 additions & 0 deletions Sources/CodexBar/Resources/gl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,24 @@
"tab_providers" = "Provedores";
"tab_display" = "Pantalla";
"tab_advanced" = "Avanzado";
"tab_hooks" = "Ganchos";

/* Hooks Pane */
"hooks_enable_title" = "Activar ganchos";
"hooks_enable_subtitle" = "Executa comandos externos cando se producen eventos de cota ou provedor.";
"hooks_trust_warning" = "Os ganchos poden executar comandos locais no teu Mac. Configura só comandos nos que confíes.";
"hooks_rules_header" = "Regras";
"hooks_empty" = "Non hai ganchos configurados.";
"hooks_add_rule" = "Engadir regra";
"hooks_delete_rule" = "Eliminar regra";
"hooks_rule_enabled" = "Activado";
"hooks_event" = "Evento";
"hooks_provider" = "Provedor";
"hooks_any_provider" = "Calquera provedor";
"hooks_threshold" = "Activar cun uso ≥";
"hooks_threshold_placeholder" = "90";
"hooks_executable_placeholder" = "/usr/local/bin/my-command";
"hooks_arguments_placeholder" = "Argumentos (separados por espazos)";
"tab_about" = "Acerca de";
"tab_debug" = "Depuración";

Expand Down
18 changes: 18 additions & 0 deletions Sources/CodexBar/Resources/id.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,24 @@
"tab_providers" = "Penyedia";
"tab_display" = "Tampilan";
"tab_advanced" = "Lanjutan";
"tab_hooks" = "Hook";

/* Hooks Pane */
"hooks_enable_title" = "Aktifkan hook";
"hooks_enable_subtitle" = "Jalankan perintah eksternal saat terjadi peristiwa kuota atau penyedia.";
"hooks_trust_warning" = "Hook dapat menjalankan perintah lokal di Mac Anda. Hanya konfigurasikan perintah yang Anda percayai.";
"hooks_rules_header" = "Aturan";
"hooks_empty" = "Belum ada hook yang dikonfigurasi.";
"hooks_add_rule" = "Tambah aturan";
"hooks_delete_rule" = "Hapus aturan";
"hooks_rule_enabled" = "Aktif";
"hooks_event" = "Peristiwa";
"hooks_provider" = "Penyedia";
"hooks_any_provider" = "Penyedia apa pun";
"hooks_threshold" = "Picu saat penggunaan ≥";
"hooks_threshold_placeholder" = "90";
"hooks_executable_placeholder" = "/usr/local/bin/my-command";
"hooks_arguments_placeholder" = "Argumen (dipisahkan spasi)";
"tab_about" = "Tentang";
"tab_debug" = "Debug";

Expand Down
18 changes: 18 additions & 0 deletions Sources/CodexBar/Resources/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,24 @@
"tab_providers" = "Provider";
"tab_display" = "Aspetto";
"tab_advanced" = "Avanzate";
"tab_hooks" = "Hook";

/* Hooks Pane */
"hooks_enable_title" = "Abilita hook";
"hooks_enable_subtitle" = "Esegui comandi esterni al verificarsi di eventi di quota o provider.";
"hooks_trust_warning" = "Gli hook possono eseguire comandi locali sul tuo Mac. Configura solo comandi di cui ti fidi.";
"hooks_rules_header" = "Regole";
"hooks_empty" = "Nessun hook configurato.";
"hooks_add_rule" = "Aggiungi regola";
"hooks_delete_rule" = "Elimina regola";
"hooks_rule_enabled" = "Abilitato";
"hooks_event" = "Evento";
"hooks_provider" = "Provider";
"hooks_any_provider" = "Qualsiasi provider";
"hooks_threshold" = "Attiva a utilizzo ≥";
"hooks_threshold_placeholder" = "90";
"hooks_executable_placeholder" = "/usr/local/bin/my-command";
"hooks_arguments_placeholder" = "Argomenti (separati da spazi)";
"tab_about" = "Informazioni";
"tab_debug" = "Diagnostica";

Expand Down
18 changes: 18 additions & 0 deletions Sources/CodexBar/Resources/pl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,24 @@
"tab_providers" = "Dostawcy";
"tab_display" = "Wygląd";
"tab_advanced" = "Zaawansowane";
"tab_hooks" = "Haki";

/* Hooks Pane */
"hooks_enable_title" = "Włącz haki";
"hooks_enable_subtitle" = "Uruchamiaj polecenia zewnętrzne, gdy wystąpią zdarzenia limitu lub dostawcy.";
"hooks_trust_warning" = "Haki mogą uruchamiać lokalne polecenia na Twoim Macu. Konfiguruj tylko polecenia, którym ufasz.";
"hooks_rules_header" = "Reguły";
"hooks_empty" = "Nie skonfigurowano haków.";
"hooks_add_rule" = "Dodaj regułę";
"hooks_delete_rule" = "Usuń regułę";
"hooks_rule_enabled" = "Włączone";
"hooks_event" = "Zdarzenie";
"hooks_provider" = "Dostawca";
"hooks_any_provider" = "Dowolny dostawca";
"hooks_threshold" = "Uruchom przy użyciu ≥";
"hooks_threshold_placeholder" = "90";
"hooks_executable_placeholder" = "/usr/local/bin/my-command";
"hooks_arguments_placeholder" = "Argumenty (oddzielone spacjami)";
"tab_about" = "O aplikacji";
"tab_debug" = "Debugowanie";

Expand Down
Loading