Files
Mover-Mouse/Mover.ps1
T
sargatxetandCopilot App 5ecdde187e Mover: utilidad para mover el ratón tras inactividad
- Mover.ps1: activación tras inactividad, parada al detectar uso, deslizamiento
  animado visible, mensajes de estado y cierre de instancias previas.
- main.py: movimiento solo tras 3 minutos de inactividad.
- Documentación: README, AGENTS, CHANGELOG y TODO.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-07 08:33:17 +02:00

259 lines
11 KiB
PowerShell

<#
.SYNOPSIS
Mueve el ratón aleatoriamente solo tras un periodo de inactividad del usuario.
.DESCRIPTION
Espera a que transcurran TiempoInactividad segundos sin uso de teclado ni ratón y, a partir
de entonces, desliza el cursor de forma animada y visible hacia un punto aleatorio cada 2-4
segundos. En cuanto detecta uso real del teclado o del ratón deja de moverlo y vuelve a
esperar. Muestra un mensaje con la hora al iniciar y al detener el movimiento. Al arrancar
cierra cualquier instancia anterior del script que siga en ejecución. Solo funciona en Windows.
.PARAMETER TiempoInactividad
Segundos sin actividad antes de empezar a mover el ratón (por defecto 180).
.PARAMETER DesplazamientoMin
Distancia mínima en píxeles de cada deslizamiento (por defecto 150).
.PARAMETER DesplazamientoMax
Distancia máxima en píxeles de cada deslizamiento (por defecto 300).
.PARAMETER DuracionMs
Duración en milisegundos de la animación de cada deslizamiento (por defecto 600).
.EXAMPLE
.\Mover.ps1
.\Mover.ps1 -TiempoInactividad 60 -DesplazamientoMax 400
#>
param(
[int]$TiempoInactividad = 180,
[int]$DesplazamientoMin = 150,
[int]$DesplazamientoMax = 300,
[int]$DuracionMs = 600
)
# Intervalo de comprobación de actividad durante las pausas (milisegundos)
$IntervaloComprobacionMs = 200
# Número de pasos intermedios de cada animación
$PasosAnimacion = 40
<#
.SYNOPSIS
Cierra las instancias anteriores de este script que sigan en ejecución.
.DESCRIPTION
Busca procesos de PowerShell cuya línea de comandos ejecute este mismo fichero mediante
-File (excluyendo el proceso actual) y los detiene, para evitar que varias instancias se
interpreten mutuamente sus movimientos como actividad del usuario.
#>
function Detener-InstanciasPrevias {
$nombre = [regex]::Escape((Split-Path -Leaf $PSCommandPath))
$patron = "-File\s+`"?[^`"]*$nombre"
$previas = Get-CimInstance Win32_Process -Filter "Name='powershell.exe' OR Name='pwsh.exe'" |
Where-Object { $_.ProcessId -ne $PID -and $_.CommandLine -match $patron }
foreach ($proceso in $previas) {
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] Cerrando instancia anterior (PID $($proceso.ProcessId))." -ForegroundColor Cyan
Stop-Process -Id $proceso.ProcessId -Force -ErrorAction SilentlyContinue
}
}
Detener-InstanciasPrevias
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
# La compilación del código C# puede tardar varios segundos en el primer arranque
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] Inicializando..."
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
/// <summary>
/// Acceso a las APIs de Win32 para consultar la inactividad y mover el ratón.
/// </summary>
public static class Inactividad
{
[StructLayout(LayoutKind.Sequential)]
private struct LASTINPUTINFO { public uint cbSize; public uint dwTime; }
[StructLayout(LayoutKind.Sequential)]
private struct MOUSEINPUT
{
public int dx; public int dy; public uint mouseData;
public uint dwFlags; public uint time; public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
private struct INPUT { public uint type; public MOUSEINPUT mi; }
[StructLayout(LayoutKind.Sequential)]
private struct POINT { public int x; public int y; }
[DllImport("user32.dll")] private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
[DllImport("kernel32.dll")] private static extern uint GetTickCount();
[DllImport("user32.dll")] private static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll")] private static extern int GetSystemMetrics(int nIndex);
[DllImport("user32.dll", SetLastError = true)]
private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
/// <summary>
/// Obtiene el instante (ticks del sistema, ms) de la última entrada de teclado o ratón.
/// </summary>
public static uint ObtenerUltimaEntrada()
{
LASTINPUTINFO info = new LASTINPUTINFO();
info.cbSize = (uint)Marshal.SizeOf(info);
if (!GetLastInputInfo(ref info)) throw new System.ComponentModel.Win32Exception();
return info.dwTime;
}
/// <summary>
/// Calcula los segundos transcurridos desde la última entrada de teclado o ratón.
/// </summary>
public static double SegundosInactivo()
{
// La resta sin signo tolera el desbordamiento de GetTickCount (~49,7 días)
return unchecked(GetTickCount() - ObtenerUltimaEntrada()) / 1000.0;
}
/// <summary>
/// Mueve el puntero a una posición absoluta de pantalla mediante SendInput (entrada real,
/// sin verse afectada por la aceleración del puntero).
/// </summary>
/// <param name="x">Coordenada X de destino en el escritorio virtual.</param>
/// <param name="y">Coordenada Y de destino en el escritorio virtual.</param>
public static void MoverAbsoluto(int x, int y)
{
int vx = GetSystemMetrics(76), vy = GetSystemMetrics(77); // SM_X/YVIRTUALSCREEN
int vw = GetSystemMetrics(78), vh = GetSystemMetrics(79); // SM_CX/CYVIRTUALSCREEN
INPUT[] entradas = new INPUT[1];
entradas[0].type = 0; // INPUT_MOUSE
// Normalización a 0..65535 sobre el escritorio virtual (todos los monitores)
entradas[0].mi.dx = (int)Math.Round((x - vx) * 65535.0 / (vw - 1));
entradas[0].mi.dy = (int)Math.Round((y - vy) * 65535.0 / (vh - 1));
// MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK
entradas[0].mi.dwFlags = 0x0001 | 0x8000 | 0x4000;
SendInput(1, entradas, Marshal.SizeOf(typeof(INPUT)));
}
/// <summary>
/// Obtiene la posición actual del puntero.
/// </summary>
/// <returns>Array con las coordenadas {x, y}.</returns>
public static int[] ObtenerPosicion()
{
POINT p;
GetCursorPos(out p);
return new int[] { p.x, p.y };
}
/// <summary>
/// Desliza el puntero de forma animada y visible desde su posición actual hasta el destino.
/// </summary>
/// <param name="xDestino">Coordenada X de destino.</param>
/// <param name="yDestino">Coordenada Y de destino.</param>
/// <param name="duracionMs">Duración total de la animación en milisegundos.</param>
/// <param name="pasos">Número de pasos intermedios de la animación.</param>
/// <returns>False si el usuario ha movido el ratón durante la animación; true en caso contrario.</returns>
public static bool Deslizar(int xDestino, int yDestino, int duracionMs, int pasos)
{
int[] origen = ObtenerPosicion();
int xEsperada = origen[0], yEsperada = origen[1];
for (int i = 1; i <= pasos; i++)
{
// Si el puntero no está donde lo dejamos, el usuario lo ha movido
int[] actual = ObtenerPosicion();
if (Math.Abs(actual[0] - xEsperada) > 2 || Math.Abs(actual[1] - yEsperada) > 2) return false;
// Curva de suavizado (aceleración y frenado) para un movimiento natural
double t = (double)i / pasos;
double s = t * t * (3 - 2 * t);
xEsperada = (int)Math.Round(origen[0] + (xDestino - origen[0]) * s);
yEsperada = (int)Math.Round(origen[1] + (yDestino - origen[1]) * s);
MoverAbsoluto(xEsperada, yEsperada);
System.Threading.Thread.Sleep(duracionMs / pasos);
}
return true;
}
}
'@
<#
.SYNOPSIS
Bloquea hasta que el usuario lleve TiempoInactividad segundos sin usar teclado ni ratón.
#>
function Esperar-Inactividad {
while ([Inactividad]::SegundosInactivo() -lt $TiempoInactividad) {
Start-Sleep -Seconds 1
}
}
<#
.SYNOPSIS
Calcula un destino aleatorio para el puntero dentro del monitor en el que se encuentra.
.DESCRIPTION
Elige una dirección aleatoria y una distancia entre DesplazamientoMin y DesplazamientoMax.
Si el destino queda fuera del monitor, se invierte la dirección y, en último caso, se limita
a sus bordes para no perder el puntero entre pantallas.
.OUTPUTS
Array con las coordenadas {x, y} de destino.
#>
function Obtener-DestinoAleatorio {
$pos = [Inactividad]::ObtenerPosicion()
$limites = [System.Windows.Forms.Screen]::FromPoint((New-Object System.Drawing.Point($pos[0], $pos[1]))).Bounds
$angulo = (Get-Random -Minimum 0.0 -Maximum 1.0) * 2 * [Math]::PI
$distancia = Get-Random -Minimum $DesplazamientoMin -Maximum ($DesplazamientoMax + 1)
$dx = [int]($distancia * [Math]::Cos($angulo))
$dy = [int]($distancia * [Math]::Sin($angulo))
# Invierte el sentido en el eje que se salga del monitor
if ($pos[0] + $dx -lt $limites.Left -or $pos[0] + $dx -ge $limites.Right) { $dx = -$dx }
if ($pos[1] + $dy -lt $limites.Top -or $pos[1] + $dy -ge $limites.Bottom) { $dy = -$dy }
$x = [Math]::Min([Math]::Max($pos[0] + $dx, $limites.Left + 5), $limites.Right - 6)
$y = [Math]::Min([Math]::Max($pos[1] + $dy, $limites.Top + 5), $limites.Bottom - 6)
return @($x, $y)
}
<#
.SYNOPSIS
Mueve el ratón aleatoriamente hasta detectar actividad real del usuario.
.DESCRIPTION
Desliza el puntero de forma animada para que el desplazamiento sea visible. Los movimientos
propios también actualizan GetLastInputInfo, por lo que tras cada deslizamiento se guarda el
instante resultante; cualquier cambio posterior indica uso real del usuario. Si el usuario
mueve el ratón durante la animación, ésta se interrumpe.
#>
function Mover-MientrasInactivo {
while ($true) {
$destino = Obtener-DestinoAleatorio
if (-not [Inactividad]::Deslizar($destino[0], $destino[1], $DuracionMs, $PasosAnimacion)) {
return
}
Start-Sleep -Milliseconds 50
$ultimaPropia = [Inactividad]::ObtenerUltimaEntrada()
$finPausa = (Get-Date).AddSeconds((Get-Random -Minimum 2 -Maximum 5))
while ((Get-Date) -lt $finPausa) {
Start-Sleep -Milliseconds $IntervaloComprobacionMs
if ([Inactividad]::ObtenerUltimaEntrada() -ne $ultimaPropia) {
return
}
}
}
}
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] Mover iniciado. Esperando $TiempoInactividad s de inactividad (Ctrl+C para salir)."
# Bucle principal: espera la inactividad, mueve el ratón y se detiene al detectar uso
while ($true) {
try {
Esperar-Inactividad
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] Inactividad detectada: iniciando movimiento del ratón." -ForegroundColor Yellow
Mover-MientrasInactivo
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] Actividad del usuario detectada: movimiento detenido." -ForegroundColor Green
}
catch {
Start-Sleep -Seconds 1
}
}