commit 5ecdde187ec8528a687449bf09404a2aea3a2be1 Author: Toni Ramiro Date: Wed Oct 7 08:33:17 2026 +0200 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> diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bb39dce --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto +*.ps1 text eol=crlf +*.md text eol=crlf +*.py text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6be1f5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +*.lnk diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..baf4c5c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,44 @@ +# AGENTS.md + +Guía para agentes de IA y colaboradores que trabajen en este repositorio. + +## Proyecto + +Utilidad para Windows que mueve el puntero del ratón solo tras un periodo de inactividad del +usuario y se detiene al detectar uso real. Ver `README.md`. + +- `Mover.ps1`: implementación principal (PowerShell + C# embebido con `Add-Type`). +- `main.py`: implementación original en Python (`pyautogui`), mantenida como referencia. + +## Normas + +- Idioma: comentarios, documentación y mensajes en **castellano**. +- Toda función nueva debe llevar documentación (`<# .SYNOPSIS #>` en PowerShell, + `/// ` en C#, docstrings en Python). Actualizar la documentación al modificarlas. +- No modificar ficheros sin autorización previa del usuario. +- Registrar los cambios en `CHANGELOG.md` y las tareas pendientes en `TODO.md`. + +## Particularidades técnicas + +- **Codificación:** `Mover.ps1` debe mantenerse en **UTF-8 con BOM**; sin BOM, Windows + PowerShell 5.1 lo lee como ANSI y corrompe las tildes. Tras editar, comprobar los primeros + bytes (`239,187,191`). +- **Detección de actividad:** los movimientos propios actualizan `GetLastInputInfo`. Por eso se + guarda `dwTime` tras cada movimiento y cualquier cambio posterior se interpreta como uso real. + No romper este mecanismo. +- **Instancias:** el script cierra al arrancar otras instancias que ejecuten `Mover.ps1` con + `-File`. Durante las pruebas no dejar instancias huérfanas. +- **Movimiento:** se usa `SendInput` con `MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK` para + evitar la aceleración del puntero y soportar varios monitores. +- `Add-Type` puede tardar ~20 s en un proceso nuevo; no confundir con un bloqueo. + +## Validación + +```powershell +# Sintaxis +$err=$null; [System.Management.Automation.Language.Parser]::ParseFile('Mover.ps1',[ref]$null,[ref]$err) | Out-Null; $err.Count +# Prueba funcional rápida +powershell -ExecutionPolicy Bypass -File .\Mover.ps1 -TiempoInactividad 10 +# Python +python -m py_compile main.py +``` diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f5298ff --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +Todos los cambios relevantes del proyecto se documentan en este fichero. +El formato se basa en [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/). + +## [1.0.0] - 2026-10-07 + +### Añadido +- `Mover.ps1`: implementación en PowerShell sin dependencias externas. + - Activación solo tras `-TiempoInactividad` segundos sin uso (por defecto 180) mediante + `GetLastInputInfo`. + - Parada inmediata al detectar uso real de teclado o ratón. + - Deslizamiento animado y visible del puntero con `SendInput` en coordenadas absolutas, + limitado al monitor actual (`-DesplazamientoMin`, `-DesplazamientoMax`, `-DuracionMs`). + - Mensajes con hora al iniciar y detener el movimiento. + - Cierre automático de instancias anteriores al arrancar. +- Documentación: `README.md`, `AGENTS.md`, `CHANGELOG.md` y `TODO.md`. + +### Cambiado +- `main.py`: el movimiento solo se activa tras 3 minutos de inactividad y se detiene al + detectar uso del usuario. Se sustituye `except:` por `except Exception` para permitir + detenerlo con `Ctrl+C`. + +## [0.1.0] + +### Añadido +- `main.py`: versión inicial que movía el ratón aleatoriamente de forma continua. diff --git a/Mover.ps1 b/Mover.ps1 new file mode 100644 index 0000000..6f14c8e --- /dev/null +++ b/Mover.ps1 @@ -0,0 +1,258 @@ +<# +.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; + +/// +/// Acceso a las APIs de Win32 para consultar la inactividad y mover el ratón. +/// +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); + + /// + /// Obtiene el instante (ticks del sistema, ms) de la última entrada de teclado o ratón. + /// + 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; + } + + /// + /// Calcula los segundos transcurridos desde la última entrada de teclado o ratón. + /// + public static double SegundosInactivo() + { + // La resta sin signo tolera el desbordamiento de GetTickCount (~49,7 días) + return unchecked(GetTickCount() - ObtenerUltimaEntrada()) / 1000.0; + } + + /// + /// Mueve el puntero a una posición absoluta de pantalla mediante SendInput (entrada real, + /// sin verse afectada por la aceleración del puntero). + /// + /// Coordenada X de destino en el escritorio virtual. + /// Coordenada Y de destino en el escritorio virtual. + 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))); + } + + /// + /// Obtiene la posición actual del puntero. + /// + /// Array con las coordenadas {x, y}. + public static int[] ObtenerPosicion() + { + POINT p; + GetCursorPos(out p); + return new int[] { p.x, p.y }; + } + + /// + /// Desliza el puntero de forma animada y visible desde su posición actual hasta el destino. + /// + /// Coordenada X de destino. + /// Coordenada Y de destino. + /// Duración total de la animación en milisegundos. + /// Número de pasos intermedios de la animación. + /// False si el usuario ha movido el ratón durante la animación; true en caso contrario. + 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 + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..17dd632 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# Mover + +Utilidad para Windows que evita que el equipo se bloquee o entre en suspensión moviendo el +puntero del ratón **solo cuando el usuario lleva un tiempo sin usar el teclado ni el ratón**. +En cuanto se detecta uso real, el movimiento se detiene y se vuelve a esperar. + +## Contenido + +| Fichero | Descripción | +|--------------|-------------| +| `Mover.ps1` | Implementación principal en PowerShell. Sin dependencias externas. | +| `main.py` | Implementación original en Python (requiere `pyautogui`). Funcionalidad básica, sin animación. | + +## Requisitos + +- Windows (usa las APIs `user32`/`kernel32`). +- `Mover.ps1`: Windows PowerShell 5.1 o PowerShell 7. +- `main.py`: Python 3 y `pip install pyautogui`. + +## Uso de `Mover.ps1` + +```powershell +powershell -ExecutionPolicy Bypass -File .\Mover.ps1 +``` + +### Parámetros + +| Parámetro | Por defecto | Descripción | +|----------------------|-------------|-------------| +| `-TiempoInactividad` | `180` | Segundos sin uso de teclado/ratón antes de empezar a mover. | +| `-DesplazamientoMin` | `150` | Distancia mínima (píxeles) de cada deslizamiento. | +| `-DesplazamientoMax` | `300` | Distancia máxima (píxeles) de cada deslizamiento. | +| `-DuracionMs` | `600` | Duración de la animación de cada deslizamiento. | + +Ejemplo de prueba rápida: + +```powershell +powershell -ExecutionPolicy Bypass -File .\Mover.ps1 -TiempoInactividad 10 +``` + +### Mensajes + +``` +[07:45:57] Inicializando... +[07:45:58] Mover iniciado. Esperando 180 s de inactividad (Ctrl+C para salir). +[07:49:01] Inactividad detectada: iniciando movimiento del ratón. +[07:50:12] Actividad del usuario detectada: movimiento detenido. +``` + +Para detenerlo: `Ctrl+C` o cerrar la ventana. + +### Acceso directo + +Se puede crear un acceso directo con: + +- **Destino:** `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File "D:\Toni\Desarrollo\Mover\Mover.ps1"` +- **Ejecutar:** Minimizada. + +## Funcionamiento + +1. Al arrancar cierra cualquier instancia anterior de `Mover.ps1` (varias instancias se + interpretarían mutuamente sus movimientos como actividad del usuario). +2. Consulta la inactividad con `GetLastInputInfo`/`GetTickCount`. +3. Superado el umbral, desliza el puntero con una animación suavizada mediante `SendInput` + (coordenadas absolutas), lo que genera entrada real y evita la aceleración del puntero. + El destino se mantiene dentro del monitor actual. +4. Tras cada deslizamiento guarda el instante de la última entrada; si cambia, ha sido el + usuario y se detiene. Si el usuario mueve el ratón durante la animación, ésta se interrumpe. + +## Notas + +- La primera compilación de `Add-Type` puede tardar hasta ~20 s en un proceso nuevo. +- El fichero `Mover.ps1` debe guardarse en **UTF-8 con BOM** para que Windows PowerShell 5.1 + muestre correctamente las tildes. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..284568f --- /dev/null +++ b/TODO.md @@ -0,0 +1,19 @@ +# TODO + +## Pendiente + +- [ ] Igualar `main.py` con `Mover.ps1` (deslizamiento animado, cierre de instancias previas, + mensajes) o retirarlo si deja de usarse. +- [ ] Añadir un icono en la bandeja del sistema para pausar/salir sin abrir la consola. +- [ ] Opción para impedir la suspensión con `SetThreadExecutionState` sin mover el puntero. +- [ ] Registro opcional en fichero de los periodos de actividad/inactividad. +- [ ] Opción de arranque automático con Windows (carpeta Inicio o Programador de tareas). +- [ ] Reducir el tiempo de arranque de `Add-Type` (p. ej. cachear el ensamblado compilado). + +## Hecho + +- [x] Activar el movimiento solo tras un periodo de inactividad. +- [x] Detener el movimiento al detectar uso del teclado o del ratón. +- [x] Movimiento visible con animación. +- [x] Mensajes al iniciar y detener el movimiento. +- [x] Cerrar instancias anteriores al arrancar. diff --git a/main.py b/main.py new file mode 100644 index 0000000..bacacd6 --- /dev/null +++ b/main.py @@ -0,0 +1,82 @@ +"""Mueve el ratón aleatoriamente solo tras un periodo de inactividad del usuario (solo Windows).""" +import ctypes +import ctypes.wintypes +import random +import time + +import pyautogui + +# Segundos sin uso de teclado/ratón antes de empezar a mover el cursor +TIEMPO_INACTIVIDAD = 180 +# Desplazamiento máximo (píxeles) en cada eje +DESPLAZAMIENTO_MAX = 50 +# Intervalo de comprobación de actividad (segundos) +INTERVALO_COMPROBACION = 0.2 + + +class LASTINPUTINFO(ctypes.Structure): + """Estructura de Win32 que recibe el instante de la última entrada del usuario.""" + + _fields_ = [("cbSize", ctypes.wintypes.UINT), ("dwTime", ctypes.wintypes.DWORD)] + + +def obtener_ultima_entrada(): + """ + Obtiene el instante (en ticks del sistema, ms) de la última entrada de teclado o ratón. + + :return: valor dwTime devuelto por GetLastInputInfo. + """ + info = LASTINPUTINFO() + info.cbSize = ctypes.sizeof(LASTINPUTINFO) + if not ctypes.windll.user32.GetLastInputInfo(ctypes.byref(info)): + raise ctypes.WinError() + return info.dwTime + + +def segundos_inactivo(): + """ + Calcula los segundos transcurridos desde la última entrada de teclado o ratón. + + :return: segundos de inactividad. + """ + ahora = ctypes.windll.kernel32.GetTickCount() & 0xFFFFFFFF + # Máscara para tolerar el desbordamiento de GetTickCount (~49,7 días) + return ((ahora - obtener_ultima_entrada()) & 0xFFFFFFFF) / 1000.0 + + +def esperar_inactividad(): + """Bloquea hasta que el usuario lleve TIEMPO_INACTIVIDAD segundos sin usar teclado ni ratón.""" + while segundos_inactivo() < TIEMPO_INACTIVIDAD: + time.sleep(1) + + +def mover_mientras_inactivo(): + """ + Mueve el ratón aleatoriamente hasta detectar actividad real del usuario. + + Los movimientos propios también actualizan GetLastInputInfo, por lo que tras cada + movimiento se guarda el dwTime resultante; cualquier cambio posterior indica uso real. + """ + while True: + pyautogui.moveRel(random.randrange(-DESPLAZAMIENTO_MAX, DESPLAZAMIENTO_MAX), + random.randrange(-DESPLAZAMIENTO_MAX, DESPLAZAMIENTO_MAX)) + ultima_propia = obtener_ultima_entrada() + fin_pausa = time.monotonic() + random.randrange(2, 5) + while time.monotonic() < fin_pausa: + time.sleep(INTERVALO_COMPROBACION) + if obtener_ultima_entrada() != ultima_propia: + return + + +def main(): + """Bucle principal: espera la inactividad, mueve el ratón y se detiene al detectar uso.""" + while True: + try: + esperar_inactividad() + mover_mientras_inactivo() + except Exception: + time.sleep(1) + + +if __name__ == "__main__": + main()