This commit is contained in:
2025-12-30 22:37:13 +02:00
parent 0dabe39be9
commit bd0b49abce
11 changed files with 1270 additions and 53 deletions

235
autoclicker-enhanced.sh Executable file
View File

@@ -0,0 +1,235 @@
#!/bin/bash
# Enhanced Autoclicker script for Sway with improved antiban features
# Uses ydotool for Wayland/Sway environments
# Features human-like timing patterns and CSV logging for visualization
# Press Ctrl+C to stop (standard shell interrupt)
# Requires ydotool and ydotoold daemon running
# Configuration
MIN_DELAY=50 # Minimum delay in milliseconds
MAX_DELAY=700 # Maximum delay in milliseconds
MIN_CLICKS_BEFORE_BREAK=30 # Minimum clicks before break
MAX_CLICKS_BEFORE_BREAK=200 # Maximum clicks before break
MIN_BREAK=3 # Minimum break duration in seconds
MAX_BREAK=15 # Maximum break duration in seconds
# Human-like timing parameters
FASTIGUE_FACTOR=0.95 # Gradually slow down over time (0.95 = 5% slower)
REACTION_VARIATION=0.3 # 30% variation in reaction times
PATTERN_BREAK_CHANCE=0.05 # 5% chance to break pattern
# File paths
STATE_FILE="/tmp/autoclicker_running"
LOCK_FILE="/tmp/autoclicker_lock"
LOG_DIR="/tmp/autoclicker_logs"
LOG_FILE="$LOG_DIR/click_timings_$(date +%Y%m%d_%H%M%S).csv"
# Create log directory if it doesn't exist
mkdir -p "$LOG_DIR"
# Initialize CSV log file with header
echo "click_number,delay_ms,timestamp" > "$LOG_FILE"
# Check if ydotool is available
if ! command -v ydotool &> /dev/null; then
echo "Error: ydotool is not installed. Install it with: sudo pacman -S ydotool"
exit 1
fi
# Check if bc is available (needed for calculations)
if ! command -v bc &> /dev/null; then
echo "Error: bc is not installed. Install it with: sudo pacman -S bc"
exit 1
fi
# Check if script is already running (prevent multiple instances)
if [ -f "$LOCK_FILE" ]; then
echo "Autoclicker is already running!"
echo "Press Ctrl+C in the terminal where it's running to stop it."
# Clean up the incomplete log file that was just created
rm -f "$LOG_FILE"
exit 0
fi
# Create lock file
echo $$ > "$LOCK_FILE"
# Global exit flag
EXIT_REQUESTED=0
# Clean up on exit
cleanup() {
EXIT_REQUESTED=1
rm -f "$LOCK_FILE"
rm -f "$STATE_FILE"
echo ""
echo "Autoclicker stopped."
exit 0
}
# Set up traps for cleanup
trap cleanup INT TERM
# Check if ydotoold daemon is running
if ! pgrep -x "ydotoold" > /dev/null; then
echo "Starting ydotoold daemon..."
sudo ydotoold &
sleep 1
fi
# Create state file to indicate running
echo "running" > "$STATE_FILE"
# Function to log click timing to CSV
log_click_timing() {
local click_number=$1
local delay_ms=$2
local timestamp=$(date +%s%3N) # Milliseconds since epoch
echo "$click_number,$delay_ms,$timestamp" >> "$LOG_FILE"
}
# Function to generate random delay with human-like distribution
get_human_delay() {
local min=$1
local max=$2
local base_delay=$((min + RANDOM % (max - min + 1)))
# Apply reaction time variation (30% up or down)
local variation=$(echo "scale=3; $base_delay * $REACTION_VARIATION * (2 * $RANDOM / 32767 - 1)" | bc)
local varied_delay=$(echo "$base_delay + $variation" | bc)
# Ensure we stay within reasonable bounds
if (( $(echo "$varied_delay < $min" | bc -l) )); then
varied_delay=$min
elif (( $(echo "$varied_delay > $max" | bc -l) )); then
varied_delay=$max
fi
# Convert to seconds for sleep function
echo "scale=3; $varied_delay / 1000" | bc
}
# Function to generate random number of clicks before break
get_random_clicks() {
local min=$1
local max=$2
echo $((min + RANDOM % (max - min + 1)))
}
# Function to sleep with interrupt checking
interruptible_sleep() {
local duration=$1
local start_time=$(date +%s.%N)
local end_time=$(echo "$start_time + $duration" | bc)
while true; do
if [ $EXIT_REQUESTED -eq 1 ]; then
return 1 # Exit requested
fi
local current_time=$(date +%s.%N)
if (( $(echo "$current_time >= $end_time" | bc -l) )); then
return 0 # Sleep completed
fi
sleep 0.1
done
}
# Function to apply fatigue effect (gradually slow down)
apply_fatigue() {
local current_delay=$1
local click_count=$2
local fatigue_effect=$(echo "scale=3; $current_delay * (1 + $FASTIGUE_FACTOR * $click_count / 1000)" | bc)
echo "$fatigue_effect"
}
# Function to randomly break pattern (5% chance)
should_break_pattern() {
local threshold=$(echo "$PATTERN_BREAK_CHANCE * 32767" | bc | cut -d'.' -f1)
[ $RANDOM -lt $threshold ]
}
echo "=========================================="
echo "Enhanced Autoclicker started!"
echo "=========================================="
echo "Configuration:"
echo " - Click delay: ${MIN_DELAY}-${MAX_DELAY}ms (human-like variation)"
echo " - Break every: ${MIN_CLICKS_BEFORE_BREAK}-${MAX_CLICKS_BEFORE_BREAK} clicks (random)"
echo " - Break duration: ${MIN_BREAK}-${MAX_BREAK}s"
echo " - Fatigue factor: ${FASTIGUE_FACTOR} (gradual slowdown)"
echo " - Pattern break chance: ${PATTERN_BREAK_CHANCE} (5%)"
echo " - Logging to: $LOG_FILE"
echo ""
echo "Press Ctrl+C to stop"
echo "=========================================="
echo ""
click_count=0
total_clicks=0
clicks_before_break=$(get_random_clicks $MIN_CLICKS_BEFORE_BREAK $MAX_CLICKS_BEFORE_BREAK)
echo "Next break will be after $clicks_before_break clicks"
echo ""
while true; do
if [ $EXIT_REQUESTED -eq 1 ]; then
echo "Exit signal received!"
break
fi
# Generate base random delay
base_delay=$(get_human_delay $MIN_DELAY $MAX_DELAY)
# Apply fatigue effect (gradually slow down)
final_delay=$(apply_fatigue "$base_delay" "$click_count")
# Randomly break pattern (5% chance)
if should_break_pattern; then
# Add a longer, unpredictable delay
pattern_break_delay=$(echo "scale=3; ($MAX_DELAY * 2 + $RANDOM % 1000) / 1000" | bc)
echo "[Pattern Break] Adding unpredictable delay of $(echo "$pattern_break_delay * 1000" | bc | cut -d'.' -f1)ms"
if ! interruptible_sleep "$pattern_break_delay"; then
break
fi
fi
# Perform click (suppress ydotool output)
ydotool click 0xC0 2>/dev/null
click_count=$((click_count + 1))
total_clicks=$((total_clicks + 1))
# Convert delay to milliseconds for display
delay_ms=$(echo "$final_delay * 1000" | bc | cut -d'.' -f1)
# Log click timing to CSV
log_click_timing "$total_clicks" "$delay_ms"
# Show each click with its delay
echo "Click #$total_clicks (delay: ${delay_ms}ms, next break in $((clicks_before_break - click_count)) clicks)"
# Check if it's time for a break
if [ $click_count -ge $clicks_before_break ]; then
break_duration=$(get_human_delay $((MIN_BREAK * 1000)) $((MAX_BREAK * 1000)))
echo ""
echo "[Break] Completed $clicks_before_break clicks (total: $total_clicks). Taking a break for ${break_duration}s..."
if ! interruptible_sleep "$break_duration"; then
break
fi
click_count=0
# Generate new random click count for next cycle
clicks_before_break=$(get_random_clicks $MIN_CLICKS_BEFORE_BREAK $MAX_CLICKS_BEFORE_BREAK)
echo "[Resumed] Next break will be after $clicks_before_break clicks"
echo ""
else
# Wait the calculated delay before next click
if ! interruptible_sleep "$final_delay"; then
break
fi
fi
done
echo "Click timings saved to: $LOG_FILE"

View File

@@ -133,4 +133,3 @@ while true; do
sleep "$delay"
fi
done

619
click-visualizer.html Normal file
View File

@@ -0,0 +1,619 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Autoclicker Timing Visualizer</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
}
.controls {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.control-group {
flex: 1;
min-width: 250px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #2c3e50;
}
input, select, button {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
width: 100%;
box-sizing: border-box;
}
button {
background-color: #3498db;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s;
font-weight: 600;
}
button:hover {
background-color: #2980b9;
}
button:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.stat-card {
flex: 1;
min-width: 200px;
background-color: #ecf0f1;
padding: 15px;
border-radius: 6px;
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #2c3e50;
margin: 5px 0;
}
.stat-label {
font-size: 12px;
color: #7f8c8d;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.chart-container {
background-color: white;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
position: relative;
height: 400px;
}
.chart-title {
font-weight: 600;
margin-bottom: 10px;
color: #2c3e50;
}
.chart-wrapper {
position: relative;
height: 300px;
width: 100%;
}
.file-list {
margin-top: 20px;
}
.file-item {
padding: 10px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.2s;
}
.file-item:hover {
background-color: #f8f9fa;
}
.file-item.active {
background-color: #e3f2fd;
font-weight: 600;
}
.file-name {
color: #3498db;
margin-bottom: 3px;
}
.file-info {
font-size: 12px;
color: #7f8c8d;
}
.loading {
text-align: center;
padding: 20px;
color: #7f8c8d;
font-style: italic;
}
.error {
color: #e74c3c;
padding: 10px;
background-color: #fadbd8;
border-radius: 4px;
margin: 10px 0;
}
.server-info {
background-color: #e8f5e9;
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
border-left: 4px solid #4caf50;
}
.api-status {
background-color: #fff3cd;
padding: 10px;
border-radius: 4px;
margin-bottom: 10px;
display: none;
}
.api-status.success {
background-color: #d4edda;
color: #155724;
display: block;
}
.api-status.error {
background-color: #f8d7da;
color: #721c24;
display: block;
}
@media (max-width: 768px) {
.controls, .stats {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🖱️ Autoclicker Timing Visualizer</h1>
<div class="server-info">
<strong>📊 Real-time Data Server</strong>
<p>This visualizer connects to a local Python server that serves real log files from <code>/tmp/autoclicker_logs/</code></p>
<p>Make sure to run: <code>python3 click_server.py</code> in a separate terminal</p>
</div>
<div id="apiStatus" class="api-status">
Testing API connection...
</div>
<div class="controls">
<div class="control-group">
<label for="logFile">Select Log File:</label>
<select id="logFile">
<option value="">-- Loading log files... --</option>
</select>
</div>
<div class="control-group">
<label for="chartType">Chart Type:</label>
<select id="chartType">
<option value="scatter">Scatter Plot</option>
<option value="line">Line Chart</option>
<option value="histogram">Histogram</option>
</select>
</div>
<div class="control-group">
<label>&nbsp;</label>
<button id="loadBtn" disabled>Load Data</button>
</div>
</div>
<div class="stats" id="statsContainer" style="display: none;">
<div class="stat-card">
<div class="stat-label">Total Clicks</div>
<div class="stat-value" id="totalClicks">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Average Delay</div>
<div class="stat-value" id="avgDelay">0 ms</div>
</div>
<div class="stat-card">
<div class="stat-label">Min Delay</div>
<div class="stat-value" id="minDelay">0 ms</div>
</div>
<div class="stat-card">
<div class="stat-label">Max Delay</div>
<div class="stat-value" id="maxDelay">0 ms</div>
</div>
<div class="stat-card">
<div class="stat-label">Std Deviation</div>
<div class="stat-value" id="stdDev">0 ms</div>
</div>
</div>
<div class="chart-container">
<div class="chart-title">Click Timing Distribution</div>
<div class="chart-wrapper">
<canvas id="timingChart"></canvas>
</div>
</div>
<div class="chart-container">
<div class="chart-title">Delay Over Time</div>
<div class="chart-wrapper">
<canvas id="timeSeriesChart"></canvas>
</div>
</div>
<div class="file-list">
<h3>Available Log Files</h3>
<div id="fileList">
<div class="loading">Loading log files from server...</div>
</div>
</div>
</div>
<!-- Load Chart.js with date adapter -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0"></script>
<script>
// Global variables
let clickData = [];
let timingChart, timeSeriesChart;
let currentFile = '';
// API base URL
const API_BASE = '/api';
// DOM elements
const logFileSelect = document.getElementById('logFile');
const chartTypeSelect = document.getElementById('chartType');
const loadBtn = document.getElementById('loadBtn');
const fileList = document.getElementById('fileList');
const statsContainer = document.getElementById('statsContainer');
const apiStatus = document.getElementById('apiStatus');
// Test API connection
async function testAPIConnection() {
try {
const response = await fetch(`${API_BASE}/logs`, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (response.ok) {
apiStatus.textContent = "✅ API connected successfully!";
apiStatus.className = "api-status success";
return true;
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
apiStatus.textContent = `❌ API connection failed: ${error.message}. Is the Python server running?`;
apiStatus.className = "api-status error";
console.error('API connection error:', error);
return false;
}
}
// Initialize charts with proper configuration
function initCharts() {
const timingCtx = document.getElementById('timingChart').getContext('2d');
const timeSeriesCtx = document.getElementById('timeSeriesChart').getContext('2d');
// Destroy existing charts if they exist
if (timingChart) timingChart.destroy();
if (timeSeriesChart) timeSeriesChart.destroy();
timingChart = new Chart(timingCtx, {
type: 'scatter',
data: { datasets: [] },
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'linear',
title: { display: true, text: 'Click Number' }
},
y: {
type: 'linear',
title: { display: true, text: 'Delay (ms)' }
}
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
return `Click ${context.raw.x}: ${context.raw.y}ms`;
}
}
}
}
}
});
timeSeriesChart = new Chart(timeSeriesCtx, {
type: 'line',
data: { datasets: [] },
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'linear',
title: { display: true, text: 'Click Number' }
},
y: {
type: 'linear',
title: { display: true, text: 'Delay (ms)' }
}
}
}
});
}
// Load available log files from server
async function loadLogFiles() {
try {
fileList.innerHTML = '<div class="loading">Loading log files...</div>';
logFileSelect.innerHTML = '<option value="">-- Loading... --</option>';
const response = await fetch(`${API_BASE}/logs`);
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
const files = data.logs;
if (files.length === 0) {
fileList.innerHTML = '<div class="error">No log files found. Run the enhanced autoclicker first.</div>';
logFileSelect.innerHTML = '<option value="">-- No log files available --</option>';
return;
}
// Populate select dropdown
logFileSelect.innerHTML = '<option value="">-- Select a log file --</option>';
files.forEach(file => {
const option = document.createElement('option');
option.value = file.name;
option.textContent = `${file.name} (${file.date})`;
logFileSelect.appendChild(option);
});
// Populate file list
fileList.innerHTML = files.map(file => `
<div class="file-item" data-file="${file.name}">
<div class="file-name">${file.name}</div>
<div class="file-info">Size: ${file.size} | Created: ${file.date}</div>
</div>
`).join('');
// Add click handlers to file items
document.querySelectorAll('.file-item').forEach(item => {
item.addEventListener('click', function() {
document.querySelectorAll('.file-item').forEach(i => i.classList.remove('active'));
this.classList.add('active');
logFileSelect.value = this.dataset.file;
loadBtn.disabled = false;
});
});
} catch (error) {
console.error('Error loading log files:', error);
fileList.innerHTML = `<div class="error">Error loading log files: ${error.message}</div>`;
logFileSelect.innerHTML = '<option value="">-- Error loading files --</option>';
}
}
// Load and parse JSON data from server
async function loadData() {
const fileName = logFileSelect.value;
if (!fileName) return;
currentFile = fileName;
loadBtn.disabled = true;
loadBtn.textContent = 'Loading...';
try {
const response = await fetch(`${API_BASE}/log/${encodeURIComponent(fileName)}`);
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const data = await response.json();
if (data.error) {
if (data.content_preview) {
fileList.innerHTML = `<div class="error">Invalid JSON in ${fileName}: ${data.error}<br><br>File content preview:<pre>${data.content_preview}</pre></div>`;
} else {
fileList.innerHTML = `<div class="error">Error loading ${fileName}: ${data.error}</div>`;
}
return;
}
if (!data.valid) {
fileList.innerHTML = `<div class="error">File ${fileName} is not valid JSON: ${data.error || 'Unknown error'}</div>`;
return;
}
clickData = data.data;
updateCharts();
updateStats();
statsContainer.style.display = 'flex';
} catch (error) {
console.error('Error loading data:', error);
fileList.innerHTML = `<div class="error">Error loading ${fileName}: ${error.message}</div>`;
} finally {
loadBtn.disabled = false;
loadBtn.textContent = 'Load Data';
}
}
// Update charts with current data
function updateCharts() {
if (clickData.length === 0) return;
// Update timing chart based on selected type
const chartType = chartTypeSelect.value;
if (chartType === 'scatter' || chartType === 'line') {
const dataPoints = clickData.map(item => ({
x: item.click,
y: item.delay_ms
}));
timingChart.data.datasets = [{
label: 'Click Delays',
data: dataPoints,
type: chartType,
backgroundColor: 'rgba(52, 152, 219, 0.5)',
borderColor: 'rgba(52, 152, 219, 1)',
pointRadius: chartType === 'scatter' ? 4 : 2,
tension: chartType === 'line' ? 0.3 : 0,
pointStyle: 'circle'
}];
timingChart.update();
} else if (chartType === 'histogram') {
// Create histogram data
const delays = clickData.map(item => item.delay_ms);
const bins = createHistogram(delays, 20);
const histogramData = bins.map((count, i) => ({
x: i * (700 / 20) + 50,
y: count
}));
timingChart.data.datasets = [{
label: 'Delay Distribution',
data: histogramData,
type: 'bar',
backgroundColor: 'rgba(52, 152, 219, 0.7)',
borderColor: 'rgba(52, 152, 219, 1)',
barPercentage: 0.9
}];
timingChart.options.scales.x.title.text = 'Delay Range (ms)';
timingChart.options.scales.y.title.text = 'Frequency';
timingChart.update();
}
// Update time series chart (using click number instead of time for simplicity)
const timeSeriesData = clickData.map(item => ({
x: item.click,
y: item.delay_ms
}));
timeSeriesChart.data.datasets = [{
label: 'Delay Over Time',
data: timeSeriesData,
borderColor: 'rgba(46, 204, 113, 1)',
backgroundColor: 'rgba(46, 204, 113, 0.1)',
tension: 0.3,
pointRadius: 2,
pointStyle: 'circle'
}];
timeSeriesChart.update();
}
// Create histogram from data
function createHistogram(data, binCount) {
if (data.length === 0) return [];
const min = Math.min(...data);
const max = Math.max(...data);
const binSize = (max - min) / binCount;
const bins = new Array(binCount).fill(0);
data.forEach(value => {
const binIndex = Math.min(Math.floor((value - min) / binSize), binCount - 1);
bins[binIndex]++;
});
return bins;
}
// Update statistics
function updateStats() {
if (clickData.length === 0) return;
const delays = clickData.map(item => item.delay_ms);
const total = delays.length;
const sum = delays.reduce((a, b) => a + b, 0);
const avg = sum / total;
const min = Math.min(...delays);
const max = Math.max(...delays);
// Calculate standard deviation
const variance = delays.reduce((sq, n) => sq + Math.pow(n - avg, 2), 0) / total;
const stdDev = Math.sqrt(variance);
document.getElementById('totalClicks').textContent = total;
document.getElementById('avgDelay').textContent = `${avg.toFixed(1)} ms`;
document.getElementById('minDelay').textContent = `${min} ms`;
document.getElementById('maxDelay').textContent = `${max} ms`;
document.getElementById('stdDev').textContent = `${stdDev.toFixed(1)} ms`;
}
// Event listeners
logFileSelect.addEventListener('change', function() {
loadBtn.disabled = !this.value;
});
chartTypeSelect.addEventListener('change', updateCharts);
loadBtn.addEventListener('click', loadData);
// Initialize
initCharts();
// Test API connection first
testAPIConnection().then(isConnected => {
if (isConnected) {
loadLogFiles();
// Auto-refresh log list every 10 seconds
setInterval(loadLogFiles, 10000);
}
});
</script>
</body>
</html>

183
click_server.py Executable file
View File

@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""
Simple web server for autoclicker visualization
Serves the HTML visualizer and provides API endpoints for CSV log files
Converts CSV to JSON for the frontend
"""
import os
import json
import csv
import http.server
import socketserver
from urllib.parse import urlparse, parse_qs
from datetime import datetime
PORT = 8661
LOG_DIR = "/tmp/autoclicker_logs"
class ClickServerHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=".", **kwargs)
def do_GET(self):
# Handle API requests
if self.path.startswith('/api/'):
self.handle_api_request()
return
# Serve the visualizer HTML for root path
if self.path == '/' or self.path == '/index.html':
self.path = '/click-visualizer.html'
# Serve static files normally
return super().do_GET()
def handle_api_request(self):
"""Handle API endpoints for log file operations"""
try:
if self.path.startswith('/api/logs'):
self.handle_logs_request()
elif self.path.startswith('/api/log/'):
self.handle_log_file_request()
else:
self.send_error(404, "API endpoint not found")
except Exception as e:
self.send_error(500, f"Server error: {str(e)}")
def handle_logs_request(self):
"""Return list of available log files"""
if not os.path.exists(LOG_DIR):
self.send_json_response({"error": "Log directory not found", "logs": []})
return
try:
files = []
for filename in sorted(os.listdir(LOG_DIR), reverse=True):
if filename.endswith('.csv'):
filepath = os.path.join(LOG_DIR, filename)
stat = os.stat(filepath)
# Check if CSV is valid
if self.is_valid_csv(filepath):
files.append({
"name": filename,
"size": self.format_file_size(stat.st_size),
"date": datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
"path": filename,
"valid": True
})
else:
files.append({
"name": filename,
"size": self.format_file_size(stat.st_size),
"date": datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
"path": filename,
"valid": False,
"error": "Invalid CSV format"
})
self.send_json_response({"logs": files})
except Exception as e:
self.send_json_response({"error": str(e), "logs": []})
def handle_log_file_request(self):
"""Return contents of a specific log file as JSON"""
# Extract filename from path
parts = self.path.split('/')
if len(parts) < 4:
self.send_error(400, "Invalid log file request")
return
filename = parts[3]
filepath = os.path.join(LOG_DIR, filename)
if not os.path.exists(filepath):
self.send_error(404, "Log file not found")
return
try:
if filename.endswith('.csv'):
data = self.csv_to_json(filepath)
if data is None:
self.send_json_response({
"error": "Failed to parse CSV file",
"filename": filename,
"valid": False
})
return
self.send_json_response({
"data": data,
"filename": filename,
"valid": True
})
else:
self.send_error(400, "Unsupported file format")
except Exception as e:
self.send_error(500, f"Error reading log file: {str(e)}")
def csv_to_json(self, filepath):
"""Convert CSV file to JSON array"""
try:
data = []
with open(filepath, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
data.append({
"click": int(row["click_number"]),
"delay_ms": int(row["delay_ms"]),
"timestamp": int(row["timestamp"])
})
return data
except Exception as e:
print(f"Error parsing CSV {filepath}: {str(e)}")
return None
def is_valid_csv(self, filepath):
"""Check if a file contains valid CSV"""
try:
with open(filepath, 'r') as f:
reader = csv.DictReader(f)
# Try to read first row
next(reader)
return True
except Exception:
return False
def send_json_response(self, data):
"""Send JSON response with proper headers"""
response = json.dumps(data, indent=2)
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Content-Length', str(len(response)))
self.end_headers()
self.wfile.write(response.encode())
def format_file_size(self, size_bytes):
"""Format file size in human-readable format"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024.0:
return f"{size_bytes:.1f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.1f} TB"
def run_server():
"""Start the web server"""
print(f"Starting autoclicker visualization server on port {PORT}")
print(f"Log directory: {LOG_DIR}")
print("Open your browser to: http://localhost:8661")
print("Press Ctrl+C to stop the server")
try:
with socketserver.TCPServer(("", PORT), ClickServerHandler) as httpd:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nServer stopped")
except Exception as e:
print(f"Server error: {str(e)}")
if __name__ == "__main__":
run_server()

View File

@@ -74,24 +74,24 @@ output * bg /home/raga/.config/sway/wallpaper.jpg fill
# Multi-monitor setup
# You can get your output names by running: swaymsg -t get_outputs
# DP-2 is the primary monitor on the left, and DP-1 is on the right.
output HDMI-A-1 {
output DP-1 {
position 0 0
# ECO
# mode 2560x1440@60Hz
# ECO
# mode 2560x1440@60Hz
# FULL
mode 2560x1440@120Hz
# FULL
mode 3840x2160@165Hz
}
output DP-1 {
position 2560 0
output DP-2 {
position 3840 0
# ECO
# mode 2560x1440@60Hz
# ECO
mode 2560x1440@60Hz
# FULL
modeline 586.59 2560 2568 2600 2640 1440 1529 1537 1543 +hsync -vsync
# FULL
# modeline 586.59 2560 2568 2600 2640 1440 1529 1537 1543 +hsync -vsync
}
###############################################################################
@@ -457,10 +457,10 @@ mode "Resize Mode" {
bindsym Up resize shrink height 6 px or 6 ppt
bindsym Right resize grow width 6 px or 6 ppt
bindsym Shift+Left resize shrink width 24 px or 24 ppt
bindsym Shift+Down resize grow height 24 px or 24 ppt
bindsym Shift+Up resize shrink height 24 px or 24 ppt
bindsym Shift+Right resize grow width 24 px or 24 ppt
bindsym Shift+Left resize shrink width 48 px or 48 ppt
bindsym Shift+Down resize grow height 48 px or 48 ppt
bindsym Shift+Up resize shrink height 48 px or 48 ppt
bindsym Shift+Right resize grow width 48 px or 48 ppt
## Resize // Resize Window // k j h l ##
bindsym $left resize shrink width 6 px or 6 ppt
@@ -468,10 +468,10 @@ mode "Resize Mode" {
bindsym $up resize shrink height 6 px or 6 ppt
bindsym $right resize grow width 6 px or 6 ppt
bindsym Shift+$left resize shrink width 24 px or 24 ppt
bindsym Shift+$down resize grow height 24 px or 24 ppt
bindsym Shift+$up resize shrink height 24 px or 24 ppt
bindsym Shift+$right resize grow width 24 px or 24 ppt
bindsym Shift+$left resize shrink width 48 px or 48 ppt
bindsym Shift+$down resize grow height 48 px or 48 ppt
bindsym Shift+$up resize shrink height 48 px or 48 ppt
bindsym Shift+$right resize grow width 48 px or 48 ppt
## Resize // Exit Resize Mode // Escape or Enter ##
bindsym Return mode "default"
@@ -546,6 +546,7 @@ exec lxqt-policykit-agent # Authentication agent (comment out if not needed)
exec --no-startup-id blueman-applet
exec --no-startup-id nm-applet
#exec --no-startup-id home_dotfiles/.config/sway/set_random_bg.sh
# Include additional config files if they exist
include /etc/sway/config.d/*

View File

@@ -10,6 +10,15 @@ click = [
{button = "left", cmd = "~/.config/sway/osrs-mode-toggle.sh", update = true}
]
[[block]]
block = "custom"
command = "echo ''"
format = " $text "
interval = 1
click = [
{button = "left", cmd = "~/.config/sway/set_random_bg.sh", update = false},
]
#[[block]]
#block = "custom"
#command = "~/.config/sway/get-focused-window.sh"

View File

@@ -0,0 +1,32 @@
#!/bin/bash
# Find a random image file
#IMAGE=$(find /run/media/raga/970/qBittorrent/nsfw/xd -type f \( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' \) | shuf -n 1)
IMAGE=$(find /run/media/raga/970/images/cars/wallpaper -type f \( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' \) | shuf -n 1)
if [ -n "$IMAGE" ]; then
echo "Image: $IMAGE"
# Try to get image dimensions using identify (ImageMagick)
DIMENSIONS=$(identify -format "%wx%h" "$IMAGE" 2>/dev/null)
if [ -n "$DIMENSIONS" ]; then
echo "Dimensions: $DIMENSIONS"
WIDTH=$(echo "$DIMENSIONS" | cut -d'x' -f1)
HEIGHT=$(echo "$DIMENSIONS" | cut -d'x' -f2)
# Determine if portrait (height > width) or landscape
if [ "$HEIGHT" -gt "$WIDTH" ]; then
# Portrait image - use "fit" to maintain aspect ratio
echo "Portrait image - using fit"
swaymsg "output * bg \"$IMAGE\" fit"
else
# Landscape image - use "fill" to fill the screen
echo "Landscape image - using fill"
swaymsg "output * bg \"$IMAGE\" fill"
fi
else
# Fallback to fill if we can't determine dimensions
echo "Could not determine dimensions - using fill"
swaymsg "output * bg \"$IMAGE\" fill"
fi
fi

View File

@@ -2,12 +2,12 @@
set -Eeu -o pipefail
# Simplified weather script for i3status-rust
# Based on the original weather script but adapted for Sway/i3status-rust
# Improved weather script for i3status-rust using JSON API
# Based on the original weather script but optimized for Sway/i3status-rust
# Information on the various formats: https://github.com/chubin/wttr.in
VALUE_WEATHER_FORMAT=${weather_format:-"%c%f"}
VALUE_WEATHER_FORMAT="?format=${VALUE_WEATHER_FORMAT}"
# Location (defaults to Pärnu as in your original script)
VALUE_WEATHER_LOCATION=${weather_location:-"Pärnu"}
VALUE_WEATHER_ERROR_MESSAGE=${error_message:-"⛔"}
# Determine units to use for temperature
# We don't supply a default here because wttr.in is "smart" enough to choose for us
@@ -16,40 +16,100 @@ if [ -n "${WEATHER_UNIT}" ]; then
WEATHER_UNIT="&${WEATHER_UNIT}"
fi
# Location (defaults to Pärnu as in your original script)
VALUE_WEATHER_LOCATION=${weather_location:-"Pärnu"}
VALUE_WEATHER_ERROR_MESSAGE=${error_message:-"⛔"}
VALUE_FETCH_WEATHER_URL="https://wttr.in/${VALUE_WEATHER_LOCATION}${VALUE_WEATHER_FORMAT}${WEATHER_UNIT}"
# Use JSON API to get all data in a single request
VALUE_FETCH_WEATHER_URL="https://wttr.in/${VALUE_WEATHER_LOCATION}?format=j1"
# Get weather data
WEATHER=$(curl -sS "$VALUE_FETCH_WEATHER_URL" 2>/dev/null || echo "${VALUE_WEATHER_ERROR_MESSAGE}")
# Get sunrise/sunset data
VALUE_SUNSET_SUNRISE_FORMAT="?format=%S-%s%20%m+"
SUNRISE_SUNSET=$(curl -sS "https://wttr.in/${VALUE_WEATHER_LOCATION}${VALUE_SUNSET_SUNRISE_FORMAT}${WEATHER_UNIT}" 2>/dev/null || echo "")
if [ -n "$SUNRISE_SUNSET" ]; then
SUNRISE_VALUE=$(echo ${SUNRISE_SUNSET} | cut -c 1-5)
SUNSET_VALUE=$(echo ${SUNRISE_SUNSET} | cut -c 10-14)
SUNRISE_SUNSET_TEXT=" (${SUNRISE_VALUE}-${SUNSET_VALUE})"
else
SUNRISE_SUNSET_TEXT=""
fi
# Get weather data using JSON API
WEATHER_JSON=$(curl -sS "$VALUE_FETCH_WEATHER_URL" 2>/dev/null || echo "{}")
# Check for errors
if echo "${WEATHER}" | grep -q -P "Unknown\slocation"; then
WEATHER=${VALUE_WEATHER_ERROR_MESSAGE}
if echo "$WEATHER_JSON" | grep -q "Unknown location"; then
echo "${VALUE_WEATHER_ERROR_MESSAGE}"
exit 0
fi
# Output for i3status-rust (plain text)
echo "${WEATHER}${SUNRISE_SUNSET_TEXT}"
# Extract data using jq (should be available on most systems)
if command -v jq &>/dev/null; then
# Extract current weather condition and temperature
WEATHER_CONDITION=$(echo "$WEATHER_JSON" | jq -r '.current_condition[0].weatherDesc[0].value' 2>/dev/null || echo "")
TEMP_C=$(echo "$WEATHER_JSON" | jq -r '.current_condition[0].temp_C' 2>/dev/null || echo "")
# Extract sunrise and sunset times
SUNRISE=$(echo "$WEATHER_JSON" | jq -r '.weather[0].astronomy[0].sunrise' 2>/dev/null || echo "")
SUNSET=$(echo "$WEATHER_JSON" | jq -r '.weather[0].astronomy[0].sunset' 2>/dev/null || echo "")
# Convert to simple format if we have data
if [ -n "$WEATHER_CONDITION" ] && [ -n "$TEMP_C" ]; then
# Map weather conditions to icons (simplified)
case "$WEATHER_CONDITION" in
*"Sunny"*|*"Clear"*) ICON="☀️";;
*"Partly cloudy"*|*"Cloudy"*) ICON="⛅";;
*"Overcast"*) ICON="☁️";;
*"Rain"*|*"Drizzle"*) ICON="🌧️";;
*"Snow"*) ICON="❄️";;
*"Thunder"*) ICON="⛈️";;
*"Fog"*|*"Mist"*) ICON="🌫️";;
*) ICON="🌤️";;
esac
WEATHER="${ICON} ${TEMP_C}°C"
# Format sunrise/sunset times (convert from "09:10 AM" to "09:10")
if [ -n "$SUNRISE" ] && [ -n "$SUNSET" ]; then
# Remove AM/PM and convert to 24-hour format
SUNRISE_24=$(echo "$SUNRISE" | sed 's/ AM//;s/ PM//;s/^0//' | awk -F: '{if($1==12) print "00:"$2; else if($1<12) print $1":"$2; else print $1":"$2}')
SUNSET_24=$(echo "$SUNSET" | sed 's/ AM//;s/ PM//;s/^0//' | awk -F: '{if($1==12) print "00:"$2; else if($1<12) print $1":"$2; else print $1":"$2}')
# Remove leading zeros from hours for consistency
SUNRISE_24=$(echo "$SUNRISE_24" | sed 's/^0//')
SUNSET_24=$(echo "$SUNSET_24" | sed 's/^0//')
SUNRISE_SUNSET_TEXT=" (${SUNRISE_24}-${SUNSET_24})"
else
SUNRISE_SUNSET_TEXT=""
fi
# Output for i3status-rust (plain text)
echo "${WEATHER}${SUNRISE_SUNSET_TEXT}"
else
echo "${VALUE_WEATHER_ERROR_MESSAGE}"
fi
else
# Fallback to original method if jq is not available
echo "⚠️ jq not found, using fallback method"
# Get basic weather data
VALUE_WEATHER_FORMAT=${weather_format:-"%c%f"}
VALUE_WEATHER_FORMAT="?format=${VALUE_WEATHER_FORMAT}"
WEATHER=$(curl -sS "https://wttr.in/${VALUE_WEATHER_LOCATION}${VALUE_WEATHER_FORMAT}${WEATHER_UNIT}" 2>/dev/null || echo "${VALUE_WEATHER_ERROR_MESSAGE}")
# Get sunrise/sunset data
VALUE_SUNSET_SUNRISE_FORMAT="?format=%S-%s"
SUNRISE_SUNSET=$(curl -sS "https://wttr.in/${VALUE_WEATHER_LOCATION}${VALUE_SUNSET_SUNRISE_FORMAT}${WEATHER_UNIT}" 2>/dev/null || echo "")
if [ -n "$SUNRISE_SUNSET" ]; then
SUNRISE_VALUE=$(echo ${SUNRISE_SUNSET} | cut -c 1-5)
SUNSET_VALUE=$(echo ${SUNRISE_SUNSET} | cut -c 7-11)
SUNRISE_SUNSET_TEXT=" (${SUNRISE_VALUE}-${SUNSET_VALUE})"
else
SUNRISE_SUNSET_TEXT=""
fi
# Check for errors
if echo "${WEATHER}" | grep -q -P "Unknown\slocation"; then
WEATHER=${VALUE_WEATHER_ERROR_MESSAGE}
fi
# Output for i3status-rust (plain text)
echo "${WEATHER}${SUNRISE_SUNSET_TEXT}"
fi
# Handle click events (for i3status-rust custom block)
if [ "${BLOCK_BUTTON:-}" = "1" ]; then
# Left click - show detailed weather
FULL_WEATHER=$(curl -sS "https://wttr.in/${VALUE_WEATHER_LOCATION}?format=%l:+%c+%f+%h+%p+%P+%m+%w+%S+%s" 2>/dev/null || echo "${VALUE_WEATHER_ERROR_MESSAGE}")
notify-send "Weather Details" "$FULL_WEATHER" -t 10000
# Left click - show detailed weather using JSON
DETAILED_WEATHER=$(curl -sS "https://wttr.in/${VALUE_WEATHER_LOCATION}?format=%l:+%c+%f+%h+%p+%P+%m+%w+%S-%s" 2>/dev/null || echo "${VALUE_WEATHER_ERROR_MESSAGE}")
notify-send "Weather Details" "$DETAILED_WEATHER" -t 10000
elif [ "${BLOCK_BUTTON:-}" = "3" ]; then
# Right click - open weather website
xdg-open "https://wttr.in/${VALUE_WEATHER_LOCATION}" >/dev/null 2>&1 &
fi
fi

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

View File

@@ -60,6 +60,13 @@ for script in osrs-mode-status.sh osrs-mode-toggle.sh get-focused-window.sh; do
fi
done
# === CHANGE WALLPAPER SCRIPT ===
echo ""
echo "Installing change wallpaper script..."
cp $SCRIPT_DIR/home_dotfiles/.config/sway/set_random_bg.sh ~/.config/sway/set_random_bg.sh
chmod +x ~/.config/sway/set_random_bg.sh
echo "✓ Change wallpaper script installed to ~/.config/sway/set_random_bg.sh"
# === WEATHER SCRIPT ===
# Comment out this section if you don't want the weather script
echo ""
@@ -103,8 +110,9 @@ echo "✓ swayimg set as default image viewer"
# === SET WALLPAPER ===
echo ""
echo "Setting wallpaper..."
#WALLPAPER_NAME="chris-czermak-PamFFHL6fVY-unsplash.jpg"
WALLPAPER_NAME="lucas-gallone-2dClJIAR404-unsplash.jpg"
WALLPAPER_NAME="chris-czermak-PamFFHL6fVY-unsplash.jpg"
#WALLPAPER_NAME="lucas-gallone-2dClJIAR404-unsplash.jpg"
#WALLPAPER_NAME="1742471076220983.jpg"
WALLPAPER_PATH="$SCRIPT_DIR/home_dotfiles/wallpapers/$WALLPAPER_NAME"
cp $WALLPAPER_PATH ~/.config/sway/wallpaper.jpg
echo "✓ Wallpaper installed to ~/.config/sway/wallpaper.jpg"

71
visualize-clicks.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/bin/bash
# Script to visualize autoclicker log files
# Starts Python server and opens the HTML visualization tool
VISUALIZER_FILE="click-visualizer.html"
LOG_DIR="/tmp/autoclicker_logs"
SERVER_SCRIPT="click_server.py"
SERVER_PORT=8661
# Check if visualization file exists
if [ ! -f "$VISUALIZER_FILE" ]; then
echo "Error: Visualization tool not found at $VISUALIZER_FILE"
exit 1
fi
# Check if server script exists
if [ ! -f "$SERVER_SCRIPT" ]; then
echo "Error: Server script not found at $SERVER_SCRIPT"
exit 1
fi
# Check if Python is available
if ! command -v python3 &> /dev/null; then
echo "Error: Python 3 is not installed. Install it with: sudo pacman -S python"
exit 1
fi
# Check if we have any log files
if [ ! -d "$LOG_DIR" ] || [ -z "$(ls -A "$LOG_DIR")" ]; then
echo "No autoclicker log files found in $LOG_DIR"
echo "Run the enhanced autoclicker first to generate data."
echo ""
echo "Starting server anyway (you can generate logs while it's running)..."
fi
echo "Starting autoclicker visualization server..."
echo "Server will be available at: http://localhost:$SERVER_PORT"
echo "Log directory: $LOG_DIR"
echo ""
# Start the Python server in the background
python3 "$SERVER_SCRIPT" &
SERVER_PID=$!
# Give the server a moment to start
sleep 2
# Check if server started successfully
if ! ps -p $SERVER_PID > /dev/null; then
echo "Error: Failed to start server"
exit 1
fi
# Open browser
if command -v xdg-open &> /dev/null; then
xdg-open "http://localhost:$SERVER_PORT"
elif command -v open &> /dev/null; then
open "http://localhost:$SERVER_PORT"
else
echo "Server started successfully!"
echo "Open your browser and navigate to: http://localhost:$SERVER_PORT"
fi
echo ""
echo "Server is running in the background (PID: $SERVER_PID)"
echo "Press Ctrl+C to stop the server when you're done"
echo ""
# Wait for server to finish (it will run until manually stopped)
wait $SERVER_PID