From 5ecdde187ec8528a687449bf09404a2aea3a2be1 Mon Sep 17 00:00:00 2001 From: Toni Ramiro Date: Wed, 7 Oct 2026 08:33:17 +0200 Subject: [PATCH] =?UTF-8?q?Mover:=20utilidad=20para=20mover=20el=20rat?= =?UTF-8?q?=C3=B3n=20tras=20inactividad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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> --- .gitattributes | 4 + .gitignore | 3 + AGENTS.md | 44 +++++++++ CHANGELOG.md | 27 ++++++ Mover.ps1 | 258 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 74 ++++++++++++++ TODO.md | 19 ++++ main.py | 82 ++++++++++++++++ 8 files changed, 511 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 Mover.ps1 create mode 100644 README.md create mode 100644 TODO.md create mode 100644 main.py 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()