edit | blame | history | raw

Apptag — Implementation Plan

Goal

A native macOS menubar app that displays a full-screen overlay (Spotlight/Launchpad style) showing installed apps grouped by their Finder tags, with user-customizable group names.

Technical Architecture

Apptag.app
├── ApptagApp.swift          — @main entry point, menubar setup
├── TagStore.swift            — reads Finder tags from .app bundles
├── AppIndexer.swift          — scans /Applications, ~/Applications for .app bundles
├── ContentView.swift         — full-screen overlay: search bar + tag-grouped grid
├── AppGridItem.swift         — individual app icon + name card
├── TagGroupView.swift        — collapsible group section with header
├── HotkeyManager.swift       — global keyboard shortcut (opt+space or custom)
├── Info.plist                — LSUIElement = YES (menubar-only, no dock icon)
└── Assets.xcassets           — app icon

Data Flow

  1. On launch: AppIndexer scans standard app directories, collects all .app paths
  2. Tag reading: For each app, TagStore reads com.apple.metadata:_kMDItemUserTags via NSURL resource values or xattr
  3. Grouping: Apps grouped by tag name; untagged apps go to a configurable default group
  4. Display: ContentView renders full-screen overlay with:
  • Top: search bar (filters apps by name in real-time)
  • Body: scrollable grid of tag-grouped app icons
  1. Trigger: HotkeyManager listens for global hotkey; menu bar icon as fallback

Key Technical Decisions

Reading Finder Tags

  • Use URLResourceValues with .tagNamesKey — this is the proper Cocoa API
  • Falls back to parsing xattr binary plist if needed
  • Tags from SIP-protected paths may fail — gracefully skip

Scanning Apps

  • Standard locations: /Applications, ~/Applications, /System/Applications
  • Use FileManager.enumerator for recursive .app discovery
  • Filter: only bundles ending in .app with valid Info.plist

UI Design

  • Full-screen NSWindow (or SwiftUI WindowGroup with .windowStyle(.hiddenTitleBar))
  • Semi-transparent blurred background (.regularMaterial or .ultraThinMaterial)
  • Grid layout: 5-6 columns, adaptive to screen width
  • Each app: large icon (64pt) + name below (SF Pro, 12pt)
  • Group header: tag name in SF Pro Display, 17pt semibold
  • Search: top-center text field, filters in real time

Hotkey

  • Register with RegisterEventHotKey (Carbon API) or NSEvent.addGlobalMonitorForEvents
  • Default: Option+Space (⌥+Space) — doesn't conflict with Spotlight (⌘+Space)
  • Configurable in preferences

Persistence

  • UserDefaults for: custom group name mappings, hotkey preference, excluded apps
  • Tag ↔ group name mapping stored as a dictionary

Build Phases

Phase 1: Core tag reading + app scanning

  • AppIndexer.swift — directory scanning
  • TagStore.swift — tag extraction
  • CLI test target to validate

Phase 2: UI skeleton

  • ApptagApp.swift — menubar entry point
  • ContentView.swift — full-screen overlay
  • Search bar + grid layout

Phase 3: Group rendering + polish

  • TagGroupView.swift — group sections
  • AppGridItem.swift — app cards
  • Animations, keyboard navigation, accessibility

Phase 4: Hotkey + preferences

  • Global hotkey registration
  • Preferences window (hotkey config, default group name)
  • Custom tag-to-group-name mappings

Risks

  1. SIP: System apps can't be tagged — handled by graceful skip
  2. macOS 26 API changes: Full-screen overlay behavior may differ — test on target OS
  3. Hotkey registration: Carbon hotkey API is deprecated but still works; NSEvent monitor is the modern alternative but requires Accessibility permission
  4. Performance: Scanning 200+ apps on every toggle is slow — implement filesystem watch (FSEvents) for incremental updates

Files to Create

All under ~/Projects/Taglauncher/Apptag/:
- ApptagApp.swift
- TagStore.swift
- AppIndexer.swift
- ContentView.swift
- AppGridItem.swift
- TagGroupView.swift
- HotkeyManager.swift
- Info.plist