Files
Loki/internal/ajean/sys_splash_windows.go
T
nathaninline 110458c133 Renommage AJEAN (1/3) : module, chemins de donnees et auto-MAJ
Socle du renommage jean -> AJEAN, sans aucun impact visible pour le parc
installe. Trois briques :

- module github.com/nathaninline/ajean, cmd/ajean, internal/ajean,
  package ajean. Purement interne.

- AjeanHome() remplace JeanHome(). Les defauts deviennent
  %ProgramData%\ajean et /etc/ajean, avec migration du dossier existant
  au premier lancement. La migration est un os.Rename dans le dossier
  parent de l'ancien chemin : intra-volume, donc atomique et instantane
  meme avec des .gguf de plusieurs dizaines de Go. Si le rename echoue
  (handle ouvert sous Windows, droits, volume en lecture seule) on
  CONTINUE sur l'ancien chemin et on retentera : jamais de copie, jamais
  de suppression, le pire cas est "rien n'a bouge".
  $AJEAN_HOME et $JEAN_HOME restent tous deux honores, ainsi que
  /etc/default/ajean et /etc/default/jean : un chemin impose a la main
  est un ordre, on ne le migre pas.

- l'auto-MAJ accepte desormais ajean-<os>-<arch> ET jean-<os>-<arch>,
  dans cet ordre. Indispensable dans les deux sens : les binaires deja
  installes ne connaissent que jean-*, et ce binaire-ci doit pouvoir
  s'installer depuis une release anterieure au renommage.

Couvert par des tests, dont le repli quand un handle ouvert bloque le
rename sous Windows.
2026-08-04 13:59:25 +02:00

275 lines
7.3 KiB
Go

//go:build windows
package ajean
// 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)
}