Files
Loki/internal/jean/sys_splash_windows.go
T
nathaninline 1f3961eaa9 v0.6.10 : dossier de travail pour l'agent, lancement Windows explicite, mise à jour lisible, icône noire
- mode agent : les outils write/edit/bash résolvent les chemins relatifs dans
  JEAN_HOME/workspace et le shell y démarre, au lieu du répertoire courant du
  processus (Bureau, ProgramData\jean\bin). Les chemins absolus demandés
  explicitement restent honorés.
- Windows : le double-clic n'installe plus en silence. Boîte de dialogue unique,
  raccourcis menu Démarrer + Bureau, puis relance depuis la copie installée — un
  seul binaire fait foi. Désinstallation : retrait des raccourcis.
- update : contrôle des droits AVANT le téléchargement (l'échec tombait sur le
  fichier temporaire, avec un message brut), message donnant la commande exacte,
  timeout UI porté de 30 s à 10 min.
- jean where + bouton « Où sont mes fichiers ? » : emplacements, et alerte quand
  le binaire lancé n'est pas celui installé.
- icônes : source unique (sys_brand_icon.go) pour la zone de notification, la
  barre de menus macOS et l'icône du .exe, alignée sur le favicon noir. macOS en
  icône template (une icône noire figée disparaît sur une barre sombre).
- CI : le bundle macOS part de icon.png au lieu de convertir le .ico via sips,
  dont l'échec était masqué par « || true » ; notes de release versionnées.
2026-08-04 11:17:43 +02:00

275 lines
7.3 KiB
Go

//go:build windows
package jean
// sys_splash_windows.go — écran de démarrage « Lancement de Jean… ».
//
// Petite fenêtre sans bordure, à coins arrondis, aux couleurs de la marque
// (fond bleu #1f6feb, logo « j » blanc, texte blanc), affichée le temps que le
// serveur monte et que le navigateur s'ouvre. Rendu GDI (pur syscall, aucun
// CGO). Elle tourne sur son propre thread avec sa boucle de messages ; close()
// lui envoie WM_CLOSE.
import (
"runtime"
"sync"
"syscall"
"unsafe"
)
const (
splashW = 440
splashH = 150
)
var (
u32s = syscall.NewLazyDLL("user32.dll")
g32s = syscall.NewLazyDLL("gdi32.dll")
k32s = syscall.NewLazyDLL("kernel32.dll")
pRegisterClassExW = u32s.NewProc("RegisterClassExW")
pCreateWindowExW = u32s.NewProc("CreateWindowExW")
pDefWindowProcW = u32s.NewProc("DefWindowProcW")
pShowWindow = u32s.NewProc("ShowWindow")
pUpdateWindow = u32s.NewProc("UpdateWindow")
pGetMessageW = u32s.NewProc("GetMessageW")
pTranslateMessage = u32s.NewProc("TranslateMessage")
pDispatchMessageW = u32s.NewProc("DispatchMessageW")
pPostQuitMessage = u32s.NewProc("PostQuitMessage")
pDestroyWindow = u32s.NewProc("DestroyWindow")
pPostMessageW = u32s.NewProc("PostMessageW")
pBeginPaint = u32s.NewProc("BeginPaint")
pEndPaint = u32s.NewProc("EndPaint")
pFillRect = u32s.NewProc("FillRect")
pDrawTextW = u32s.NewProc("DrawTextW")
pGetSystemMetrics = u32s.NewProc("GetSystemMetrics")
pLoadCursorW = u32s.NewProc("LoadCursorW")
pSetWindowRgn = u32s.NewProc("SetWindowRgn")
pCreateSolidBrush = g32s.NewProc("CreateSolidBrush")
pDeleteObject = g32s.NewProc("DeleteObject")
pCreateFontW = g32s.NewProc("CreateFontW")
pSelectObject = g32s.NewProc("SelectObject")
pSetTextColor = g32s.NewProc("SetTextColor")
pSetBkMode = g32s.NewProc("SetBkMode")
pCreateRoundRectRgn = g32s.NewProc("CreateRoundRectRgn")
pGetModuleHandleW = k32s.NewProc("GetModuleHandleW")
splashClassOnce sync.Once
splashProc = syscall.NewCallback(splashWndProc)
)
type rect struct{ left, top, right, bottom int32 }
type point struct{ x, y int32 }
type msg struct {
hwnd uintptr
message uint32
wParam uintptr
lParam uintptr
time uint32
pt point
}
type paintStruct struct {
hdc uintptr
fErase int32
rcPaint rect
fRestore int32
fIncUpdate int32
rgbReserved [32]byte
}
type wndClassExW struct {
cbSize uint32
style uint32
lpfnWndProc uintptr
cbClsExtra int32
cbWndExtra int32
hInstance uintptr
hIcon uintptr
hCursor uintptr
hbrBackground uintptr
lpszMenuName *uint16
lpszClassName *uint16
hIconSm uintptr
}
const (
// Fond noir + « j » blanc : même marque que le favicon et que les icônes
// système (voir sys_brand_icon.go). L'écran de démarrage restait bleu.
colBrand = 0x00000000 // COLORREF = 0x00BBGGRR
colWhite = 0x00FFFFFF
)
type splash struct{ hwnd uintptr }
func showSplash(text string) *splash {
s := &splash{}
ready := make(chan uintptr, 1)
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
hwnd := createSplashWindow()
ready <- hwnd
if hwnd != 0 {
pumpMessages()
}
}()
s.hwnd = <-ready
return s
}
func (s *splash) close() {
if s == nil || s.hwnd == 0 {
return
}
const wmClose = 0x0010
pPostMessageW.Call(s.hwnd, wmClose, 0, 0)
}
func createSplashWindow() uintptr {
className, _ := syscall.UTF16PtrFromString("JeanSplash")
hInst, _, _ := pGetModuleHandleW.Call(0)
splashClassOnce.Do(func() {
idcArrow := uintptr(32512)
cursor, _, _ := pLoadCursorW.Call(0, idcArrow)
wc := wndClassExW{
lpfnWndProc: splashProc,
hInstance: hInst,
hCursor: cursor,
lpszClassName: className,
}
wc.cbSize = uint32(unsafe.Sizeof(wc))
pRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc)))
})
const (
wsPopup = 0x80000000
exTopmost = 0x00000008
exToolwindow = 0x00000080
smCX = 0
smCY = 1
)
cx, _, _ := pGetSystemMetrics.Call(smCX)
cy, _, _ := pGetSystemMetrics.Call(smCY)
x := (int(cx) - splashW) / 2
y := (int(cy) - splashH) / 2
title, _ := syscall.UTF16PtrFromString("AJEAN")
hwnd, _, _ := pCreateWindowExW.Call(
uintptr(exTopmost|exToolwindow),
uintptr(unsafe.Pointer(className)),
uintptr(unsafe.Pointer(title)),
uintptr(uint32(wsPopup)),
uintptr(x), uintptr(y), uintptr(splashW), uintptr(splashH),
0, 0, hInst, 0,
)
if hwnd == 0 {
return 0
}
// Coins arrondis.
rgn, _, _ := pCreateRoundRectRgn.Call(0, 0, splashW+1, splashH+1, 22, 22)
pSetWindowRgn.Call(hwnd, rgn, 1)
const swShowNoActivate = 4
pShowWindow.Call(hwnd, swShowNoActivate)
pUpdateWindow.Call(hwnd)
return hwnd
}
func pumpMessages() {
var m msg
for {
r, _, _ := pGetMessageW.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
if int32(r) <= 0 { // 0 = WM_QUIT, -1 = erreur
return
}
pTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
pDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))
}
}
func splashWndProc(hwnd, message, wParam, lParam uintptr) uintptr {
const (
wmDestroy = 0x0002
wmClose = 0x0010
wmPaint = 0x000F
)
switch message {
case wmPaint:
var ps paintStruct
hdc, _, _ := pBeginPaint.Call(hwnd, uintptr(unsafe.Pointer(&ps)))
paintSplash(hdc)
pEndPaint.Call(hwnd, uintptr(unsafe.Pointer(&ps)))
return 0
case wmClose:
pDestroyWindow.Call(hwnd)
return 0
case wmDestroy:
pPostQuitMessage.Call(0)
return 0
}
r, _, _ := pDefWindowProcW.Call(hwnd, message, wParam, lParam)
return r
}
func paintSplash(hdc uintptr) {
// Fond aux couleurs de la marque.
bg, _, _ := pCreateSolidBrush.Call(colBrand)
full := rect{0, 0, splashW, splashH}
pFillRect.Call(hdc, uintptr(unsafe.Pointer(&full)), bg)
pDeleteObject.Call(bg)
// Logo « j » blanc — mêmes rectangles que l'icône et le favicon
// (glyphRects, sys_brand_icon.go), mis à l'échelle.
white, _, _ := pCreateSolidBrush.Call(colWhite)
const logo = 58
ox, oy := int32(40), int32((splashH-logo)/2)
scale := float64(logo) / 12
for _, rc := range glyphRects {
r := rect{
left: ox + int32(rc[0]*scale),
top: oy + int32(rc[1]*scale),
right: ox + int32((rc[0]+rc[2])*scale),
bottom: oy + int32((rc[1]+rc[3])*scale),
}
pFillRect.Call(hdc, uintptr(unsafe.Pointer(&r)), white)
}
pDeleteObject.Call(white)
// Textes (blanc, fond transparent).
const transparent = 1
pSetBkMode.Call(hdc, transparent)
pSetTextColor.Call(hdc, colWhite)
textX := ox + logo + 26
drawText(hdc, "AJEAN", -30, 700, rect{textX, 40, splashW - 20, 82})
drawText(hdc, "Lancement en cours…", -17, 400, rect{textX, 84, splashW - 20, 118})
}
func drawText(hdc uintptr, s string, height, weight int32, r rect) {
face, _ := syscall.UTF16PtrFromString("Segoe UI")
const (
defaultCharset = 1
cleartypeQuality = 5
)
font, _, _ := pCreateFontW.Call(
uintptr(height), 0, 0, 0, uintptr(weight),
0, 0, 0, defaultCharset, 0, 0, cleartypeQuality, 0,
uintptr(unsafe.Pointer(face)),
)
old, _, _ := pSelectObject.Call(hdc, font)
txt, _ := syscall.UTF16PtrFromString(s)
const (
dtLeft = 0x0000
dtSingleline = 0x0020
dtVcenter = 0x0004
dtNoclip = 0x0100
)
rc := r
pDrawTextW.Call(hdc, uintptr(unsafe.Pointer(txt)), ^uintptr(0),
uintptr(unsafe.Pointer(&rc)), dtLeft|dtSingleline|dtVcenter|dtNoclip)
pSelectObject.Call(hdc, old)
pDeleteObject.Call(font)
}