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>
This commit is contained in:
sargatxetandCopilot App committed 2026-10-07 08:33:17 +02:00
commit 5ecdde187e
8 files changed
+511

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
* text=auto
*.ps1 text eol=crlf
*.md text eol=crlf
*.py text eol=lf
+3
View File
@@ -0,0 +1,3 @@
__pycache__/
*.pyc
*.lnk
+44
View File
@@ -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,
`/// <summary>` 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
```
+27
View File
@@ -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.
+258
View File
@@ -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;
/// <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
}
}
+74
View File
@@ -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.
+19
View File
@@ -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.
+82
View File
@@ -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()