Detectar atajos de teclado en una aplicación Vue

Para asociar atajos de teclado globales lo más sencillo es establecerlos en el div principal de nuestra aplicación Vue.

Así que si queremos por ejemplo lanzar nuestro menú de aplicaciones cuando el usuario haga Ctrl + Clic en cualquier parte de la página o pulse la combinación de teclas Meta + Espacio usaríamos este código:

<div id="app"
     v-on:keyup.meta.space="launchAppsMenu"
     v-on:click.ctrl="launchAppsMenu"></div>

Sin embargo al probar esto veríamos que el Ctrl + Clic funciona pero no así el Meta + Espacio. Esto es porque los eventos de teclado solo se generan para elementos que pueden tener el foco (focusables) y los div no son por defecto focusables. Para que lo sean solamente tenemos que añadirle el atributo tabindex, una vez lo tenga podrá recibir eventos de teclado. Así quedaría entonces nuestro código:

<div id="app" tabindex="0" 
     v-on:keyup.meta.space="launchAppsMenu"
     v-on:click.ctrl="launchAppsMenu"></div>

Número de compilación automático en aplicaciones Vue

Para disponer del número de compilación en nuestra aplicación Vue he creado este pequeño script. Para usarlo hay que modificar el fichero package.json y sustituir en “scripts” el valor de “build” por ./buildAndUpload.php

Luego creamos el fichero ./buildAndUpload.php con este contenido:

#!/usr/bin/php
<?php
// Get version number
$version = json_decode(file_get_contents('package.json'), true)['version'];
$version = substr($version, 0, -2);

// Get build number
$build = trim(file_get_contents('.buildNumber')) + 1;
file_put_contents('.buildNumber', $build);

echo "Shipping version $version build $build\n\n";

// Save version file
$js = "var appVersion = $version;var buildNumber = $build;";
file_put_contents('./public/appVersion.js', $js);

exec('vue-cli-service build');

// Upload dist folder to server

El número de compilación inicial lo guardaremos en el fichero .buildNumber. Y con esto ya estaría todo, ahora cada vez que compilemos nuestra aplicación se generará en la carpeta dist un fichero appVersion.js que podremos incluir en nuestra aplicación y que contendrá el número de versión (que se extrae del fichero package.json) y el número de compilación.

Server Sent Events en PHP

Estos días he estado muy atareado con el lanzamiento de PocaPoca, una plataforma de formación que empieza su negocio con varios cursos de electrónica.

La plataforma consta de tres partes: una página de presentación de la plataforma (web), la propia plataforma (front) y un entorno de gestión de la misma (back).

Las tres están desarrolladas con Vue. Para cada proyecto con Vue yo me abro siempre dos pestañas de terminal: uno para lanzar el entorno de desarrollo local (npm run serve) y otro para compilar y subir al servidor remoto el proyecto (npm run build). Así que para este proyecto (que son realmente tres) tenía seis pestañas abiertas en el terminal y era un lío tener que ir pasando de una a otra para compilar y subir ficheros. Incluso usando el interfaz web de Vue-cli serían tres pestañas. Y eso suponiendo que no necesite por algún motivo lanzar otro proyecto… Vamos, que había ocasiones en que aquello era el festival de la pestaña.

Una vez lanzada la página me puse a pensar cómo evitar que esto me vuelva a pasar en el futuro. El primer paso ha sido sustituir Gnome Terminal por Terminator, que permite partir una pantalla en varias bloques horizontales y verticales. Esto ya es un avance, porque me permitiría (siguiendo con el ejemplo de PocaPoca) tener solo tres pestañas, una para cada proyecto, o incluso una única pestaña con todas las ventanas juntas, porque en Terminator es rápido moverse entre los bloques de una misma pestaña.

Pero se me ha ocurrido algo mejor, y es aprovechar un iPad antiquísimo que tengo para que muestre todos los proyectos que tengo activos y me permita con solo pulsar un botón ejecutarlos o compilarlos. Esto es mucho más rápido incluso que cambiar a la ventana de Terminator y localizar el bloque y lanzar el comando.

Para no perder funcionalidad necesito poder ver la salida de los comandos, para ver si hay algún error. El entorno de ejecución (npm run serve) está continuamente mostrando información que tengo que poder ver, y esto es lo que nos lleva al asunto de la entrada de hoy: ¿cómo mostrar en una página web la salida de un comando que se está ejecutando de forma continua?

La respuesta son los Server Sent Events, es como un websocket pero en un solo sentido, del servidor al cliente. El navegador abre una conexión permanente con nuestro fichero PHP, que le va devolviendo datos según los vaya generando, y esto nos permite recibirlos en JS. Es la solución perfecta para lo que necesito, y en este vídeo se ve cómo desde el iPad puedo ver la salida de un ping según se va generando:

Copio aquí el código, cuando tenga un poco de tiempo quiero ponerlo un poco más bonito y subirlo a GitHub.

<?php
$mode = $_REQUEST['mode'] ?? '';
if ($mode == 'event')
{
  header('Content-Type: text/event-stream');
  header('X-Accel-Buffering: no');
  
  echo "event: message\n";
	
  $descriptorspec = [
    0 => ["pipe", "r"],   // stdin
    1 => ["pipe", "w"],   // stdout
    2 => ["pipe", "w"]];  // stderr
  
  $cmd = "ping 127.0.0.1";
  $process = proc_open($cmd, $descriptorspec, $pipes, realpath('./'), array());
  if (is_resource($process))
    while ($s = fgets($pipes[1]))
    {
      $s = nl2br($s);
      echo "data:$s\n\n";
      while (ob_get_level() > 0)
	ob_end_flush();
      flush();
    }
  
  die("data:#CLOSE\n\n");
}

?><!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <meta name="quality" content="Koas' Code Inside" />
  </head>
  <body>
  	<button onclick="run()">Run process</button>
  	<div id="result" style="white-space: pre"></div>

<script>
function run()
{
	document.getElementById("result").innerHTML = "";

	var source = new EventSource("dashboard.php?mode=event");
	source.onmessage = function(event) 
	{
		if (event.data == "#CLOSE")
			source.close();
		else document.getElementById("result").innerHTML += event.data;
	}
}
</script>

  </body>
</html>