-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchaqkey.py
More file actions
348 lines (292 loc) · 12.9 KB
/
chaqkey.py
File metadata and controls
348 lines (292 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# =============================================================================
# ChaqKey v1.0 - Simulador de sonido de teclado mecanico liviano
# Autor: IRS | Fecha: 05/04/2026
# =============================================================================
# Descripcion:
# Programa de bandeja del sistema (system tray) que reproduce sonidos de
# teclado mecanico cada vez que el usuario presiona una tecla.
# No necesita instalacion: es un solo ejecutable .exe autocontenido.
#
# Dependencias externas (instalar con pip):
# - pygame : motor de audio de baja latencia
# - pynput : captura global de teclado (funciona aunque otra app este activa)
# - pystray : icono y menu en la bandeja del sistema de Windows
# - Pillow : carga la imagen del icono para el tray
#
# Dependencias internas (incluidas en Python, sin instalar nada):
# - sys, os : rutas de archivos y control del proceso
# - winreg : leer/escribir en el registro de Windows (para autostart)
# - webbrowser : abrir URLs en el navegador predeterminado
# - json : guardar y leer la configuracion del usuario
# =============================================================================
import sys, os, winreg, webbrowser, json
import pygame
from pynput import keyboard
from PIL import Image
import pystray
# =============================================================================
# CONFIGURACION GENERAL
# =============================================================================
APP_NAME = "ChaqKey"
VERSION = "v1.0"
GITHUB_URL = "https://github.com/Filter27/chaqkey"
COFFEE_URL = "https://paypal.me/Filter27"
# Nombres de sonidos disponibles (deben existir como .wav en la carpeta Sounds/)
SOUND_NAMES = ["Rica", "Osquitar", "Bubu", "Chancho", "Krilin"]
# Valores por defecto: se usan SOLO la primera vez que corre el programa.
# Desde la segunda ejecucion en adelante se usan los valores guardados en config.json
DEFAULT_SOUND = "Bubu"
DEFAULT_VOLUME = 0.5
DEFAULT_LANG = "es"
# =============================================================================
# PERSISTENCIA DE CONFIGURACION
# Se guarda un archivo config.json en:
# C:\Users\<usuario>\AppData\Roaming\ChaqKey\config.json
# AppData\Roaming es la ubicacion estandar de Windows para configuraciones
# de usuario. Se mantiene aunque se reinstale o mueva el .exe.
# =============================================================================
# Carpeta de configuracion dentro de AppData\Roaming
CONFIG_DIR = os.path.join(os.environ.get("APPDATA", ""), APP_NAME)
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
def load_config():
"""
Lee la configuracion guardada desde config.json.
Si el archivo no existe (primera ejecucion) o tiene un error,
retorna los valores por defecto definidos arriba.
"""
try:
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
# Validar que los valores leidos sean coherentes antes de usarlos
sound = data.get("sound", DEFAULT_SOUND)
volume = data.get("volume", DEFAULT_VOLUME)
muted = data.get("muted", False)
lang = data.get("lang", DEFAULT_LANG)
# Si el sonido guardado ya no existe en la carpeta Sounds/, usar el primero
if sound not in SOUND_NAMES:
sound = DEFAULT_SOUND
# Asegurar que el volumen este en rango valido
volume = max(0.0, min(1.0, float(volume)))
return {"sound": sound, "volume": volume, "muted": muted, "lang": lang}
except Exception:
# Primera ejecucion o archivo corrupto: usar valores por defecto
return {"sound": DEFAULT_SOUND, "volume": DEFAULT_VOLUME,
"muted": False, "lang": DEFAULT_LANG}
def save_config():
"""
Guarda la configuracion actual en config.json.
Se llama automaticamente cada vez que el usuario cambia
el sonido, volumen, mute o idioma desde el menu.
Crea la carpeta AppData\Roaming\ChaqKey si no existe.
"""
try:
os.makedirs(CONFIG_DIR, exist_ok=True) # crear carpeta si no existe
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump({
"sound": state["sound"],
"volume": state["volume"],
"muted": state["muted"],
"lang": state["lang"],
}, f, indent=2)
except Exception:
pass # si no se puede guardar (ej: sin permisos), el programa sigue funcionando
# =============================================================================
# TEXTOS DE LA INTERFAZ (bilingue: Espanol / English)
# =============================================================================
TEXTS = {
"es": {
"sound": "Sonido",
"volume": "Volumen",
"mute": "Silenciar",
"autostart": "Iniciar con Windows",
"language": "Idioma",
"spanish": "Español",
"english": "Inglés",
"about": "ChaqKey " + VERSION,
"subtitle": "Simulador de Teclado Mecánico",
"github": "Ver proyecto código abierto en GitHub",
"coffee": "Apóyame a seguir creando ☕",
"quit": "Salir",
"25": "25%", "50": "50%", "75": "75%", "100": "100%",
},
"en": {
"sound": "Sound",
"volume": "Volume",
"mute": "Mute",
"autostart": "Start with Windows",
"language": "Language",
"spanish": "Spanish",
"english": "English",
"about": "ChaqKey " + VERSION,
"subtitle": "Mechanical Keyboard Simulator",
"github": "View open source project on GitHub",
"coffee": "Support me to keep creating ☕",
"quit": "Quit",
"25": "25%", "50": "50%", "75": "75%", "100": "100%",
},
}
def t(key):
"""Retorna el texto de 'key' en el idioma activo (state['lang'])."""
return TEXTS[state["lang"]][key]
# =============================================================================
# RUTAS DE ARCHIVOS
# =============================================================================
BASE = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
SOUNDS_DIR = os.path.join(BASE, "Sounds")
ICON_PATH = os.path.join(BASE, "icon.ico")
EXE_PATH = sys.executable if getattr(sys, "frozen", False) else os.path.abspath(__file__)
# =============================================================================
# INICIALIZACION DE AUDIO
# =============================================================================
pygame.mixer.pre_init(frequency=44100, size=-16, channels=2, buffer=256)
pygame.mixer.init()
pygame.mixer.set_num_channels(32)
# =============================================================================
# CARGA DE SONIDOS EN MEMORIA RAM
# =============================================================================
sounds = {}
for name in SOUND_NAMES:
path = os.path.join(SOUNDS_DIR, f"{name}.wav")
if os.path.exists(path):
sounds[name] = pygame.mixer.Sound(path)
if not sounds:
sys.exit(1)
# =============================================================================
# ESTADO: cargar desde config.json (o valores por defecto en primera ejecucion)
# =============================================================================
cfg = load_config()
# Si el sonido guardado no esta disponible en esta instalacion, usar el primero
if cfg["sound"] not in sounds:
cfg["sound"] = list(sounds.keys())[0]
state = {
"sound": cfg["sound"],
"volume": cfg["volume"],
"muted": cfg["muted"],
"lang": cfg["lang"],
}
def apply_volume():
"""Aplica el volumen actual a todos los sonidos. Respeta el estado muted."""
vol = 0.0 if state["muted"] else state["volume"]
for s in sounds.values():
s.set_volume(vol)
apply_volume()
# =============================================================================
# REGISTRO DE WINDOWS - INICIO AUTOMATICO
# =============================================================================
REG_RUN = r"Software\Microsoft\Windows\CurrentVersion\Run"
def is_autostart():
"""Retorna True si ChaqKey esta registrado para iniciar con Windows."""
try:
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_RUN) as k:
winreg.QueryValueEx(k, APP_NAME)
return True
except OSError:
return False
def toggle_autostart(icon, item):
"""Activa o desactiva el inicio automatico con Windows."""
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_RUN, 0, winreg.KEY_SET_VALUE) as k:
if is_autostart():
winreg.DeleteValue(k, APP_NAME)
else:
winreg.SetValueEx(k, APP_NAME, 0, winreg.REG_SZ, f'"{EXE_PATH}"')
# =============================================================================
# LISTENER DE TECLADO
# =============================================================================
held = set()
def on_press(key):
"""Reproduce el sonido al presionar una tecla (ignora el auto-repeat)."""
k = str(key)
if k not in held:
held.add(k)
snd = sounds.get(state["sound"])
if snd:
snd.play()
def on_release(key):
"""Libera la tecla del conjunto para permitir su proxima pulsacion."""
held.discard(str(key))
listener = keyboard.Listener(on_press=on_press, on_release=on_release)
listener.start()
# =============================================================================
# CALLBACKS DEL MENU (con guardado automatico de configuracion)
# Cada vez que el usuario cambia algo, se llama save_config() para
# persistir el cambio en config.json antes de que cierre el programa.
# =============================================================================
def select_sound(name):
"""Cambia el sonido activo y guarda la configuracion."""
def cb(icon, item):
state["sound"] = name
save_config() # <- guardar inmediatamente al cambiar
return cb
def set_volume(level):
"""Fija el volumen, desactiva el mute y guarda la configuracion."""
def cb(icon, item):
state["volume"] = level
state["muted"] = False
apply_volume()
save_config() # <- guardar inmediatamente al cambiar
return cb
def toggle_mute(icon, item):
"""Alterna el mute y guarda la configuracion."""
state["muted"] = not state["muted"]
apply_volume()
save_config() # <- guardar inmediatamente al cambiar
def set_lang(lang):
"""Cambia el idioma de la interfaz y guarda la configuracion."""
def cb(icon, item):
state["lang"] = lang
save_config() # <- guardar inmediatamente al cambiar
return cb
def open_github(icon, item):
"""Abre el repositorio del proyecto en el navegador."""
webbrowser.open(GITHUB_URL)
def open_coffee(icon, item):
"""Abre la pagina de donaciones en el navegador."""
webbrowser.open(COFFEE_URL)
def quit_app(icon, item):
"""Cierra la aplicacion de forma ordenada."""
listener.stop()
pygame.mixer.quit()
icon.stop()
# =============================================================================
# MENU DEL SYSTEM TRAY
# =============================================================================
VOLUME_STEPS = [("25", 0.25), ("50", 0.5), ("75", 0.75), ("100", 1.0)]
menu = pystray.Menu(
pystray.MenuItem(lambda item: t("sound"), pystray.Menu(*[
pystray.MenuItem(
name, select_sound(name),
checked=lambda item, n=name: state["sound"] == n,
radio=True
) for name in sounds
])),
pystray.MenuItem(lambda item: t("volume"), pystray.Menu(*[
pystray.MenuItem(
lambda item, k=key: t(k), set_volume(val),
checked=lambda item, v=val: not state["muted"] and state["volume"] == v,
radio=True
) for key, val in VOLUME_STEPS
])),
pystray.Menu.SEPARATOR,
pystray.MenuItem(lambda item: t("mute"), toggle_mute,
checked=lambda item: state["muted"]),
pystray.MenuItem(lambda item: t("autostart"), toggle_autostart,
checked=lambda item: is_autostart()),
pystray.MenuItem(lambda item: t("language"), pystray.Menu(
pystray.MenuItem(lambda item: t("spanish"), set_lang("es"),
checked=lambda item: state["lang"] == "es", radio=True),
pystray.MenuItem(lambda item: t("english"), set_lang("en"),
checked=lambda item: state["lang"] == "en", radio=True),
)),
pystray.Menu.SEPARATOR,
pystray.MenuItem(lambda item: t("about"), pystray.Menu(
pystray.MenuItem(lambda item: t("subtitle"), None, enabled=False),
pystray.Menu.SEPARATOR,
pystray.MenuItem(lambda item: t("github"), open_github),
pystray.MenuItem(lambda item: t("coffee"), open_coffee),
)),
pystray.MenuItem(lambda item: t("quit"), quit_app)
)
# =============================================================================
# PUNTO DE ENTRADA
# =============================================================================
pystray.Icon(APP_NAME, Image.open(ICON_PATH), APP_NAME, menu=menu).run()