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
1 change: 1 addition & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ build_app "$APP" "stack-nudge" \
panel/Settings.swift \
panel/SessionStore.swift \
panel/Sessions.swift \
panel/Phrases.swift \
panel/Welcome.swift \
shared/AppActivator.swift \
-framework Foundation -framework AppKit -framework SwiftUI -framework Carbon \
Expand Down
35 changes: 35 additions & 0 deletions notify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,41 @@ voice_phrase_for() {
# shellcheck disable=SC1090
source "$lang_file"

# Layer user-managed phrases from phrases.user.json on top of the shipped
# arrays. Each pool can declare disabled defaults (skipped) and custom
# additions (appended). Quiet failure if jq isn't installed or the file's
# missing — the user just gets the unmodified defaults.
local user_json="${HOME}/.stack-nudge/phrases.user.json"
if [[ -f "$user_json" ]] && command -v jq >/dev/null 2>&1; then
# filter_pool <jq-path-prefix> <bash-array-name>
filter_pool() {
local pool="$1" arr="$2"
local disabled
disabled=$(jq -r --arg lang "$lang" --arg pool "$pool" \
'.[$lang][$pool].disabled[]?' "$user_json" 2>/dev/null)
if [[ -n "$disabled" ]]; then
local kept=()
local existing
eval "existing=(\"\${${arr}[@]}\")"
for existing_item in "${existing[@]}"; do
local skip=0
while IFS= read -r d; do
[[ "$existing_item" == "$d" ]] && { skip=1; break; }
done <<< "$disabled"
[[ $skip -eq 0 ]] && kept+=("$existing_item")
done
eval "${arr}=(\"\${kept[@]}\")"
fi
local extra
while IFS= read -r extra; do
[[ -n "$extra" ]] && eval "${arr}+=(\"\$extra\")"
done < <(jq -r --arg lang "$lang" --arg pool "$pool" \
'.[$lang][$pool].custom[]?' "$user_json" 2>/dev/null)
}
filter_pool "response" TEMPLATES_RESPONSE
filter_pool "notification" TEMPLATES_NOTIFICATION
fi

local templates=()
case "$event" in
permission) templates=("${TEMPLATES_NOTIFICATION[@]}") ;;
Expand Down
40 changes: 39 additions & 1 deletion panel/Panel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ struct PanelContentView: View {
@ObservedObject var store: EventStore
@ObservedObject var sessions: SessionStore
@ObservedObject var nav: PanelNav
@ObservedObject var phrases: PhrasesViewModel

let onGrantPermissions: () -> Void

Expand All @@ -83,6 +84,7 @@ struct PanelContentView: View {
case .events: eventsBody
case .sessions: SessionsView(store: sessions)
case .settings: SettingsView(nav: nav)
case .phrases: PhrasesView(model: phrases) { nav.mode = .settings }
}
}
}
Expand Down Expand Up @@ -298,6 +300,7 @@ final class PanelController: NSObject, NSApplicationDelegate, PanelKeyDelegate,
private let store = EventStore()
private let sessions = SessionStore()
private let nav = PanelNav()
private let phrases = PhrasesViewModel()
private var listener: EventListener?
private var menuBar: MenuBarController?
private var permissionsWC: PermissionsWindowController?
Expand All @@ -317,7 +320,7 @@ final class PanelController: NSObject, NSApplicationDelegate, PanelKeyDelegate,
blur.layer?.masksToBounds = true

let host = NSHostingView(rootView: PanelContentView(
store: store, sessions: sessions, nav: nav,
store: store, sessions: sessions, nav: nav, phrases: phrases,
onGrantPermissions: { [weak self] in self?.handleGrantPermissions() }
))
host.translatesAutoresizingMaskIntoConstraints = false
Expand Down Expand Up @@ -348,6 +351,11 @@ final class PanelController: NSObject, NSApplicationDelegate, PanelKeyDelegate,
self?.nav.mode = .events
NSWorkspace.shared.open(URL(fileURLWithPath: ConfigFile.path))
},
editPhrases: { [weak self] in
self?.phrases.load()
self?.phrases.selectedRow = nil
self?.nav.mode = .phrases
},
quit: { NSApp.terminate(nil) }
)
nav.setHotkey = { [weak self] spec in
Expand Down Expand Up @@ -743,6 +751,36 @@ final class PanelController: NSObject, NSApplicationDelegate, PanelKeyDelegate,
return true
}

// Phrases mode: ↑/↓ navigate every row (defaults + custom),
// Space toggles the selected default, ⌫ removes the selected
// custom, Esc returns to Settings. Typing / Tab / Enter for
// adding still fall through to SwiftUI's TextField.
if nav.mode == .phrases {
let plain = mods.intersection([.command, .control, .option, .shift]).isEmpty
guard plain else { return false }
switch event.keyCode {
case KeyCode.escape:
nav.mode = .settings
return true
case KeyCode.upArrow:
phrases.selectPrevious()
return true
case KeyCode.downArrow:
phrases.selectNext()
return true
case KeyCode.space:
guard let row = phrases.selectedRow, row.isDefault else { return false }
phrases.toggleSelected()
return true
case KeyCode.delete, KeyCode.forwardDelete:
guard let row = phrases.selectedRow, !row.isDefault else { return false }
phrases.removeSelected()
return true
default:
return false
}
}

// In settings mode, the controller drives row selection and value
// cycling on PanelNav so the SettingsView stays a pure renderer.
if nav.mode == .settings {
Expand Down
18 changes: 11 additions & 7 deletions panel/PanelNav.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ enum PanelMode {
case events
case sessions
case settings
case phrases
}

// Action callbacks the controller wires into nav so settings rows like
Expand All @@ -13,6 +14,7 @@ enum PanelMode {
struct SettingsActions {
let checkPermissions: () -> Void
let openConfig: () -> Void
let editPhrases: () -> Void
let quit: () -> Void
}

Expand Down Expand Up @@ -71,7 +73,7 @@ final class PanelNav: ObservableObject {
"I'd love your input on this.",
]

var rowCount: Int { 12 }
var rowCount: Int { 13 }

// Row layout (kept in one place so the controller, view, and indexing
// logic all agree on what each row index means):
Expand All @@ -84,9 +86,10 @@ final class PanelNav: ObservableObject {
// 6 Permission sound cycle
// 7 Voice cycle
// 8 Speed cycle
// 9 Check permissions… action
// 10 Open config file… action
// 11 Quit panel action
// 9 Edit phrases… action
// 10 Check permissions… action
// 11 Open config file… action
// 12 Quit panel action

// MARK: - Disk I/O

Expand Down Expand Up @@ -160,9 +163,10 @@ final class PanelNav: ObservableObject {
func activate() {
switch selectedSettingIndex {
case 0: startRecordingHotkey()
case 9: actions?.checkPermissions()
case 10: actions?.openConfig()
case 11: actions?.quit()
case 9: actions?.editPhrases()
case 10: actions?.checkPermissions()
case 11: actions?.openConfig()
case 12: actions?.quit()
default: applyCycle(forward: true)
}
}
Expand Down
Loading