v0.3.0 : migration des dialogues vers wNim + renommage NimPulse + installeur une-ligne

This commit is contained in:
2026-08-14 11:32:46 -04:00
parent 63fb250d25
commit fd2dc7fbd7
16 changed files with 451 additions and 649 deletions
+4 -4
View File
@@ -1,6 +1,6 @@
## config.nim
## ===========
## Chargement / sauvegarde de la configuration (JSON dans %APPDATA%\TrayTempo)
## Chargement / sauvegarde de la configuration (JSON dans %APPDATA%\NimPulse)
## et gestion du démarrage automatique (clé de registre Run).
##
## Le module définit les types de données partagés (Config, Profile,
@@ -11,7 +11,7 @@ import winim/lean
import winim/inc/winreg
const
AppName* = "TrayTempo"
AppName* = "NimPulse"
AppVersion* = "0.2.0"
RunKey* = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"
DefaultHotkeyModifiers* = MOD_CONTROL or MOD_ALT ## Ctrl+Alt
@@ -116,7 +116,7 @@ proc defaultConfig*(lang: Lang = langFr): Config =
# ---------------------------------------------------------------------------
proc configDir*(): string =
## Répertoire de configuration : `%APPDATA%\TrayTempo`, sauf en mode
## Répertoire de configuration : `%APPDATA%\NimPulse`, sauf en mode
## portable (fichier `portable` placé à côté de l'exécutable) où l'on utilise
## le répertoire de l'exécutable lui-même.
let exeDir = getAppFilename().parentDir
@@ -254,7 +254,7 @@ proc saveConfig*(cfg: Config) =
# ---------------------------------------------------------------------------
proc isAutostartEnabled*(): bool =
## Vérifie la présence de la valeur TrayTempo dans la clé Run.
## Vérifie la présence de la valeur NimPulse dans la clé Run.
var hKey: HKEY
if RegOpenKeyEx(HKEY_CURRENT_USER, RunKey, 0, KEY_QUERY_VALUE, &hKey) == ERROR_SUCCESS:
var dataType: DWORD = 0
+290 -529
View File
@@ -1,131 +1,30 @@
## dialogs.nim
## ============
## Interfaces graphiques natives (Win32) construites sans fichier de ressource :
## panneau de réglages, édition des rappels santé, assistant de premier
## lancement et boîte de dialogue de statistiques.
##
## Ces dialogues sont modaux et utilisent des fenêtres/contrôles créés à la
## volée via CreateWindowExW. Les valeurs sont lues/écrites dans un objet
## `Config` passé par référence.
## Interfaces graphiques basées sur wNim (framework GUI Windows natif en Nim).
## Remplace l'ancienne couche Win32 brute pour un rendu plus propre et moderne :
## panneau de réglages, gestion des rappels santé, assistant de premier
## lancement et boîte de statistiques.
import winim/lean
import winim/inc/commctrl
import std/strutils
import wnim
import config
import i18n
import stats
# ---------------------------------------------------------------------------
# Identifiants des contrôles
# ---------------------------------------------------------------------------
const
# Réglages
IdEditWork = 2001
IdEditShortBreak = 2002
IdEditLongBreak = 2003
IdEditCycles = 2004
IdChkSound = 2005
IdChkDnd = 2006
IdChkAutoStart = 2007
IdEditGoal = 2008
IdRadioAuto = 2009
IdRadioFr = 2010
IdRadioEn = 2011
IdBtnSave = 2012
IdBtnCancel = 2013
IdBtnReminders = 2014
# Rappels santé (liste + boutons)
IdRemList = 2100
IdRemAdd = 2101
IdRemEdit = 2102
IdRemRemove = 2103
IdRemOk = 2104
IdRemCancel = 2105
# Invite de saisie de texte (ajout/édition de rappel)
IdPromptEdit = 2110
IdPromptOk = 2111
IdPromptCancel = 2112
# Assistant
IdWizBack = 2201
IdWizNext = 2202
IdWizRadioBase = 2203
IdWizChkBase = 2250
# ---------------------------------------------------------------------------
# État global des dialogues (ils sont modaux et mono-thread, donc sûrs)
# État partagé (les dialogues sont modaux, donc mono-thread, donc sûrs)
# ---------------------------------------------------------------------------
var
dlgCfg: Config
dlgClosed: bool
dlgSaved: bool
dlgHwnd: HWND
dlgParent: HWND
# Assistant
wizStep: int
wizCfg: Config
dlgReminders: seq[HealthReminder]
# ---------------------------------------------------------------------------
# Helpers de création de contrôles
# Boîte de dialogue des statistiques (wMessageDialog)
# ---------------------------------------------------------------------------
proc makeCtrl(cls, text: string, style: int, x, y, w, h: int, id: int32,
parent: HWND, hInstance: HINSTANCE): HWND =
CreateWindowExW(0, cls, text, DWORD(style), int32(x), int32(y), int32(w),
int32(h), parent, cast[HMENU](id), hInstance, nil)
proc label(text: string, x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE): HWND =
makeCtrl("STATIC", text, WS_CHILD or WS_VISIBLE or SS_LEFT, x, y, w, h, id, parent, inst)
proc editNum(x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE): HWND =
makeCtrl("EDIT", "", WS_CHILD or WS_VISIBLE or WS_BORDER or WS_TABSTOP or ES_NUMBER,
x, y, w, h, id, parent, inst)
proc checkbox(text: string, x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE): HWND =
makeCtrl("BUTTON", text, WS_CHILD or WS_VISIBLE or BS_AUTOCHECKBOX or WS_TABSTOP,
x, y, w, h, id, parent, inst)
proc radio(text: string, x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE,
group: bool): HWND =
let style = WS_CHILD or WS_VISIBLE or BS_AUTORADIOBUTTON or WS_TABSTOP or
(if group: WS_GROUP else: 0)
makeCtrl("BUTTON", text, style, x, y, w, h, id, parent, inst)
proc button(text: string, x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE): HWND =
makeCtrl("BUTTON", text, WS_CHILD or WS_VISIBLE or WS_TABSTOP, x, y, w, h, id, parent, inst)
proc getEditInt(hwnd: HWND, id: int32, dflt: int): int =
var ok: BOOL
let v = GetDlgItemInt(hwnd, id, &ok, TRUE)
if ok != 0: int(v) else: dflt
proc setEditInt(hwnd: HWND, id: int32, value: int) =
discard SetDlgItemInt(hwnd, id, UINT(value), TRUE)
proc isChecked(hwnd: HWND, id: int32): bool =
SendMessage(GetDlgItem(hwnd, id), BM_GETCHECK, 0, 0) == BST_CHECKED
proc setChecked(hwnd: HWND, id: int32, value: bool) =
discard SendMessage(GetDlgItem(hwnd, id), BM_SETCHECK,
if value: BST_CHECKED else: BST_UNCHECKED, 0)
proc centerWindow(hwnd: HWND, w, h: int32) =
let scrW = GetSystemMetrics(SM_CXSCREEN)
let scrH = GetSystemMetrics(SM_CYSCREEN)
let x = (scrW - w) div 2
let y = (scrH - h) div 2
discard SetWindowPos(hwnd, 0, x, y, w, h, SWP_NOZORDER)
# ---------------------------------------------------------------------------
# Boîte de dialogue des statistiques
# ---------------------------------------------------------------------------
proc showStatsDialog*(hwnd: HWND, cfg: Config, st: Stats) =
proc showStatsDialog*(cfg: Config, st: Stats) =
var msg = S.statsToday & " : " & $st.completedToday()
msg &= "\n" & S.statsWeek & " : " & $st.completedThisWeek()
msg &= "\n" & S.statsStreak & " : " & $st.streak() & " " & S.statsDays
@@ -135,477 +34,339 @@ proc showStatsDialog*(hwnd: HWND, cfg: Config, st: Stats) =
msg &= "\n\n" & S.statsGoal & " : " & $done & " / " & $cfg.dailyGoal
if done >= cfg.dailyGoal:
msg &= " ✓"
msg &= "\n\n---\n" & S.statsDayWord & " " & "count"
msg &= "\n\n---\n" & S.statsDayWord & " count"
for (date, n) in st.lastNDays(7):
msg &= "\n" & date & " " & $n
discard MessageBox(hwnd, msg, S.statsTitle, MB_OK or MB_ICONINFORMATION)
discard MessageDialog(message=msg, caption=S.statsTitle, style=wOK).display()
# ---------------------------------------------------------------------------
# Boîte de dialogue des rappels santé : liste modifiable (ajouter / modifier /
# supprimer / activer-désactiver), avec invite de saisie pour le texte.
# Saisie d'une ligne de texte (wTextEntryDialog)
# ---------------------------------------------------------------------------
var
commonControlsInited = false
remListHwnd: HWND
remWorking: seq[HealthReminder] ## Copie de travail (validée à la fermeture).
promptResult: string
promptDone: bool
proc promptText(owner: wWindow, title, initial: string): string =
let dlg = TextEntryDialog(owner=owner, message=S.reminderTextPrompt,
caption=title, value=initial)
result = dlg.display().strip()
proc ensureCommonControls() =
if not commonControlsInited:
var icc: TINITCOMMONCONTROLSEX
icc.dwSize = DWORD(sizeof(TINITCOMMONCONTROLSEX))
icc.dwICC = ICC_LISTVIEW_CLASSES
discard InitCommonControlsEx(&icc)
commonControlsInited = true
# ---------------------------------------------------------------------------
# Gestion des rappels santé (liste + ajout / modification / suppression)
# ---------------------------------------------------------------------------
proc refreshReminderList(list: HWND) =
## Reconstruit la liste à partir de la copie de travail.
discard SendMessage(list, LVM_DELETEALLITEMS, 0, 0)
for i, r in remWorking:
let wtext = +$r.text
var item: LVITEM
item.mask = LVIF_TEXT
item.iItem = int32(i)
item.pszText = cast[LPWSTR](&wtext)
ListView_InsertItem(list, &item)
ListView_SetItemState(list, int32(i),
UINT(INDEXTOSTATEIMAGEMASK(if r.enabled: 2 else: 1)),
UINT(LVIS_STATEIMAGEMASK))
proc selectedReminderIndex(list: HWND): int =
int(ListView_GetNextItem(list, -1, LVNI_SELECTED))
proc getEditText(hwnd: HWND, id: int32): string =
## Lit le texte d'un contrôle EDIT et le convertit en chaîne UTF-8.
var buf: array[512, WCHAR]
let n = GetDlgItemTextW(hwnd, id, cast[LPWSTR](addr buf[0]), 512)
if n > 0:
result = $cast[WideCString](addr buf[0])
proc promptProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT {.stdcall.} =
case msg
of WM_COMMAND:
let id = int(wParam and 0xFFFF)
case id
of IdPromptOk:
promptResult = getEditText(hwnd, IdPromptEdit)
promptDone = true
DestroyWindow(hwnd)
of IdPromptCancel:
promptResult = ""
promptDone = true
DestroyWindow(hwnd)
else: discard
return 0
of WM_DESTROY:
promptDone = true
return 0
else: discard
return DefWindowProc(hwnd, msg, wParam, lParam)
proc promptText(parent: HWND, inst: HINSTANCE, title, initial: string): string =
## Petite boîte de saisie d'une ligne. Retourne "" si annulé.
promptResult = ""
promptDone = false
let w = 360'i32
let h = 140'i32
var wc: WNDCLASSW
wc.lpfnWndProc = cast[WNDPROC](promptProc)
wc.hInstance = inst
wc.lpszClassName = L"TrayTempoPromptClass"
wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1)
discard RegisterClass(&wc)
let hwnd = CreateWindowExW(0, L"TrayTempoPromptClass", title,
WS_CAPTION or WS_SYSMENU or WS_VISIBLE,
0, 0, w, h, parent, 0, inst, nil)
centerWindow(hwnd, w, h)
discard label(S.reminderTextPrompt, 12, 16, 320, 18, 0, hwnd, inst)
discard makeCtrl("EDIT", initial, WS_CHILD or WS_VISIBLE or WS_BORDER or WS_TABSTOP,
12, 38, w - 24, 24, IdPromptEdit, hwnd, inst)
discard button(S.btnOk, w - 210, h - 50, 95, 28, IdPromptOk, hwnd, inst)
discard button(S.btnCancel, w - 105, h - 50, 95, 28, IdPromptCancel, hwnd, inst)
var m: MSG
while not promptDone and GetMessage(&m, 0, 0, 0) != 0:
if not IsDialogMessage(hwnd, &m):
TranslateMessage(&m)
DispatchMessage(&m)
result = promptResult
proc remindersProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT {.stdcall.} =
case msg
of WM_COMMAND:
let id = int(wParam and 0xFFFF)
case id
of IdRemAdd:
let text = promptText(hwnd, GetModuleHandleW(nil), S.reminderNewTitle, "")
if text.strip().len > 0:
remWorking.add HealthReminder(text: text, enabled: true)
refreshReminderList(remListHwnd)
of IdRemEdit:
let idx = selectedReminderIndex(remListHwnd)
if idx >= 0 and idx < remWorking.len:
let text = promptText(hwnd, GetModuleHandleW(nil), S.reminderEditTitle,
remWorking[idx].text)
if text.strip().len > 0:
remWorking[idx].text = text
refreshReminderList(remListHwnd)
of IdRemRemove:
let idx = selectedReminderIndex(remListHwnd)
if idx >= 0 and idx < remWorking.len:
remWorking.delete(idx)
refreshReminderList(remListHwnd)
of IdRemOk:
for i in 0 ..< remWorking.len:
remWorking[i].enabled = ListView_GetCheckState(remListHwnd, UINT(i)) != 0
dlgSaved = true
dlgClosed = true
DestroyWindow(hwnd)
of IdRemCancel:
dlgClosed = true
DestroyWindow(hwnd)
else: discard
return 0
of WM_DESTROY:
dlgClosed = true
return 0
else: discard
return DefWindowProc(hwnd, msg, wParam, lParam)
proc showRemindersDialog(parent: HWND, inst: HINSTANCE, cfg: var Config) =
## Dialogue de gestion des rappels santé. Les modifications ne sont appliquées
## qu'à la validation (bouton OK).
remWorking = cfg.healthReminders
dlgClosed = false
proc showRemindersDialog*(owner: wWindow, cfg: var Config) =
dlgReminders = cfg.healthReminders
dlgSaved = false
ensureCommonControls()
let w = 440'i32
let h = 340'i32
let frame = Frame(owner=owner, title=S.settingsHealth, style=wDefaultDialogStyle)
let panel = Panel(frame)
var wc: WNDCLASSW
wc.lpfnWndProc = cast[WNDPROC](remindersProc)
wc.hInstance = inst
wc.lpszClassName = L"TrayTempoRemindersClass"
wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1)
discard RegisterClass(&wc)
let list = ListCtrl(panel, style=wLcReport or wLcSingleSel)
let btnAdd = Button(panel, label=S.reminderAdd)
let btnEdit = Button(panel, label=S.reminderEdit)
let btnRemove = Button(panel, label=S.reminderRemove)
let chkEnabled = CheckBox(panel, label=S.settingsHealth)
let btnOk = Button(panel, label=S.btnOk)
let btnCancel = Button(panel, label=S.btnCancel)
let hwnd = CreateWindowExW(0, L"TrayTempoRemindersClass", S.settingsHealth,
WS_CAPTION or WS_SYSMENU or WS_VISIBLE,
0, 0, w, h, parent, 0, inst, nil)
centerWindow(hwnd, w, h)
proc refresh() =
list.clearAll()
for r in dlgReminders:
list.appendItem(r.text)
remListHwnd = makeCtrl("SysListView32", "", WS_CHILD or WS_VISIBLE or WS_TABSTOP or
LVS_REPORT or LVS_SINGLESEL or LVS_SHOWSELALWAYS,
12, 12, w - 120, h - 80, IdRemList, hwnd, inst)
ListView_SetExtendedListViewStyle(remListHwnd, LVS_EX_CHECKBOXES or LVS_EX_FULLROWSELECT)
proc selectedIndex(): int =
list.getNextItem(-1, wListNextAll, wListStateSelected)
# Une colonne pleine largeur pour afficher le texte des rappels.
var col: LVCOLUMN
col.mask = LVCF_WIDTH
col.cx = w - 140
discard SendMessage(remListHwnd, LVM_INSERTCOLUMN, 0, cast[LPARAM](&col))
proc updateEnabled() =
let idx = selectedIndex()
if idx >= 0 and idx < dlgReminders.len:
chkEnabled.setValue(dlgReminders[idx].enabled)
discard button(S.reminderAdd, w - 100, 12, 88, 28, IdRemAdd, hwnd, inst)
discard button(S.reminderEdit, w - 100, 46, 88, 28, IdRemEdit, hwnd, inst)
discard button(S.reminderRemove, w - 100, 80, 88, 28, IdRemRemove, hwnd, inst)
list.appendColumn("", 320)
discard button(S.btnOk, w - 210, h - 50, 95, 28, IdRemOk, hwnd, inst)
discard button(S.btnCancel, w - 105, h - 50, 95, 28, IdRemCancel, hwnd, inst)
btnAdd.wEvent_Button do ():
let text = promptText(frame, S.reminderNewTitle, "")
if text.len > 0:
dlgReminders.add HealthReminder(text: text, enabled: true)
refresh()
refreshReminderList(remListHwnd)
btnEdit.wEvent_Button do ():
let idx = selectedIndex()
if idx >= 0 and idx < dlgReminders.len:
let text = promptText(frame, S.reminderEditTitle, dlgReminders[idx].text)
if text.len > 0:
dlgReminders[idx].text = text
refresh()
var m: MSG
while not dlgClosed and GetMessage(&m, 0, 0, 0) != 0:
if not IsDialogMessage(hwnd, &m):
TranslateMessage(&m)
DispatchMessage(&m)
btnRemove.wEvent_Button do ():
let idx = selectedIndex()
if idx >= 0 and idx < dlgReminders.len:
dlgReminders.delete(idx)
refresh()
chkEnabled.wEvent_CheckBox do ():
let idx = selectedIndex()
if idx >= 0 and idx < dlgReminders.len:
dlgReminders[idx].enabled = chkEnabled.getValue()
list.wEvent_ListItemSelected do ():
updateEnabled()
btnOk.wEvent_Button do ():
dlgSaved = true
frame.close()
btnCancel.wEvent_Button do ():
frame.close()
# Mise en page (positions fixes).
list.move(12, 12)
list.setSize(320, 250)
btnAdd.move(344, 12); btnAdd.setSize(88, 28)
btnEdit.move(344, 46); btnEdit.setSize(88, 28)
btnRemove.move(344, 80); btnRemove.setSize(88, 28)
chkEnabled.move(12, 270); chkEnabled.setSize(320, 22)
btnOk.move(232, 300); btnOk.setSize(95, 28)
btnCancel.move(337, 300); btnCancel.setSize(95, 28)
frame.setClientSize(444, 340)
frame.center()
refresh()
updateEnabled()
discard frame.showModal()
if dlgSaved:
cfg.healthReminders = remWorking
cfg.healthReminders = dlgReminders
# ---------------------------------------------------------------------------
# Panneau de réglages
# ---------------------------------------------------------------------------
proc saveSettingsFromControls(hwnd: HWND, cfg: var Config) =
cfg.workMin = getEditInt(hwnd, IdEditWork, cfg.workMin)
cfg.shortBreakMin = getEditInt(hwnd, IdEditShortBreak, cfg.shortBreakMin)
cfg.longBreakMin = getEditInt(hwnd, IdEditLongBreak, cfg.longBreakMin)
cfg.cyclesBeforeLongBreak = getEditInt(hwnd, IdEditCycles, cfg.cyclesBeforeLongBreak)
cfg.dailyGoal = getEditInt(hwnd, IdEditGoal, cfg.dailyGoal)
cfg.soundEnabled = isChecked(hwnd, IdChkSound)
cfg.doNotDisturb = isChecked(hwnd, IdChkDnd)
cfg.autoStart = isChecked(hwnd, IdChkAutoStart)
if isChecked(hwnd, IdRadioFr): cfg.lang = "fr"
elif isChecked(hwnd, IdRadioEn): cfg.lang = "en"
else: cfg.lang = "auto"
proc settingsProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT {.stdcall.} =
case msg
of WM_COMMAND:
let id = int(wParam and 0xFFFF)
case id
of IdBtnSave:
saveSettingsFromControls(hwnd, dlgCfg)
dlgSaved = true
dlgClosed = true
DestroyWindow(hwnd)
of IdBtnCancel:
dlgClosed = true
DestroyWindow(hwnd)
of IdBtnReminders:
showRemindersDialog(hwnd, GetModuleHandleW(nil), dlgCfg)
else: discard
return 0
of WM_CLOSE:
dlgClosed = true
DestroyWindow(hwnd)
return 0
of WM_DESTROY:
dlgClosed = true
return 0
else: discard
return DefWindowProc(hwnd, msg, wParam, lParam)
proc buildSettingsDialog*(parent: HWND, cfg: Config): HWND =
## Construit la fenêtre et les contrôles du panneau de réglages (sans lancer
## la boucle modale). Retourne le HWND créé. Utilisé par `showSettingsDialog`
## et par les tests d'intégration.
proc showSettingsDialog*(cfg: var Config): bool =
dlgCfg = cfg
dlgClosed = false
dlgSaved = false
let inst = GetModuleHandleW(nil)
let w = 420'i32
let h = 430'i32
let frame = Frame(title=S.settingsTitle, style=wDefaultDialogStyle)
let panel = Panel(frame)
var wc: WNDCLASSW
wc.lpfnWndProc = cast[WNDPROC](settingsProc)
wc.hInstance = inst
wc.lpszClassName = L"TrayTempoSettingsClass"
wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1)
discard RegisterClass(&wc)
let lblWork = StaticText(panel, label=S.settingsWork)
let lblShort = StaticText(panel, label=S.settingsShortBreak)
let lblLong = StaticText(panel, label=S.settingsLongBreak)
let lblCycles = StaticText(panel, label=S.settingsCycles)
let lblGoal = StaticText(panel, label=S.settingsGoal)
let lblLang = StaticText(panel, label=S.settingsLanguage)
let hwnd = CreateWindowExW(0, L"TrayTempoSettingsClass", S.settingsTitle,
WS_CAPTION or WS_SYSMENU or WS_VISIBLE,
0, 0, w, h, parent, 0, inst, nil)
centerWindow(hwnd, w, h)
let editWork = TextCtrl(panel)
let editShort = TextCtrl(panel)
let editLong = TextCtrl(panel)
let editCycles = TextCtrl(panel)
let editGoal = TextCtrl(panel)
# --- Durées ---
var y = 12'i32
discard label(S.settingsWork, 12, y + 3, 120, 18, 0, hwnd, inst)
discard editNum(140, y, 60, 22, IdEditWork, hwnd, inst)
discard label(S.settingsMinutes, 206, y + 3, 40, 18, 0, hwnd, inst)
let chkSound = CheckBox(panel, label=S.settingsSound)
let chkDnd = CheckBox(panel, label=S.settingsDoNotDisturb)
let chkAutoStart = CheckBox(panel, label=S.settingsAutoStart)
y += 28
discard label(S.settingsShortBreak, 12, y + 3, 120, 18, 0, hwnd, inst)
discard editNum(140, y, 60, 22, IdEditShortBreak, hwnd, inst)
discard label(S.settingsMinutes, 206, y + 3, 40, 18, 0, hwnd, inst)
let cboLang = ComboBox(panel, style=wCbReadOnly)
y += 28
discard label(S.settingsLongBreak, 12, y + 3, 120, 18, 0, hwnd, inst)
discard editNum(140, y, 60, 22, IdEditLongBreak, hwnd, inst)
discard label(S.settingsMinutes, 206, y + 3, 40, 18, 0, hwnd, inst)
let btnReminders = Button(panel, label=S.settingsEditReminders)
let btnSave = Button(panel, label=S.settingsSave)
let btnCancel = Button(panel, label=S.settingsCancel)
y += 28
discard label(S.settingsCycles, 12, y + 3, 180, 18, 0, hwnd, inst)
discard editNum(200, y, 60, 22, IdEditCycles, hwnd, inst)
cboLang.append(["Auto", "Français", "English"])
y += 34
discard checkbox(S.settingsSound, 12, y, 240, 20, IdChkSound, hwnd, inst)
y += 24
discard checkbox(S.settingsDoNotDisturb, 12, y, 320, 20, IdChkDnd, hwnd, inst)
y += 24
discard checkbox(S.settingsAutoStart, 12, y, 240, 20, IdChkAutoStart, hwnd, inst)
y += 30
discard label(S.settingsGoal, 12, y + 3, 140, 18, 0, hwnd, inst)
discard editNum(160, y, 60, 22, IdEditGoal, hwnd, inst)
discard label(S.settingsGoalHint, 12, y + 24, 380, 16, 0, hwnd, inst)
y += 46
discard label(S.settingsLanguage, 12, y, 100, 18, 0, hwnd, inst)
discard radio("Auto", 120, y, 70, 20, IdRadioAuto, hwnd, inst, true)
discard radio("Français", 190, y, 90, 20, IdRadioFr, hwnd, inst, false)
discard radio("English", 285, y, 90, 20, IdRadioEn, hwnd, inst, false)
y += 30
discard button(S.settingsEditReminders, 12, y, 220, 28, IdBtnReminders, hwnd, inst)
# --- Boutons ---
discard button(S.settingsSave, w - 210, h - 62, 100, 30, IdBtnSave, hwnd, inst)
discard button(S.settingsCancel, w - 105, h - 62, 100, 30, IdBtnCancel, hwnd, inst)
# --- Pré-remplissage ---
setEditInt(hwnd, IdEditWork, cfg.workMin)
setEditInt(hwnd, IdEditShortBreak, cfg.shortBreakMin)
setEditInt(hwnd, IdEditLongBreak, cfg.longBreakMin)
setEditInt(hwnd, IdEditCycles, cfg.cyclesBeforeLongBreak)
setEditInt(hwnd, IdEditGoal, cfg.dailyGoal)
setChecked(hwnd, IdChkSound, cfg.soundEnabled)
setChecked(hwnd, IdChkDnd, cfg.doNotDisturb)
setChecked(hwnd, IdChkAutoStart, cfg.autoStart)
# Pré-remplissage.
editWork.setValue($cfg.workMin)
editShort.setValue($cfg.shortBreakMin)
editLong.setValue($cfg.longBreakMin)
editCycles.setValue($cfg.cyclesBeforeLongBreak)
editGoal.setValue($cfg.dailyGoal)
chkSound.setValue(cfg.soundEnabled)
chkDnd.setValue(cfg.doNotDisturb)
chkAutoStart.setValue(cfg.autoStart)
case cfg.lang
of "fr": setChecked(hwnd, IdRadioFr, true)
of "en": setChecked(hwnd, IdRadioEn, true)
else: setChecked(hwnd, IdRadioAuto, true)
of "fr": cboLang.setSelection(1)
of "en": cboLang.setSelection(2)
else: cboLang.setSelection(0)
discard SetFocus(GetDlgItem(hwnd, IdEditWork))
result = hwnd
proc parseIntOr(text: string, dflt: int): int =
try: result = parseInt(text)
except ValueError: result = dflt
proc showSettingsDialog*(parent: HWND, cfg: var Config): bool =
## Affiche le panneau de réglages. Retourne true si l'utilisateur a enregistré.
let hwnd = buildSettingsDialog(parent, cfg)
btnSave.wEvent_Button do ():
dlgCfg.workMin = parseIntOr(editWork.getValue(), dlgCfg.workMin)
dlgCfg.shortBreakMin = parseIntOr(editShort.getValue(), dlgCfg.shortBreakMin)
dlgCfg.longBreakMin = parseIntOr(editLong.getValue(), dlgCfg.longBreakMin)
dlgCfg.cyclesBeforeLongBreak = parseIntOr(editCycles.getValue(), dlgCfg.cyclesBeforeLongBreak)
dlgCfg.dailyGoal = parseIntOr(editGoal.getValue(), dlgCfg.dailyGoal)
dlgCfg.soundEnabled = chkSound.getValue()
dlgCfg.doNotDisturb = chkDnd.getValue()
dlgCfg.autoStart = chkAutoStart.getValue()
case cboLang.getSelection()
of 1: dlgCfg.lang = "fr"
of 2: dlgCfg.lang = "en"
else: dlgCfg.lang = "auto"
dlgSaved = true
frame.close()
# Boucle de messages locale (dialogue modal).
var m: MSG
while not dlgClosed and GetMessage(&m, 0, 0, 0) != 0:
if not IsDialogMessage(hwnd, &m):
TranslateMessage(&m)
DispatchMessage(&m)
btnCancel.wEvent_Button do ():
frame.close()
btnReminders.wEvent_Button do ():
showRemindersDialog(frame, dlgCfg)
# Mise en page (positions fixes).
var y = 12
lblWork.move(12, y + 3); lblWork.setSize(150, 18)
editWork.move(170, y); editWork.setSize(60, 22)
y += 28
lblShort.move(12, y + 3); lblShort.setSize(150, 18)
editShort.move(170, y); editShort.setSize(60, 22)
y += 28
lblLong.move(12, y + 3); lblLong.setSize(150, 18)
editLong.move(170, y); editLong.setSize(60, 22)
y += 28
lblCycles.move(12, y + 3); lblCycles.setSize(150, 18)
editCycles.move(170, y); editCycles.setSize(60, 22)
y += 30
lblGoal.move(12, y + 3); lblGoal.setSize(150, 18)
editGoal.move(170, y); editGoal.setSize(60, 22)
y += 30
chkSound.move(12, y); chkSound.setSize(240, 20)
y += 24
chkDnd.move(12, y); chkDnd.setSize(320, 20)
y += 24
chkAutoStart.move(12, y); chkAutoStart.setSize(240, 20)
y += 30
lblLang.move(12, y + 3); lblLang.setSize(100, 18)
cboLang.move(120, y); cboLang.setSize(120, 120)
y += 30
btnReminders.move(12, y); btnReminders.setSize(220, 28)
btnSave.move(190, 400); btnSave.setSize(100, 30)
btnCancel.move(300, 400); btnCancel.setSize(100, 30)
frame.setClientSize(420, 440)
frame.center()
discard frame.showModal()
if dlgSaved:
cfg = dlgCfg
return dlgSaved
result = dlgSaved
# ---------------------------------------------------------------------------
# Assistant de premier lancement
# ---------------------------------------------------------------------------
const WizSteps = 5 # 0 welcome, 1 langue, 2 profil, 3 santé, 4 fin
proc wizRebuild(hwnd: HWND, inst: HINSTANCE) =
# Efface tous les contrôles enfants existants.
var child = GetWindow(hwnd, GW_CHILD)
while child != 0:
let next = GetWindow(child, GW_HWNDNEXT)
DestroyWindow(child)
child = next
case wizStep
of 0:
discard label(S.wizardWelcomeTitle, 16, 16, 460, 26, 0, hwnd, inst)
discard label(S.wizardWelcomeBody, 16, 48, 460, 60, 0, hwnd, inst)
of 1:
discard label(S.wizardLangPrompt, 16, 16, 460, 22, 0, hwnd, inst)
discard radio("Français", 30, 48, 200, 22, IdWizRadioBase, hwnd, inst, true)
discard radio("English", 30, 76, 200, 22, int32(IdWizRadioBase + 1), hwnd, inst, false)
# Pré-sélection selon la langue détectée.
setChecked(hwnd, IdWizRadioBase, detectSystemLang() == langFr)
setChecked(hwnd, int32(IdWizRadioBase + 1), detectSystemLang() != langFr)
of 2:
discard label(S.wizardProfilePrompt, 16, 16, 460, 22, 0, hwnd, inst)
let profs = wizCfg.profiles
for i, p in profs:
let name = p.name & " (" & $p.workMin & "/" & $p.shortBreakMin & "/" &
$p.longBreakMin & " min)"
discard radio(name, 30, 48 + i * 26, 440, 22, int32(IdWizRadioBase + i), hwnd, inst, i == 0)
# Pré-sélectionne le profil actif.
for i, p in profs:
if p.name == wizCfg.activeProfile:
setChecked(hwnd, int32(IdWizRadioBase + i), true)
of 3:
discard label(S.wizardHealthPrompt, 16, 16, 460, 22, 0, hwnd, inst)
for i, r in wizCfg.healthReminders:
discard checkbox(r.text, 30, 48 + i * 26, 440, 22, int32(IdWizChkBase + i), hwnd, inst)
setChecked(hwnd, int32(IdWizChkBase + i), r.enabled)
of 4:
discard label(S.wizardDoneTitle, 16, 16, 460, 26, 0, hwnd, inst)
discard label(S.wizardDoneBody, 16, 48, 460, 80, 0, hwnd, inst)
else: discard
# Boutons Bas / Suivant / Terminer.
let backEnabled = wizStep > 0
discard button(S.btnCancel, 20, 300, 90, 30, IdWizBack, hwnd, inst)
if not backEnabled:
discard EnableWindow(GetDlgItem(hwnd, IdWizBack), FALSE)
let nextLabel = if wizStep == WizSteps - 1: S.settingsSave else: S.btnOk
discard button(nextLabel, 400, 300, 90, 30, IdWizNext, hwnd, inst)
proc wizCollectStep(hwnd: HWND) =
## Lit les choix de l'étape courante dans wizCfg.
case wizStep
of 1:
if isChecked(hwnd, IdWizRadioBase): wizCfg.lang = "fr"
elif isChecked(hwnd, int32(IdWizRadioBase + 1)): wizCfg.lang = "en"
of 2:
let profs = wizCfg.profiles
for i in 0 ..< profs.len:
if isChecked(hwnd, int32(IdWizRadioBase + i)):
wizCfg.activeProfile = profs[i].name
of 3:
for i in 0 ..< wizCfg.healthReminders.len:
wizCfg.healthReminders[i].enabled = isChecked(hwnd, int32(IdWizChkBase + i))
else: discard
proc wizardProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT {.stdcall.} =
case msg
of WM_COMMAND:
let id = int(wParam and 0xFFFF)
case id
of IdWizNext:
wizCollectStep(hwnd)
if wizStep < WizSteps - 1:
inc wizStep
wizRebuild(hwnd, GetModuleHandleW(nil))
else:
dlgSaved = true
dlgClosed = true
DestroyWindow(hwnd)
of IdWizBack:
if wizStep > 0:
dec wizStep
wizRebuild(hwnd, GetModuleHandleW(nil))
else: discard
return 0
of WM_CLOSE:
dlgClosed = true
DestroyWindow(hwnd)
return 0
of WM_DESTROY:
dlgClosed = true
return 0
else: discard
return DefWindowProc(hwnd, msg, wParam, lParam)
proc showFirstRunWizard*(cfg: var Config): bool =
## Assistant de premier lancement. Retourne true si l'utilisateur l'a terminé.
wizCfg = cfg
wizStep = 0
dlgClosed = false
dlgCfg = cfg
dlgSaved = false
let inst = GetModuleHandleW(nil)
let w = 520'i32
let h = 360'i32
var step = 0
var wc: WNDCLASSW
wc.lpfnWndProc = cast[WNDPROC](wizardProc)
wc.hInstance = inst
wc.lpszClassName = L"TrayTempoWizardClass"
wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1)
discard RegisterClass(&wc)
let frame = Frame(title=S.wizardWelcomeTitle, style=wDefaultDialogStyle)
let panel = Panel(frame)
let hwnd = CreateWindowExW(0, L"TrayTempoWizardClass", S.wizardWelcomeTitle,
WS_CAPTION or WS_SYSMENU or WS_VISIBLE,
0, 0, w, h, 0, 0, inst, nil)
centerWindow(hwnd, w, h)
wizRebuild(hwnd, inst)
let text = StaticText(panel, label="")
let radio1 = RadioButton(panel, label="")
let radio2 = RadioButton(panel, label="")
let radio3 = RadioButton(panel, label="")
let cboProfile = ComboBox(panel, style=wCbReadOnly)
let chk1 = CheckBox(panel, label="")
let chk2 = CheckBox(panel, label="")
let chk3 = CheckBox(panel, label="")
let chk4 = CheckBox(panel, label="")
let chk5 = CheckBox(panel, label="")
let btnBack = Button(panel, label=S.btnCancel)
let btnNext = Button(panel, label=S.btnOk)
var m: MSG
while not dlgClosed and GetMessage(&m, 0, 0, 0) != 0:
if not IsDialogMessage(hwnd, &m):
TranslateMessage(&m)
DispatchMessage(&m)
proc showAll(b: bool) =
text.show(b)
radio1.show(b); radio2.show(b); radio3.show(b)
cboProfile.show(b)
chk1.show(b); chk2.show(b); chk3.show(b); chk4.show(b); chk5.show(b)
proc render() =
showAll(false)
btnBack.enable(step > 0)
case step
of 0:
text.setLabel(S.wizardWelcomeTitle & "\n\n" & S.wizardWelcomeBody)
text.show()
of 1:
text.setLabel(S.wizardLangPrompt); text.show()
radio1.setLabel("Français"); radio2.setLabel("English")
radio1.setValue(detectSystemLang() == langFr)
radio2.setValue(detectSystemLang() != langFr)
radio1.show(); radio2.show()
of 2:
text.setLabel(S.wizardProfilePrompt); text.show()
cboProfile.show()
of 3:
text.setLabel(S.wizardHealthPrompt); text.show()
chk1.show(); chk2.show(); chk3.show(); chk4.show(); chk5.show()
of 4:
text.setLabel(S.wizardDoneTitle & "\n\n" & S.wizardDoneBody); text.show()
btnNext.setLabel(S.settingsSave)
else: discard
btnNext.wEvent_Button do ():
case step
of 1:
if radio1.getValue(): dlgCfg.lang = "fr" else: dlgCfg.lang = "en"
of 2:
if cboProfile.getSelection() >= 0 and cboProfile.getSelection() < dlgCfg.profiles.len:
dlgCfg.activeProfile = dlgCfg.profiles[cboProfile.getSelection()].name
of 3:
dlgCfg.healthReminders[0].enabled = chk1.getValue()
dlgCfg.healthReminders[1].enabled = chk2.getValue()
dlgCfg.healthReminders[2].enabled = chk3.getValue()
dlgCfg.healthReminders[3].enabled = chk4.getValue()
dlgCfg.healthReminders[4].enabled = chk5.getValue()
else: discard
if step < 4:
inc step
render()
else:
dlgSaved = true
frame.close()
btnBack.wEvent_Button do ():
if step > 0:
dec step
render()
# Initialisation du profil et des rappels.
cboProfile.append(@["Classique", "Deep Work", "Sprint"])
cboProfile.setSelection(0)
for i, r in cfg.healthReminders:
case i
of 0: chk1.setLabel(r.text); chk1.setValue(r.enabled)
of 1: chk2.setLabel(r.text); chk2.setValue(r.enabled)
of 2: chk3.setLabel(r.text); chk3.setValue(r.enabled)
of 3: chk4.setLabel(r.text); chk4.setValue(r.enabled)
of 4: chk5.setLabel(r.text); chk5.setValue(r.enabled)
else: discard
text.move(16, 16); text.setSize(460, 200)
radio1.move(30, 100); radio1.setSize(200, 22)
radio2.move(30, 128); radio2.setSize(200, 22)
radio3.move(30, 156); radio3.setSize(200, 22)
cboProfile.move(30, 100); cboProfile.setSize(200, 120)
chk1.move(30, 90); chk1.setSize(440, 22)
chk2.move(30, 116); chk2.setSize(440, 22)
chk3.move(30, 142); chk3.setSize(440, 22)
chk4.move(30, 168); chk4.setSize(440, 22)
chk5.move(30, 194); chk5.setSize(440, 22)
btnBack.move(20, 300); btnBack.setSize(90, 30)
btnNext.move(400, 300); btnNext.setSize(90, 30)
frame.setClientSize(510, 350)
frame.center()
render()
discard frame.showModal()
if dlgSaved:
cfg = wizCfg
return dlgSaved
cfg = dlgCfg
result = dlgSaved
+2 -2
View File
@@ -43,7 +43,7 @@ proc showFloating*() =
var wc: WNDCLASSW
wc.lpfnWndProc = cast[WNDPROC](floatingProc)
wc.hInstance = inst
wc.lpszClassName = L"TrayTempoFloatingClass"
wc.lpszClassName = L"NimPulseFloatingClass"
wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1)
discard RegisterClass(&wc)
@@ -53,7 +53,7 @@ proc showFloating*() =
let scrH = GetSystemMetrics(SM_CYSCREEN)
fHwnd = CreateWindowExW(WS_EX_TOPMOST or WS_EX_TOOLWINDOW,
L"TrayTempoFloatingClass", L"TrayTempo",
L"NimPulseFloatingClass", L"NimPulse",
WS_POPUP or WS_BORDER or WS_VISIBLE,
scrW - w - 24, scrH - h - 80, w, h,
0, 0, inst, nil)
+17 -17
View File
@@ -6,7 +6,7 @@
## démarrage (détection système ou préférence utilisateur dans config.json).
##
## Ce module importe `config` (types) et `timer` (pour la phase), mais il est
## lui-même importé par les modules d'interface (tray, notifications, traytempo).
## lui-même importé par les modules d'interface (tray, notifications, nimpulse).
import winim/lean
import config
@@ -130,7 +130,7 @@ type Strings* = object
# ---------------------------------------------------------------------------
const fr* = Strings(
appName: "TrayTempo",
appName: "NimPulse",
phaseIdle: "Inactif",
phaseWorking: "Travail",
@@ -138,8 +138,8 @@ const fr* = Strings(
phaseLongBreak: "Pause longue",
phasePaused: "En pause",
tipIdle: "TrayTempo - Inactif",
tipPaused: "TrayTempo - En pause",
tipIdle: "NimPulse - Inactif",
tipPaused: "NimPulse - En pause",
menuStart: "Démarrer",
menuResume: "Reprendre",
@@ -165,7 +165,7 @@ const fr* = Strings(
breakShort: "courte",
breakLong: "longue",
statsTitle: "Statistiques TrayTempo",
statsTitle: "Statistiques NimPulse",
statsToday: "Pomodoros aujourd'hui",
statsWeek: "Pomodoros (7 jours)",
statsStreak: "Série",
@@ -176,8 +176,8 @@ const fr* = Strings(
statsNone: "Aucun pomodoro complété pour l'instant.",
statsExportError: "Impossible d'exporter les statistiques.",
wizardWelcomeTitle: "Bienvenue dans TrayTempo",
wizardWelcomeBody: "Configurons TrayTempo en quelques étapes simples pour que " &
wizardWelcomeTitle: "Bienvenue dans NimPulse",
wizardWelcomeBody: "Configurons NimPulse en quelques étapes simples pour que " &
"vous soyez productif tout en prenant soin de votre santé.",
wizardLangTitle: "Langue / Language",
wizardLangPrompt: "Choisissez votre langue :",
@@ -186,10 +186,10 @@ const fr* = Strings(
wizardHealthTitle: "Rappels santé",
wizardHealthPrompt: "Quels rappels souhaitez-vous pendant vos pauses ?",
wizardDoneTitle: "C'est prêt !",
wizardDoneBody: "TrayTempo vit maintenant dans la zone de notification. " &
wizardDoneBody: "NimPulse vit maintenant dans la zone de notification. " &
"Clic droit sur l'icône pour ouvrir le menu, ou Ctrl+Alt+P pour démarrer/mettre en pause.",
settingsTitle: "Paramètres TrayTempo",
settingsTitle: "Paramètres NimPulse",
settingsDurations: "Durées",
settingsWork: "Travail",
settingsShortBreak: "Pause courte",
@@ -232,7 +232,7 @@ const fr* = Strings(
)
const en* = Strings(
appName: "TrayTempo",
appName: "NimPulse",
phaseIdle: "Idle",
phaseWorking: "Working",
@@ -240,8 +240,8 @@ const en* = Strings(
phaseLongBreak: "Long break",
phasePaused: "Paused",
tipIdle: "TrayTempo - Idle",
tipPaused: "TrayTempo - Paused",
tipIdle: "NimPulse - Idle",
tipPaused: "NimPulse - Paused",
menuStart: "Start",
menuResume: "Resume",
@@ -267,7 +267,7 @@ const en* = Strings(
breakShort: "short",
breakLong: "long",
statsTitle: "TrayTempo Statistics",
statsTitle: "NimPulse Statistics",
statsToday: "Pomodoros today",
statsWeek: "Pomodoros (7 days)",
statsStreak: "Streak",
@@ -278,8 +278,8 @@ const en* = Strings(
statsNone: "No pomodoros completed yet.",
statsExportError: "Could not export statistics.",
wizardWelcomeTitle: "Welcome to TrayTempo",
wizardWelcomeBody: "Let's set up TrayTempo in a few simple steps so you can " &
wizardWelcomeTitle: "Welcome to NimPulse",
wizardWelcomeBody: "Let's set up NimPulse in a few simple steps so you can " &
"be productive while taking care of your health.",
wizardLangTitle: "Language",
wizardLangPrompt: "Choose your language:",
@@ -288,10 +288,10 @@ const en* = Strings(
wizardHealthTitle: "Health reminders",
wizardHealthPrompt: "Which reminders do you want during your breaks?",
wizardDoneTitle: "You're ready!",
wizardDoneBody: "TrayTempo now lives in the system tray. " &
wizardDoneBody: "NimPulse now lives in the system tray. " &
"Right-click the icon for the menu, or press Ctrl+Alt+P to start/pause.",
settingsTitle: "TrayTempo Settings",
settingsTitle: "NimPulse Settings",
settingsDurations: "Durations",
settingsWork: "Work",
settingsShortBreak: "Short break",
+14 -10
View File
@@ -1,17 +1,18 @@
## traytempo.nim
## nimpulse.nim
## =============
## Point d'entrée de l'application. Crée une fenêtre cachée, enregistre
## l'icône tray, le minuteur (WM_TIMER) et le raccourci global, puis exécute
## la boucle de messages Windows.
##
## Compilation :
## nim c -d:release --app:gui src/traytempo.nim
## nim c -d:release --app:gui src/nimpulse.nim
import std/os
import std/typedthreads
import std/strutils
import winim/lean
import winim/inc/shellapi
import wnim
import config
import timer
@@ -26,7 +27,7 @@ import floating
const
TimerId = 1'i64
HotkeyId = 1'i32
SingleInstanceMutexName = "TrayTempo_SingleInstance_Mutex"
SingleInstanceMutexName = "NimPulse_SingleInstance_Mutex"
WmUpdateAvailable = WM_APP + 2
# ---------------------------------------------------------------------------
@@ -93,7 +94,7 @@ proc quitApp() =
proc openSettings() =
## Ouvre le panneau de réglages graphique.
if showSettingsDialog(gHwnd, gCfg):
if showSettingsDialog(gCfg):
saveConfig(gCfg)
setLang(resolveLang(gCfg))
discard setAutostart(gCfg.autoStart)
@@ -102,7 +103,7 @@ proc openSettings() =
showToast(gHwnd, S.appName, S.settingsSaved)
proc showStatsDialog() =
dialogs.showStatsDialog(gHwnd, gCfg, gStats)
dialogs.showStatsDialog(gCfg, gStats)
proc exportStatsCsv() =
## Exporte l'historique en CSV dans le répertoire de configuration.
@@ -230,14 +231,17 @@ proc isAlreadyRunning(): bool =
proc main() =
if isAlreadyRunning():
discard MessageBox(0, "TrayTempo est déjà en cours d'exécution.",
"TrayTempo", MB_OK or MB_ICONINFORMATION)
discard MessageBox(0, "NimPulse est déjà en cours d'exécution.",
"NimPulse", MB_OK or MB_ICONINFORMATION)
return
gCfg = loadConfig()
gStats = loadStats()
gTimer = initTimer()
# Initialise le framework wNim (contrôles communs, OLE, etc.) pour les dialogues.
discard App()
# Premier lancement : assistant de configuration guidée.
let firstRun = not fileExists(configPath())
if firstRun:
@@ -255,17 +259,17 @@ proc main() =
var wc: WNDCLASSW
wc.lpfnWndProc = cast[WNDPROC](wndProc)
wc.hInstance = hInstance
wc.lpszClassName = L"TrayTempoWindowClass"
wc.lpszClassName = L"NimPulseWindowClass"
wc.hIcon = phaseIcon(phIdle)
if RegisterClass(&wc) == 0:
# La classe peut déjà être enregistrée ; on poursuit quand même.
discard
gHwnd = CreateWindowExW(0, L"TrayTempoWindowClass", L"TrayTempo",
gHwnd = CreateWindowExW(0, L"NimPulseWindowClass", L"NimPulse",
WS_OVERLAPPED, 0, 0, 0, 0, 0, 0, hInstance, nil)
if gHwnd == 0:
discard MessageBox(0, "Impossible de créer la fenêtre de l'application.",
"TrayTempo", MB_ICONERROR)
"NimPulse", MB_ICONERROR)
return
# Icône dans le tray.