Engineering

Redesigning the macOS Context Boundary: Why I Abandoned the Dock

Redesigning the macOS Context Boundary: Why I Abandoned the Dock

Imagine deploying a heavily decoupled microservices architecture, but your orchestration layer insists on grouping every single pod into one monolithic dashboard. You cannot inspect individual instances; you merely get a generic glowing dot indicating that "Services" are running. You would probably fire your DevOps lead by lunchtime. Yet, this is exactly the user experience we tolerate every single day with the native macOS Dock.

For years, I accepted this subpar workflow. I convinced myself that memorizing complex trackpad gestures and navigating the visually jarring animations of Mission Control was just the cost of doing business on an Apple machine. I was wrong. Your operating system is merely another system architecture, and accepting a flawed default configuration is a failure of engineering imagination.

The Problem: Application-Centric Cognitive Overload

Operating systems evolved around the concept of the "Application." You might open Visual Studio Code or launch Chrome. Starting a terminal is a separate action. But as modern engineers, our work revolves around contexts instead of isolated applications.

When I am deeply immersed in tracking down a race condition, my context spans a specific VS Code workspace along with a browser window tailored to my staging environment. A terminal tailing production logs is also part of this setup. If I switch to my secondary desktop to check my email or review a pull request, the macOS Dock betrays me. It displays the exact same icons. It groups my staging browser window with my personal Twitter window. It is the UI equivalent of a global variable, polluting the namespace of my current workflow with state from entirely unrelated tasks.

This application-centric grouping forces the brain to constantly serialize and deserialize context. You click the browser icon hoping for your AWS console, but macOS enthusiastically presents you with a YouTube tab you left open on another monitor. The friction is microscopic, but compounded over fifty times a day, it shatters the flow state.

Furthermore, the notification system is fundamentally broken for power users. A red badge on a chat application tells me someone sent a message, but it does not tell me which window or workspace requires my attention. We are operating in a multi-threaded physical environment with a single-threaded task manager.

The Solution: A Window-Centric Paradigm Shift

I realized the missing link was a persistent, window-centric interface bounded strictly to the active desktop. I recently overhauled my entire workspace philosophy after discovering a utility designed around this exact premise. It completely hides the default Dock and replaces it with a lean, functional taskbar that lists individual windows.

If I have three terminal instances open across three different spaces, my taskbar on Desktop 1 only shows the terminal relevant to Desktop 1. It is a localized state manager. Hovering yields immediate thumbnails. Notifications are routed directly to the specific window chip that triggered them, complete with subtle attention pulses that do not hijack my entire screen.

As a solopreneur, I also took away a vital business lesson from this transition. The tool I adopted initially launched with an aggressive SaaS subscription model. In the developer tools space, charging a recurring monthly fee for a local, non-cloud utility is the quickest way to alienate your user base. Developers despise renting binaries. Recognizing the backlash, the creator swiftly pivoted to a $40 perpetual license (which covers two machines and two years of updates).

This is a masterclass in reading the market. If you are building tools for engineers, respect their intelligence and their wallets. A well-priced, one-time fee with optional future upgrade costs aligns the incentives perfectly: the developer is motivated to build compelling features to earn the upgrade, and the user feels ownership over their environment.

Implementation: Extending the Paradigm with TypeScript

Adopting a visual taskbar solves the UI layer, but true system thinkers automate the underlying state. If we are treating macOS workspaces as isolated contexts, we should programmatically control them.

While macOS relies heavily on Swift and Objective-C, we can orchestrate window layouts and space management using TypeScript via Node.js and JavaScript for Automation (JXA). By doing this, we can create CLI commands that instantly spin up our contextual workspaces, automatically aligning with our new window-centric philosophy.

Below is a robust Node.js implementation using TypeScript that interfaces with macOS to query and arrange windows according to predefined contexts.

Step 1: Project Setup

First, initialize the environment. We need typescript and @types/node to execute our automation bridging script.

mkdir macos-context-manager
cd macos-context-manager
npm init -y
npm install -D typescript @types/node ts-node
npx tsc --init

Step 2: The Core Bridge

We will construct a class that uses Node's child_process to execute JXA snippets. This allows us to keep our orchestration logic in strongly-typed TypeScript while manipulating the OS layers directly.

// src/ContextManager.ts
import { execSync } from 'child_process';
 
export interface WindowState {
  appName: string;
  windowTitle: string;
  bounds: { x: number; y: number; width: number; height: number };
}
 
export class ContextManager {
  /**
   * Executes a piece of JavaScript for Automation (JXA) code.
   */
  private executeJXA<T>(script: string): T {
    const command = `osascript -l JavaScript -e \"${script.replace(/\"/g, '\\\"')}\"`;
    try {
      const result = execSync(command, { encoding: 'utf8' });
      return JSON.parse(result.trim()) as T;
    } catch (error) {
      console.error('Failed to execute JXA script:', error);
      throw error;
    }
  }
 
  /**
   * Retrieves all visible windows on the current desktop.
   */
  public getActiveWindows(): WindowState[] {
    const script = `
      const se = Application('System Events');
      const visibleProcesses = se.processes.where({ visible: true });
      const windows = [];
      
      for (let i = 0; i < visibleProcesses.length; i++) {
        const proc = visibleProcesses[i];
        const procWindows = proc.windows();
        for (let j = 0; j < procWindows.length; j++) {
          const win = procWindows[j];
          windows.push({
            appName: proc.name(),
            windowTitle: win.name(),
            bounds: {
              x: win.position()[0],
              y: win.position()[1],
              width: win.size()[0],
              height: win.size()[1]
            }
          });
        }
      }
      JSON.stringify(windows);
    `;
    
    return this.executeJXA<WindowState[]>(script);
  }
 
  /**
   * Arranges a specific application window to requested dimensions.
   */
  public setWindowBounds(appName: string, bounds: WindowState['bounds']): void {
    const script = `
      const se = Application('System Events');
      const proc = se.processes.byName('${appName}');
      if (proc.exists() && proc.windows.length > 0) {
        const win = proc.windows[0];
        win.position = [${bounds.x}, ${bounds.y}];
        win.size = [${bounds.width}, ${bounds.height}];
        JSON.stringify({ success: true });
      } else {
        JSON.stringify({ success: false, error: 'Window not found' });
      }
    `;
    
    this.executeJXA(script);
  }
}

Step 3: Orchestrating the Context

Now we tie it together. Imagine you are starting your morning. Instead of manually dragging windows and relying on the OS to guess your intent, you execute a script that explicitly constructs your "Engineering" context.

// src/index.ts
import { ContextManager } from './ContextManager';
 
const manager = new ContextManager();
 
console.log('Fetching current window states...');
const currentWindows = manager.getActiveWindows();
console.dir(currentWindows, { depth: null, colors: true });
 
// Define our ideal engineering layout
const IDEAL_LAYOUT = [
  {
    appName: 'Code', // VS Code
    bounds: { x: 0, y: 0, width: 1200, height: 1080 }
  },
  {
    appName: 'iTerm2',
    bounds: { x: 1200, y: 0, width: 720, height: 540 }
  },
  {
    appName: 'Google Chrome',
    bounds: { x: 1200, y: 540, width: 720, height: 540 }
  }
];
 
console.log('\nApplying Engineering Context Layout...');
IDEAL_LAYOUT.forEach(layout => {
  try {
    manager.setWindowBounds(layout.appName, layout.bounds);
    console.log(`Successfully positioned ${layout.appName}`);
  } catch (err) {
    console.log(`Skipped ${layout.appName} - Not currently running in this space.`);
  }
});

By running npx ts-node src/index.ts, you transform your machine from a chaotic spread of overlapping interfaces into a deterministic, reproducible workspace. Paired with a window-centric taskbar UI, you have effectively bypassed Apple's stubborn UX paradigms entirely. You know exactly what is running on your current screen. Because its physical boundaries are automated, you suffer zero cognitive bleed from background tasks.

Conclusion

We spend so much time optimizing the performance of the code we write, yet we passively accept massive inefficiencies in the environments where we write it. The macOS default layout is designed for casual consumers who think in terms of "Apps." As builders, we must think in terms of "Systems and States."

Taking control of your context boundary, whether by purchasing a well-crafted indie utility or writing your own automation scripts, is about protecting your focus rather than mere aesthetics. Guard your attention with the same ruthless pragmatism you apply to guarding your production databases.