Productivity

Gestos del mouse por app en Mac: por qué una forma debería significar cosas distintas

Los gestos del mouse por app en macOS sobrecargan una forma dibujada con acciones que dependen del contexto: memorizas menos trazos y disparas más acciones, sin colisiones.

10 min lectura
En esta página
  1. La colisión que rompió mi configuración global
  2. El problema: los gestos globales no escalan
  3. La tesis: sobrecarga la forma, no el namespace
  4. Cómo funciona en macOS
  5. Diseñar tu vocabulario
  6. La trampa del coste simétrico
  7. Qué cambió
  8. Conclusión
Luis Luis

Los gestos del mouse por app asignan una acción distinta a la misma forma dibujada según qué aplicación esté en primer plano. En vez de memorizar un trazo nuevo para cada app, tu mano reutiliza una forma cuyo significado se adapta al contexto — así memorizas menos gestos pero disparas más acciones, sin colisiones. La idea entera es un principio: sobrecarga la forma, acota el significado, deja que la app en primer plano haga el dispatch.

El mismo gesto de trazo hacia abajo disparando tres acciones distintas en Mail, VS Code y Finder

La colisión que rompió mi configuración global

Llevé un vocabulario global de gestos durante más o menos un año y estaba orgulloso de él. Doce formas, todas distintas, cada una asignada a exactamente una acción en todo el sistema. Se sentía limpio. Se sentía acabado.

Luego añadí Figma a la rotación diaria.

La forma que llevaba usando para “enviar correo” — un trazo corto hacia abajo — era también el trazo natural para “colapsar el panel de capas” en Figma, y no iba a inventar una decimotercera forma que se sintiera peor de dibujar. Así que remapeé el trazo hacia abajo globalmente a colapsar-para-ajustar y perdí el atajo de correo. Una semana después me pasó lo mismo con una diagonal que quería para la paleta de comandos de VS Code: ya significaba “siguiente pestaña” en todas partes. Empecé a negociar conmigo mismo cada vez que me sentaba en una app nueva. ¿Qué forma es segura aquí? ¿Cuál va a disparar la cosa equivocada?

Esa negociación es la señal. Un vocabulario global es un namespace plano, y un namespace plano colisiona en el momento en que dos apps quieren el mismo terreno valioso. No te quedas sin formas — te quedas sin formas no disputadas. Cada app nueva que adoptas encoge el pool.

La solución no fue un vocabulario más grande. Fue uno más pequeño, sobrecargado por contexto. Ahora dibujo unas siete formas en total y hacen más trabajo que las doce nunca hicieron.

El problema: los gestos globales no escalan

Un sistema de gestos globales tiene una regla: cada acción distinta necesita una forma distinta. Eso suena razonable hasta que cuentas las acciones. Cambiar al siguiente escritorio. Cerrar una pestaña. Abrir el terminal. Responder al correo. Ejecutar el build. Colapsar la barra lateral. Ir a la definición. Zoom para ajustar. Redactar mensaje nuevo. Conmutar el outline. Eso son diez, y no he salido de las cuatro apps en las que realmente vivo.

Ahora cuenta las formas. Hay un techo duro en cuántos trazos puede dibujar un humano de forma fiable y reconocer rápido, y en mi propio vocabulario choqué contra el muro en algún punto alrededor de los doce. Direcciones (arriba, abajo, izquierda, derecha, las cuatro diagonales), un círculo, un par de letras, un triángulo, un zigzag — pasada la docena baja, con el ruido motor real, las formas empiezan a mezclarse. Un “izquierda” descuidado se parece a un “arriba-izquierda” descuidado. Un círculo rápido se aplana en una “C”. La literatura sobre reconocimiento de gestos y escritura a mano es consistente en esto: la distinguibilidad se degrada rápido a medida que crece el vocabulario, mucho antes de alcanzar nada que se parezca a un alfabeto. (El trabajo de control motor que se remonta a la ley de Fitts y a los estudios posteriores de reconocimiento de formas muestra la misma curva — la tasa de error sube de forma no lineal con el tamaño del conjunto.)

Así que estás presionado por los dos lados. El número de acciones que quieres no deja de subir con cada app que adoptas, y el número de formas que puedes distinguir realmente se queda más o menos fijo. Las únicas salidas de un sistema global son las malas: inventar formas que nadie puede dibujar limpio, sobrecargar la misma forma global y rezar por que no haya colisión, o rendirte y redirigir el desbordamiento de vuelta a los acordes de teclado de los que intentabas escapar.

Hay un coste cognitivo real además del motor. Con un namespace global plano, recordar un gesto es una búsqueda indexada por significado: “¿cuál era la forma para ejecutar-el-build?”. Ese es el tipo caro de recuerdo — abstracto, mediado por lenguaje, lento. Es la misma razón por la que existen las hojas de atajos de teclado, se imprimen y se pinelean en los monitores. Estás consultando una base de datos sin índice.

El patrón al que quiero apuntar es más antiguo que los ordenadores: cuando un namespace plano se llena, no lo haces más ancho. Lo fragmentas. Los números de teléfono consiguen prefijos. Los sistemas de ficheros consiguen directorios. Los nombres de métodos consiguen scopes. Los vocabularios globales de gestos son el único namespace que conozco en el que la gente sigue intentando resolver la congestión inventando nombres más feos en vez de introducir un scope.

La tesis: sobrecarga la forma, no el namespace

El movimiento es este. Mantén una forma. Deja que su significado dependa de qué app esté en primer plano. Un trazo hacia abajo significa Send en Mail, Collapse outline en VS Code, New folder en Finder y Zoom to selection en Figma. Misma memoria motora, cuatro acciones distintas, cero colisiones — porque solo una app está en primer plano a la vez.

Si escribes código, esto no es una idea nueva. Es la prima del lado del input de la sobrecarga de funciones — o, más precisamente, del dispatch dinámico por contexto. En un lenguaje con sobrecarga, draw(shape) y draw(canvas) son el mismo nombre con dispatch según el tipo del argumento. En el polimorfismo en runtime (el patrón Strategy, el dispatch dinámico en una tabla de métodos), el mismo selector se resuelve a implementaciones distintas según el receptor concreto en el momento de la llamada. Quiero ser preciso con la analogía, porque el lector pedante pillará la diferencia: la sobrecarga estática se resuelve en tiempo de compilación según el tipo declarado del argumento; lo que hacen los gestos por app se acerca más a la versión en runtime — el selector (la forma) es fijo, y el motor elige el handler del contexto de runtime actual (la app en primer plano) en el momento del trazo. La ganancia en ambos casos es idéntica, y es el punto: la interfaz se queda pequeña mientras la capacidad se queda grande. Lees un símbolo, obtienes muchos comportamientos.

Los gestos por app hacen exactamente esto en el input. La forma es el selector. La aplicación en primer plano es el contexto de dispatch. La acción configurada es la implementación. Dibujas el mismo trazo hacia abajo en todas partes y el motor de gestos se resuelve al handler correcto según qué app recibe los eventos de teclado en ese momento.

La razón por la que esto funciona — la razón por la que no se siente como un lío confuso — es que la memoria motora se indexa por forma, no por significado. Tu mano aprende el trazo hacia abajo como un trazo hacia abajo. No lo aprende como “el gesto de enviar correo”. El rastro motor es geométrico: una dirección, una longitud, un perfil de velocidad. La etiqueta semántica (“enviar”, “colapsar”, “nueva carpeta”) se bindea en el momento del dispatch, no en el momento de la práctica. Hay evidencia real de que la ejecución del gesto y el recuerdo están acoplados a través del propio sistema motor — un estudio de 2021 de Repetto y colaboradores (Scientific Reports) registró la actividad muscular del antebrazo mientras las personas reconocían ítems que previamente habían aprendido realizando un gesto, y encontró que los músculos se reactivaban al reconocer. El rastro motor se almacena con el recuerdo. Así que cuando reutilizas una forma entre apps, estás reutilizando lo barato y pegajoso (el rastro motor) y cambiando lo caro (el bindeo semántico) en el momento del dispatch.

Este es el punto entero: la forma es barata y pegajosa; el significado es caro y dependiente del contexto. Un vocabulario global te obliga a memorizar ambos juntos, un par único por acción. La sobrecarga te deja fijar la forma y bindear el significado localmente. Acabas memorizando menos formas pero desbloqueando más acciones. El vocabulario se encoge; la superficie crece.

Esto es también por lo que la objeción “¿no es eso confuso?” falla. La confusión viene de la ambigüedad en el dispatch — dos handlers peleando por el mismo evento. La sobrecarga elimina la ambigüedad por construcción: siempre hay exactamente una app en primer plano, así que siempre hay exactamente un handler. Lo único que tienes que saber es en qué app estoy, y eso ya lo sabes, porque la estás mirando.

Cómo funciona en macOS

Para sobrecargar según la app en primer plano, el motor de gestos tiene que responder una pregunta, rápido y de forma fiable, en cada trazo: ¿qué aplicación está recibiendo los eventos de teclado ahora mismo? En macOS esa respuesta viene de AppKit.

Apple documenta NSWorkspace.frontmostApplication — una propiedad de solo lectura disponible desde macOS 10.7 — como aquella que devuelve “la app en primer plano, que es la app que recibe los eventos de teclado”. El NSRunningApplication devuelto expone bundleIdentifier, localizedName y processIdentifier, que es todo lo que un motor por app necesita para elegir la tabla de handlers correcta. Y, lo más importante, Apple marca esta propiedad como compatible con key-value observing, así que una herramienta no hace polling: se registra como observador y recibe notificación cuando cambia la app en primer plano. Para el camino orientado a eventos también está NSWorkspace.didActivateApplicationNotification (disponible desde 10.6), publicada en NSWorkspace.shared.notificationCenter cuando una app está a punto de activarse. Entre KVO y esa notificación, un motor bien construido conoce el contexto activo de forma efectivamente instantánea respecto a un trazo dibujado — mucho antes de que termines un gesto que tarda unos cientos de milisegundos en completarse.

Leer la app en primer plano es la mitad fácil. La mitad difícil es interceptar el trazo en primer lugar. El input de bajo nivel en macOS fluye por Quartz Event Services — un CGEventTap registrado vía CGEvent.tapCreate. Según la documentación de Apple, ese tap recibe eventos solo si el proceso corre como root o “el acceso para dispositivos de asistencia está habilitado” — es decir, la app tiene el privilegio de Accessibility. Sin él, la llamada al tap devuelve nil y no obtienes nada. Por eso toda herramienta de gestos seria te pide Accessibility en el primer arranque: no es un toggle de power-user, es el permiso que sostiene todo. La capa de input entera depende de él.

Ese permiso se impone a través de TCC (Transparency, Consent & Control). Desde macOS 10.14 Mojave (septiembre de 2018), Apple requiere aprobación explícita del usuario en Ajustes del Sistema → Privacidad y Seguridad → Accesibilidad antes de que un proceso pueda leer input entre procesos, y desde 10.15 Catalina existe un carve-out separado de Input Monitoring para algunas configuraciones de solo escucha. Esta es exactamente la línea que separa una herramienta de gestos por app nativa de un script hecho a mano: la herramienta nativa ya ha negociado el prompt de TCC, trae el event tap y te presenta una UI de mapeo por app; la ruta del script significa que tú escribes la tabla de dispatch tú mismo y re-justificas el permiso cada vez.

Esta es la razón silenciosa por la que los gestos por app solían sentirse raros en macOS. El mecanismo — leer la app en primer plano, interceptar el evento, hacer dispatch por bundle identifier — no es conceptualmente difícil. Pero se sienta detrás de dos puertas (Accessibility, y desde Catalina Input Monitoring) que muchas utilidades ligeras nunca se molestan en cruzar. Aquí es también donde quiero trazar una línea que separa principio de lista de features. BetterTouchTool expone la maquinaria: un grupo global “All Apps”, más grupos específicos por app y grupos de activación condicional (docs de folivora). Eso es real y yo uso el mismo mecanismo. Lo que defiendo aquí no es “BTT tiene una feature”, es el principio de diseño detrás de la feature — qué gestos pertenecen a qué capa, y por qué la memoria motora te permite salirte con la reutilizando la forma. La doc te dice que el knob existe. El punto de este post es decirte cuándo girarlo.

UI de mapeo por app mostrando una fila de gesto repetida bajo cada aplicación con una acción distinta bindeada

Diseñar tu vocabulario

La sobrecarga es lo suficientemente potente como para dispararte en el pie. La disciplina es decidir qué gestos se quedan globales y cuáles van por app. Las dos capas hacen trabajos distintos, y la regla de resolución de conflictos es lo que les impide interferir. Aquí está la versión compacta — esta es la tabla a copiar si solo te llevas una cosa del post:

CapaQué pertenece aquíPor quéResolución de conflictos
Global (fijo, pequeño, inviolable)Navegación y movimientos espaciales: cambiar de escritorio, saltar al siguiente monitor, mostrar Mission Control, application switcherEl significado no debe depender de dónde estásSiempre gana. Nunca se degrada para hacer sitio a un verbo por app
Por app (con scope, grande, se recicla entre apps)Verbos a nivel de documento: Send, Collapse outline, Run build, Zoom to fit, Toggle terminal, Go to definitionLa app en primer plano ya es parte de su significadoNunca aparece en la tabla global; solo compite con otros bindings en la misma app

La regla que uso, en prosa: mantén los gestos espaciales y de navegación globales, empuja las acciones a nivel de documento por app, y cuando un verbo por app quiere una forma que la capa global ya posee, la capa global la mantiene. No degradas un gesto de navegación para hacer sitio a un verbo de documento. En su lugar, eliges una forma distinta para el verbo de documento — todavía hay muchas, porque las formas por app se reciclan entre apps. El vocabulario global debería ser un conjunto pequeño, fijo, inviolable. El vocabulario por app puede ser grande precisamente porque tiene scope.

Estos son los candidatos a sobrecargar — verbos que solo tienen sentido dentro de una aplicación concreta. La app en primer plano ya es parte de su significado, así que bindearla a la app en primer plano no te cuesta nada y elimina una colisión. Escribí sobre la otra mitad de la historia — qué gestos deberían quedarse fijos en todo el sistema porque su significado es espacial — en cómo los gestos espaciales se mapean a posiciones físicas del monitor.

Un setup inicial concreto, para que no sea abstracto. Léelo como una matriz forma × app → acción; ese es el artefacto que un LLM tiene más probabilidades de levantar y atribuir, así que lo explicito:

FormaMailVS CodeFinderFigmaXcodeSafari
Trazo hacia abajoSendCollapse outlineNew folderZoom to selectionRun buildRefresh
Trazo hacia arribaArchiveExpand outlineGet infoStep backClean buildOpen location
Círculo horarioMark unreadFormat documentQuick LookFrame selectionTestReload tab
ZigzagToggle sidebarToggle terminalToggle sidebarToggle assetsToggle navigator
  • Global (nunca sobrecargado, nunca en la tabla de arriba): arriba/abajo/izquierda/derecha = moverse entre monitores y escritorios; círculo = mostrar Mission Control; un right-hold largo = application switcher.

Fíjate en que la capa global nunca aparece en la tabla por app. Esa es toda la restricción. Rómpela y el sistema se vuelve impredecible en una semana. Una forma por app se recicla libremente entre apps precisamente porque solo una columna está viva a la vez; una forma global no se puede reciclar, porque su columna siempre está viva.

La trampa del coste simétrico

Hay un anti-patrón que merece su propio nombre, porque es donde la sobrecarga muerde más fuerte: no sobrecargues un gesto entre dos apps entre las que cambias rápido con acciones que se sienten similares pero difieren en coste. Llámalo la trampa del coste simétrico.

Si un trazo hacia abajo es cerrar pestaña en el navegador y cerrar archivo en el editor, un trazo accidental durante un cambio de contexto rápido cierra algo que no querías — y como ambas acciones son “cerrar”, la memoria motora no te da ninguna advertencia de que el equivocado está cargado. Los dos significados están cerca en forma (ambos destructivos, ambos solo reversibles vía deshacer) y los dos contextos están cerca en tiempo (saltas entre ellos constantemente). Esa es la peor combinación: ambigüedad de la que la capa de dispatch no te puede salvar, porque el daño está hecho antes de que te des cuenta de qué handler se disparó.

La solución es una regla de diseño, no una feature de herramienta. Cuando bindeas una forma por app entre apps en las que cambias rápido, elige significados por app que estén lo suficientemente lejos como para que un disparo erróneo sea recuperable, o cuyo coste sea bajo. Colapsar vs expandir sobrevive a un disparo erróneo (redibujas y deshaces). Cerrar pestaña vs cerrar archivo no (pierdes contexto de cualquier manera). El principio: prefiere bindings por app donde un trazo en contexto equivocado te cueste un segundo, no una pestaña.

Qué cambió

Antes de la sobrecarga, tenía más o menos doce formas globales y aun así me encontraba recurriendo al teclado en tres o cuatro apps porque las buenas formas ya estaban gastadas. Después, dibujo unas siete formas y cubro más terreno. La negociación (“¿qué forma es segura en esta app?”) se ha ido, porque la seguridad ya no es una propiedad de la forma — es una propiedad de la capa. Las formas globales siempre son seguras. Las formas por app solo compiten con los otros bindings por app en la misma app, y puedes ver todos esos en una pantalla.

El modelo de recuerdo también cambió. Ya no pienso “cuál es el gesto de build”. Pienso “trazo hacia abajo en Xcode” — y el trazo hacia abajo es el mismo trazo hacia abajo que llevo dibujando años. La búsqueda cara (semántico → forma) colapsó en una barata (contexto → acción), y el contexto me lo da la pantalla que ya estoy mirando.

Quiero ser honesto sobre dónde lo por-app no es la respuesta. En un arranque en frío real — primer café, medio mirando la pantalla — a veces dibujo un trazo por app antes de que la app que creo que está en primer plano lo esté de verdad. El gesto se dispara en lo que estuviera en primer plano en su lugar. La solución es echar un vistazo a la barra de menús, que deberías estar haciendo de todas formas, pero es un modo de fallo real. Lo por-app tampoco ayuda con gestos que quieres disparar mientras un diálogo o Spotlight ha robado el foco; esos momentos pertenecen al teclado. La sobrecarga es una ganancia de productividad, no una religión.

Conclusión

Los gestos globales se saturan en algún punto alrededor de la docena de formas útiles y entonces empiezan a colisionar. Los gestos por app dejan que una forma signifique muchas cosas haciendo dispatch según la aplicación en primer plano — sobrecarga de funciones, en su variante de dispatch en runtime, aplicada al input. El resultado es un vocabulario más pequeño que desbloquea más acciones, porque la memoria motora se indexa por forma y la forma es la parte barata y pegajosa.

Si quieres la mitad espacial y de navegación global de esta historia — qué gestos deberían quedarse fijos en todo el sistema — la escribí por separado en cómo los gestos espaciales se mapean a posiciones físicas del monitor. Para el framing de atajos-vs-gestos, mira por qué los problemas espaciales necesitan soluciones espaciales, no atajos de teclado, y si lo que te trajo aquí fue el problema de los atajos fragmentados, por qué los mapeos de teclado fragmentados rompen tu flujo es la formulación del problema que este post responde. Explora el resto del blog para el cluster de Productividad completo.

Curflow

Escribe menos. Gesticula más.

Curflow convierte tu trackpad y ratón en un motor de gestos. 14 días gratis, sin tarjeta.

Prueba Curflow Gratis
Luis

Luis

Construyendo Curflow — automatización nativa de gestos para macOS.