#Region ;**** Directives created by AutoIt3Wrapper_GUI **** #AutoIt3Wrapper_UseX64=n #AutoIt3Wrapper_Outfile=MDReader.exe #AutoIt3Wrapper_UseUpx=n #AutoIt3Wrapper_Res_Description=Lightweight Offline Dark-Mode Markdown Reader (WebView2) #AutoIt3Wrapper_Res_Fileversion=0.0.0.3 #AutoIt3Wrapper_Au3Check_Parameters=-q -d -w 1 -w 2 -w 3 -w 4 -w 6 #EndRegion ;**** Directives created by AutoIt3Wrapper_GUI **** ; ============================================================================== ; MDReader.au3 v0.0.0.3 - Dark-Mode Markdown reader on the WebView2 UDF ; ============================================================================== ; What a reviewer should notice in this revision: ; * Works on WebView2.au3 0.3.0 and 0.6.0 alike (no call was removed between them). ; * No _WV2_* call happens inside a GUIRegisterMsg callback: WM_SIZE and ; WM_DPICHANGED only raise $g_bNeedFit, the message loop does the work. ; * $WV2_EVENT_NEW_WINDOW is armed, so a target="_blank" link cannot open a ; Chromium window of its own outside the dark UI. ; * The view is released in the message loop, before it is left, not in an ; OnAutoItExitRegister handler. ; * Zoom uses _WV2_SetZoom - no re-render, so the scroll position survives. ; * Auto-reload re-renders, then restores the scroll position it read first. ; * A file dropped on the page is written to a real temp file, so Open, Edit, ; Reload, the title and the watcher all keep working on it. ; * The reader sniffs the encoding instead of assuming UTF-8: BOM, then a UTF-16 ; NUL pattern, then a strict UTF-8 probe, then the Windows ANSI code page - and ; when that page answers 65001 ("UTF-8 beta" build) it falls back on the code ; page the user locale used before the switch, because bytes that got this far ; are provably not UTF-8. ; * Markdown: images (inlined as data: URIs so they load from a string page), ; nested lists, ordered lists that keep their start number, setext headings. ; * Inline links are filtered by scheme, quotes are escaped, so a document can ; neither hand a javascript: URI to href nor walk out of an attribute. ; * Built 32-bit: the UDF's x64 entry point is not installed on the machine these ; numbers were measured on, so every 0.4/0.5/0.6 figure is a 32-bit figure. ; ============================================================================== #include #include #include #include "WebView2.au3" ; https://www.autoitscript.com/forum/topic/213932-udf-webview2-%E2%80%94-a-modern-chromium-browser-inside-your-autoit-gui-pure-autoit-no-dll-to-ship/ Opt("MustDeclareVars", 1) Opt("WinTitleMatchMode", 2) Global Const $WM_DPICHANGED = 0x02E0 ; not in WindowsConstants.au3 ; --- Global State --- Global $g_hGUI = 0 Global $g_iWV = 0 Global $g_hDarkBrush = 0 Global $g_sCurrentFile = "" Global $g_sLastFileTime = "" Global $g_sWelcomeMD = "" Global $g_fZoom = 1.0 Global $g_hWatchTimer = TimerInit() Global $g_bNeedFit = False ; List state, declared here and not further down: a Global below the _Main() call is ; not initialised by the time _Main runs, and the first read stops on an error dialog. Global $g_aListType[16] = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""] Global $g_iListDepth = 0 _Main() Func _Main() OnAutoItExitRegister("_ReleaseBrush") DllCall("User32.dll", "bool", "SetProcessDpiAwarenessContext", "int_ptr", Ptr(-4)) $g_hDarkBrush = _WinAPI_CreateSolidBrush(0x1B1818) _EnableDarkModeApp() Local $fScale = _GetDpiScale() Local $iWinW = Int(960 * $fScale) Local $iWinH = Int(720 * $fScale) If $iWinW > @DesktopWidth - 60 Then $iWinW = @DesktopWidth - 60 If $iWinH > @DesktopHeight - 80 Then $iWinH = @DesktopHeight - 80 $g_hGUI = GUICreate("MDReader - Dark Markdown Viewer", $iWinW, $iWinH, -1, -1, _ BitOR($WS_OVERLAPPEDWINDOW, $WS_CLIPCHILDREN), $WS_EX_ACCEPTFILES) _ApplyDarkWindow($g_hGUI) GUISetBkColor(0x18181B, $g_hGUI) GUIRegisterMsg($WM_ERASEBKGND, "_WM_ERASEBKGND") GUISetState(@SW_SHOW, $g_hGUI) ; A cold environment can outlast the UDF's 15 s default: measured on this machine, the ; first attempt came back 0 / @error = $WV2_ERR_TIMEOUT after 15 241 ms, the second ; answered in 6 101 ms. A viewer that exits on that is a viewer that fails at random. Local $iErr = "" For $iTry = 1 To 2 $g_iWV = _WV2_Create($g_hGUI, 0, 0, -1, -1, "", 60000) If $g_iWV <> 0 Then ExitLoop $iErr = " @error=" & @error & " (" & _WV2_GetLastErrorMessage() & ")" Next If $g_iWV = 0 Then _ReleaseBrush() MsgBox(16, "MDReader - WebView2 Error", "Could not initialize WebView2:" & $iErr, 0, $g_hGUI) Exit 1 EndIf ; These two only ever raise a flag; the loop is the one that touches the view. GUIRegisterMsg($WM_SIZE, "_WM_SIZE") GUIRegisterMsg($WM_DPICHANGED, "_WM_DPICHANGED") _WV2_EnableEvent($g_iWV, $WV2_EVENT_WEB_MESSAGE) _WV2_EnableEvent($g_iWV, $WV2_EVENT_NEW_WINDOW) Local $idKeyOpen = GUICtrlCreateDummy() Local $idKeyReload = GUICtrlCreateDummy() Local $idKeyZoomIn = GUICtrlCreateDummy() Local $idKeyZoomOut = GUICtrlCreateDummy() ; No ^f here on purpose: Ctrl+F belongs to the Chromium find bar inside the ; runtime, which works without one line of code. Stealing it as an accelerator ; would only take it away. Local $aAccel[5][2] = [ _ ["^o", $idKeyOpen], _ ["{F5}", $idKeyReload], _ ["^=", $idKeyZoomIn], _ ["^{", $idKeyZoomIn], _ ["^-", $idKeyZoomOut] _ ] GUISetAccelerators($aAccel, $g_hGUI) $g_sWelcomeMD = _GetWelcomeMarkdown() Local $sInitial = _FindInitialFile() If $sInitial <> "" Then _LoadMarkdownFile($sInitial) Else _RenderMarkdownText($g_sWelcomeMD, "Welcome - Drop any .md file or press Ctrl+O", "") EndIf ; --- Main Loop --- Local $iMsg, $sNewTime While 1 $iMsg = GUIGetMsg() Switch $iMsg Case $GUI_EVENT_CLOSE ExitLoop Case $GUI_EVENT_DROPPED If @GUI_DragFile <> "" And FileExists(@GUI_DragFile) Then _LoadMarkdownFile(@GUI_DragFile) Case $idKeyOpen _ActionOpenFile() Case $idKeyReload _ActionReload() Case $idKeyZoomOut _ActionZoom(-1) Case $idKeyZoomIn _ActionZoom(1) EndSwitch ; Resize work belongs here, not in the callback. If $g_bNeedFit Then $g_bNeedFit = False _WV2_FitToParent($g_iWV) EndIf _DrainWebViewEvents() If $g_sCurrentFile <> "" And TimerDiff($g_hWatchTimer) > 1000 Then $g_hWatchTimer = TimerInit() $sNewTime = FileGetTime($g_sCurrentFile, 0, 1) If $sNewTime <> "" And $sNewTime <> $g_sLastFileTime Then _LoadMarkdownFile($g_sCurrentFile, True) EndIf WEnd ; Released inside the loop: the answers this call waits for travel through it. _WV2_Destroy($g_iWV) $g_iWV = 0 _ReleaseBrush() EndFunc ;==>_Main Func _ReleaseBrush() If $g_hDarkBrush <> 0 Then _WinAPI_DeleteObject($g_hDarkBrush) $g_hDarkBrush = 0 EndIf EndFunc ;==>_ReleaseBrush ; ============================================================================== ; WebView2 Event Bridge & Toolbar Actions ; ============================================================================== Func _DrainWebViewEvents() Local $sData = "", $iVal = 0, $iEv = 0 While 1 $iEv = _WV2_GetEvent($g_iWV, $sData, $iVal) If $iEv <= 0 Then ExitLoop ; 0 = the queue is empty, -1 = bad handle Switch $iEv Case $WV2_EVENT_WEB_MESSAGE _HandleToolbarMessage($sData) Case $WV2_EVENT_NEW_WINDOW ; Arming this event is what makes the UDF answer put_Handled(True) for ; the popup. Nothing is lost: the request still reaches us here, and we ; decide where it goes - in this view, never in a window of Chromium's ; own that ignores the dark title bar. If StringLeft(StringLower($sData), 8) = "https://" Or StringLeft(StringLower($sData), 7) = "http://" Then _WV2_Navigate($g_iWV, $sData) EndIf Case $WV2_EVENT_NAVIGATION_COMPLETED ; $iVal is 1 on success, otherwise the web error id. A reader that only ; ever calls NavigateToString will not see one, but the external link ; above can fail offline and the title is the cheapest honest signal. If $iVal <> 1 Then WinSetTitle($g_hGUI, "", "MDReader - navigation failed (web error " & $iVal & ")") EndSwitch WEnd EndFunc ;==>_DrainWebViewEvents Func _HandleToolbarMessage($sData) Switch $sData Case "open" _ActionOpenFile() Case "edit" _ActionEditNotepad() Case "reload" _ActionReload() Case "zoom_in" _ActionZoom(1) Case "zoom_out" _ActionZoom(-1) Case "pdf" _ActionExportPDF() Case Else ; "DROP:name|base64" from the page's FileReader. It becomes a real file ; first, because everything else in this app hangs off $g_sCurrentFile. If StringLeft($sData, 5) = "DROP:" Then Local $iPipe = StringInStr($sData, "|") If $iPipe > 6 Then _LoadDroppedFile(StringMid($sData, 6, $iPipe - 6), StringMid($sData, $iPipe + 1)) EndIf EndSwitch EndFunc ;==>_HandleToolbarMessage ; The page hands over the bytes as base64, not as text: readAsText would guess UTF-8 ; and lose the accents of a cp1252 file before AutoIt ever saw them. Writing the bytes ; to a temp file keeps Open, Edit in Notepad, Reload, the title and the watcher working ; on a dropped file exactly as on an opened one. Func _LoadDroppedFile($sName, $sBase64) Local $b = _Base64Decode($sBase64) If @error Or BinaryLen($b) = 0 Then Return SetError(1, 0, "") Local $sDir = @TempDir & "\MDReader" DirCreate($sDir) $sName = StringRegExpReplace($sName, '[\\/:*?"<>|]', "_") If $sName = "" Then $sName = "dropped.md" Local $sPath = $sDir & "\" & $sName Local $hFile = FileOpen($sPath, 16 + 2) ; $FO_BINARY + overwrite If $hFile = -1 Then Return SetError(1, 0, "") FileWrite($hFile, $b) FileClose($hFile) _LoadMarkdownFile($sPath) EndFunc ;==>_LoadDroppedFile Func _Base64Decode($s) $s = StringStripWS($s, 3) If $s = "" Then Return SetError(1, 0, Binary("")) Local $iMax = Int(StringLen($s) / 4 + 1) * 3 + 8 Local $tOut = DllStructCreate("byte[" & $iMax & "]") Local $tLen = DllStructCreate("dword") DllStructSetData($tLen, 1, $iMax) ; The size argument is a DWORD* in both directions: passing the number itself makes ; the API write through it and takes the process down with it. Local $aRet = DllCall("crypt32.dll", "bool", "CryptStringToBinaryW", "wstr", $s, "dword", 0, "dword", 1, _ "ptr", DllStructGetPtr($tOut), "ptr", DllStructGetPtr($tLen), "dword", 0, "dword", 0) If @error Or Not IsArray($aRet) Or $aRet[0] = 0 Then Return SetError(1, 0, Binary("")) Return SetError(0, 0, BinaryMid(DllStructGetData($tOut, 1), 1, DllStructGetData($tLen, 1))) EndFunc ;==>_Base64Decode Func _ActionOpenFile() Local $sInitDir = ($g_sCurrentFile <> "") ? _DirOf($g_sCurrentFile) : @ScriptDir Local $sPicked = FileOpenDialog("Open Markdown File", $sInitDir, _ "Markdown Files (*.md;*.markdown;*.txt)|All Files (*.*)", 1, "", $g_hGUI) If Not @error And $sPicked <> "" Then _LoadMarkdownFile($sPicked) EndFunc ;==>_ActionOpenFile Func _DirOf($sPath) Local $i = StringInStr($sPath, "\", 4, -1) ; 4 = search from the right Return ($i > 1) ? StringLeft($sPath, $i - 1) : @ScriptDir EndFunc ;==>_DirOf Func _ActionEditNotepad() If $g_sCurrentFile = "" Or Not FileExists($g_sCurrentFile) Then Return ; Run() hands the string to cmd-style parsing, so a path with a quote - or a ; space, in some shells - needs the 8.3 form first. Local $sPath = $g_sCurrentFile If StringInStr($sPath, '"') Or StringInStr($sPath, " ") Then Local $aRet = DllCall("kernel32.dll", "dword", "GetShortPathNameW", "wstr", $sPath, "wstr", "", "dword", 260) If Not @error And IsArray($aRet) And $aRet[2] <> "" Then $sPath = $aRet[2] EndIf Run('notepad.exe "' & $sPath & '"') EndFunc ;==>_ActionEditNotepad Func _ActionReload() If $g_sCurrentFile <> "" And FileExists($g_sCurrentFile) Then _LoadMarkdownFile($g_sCurrentFile, True) EndFunc ;==>_ActionReload Func _ActionZoom($iDelta) ; The old code changed the CSS font-size and re-rendered the document. That resets ; the scroll position and repaints everything for a change of one pixel. The view ; has a zoom of its own, it is one call, and the page does not move under you. Local $fNew = $g_fZoom * (($iDelta > 0) ? 1.1 : 1 / 1.1) If $fNew < 0.5 Then $fNew = 0.5 If $fNew > 2.5 Then $fNew = 2.5 If _WV2_SetZoom($g_iWV, $fNew) Then $g_fZoom = $fNew EndFunc ;==>_ActionZoom Func _ActionExportPDF() Local $sDefault = ($g_sCurrentFile <> "") ? StringRegExpReplace($g_sCurrentFile, "\.[^\.]+$", "") & ".pdf" : @ScriptDir & "\document.pdf" Local $sSavePDF = FileSaveDialog("Export Markdown to PDF", @ScriptDir, "PDF Document (*.pdf)", 16, $sDefault, $g_hGUI) If @error Or $sSavePDF = "" Then Return If StringRight(StringLower($sSavePDF), 4) <> ".pdf" Then $sSavePDF &= ".pdf" If _WV2_PrintToPdf($g_iWV, $sSavePDF) Then MsgBox(64, "PDF Exported", "Saved PDF to:" & @CRLF & $sSavePDF, 0, $g_hGUI) Else MsgBox(48, "PDF Export failed", _WV2_GetLastErrorMessage(), 0, $g_hGUI) EndIf EndFunc ;==>_ActionExportPDF ; ============================================================================== ; Window Messages - they set flags, the message loop does the WebView2 work ; ============================================================================== Func _WM_ERASEBKGND($hWnd, $iMsg, $wParam, $lParam) #forceref $iMsg, $lParam If $hWnd = $g_hGUI And $g_hDarkBrush <> 0 Then Local $tRect = _WinAPI_GetClientRect($hWnd) _WinAPI_FillRect($wParam, $tRect, $g_hDarkBrush) Return 1 EndIf Return $GUI_RUNDEFMSG EndFunc ;==>_WM_ERASEBKGND Func _WM_SIZE($hWnd, $iMsg, $wParam, $lParam) #forceref $iMsg, $wParam, $lParam If $hWnd = $g_hGUI And $g_iWV <> 0 Then $g_bNeedFit = True Return $GUI_RUNDEFMSG EndFunc ;==>_WM_SIZE Func _WM_DPICHANGED($hWnd, $iMsg, $wParam, $lParam) #forceref $iMsg, $wParam If $hWnd = $g_hGUI And $lParam <> 0 Then Local $tNewRect = DllStructCreate("long left;long top;long right;long bottom", $lParam) Local $iX = DllStructGetData($tNewRect, "left") Local $iY = DllStructGetData($tNewRect, "top") Local $iW = DllStructGetData($tNewRect, "right") - $iX Local $iH = DllStructGetData($tNewRect, "bottom") - $iY _WinAPI_SetWindowPos($hWnd, 0, $iX, $iY, $iW, $iH, BitOR($SWP_NOZORDER, $SWP_NOACTIVATE)) ; The SetWindowPos above raises WM_SIZE, which raises $g_bNeedFit. Nothing else to do. Return 0 EndIf Return $GUI_RUNDEFMSG EndFunc ;==>_WM_DPICHANGED Func _GetDpiScale() Local $aRet = DllCall("user32.dll", "uint", "GetDpiForSystem") If Not @error And IsArray($aRet) And $aRet[0] > 0 Then Return $aRet[0] / 96.0 Return 1.0 EndFunc ;==>_GetDpiScale ; ============================================================================== ; File Loading & Rendering ; ============================================================================== Func _FindInitialFile() If $CmdLine[0] >= 1 And FileExists($CmdLine[1]) Then Return $CmdLine[1] If FileExists(@ScriptDir & "\README.md") Then Return @ScriptDir & "\README.md" Return "" EndFunc ;==>_FindInitialFile Func _LoadMarkdownFile($sFilePath, $bKeepScroll = False) If Not FileExists($sFilePath) Then Return Local $sContent = _ReadTextFile($sFilePath) If @error Then WinSetTitle($g_hGUI, "", StringRegExpReplace($sFilePath, "^.*\\", "") & " - unreadable - MDReader") Return EndIf Local $iScroll = 0 If $bKeepScroll Then $iScroll = _GetScrollY() $g_sCurrentFile = $sFilePath $g_sLastFileTime = FileGetTime($sFilePath, 0, 1) WinSetTitle($g_hGUI, "", StringRegExpReplace($sFilePath, "^.*\\", "") & " - MDReader") _RenderMarkdownText($sContent, $sFilePath, _DirOf($sFilePath)) If $bKeepScroll And $iScroll > 0 Then _SetScrollY($iScroll) EndFunc ;==>_LoadMarkdownFile Func _RenderMarkdownText($sMarkdown, $sDisplayPath, $sBaseDir) Local $sBodyHTML = _MarkdownToHTML($sMarkdown, $sBaseDir) Local $sFullHTML = _BuildHTMLDocument($sBodyHTML, $sDisplayPath) _WV2_NavigateToString($g_iWV, $sFullHTML) EndFunc ;==>_RenderMarkdownText Func _GetScrollY() Local $sResult = "" If _WV2_ExecuteScript($g_iWV, "window.scrollY", $sResult, 1000) Then Return Int(Number($sResult)) Return 0 EndFunc ;==>_GetScrollY Func _SetScrollY($iY) ; NavigateToString only asks for the navigation; the new document is scrollable ; once the queue has answered, so wait for it before running the script. _WV2_WaitNavigation($g_iWV, 3000) Local $sResult = "" _WV2_ExecuteScript($g_iWV, "window.scrollTo(0," & Int($iY) & ");1", $sResult, 1000) EndFunc ;==>_SetScrollY ; ============================================================================== ; Encoding-Aware Text Reader ; ============================================================================== ; FileOpen($p, 256) is $FO_UTF8_NOBOM: it decodes as UTF-8 whatever the bytes say. ; A cp1252 French document then arrives with U+FFFD where the accents were, and the ; bytes are gone by the time you look at the string, so nothing downstream can undo it. ; The order below was measured on five files holding "Accentues : é ... — tiret": ; UTF-8 BOM, then UTF-16 (BOM, or the NUL-alternating pattern), then a strict UTF-8 ; probe, then the Windows ANSI code page as the last answer. Func _ReadTextFile($sPath) Local $hFile = FileOpen($sPath, 16) ; $FO_BINARY - the raw bytes are what we judge If $hFile = -1 Then Return SetError(1, 0, "") Local $b = FileRead($hFile) FileClose($hFile) If @error Or BinaryLen($b) = 0 Then Return SetError(1, 0, "") If BinaryMid($b, 1, 3) = Binary("0xEFBBBF") Then Return SetError(0, 0, BinaryToString(BinaryMid($b, 4), 1)) If BinaryMid($b, 1, 2) = Binary("0xFFFE") Then Return SetError(0, 0, BinaryToString(BinaryMid($b, 3), 2)) If BinaryMid($b, 1, 2) = Binary("0xFEFF") Then Return SetError(0, 0, BinaryToString(BinaryMid($b, 3), 3)) Local $iLen = BinaryLen($b) If $iLen >= 4 And Mod($iLen, 2) = 0 Then If BinaryMid($b, 1, 1) = Binary("0x00") Or BinaryMid($b, 2, 1) = Binary("0x00") Then Local $iMode = (BinaryMid($b, 2, 1) = Binary("0x00")) ? 2 : 3 Return SetError(0, 0, BinaryToString($b, $iMode)) EndIf EndIf If _IsUtf8($b) Then Return SetError(0, 0, BinaryToString($b, 1)) Return SetError(0, 0, _DecodeCP($b, _GetAnsiCP())) EndFunc ;==>_ReadTextFile Func _GetAnsiCP() Local $aRet = DllCall("kernel32.dll", "uint", "GetACP") If @error Or Not IsArray($aRet) Or $aRet[0] = 0 Then Return _LegacyCPFromLocale() ; Bytes that reach this point already failed the strict UTF-8 probe, so an answer of ; 65001 is the "Use Unicode UTF-8 for worldwide language support" switch, not the real ; page: decoding with it would turn every accent into U+FFFD. Ask the user locale. If $aRet[0] = 65001 Then Return _LegacyCPFromLocale() Return $aRet[0] EndFunc ;==>_GetAnsiCP ; The page the user locale used before it switched to UTF-8. LOCALE_IDEFAULTANSICODEPAGE ; is not an option here: on this machine GetLocaleInfoEx and GetLocaleInfoW both answer 0 ; for it, whatever locale is passed (fr-FR, user default, system default, invariant). Func _LegacyCPFromLocale() Local $tName = DllStructCreate("wchar name[85]") Local $aRet = DllCall("kernel32.dll", "int", "GetUserDefaultLocaleName", "ptr", DllStructGetPtr($tName, "name"), "int", 85) Local $sLocale = (Not @error And IsArray($aRet) And $aRet[0] > 0) ? DllStructGetData($tName, "name") : "" Local $sTag = StringLower(StringLeft($sLocale, 2)) Switch $sTag Case "ru", "uk", "be", "kk", "ky", "bg", "mk", "mn", "tg", "uz" Return 1251 ; Cyrillic Case "el" Return 1253 Case "tr" Return 1254 Case "he" Return 1255 Case "ar" Return 1256 Case "th" Return 874 Case "vi" Return 1258 Case "lt", "lv", "et" Return 1257 ; Baltic Case "cs", "pl", "hu", "ro", "sk", "hr", "sl", "sr", "sq" Return 1250 ; Central European Case "ja" Return 932 Case "ko" Return 949 Case "zh" Local $sUp = StringUpper($sLocale) Return (StringInStr($sUp, "-TW") Or StringInStr($sUp, "-HK") Or StringInStr($sUp, "-MO") Or StringInStr($sUp, "HANT")) ? 950 : 936 Case Else Return 1252 ; Western European, and the answer for an unknown or empty locale EndSwitch EndFunc ;==>_LegacyCPFromLocale Func _IsUtf8($b) Local $iLen = BinaryLen($b) Local $t = DllStructCreate("byte[" & $iLen & "]") DllStructSetData($t, 1, $b) ; MB_ERR_INVALID_CHARS (8): the call answers 0 for any byte sequence that is not ; well-formed UTF-8, which is exactly the question being asked here. Local $aRet = DllCall("kernel32.dll", "int", "MultiByteToWideChar", "uint", 65001, "dword", 8, "struct*", $t, "int", $iLen, "ptr", 0, "int", 0) If @error Or Not IsArray($aRet) Then Return False Return $aRet[0] > 0 EndFunc ;==>_IsUtf8 Func _DecodeCP($b, $iCP) Local $iLen = BinaryLen($b) If $iLen = 0 Then Return "" Local $tIn = DllStructCreate("byte[" & $iLen & "]") DllStructSetData($tIn, 1, $b) Local $a = DllCall("kernel32.dll", "int", "MultiByteToWideChar", "uint", $iCP, "dword", 0, "struct*", $tIn, "int", $iLen, "ptr", 0, "int", 0) If @error Or Not IsArray($a) Or $a[0] <= 0 Then Return BinaryToString($b, 1) Local $tOut = DllStructCreate("wchar[" & ($a[0] + 1) & "]") Local $c = DllCall("kernel32.dll", "int", "MultiByteToWideChar", "uint", $iCP, "dword", 0, "struct*", $tIn, "int", $iLen, "ptr", DllStructGetPtr($tOut), "int", $a[0]) If @error Or Not IsArray($c) Or $c[0] <= 0 Then Return BinaryToString($b, 1) Return DllStructGetData($tOut, 1) EndFunc ;==>_DecodeCP ; ============================================================================== ; Pure-AutoIt Markdown to HTML Converter ; ============================================================================== ; The list state it uses ($g_aListType / $g_iListDepth) sits with the other globals. Func _MarkdownToHTML($sMD, $sBaseDir) $sMD = StringReplace($sMD, @CRLF, @LF) $sMD = StringReplace($sMD, @CR, @LF) Local $aLines = StringSplit($sMD, @LF, 1) Local $sOut = "" Local $bInCode = False, $sCodeLang = "", $sCodeBuf = "" Local $bInQuote = False, $bInTable = False, $bTableHead = False Local $sPara = "" ; buffered, so a following ==== / ---- line can still become a heading Local $bAfterBlank = False Local $iIndent, $iLevel, $sTrim, $sLine, $aMatch For $i = 1 To $aLines[0] $sLine = $aLines[$i] $sTrim = StringStripWS($sLine, 3) ; ---- 1. Fenced code. A fence with a language on it never closes a block: ; only a bare fence does, so ```au3 inside a code sample stays text. If StringLeft($sTrim, 3) = "```" Then Local $sFenceArg = StringStripWS(StringMid($sTrim, 4), 3) If Not $bInCode Then _FlushPara($sOut, $sPara) _CloseLists($sOut) _CloseAside($sOut, $bInQuote, $bInTable, $bTableHead) $bInCode = True $sCodeLang = $sFenceArg $sCodeBuf = "" ContinueLoop ElseIf $sFenceArg = "" Then $bInCode = False Local $sBadge = ($sCodeLang <> "") ? '
' & _EscapeHTML($sCodeLang) & '
' : '' $sOut &= '
' & $sBadge & '
' & $sCodeBuf & '
' & @LF $sCodeLang = "" $sCodeBuf = "" ContinueLoop EndIf EndIf If $bInCode Then $sCodeBuf &= _EscapeHTML($sLine) & @LF ContinueLoop EndIf ; ---- 2. Blank line: it ends a paragraph, a quote and a table, but not a list. ; Whether the list survives is decided by the next line that has text. If $sTrim = "" Then _FlushPara($sOut, $sPara) _CloseAside($sOut, $bInQuote, $bInTable, $bTableHead) $bAfterBlank = True ContinueLoop EndIf If $bAfterBlank Then $bAfterBlank = False If Not StringRegExp($sTrim, "^([-*+]|\d+[.)])\s+") Then _CloseLists($sOut) EndIf ; ---- 3. Setext heading: the buffered paragraph is the text (already formatted) If StringRegExp($sTrim, "^={2,}$") And $sPara <> "" Then $sOut &= "

" & $sPara & "

" & @LF $sPara = "" _CloseLists($sOut) ContinueLoop EndIf If StringRegExp($sTrim, "^-{2,}$") And $sPara <> "" Then $sOut &= "

" & $sPara & "

" & @LF $sPara = "" _CloseLists($sOut) ContinueLoop EndIf ; ---- 4. Horizontal rule If StringRegExp($sTrim, "^(-{3,}|\*{3,}|_{3,})$") Then _FlushPara($sOut, $sPara) _CloseLists($sOut) _CloseAside($sOut, $bInQuote, $bInTable, $bTableHead) $sOut &= "
" & @LF ContinueLoop EndIf ; ---- 5. ATX heading # .. ###### If StringRegExp($sTrim, "^#{1,6}\s") Then _FlushPara($sOut, $sPara) _CloseLists($sOut) _CloseAside($sOut, $bInQuote, $bInTable, $bTableHead) $aMatch = StringRegExp($sTrim, "^(#{1,6})\s+(.*)$", 1) If Not @error Then _OpenHeading($sOut, StringLen($aMatch[0]), $aMatch[1], $sBaseDir) ContinueLoop EndIf ; ---- 6. Table row If StringLeft($sTrim, 1) = "|" And StringRight($sTrim, 1) = "|" And StringLen($sTrim) > 1 Then _FlushPara($sOut, $sPara) _CloseLists($sOut) If StringRegExp($sTrim, "^\|[\s:\-\|]+\|$") Then ContinueLoop ; the separator row If Not $bInTable Then _CloseAside($sOut, $bInQuote, $bInTable, $bTableHead) $bInTable = True $bTableHead = False $sOut &= "" & @LF EndIf Local $aCells = StringSplit(StringMid($sTrim, 2, StringLen($sTrim) - 2), "|", 1) Local $sRow = ($bTableHead) ? "" : "" For $c = 1 To $aCells[0] If $bTableHead Then $sRow &= "" Else $sRow &= "" EndIf Next $sRow &= ($bTableHead) ? "" & @LF : "" & @LF $sOut &= $sRow $bTableHead = True ContinueLoop EndIf ; ---- 7. Blockquote If StringRegExp($sTrim, "^>\s?") Then _FlushPara($sOut, $sPara) _CloseLists($sOut) If Not $bInQuote Then _CloseAside($sOut, $bInQuote, $bInTable, $bTableHead) $bInQuote = True $sOut &= "
" & @LF EndIf $sOut &= "

" & _FormatInline(StringRegExpReplace($sTrim, "^>\s?", ""), $sBaseDir) & "

" & @LF ContinueLoop EndIf ; ---- 8. List item, with the indentation kept If StringRegExp($sTrim, "^([-*+]|\d+[.)])\s+") Then _FlushPara($sOut, $sPara) _CloseAside($sOut, $bInQuote, $bInTable, $bTableHead) $iIndent = StringLen($sLine) - StringLen(StringStripWS($sLine, 1)) $iLevel = Int($iIndent / 2) If $iLevel > 15 Then $iLevel = 15 _OpenListTo($sOut, $iLevel, (StringRegExp($sTrim, "^\d") ? "ol" : "ul"), $sTrim, $sBaseDir) ContinueLoop EndIf ; ---- 9. Anything else is paragraph text; consecutive lines join with
. ; A list or a quote left open is closed first, by rule 2 and the flag. If $sPara = "" Then _CloseAside($sOut, $bInQuote, $bInTable, $bTableHead) $sPara = _FormatInline($sTrim, $sBaseDir) Else $sPara &= "
" & _FormatInline($sTrim, $sBaseDir) EndIf Next If $bInCode Then $sOut &= '
' & $sCodeBuf & '
' & @LF _FlushPara($sOut, $sPara) _CloseLists($sOut) _CloseAside($sOut, $bInQuote, $bInTable, $bTableHead) Return $sOut EndFunc ;==>_MarkdownToHTML Func _OpenHeading(ByRef $sOut, $iLevel, $sText, $sBaseDir) $sOut &= "" & _FormatInline(StringStripWS($sText, 3), $sBaseDir) & "" & @LF EndFunc ;==>_OpenHeading Func _FlushPara(ByRef $sOut, ByRef $sPara) If $sPara = "" Then Return $sOut &= "

" & $sPara & "

" & @LF ; already formatted line by line $sPara = "" EndFunc ;==>_FlushPara Func _CloseAside(ByRef $sOut, ByRef $bInQuote, ByRef $bInTable, ByRef $bTableHead) If $bInQuote Then $sOut &= "
" & @LF $bInQuote = False EndIf If $bInTable Then $sOut &= "
" & _FormatInline(StringStripWS($aCells[$c], 3), $sBaseDir) & "" & _FormatInline(StringStripWS($aCells[$c], 3), $sBaseDir) & "
" & @LF $bInTable = False $bTableHead = False EndIf EndFunc ;==>_CloseAside ; Open (or reuse) the list at $iLevel and emit one item. Going one level deeper opens a ; nested
    /
      inside the last
    1. ; coming back out closes the deeper ones first. Func _OpenListTo(ByRef $sOut, $iLevel, $sType, $sTrim, $sBaseDir) While $g_iListDepth > $iLevel + 1 $g_iListDepth -= 1 $sOut &= "" & @LF WEnd While $g_iListDepth < $iLevel + 1 $g_iListDepth += 1 $g_aListType[$g_iListDepth - 1] = $sType $sOut &= _ListOpenTag($sType, $sTrim) WEnd If $g_aListType[$iLevel] <> $sType Then $sOut &= "" & @LF $g_aListType[$iLevel] = $sType $sOut &= _ListOpenTag($sType, $sTrim) EndIf Local $sItem = StringRegExpReplace($sTrim, "^([-*+]|\d+[.)])\s+", "") If StringRegExp($sItem, "^\[[xX]\]\s+") Then $sItem = StringRegExpReplace($sItem, "^\[[xX]\]\s+", "") $sOut &= '
    2. ✓ ' & _FormatInline($sItem, $sBaseDir) & "
    3. " & @LF ElseIf StringRegExp($sItem, "^\[\s\]\s+") Then $sItem = StringRegExpReplace($sItem, "^\[\s\]\s+", "") $sOut &= '
    4. □ ' & _FormatInline($sItem, $sBaseDir) & "
    5. " & @LF Else $sOut &= "
    6. " & _FormatInline($sItem, $sBaseDir) & "
    7. " & @LF EndIf EndFunc ;==>_OpenListTo Func _ListOpenTag($sType, $sTrim) If $sType = "ol" Then Local $aNum = StringRegExp($sTrim, "^(\d+)", 1) If Not @error And Number($aNum[0]) <> 1 Then Return '
        ' & @LF Return "
          " & @LF EndIf Return "
            " & @LF EndFunc ;==>_ListOpenTag Func _CloseLists(ByRef $sOut) While $g_iListDepth > 0 $g_iListDepth -= 1 $sOut &= "" & @LF WEnd EndFunc ;==>_CloseLists ; The inline pass works in three steps: escape everything, lift the constructs that ; produce HTML (code, image, link) out into placeholders, then let emphasis run over ; what is left. Without the placeholders a * inside a URL would be eaten by , and ; with them the emphasis can no longer touch text that is already markup. Func _FormatInline($sText, $sBaseDir) $sText = _EscapeHTML($sText) Local $aProt[1] = [0] ; 1. Inline code, so a * or _ inside it is never italicised. Local $aM While 1 $aM = StringRegExp($sText, "`([^`]+)`", 1) If @error Then ExitLoop _Protect($aProt, $sText, "`" & $aM[0] & "`", "" & $aM[0] & "") WEnd ; 2. Images, before links: the leading ! is what tells the two apart. While 1 $aM = StringRegExp($sText, "!\[([^\]]*)\]\(([^\)]+)\)", 1) If @error Then ExitLoop _Protect($aProt, $sText, "![" & $aM[0] & "](" & $aM[1] & ")", _ImageTag($aM[1], $aM[0], $sBaseDir)) WEnd ; 3. Links - the scheme is checked before anything reaches href. While 1 $aM = StringRegExp($sText, "(?$1") $sText = StringRegExpReplace($sText, "\*\*([^\*]+)\*\*", "$1") $sText = StringRegExpReplace($sText, "\*([^\*]+)\*", "$1") $sText = StringRegExpReplace($sText, "~~([^~]+)~~", "$1") ; 5. Put the protected HTML back. For $i = 1 To $aProt[0] $sText = StringReplace($sText, Chr(2) & "P" & $i & Chr(3), $aProt[$i]) Next Return $sText EndFunc ;==>_FormatInline Func _Protect(ByRef $aProt, ByRef $sText, $sMatched, $sHTML) Local $iIdx = UBound($aProt) ReDim $aProt[$iIdx + 1] $aProt[0] = $iIdx $aProt[$iIdx] = $sHTML $sText = StringReplace($sText, $sMatched, Chr(2) & "P" & $iIdx & Chr(3), 1) EndFunc ;==>_Protect ; Only these schemes may become an href. Everything else keeps the label and loses the ; link, so a document cannot hand javascript:, data: or file: to the view. Func _LinkTag($sURL, $sLabel) If Not _SchemeAllowed($sURL) Then Return $sLabel Return '' & $sLabel & "" EndFunc ;==>_LinkTag Func _SchemeAllowed($sURL) Local $sLow = StringLower(StringStripWS($sURL, 3)) Local $iColon = StringInStr($sLow, ":") Local $iSlash = StringInStr($sLow, "/") If $iColon = 0 Then Return True ; no scheme at all: an anchor or a relative path If $iSlash > 0 And $iSlash < $iColon Then Return True ; the colon is part of the path Return (StringLeft($sLow, 8) = "https://" Or StringLeft($sLow, 7) = "http://" Or StringLeft($sLow, 7) = "mailto:") EndFunc ;==>_SchemeAllowed ; --------------------------------------------------------------------------- ; Images ; --------------------------------------------------------------------------- ; The document is shown with NavigateToString, so it has no base URL of its own and a ; relative src would resolve against about:blank and fail. A local image is therefore ; read from disk and inlined as a data: URI - no server, no origin, nothing to map. Func _ImageTag($sSrc, $sAlt, $sBaseDir) Local $sLow = StringLower(StringStripWS($sSrc, 3)) If StringLeft($sLow, 5) = "data:" Or StringLeft($sLow, 4) = "http" Then Return '' & $sAlt & '' Local $sPath = $sSrc If Not StringRegExp($sPath, "^[a-zA-Z]:\\") And StringLeft($sPath, 1) <> "\" Then $sPath = $sBaseDir & "\" & $sPath $sPath = _NormPath($sPath) If Not FileExists($sPath) Then Return '' & $sAlt & "" If FileGetSize($sPath) > 4 * 1024 * 1024 Then Return '' & $sAlt & " (over 4 MB)" Local $sB64 = _B64EncodeFile($sPath) If @error Then Return '' & $sAlt & "" Return '' & $sAlt & '' EndFunc ;==>_ImageTag ; Collapse . and .. without trusting a regex on backslashes: AutoIt's own handling of ; them in a string is confusing enough that the segment walk below is the readable fix. Func _NormPath($sPath) Local $aSplit = StringSplit(StringReplace($sPath, "/", "\"), "\", 1) Local $iTop = 0, $i For $i = 1 To $aSplit[0] If $aSplit[$i] = "." Or $aSplit[$i] = "" Then ContinueLoop If $aSplit[$i] = ".." Then If $iTop > 0 Then $iTop -= 1 ContinueLoop EndIf $iTop += 1 $aSplit[$iTop] = $aSplit[$i] Next Local $sOut = "" For $i = 1 To $iTop $sOut = ($sOut = "") ? $aSplit[$i] : $sOut & "\" & $aSplit[$i] Next Return $sOut EndFunc ;==>_NormPath Func _MimeOf($sPath) Switch StringLower(StringRegExpReplace($sPath, "^.*\.", "")) Case "png" Return "image/png" Case "jpg", "jpeg" Return "image/jpeg" Case "gif" Return "image/gif" Case "webp" Return "image/webp" Case "svg" Return "image/svg+xml" Case "ico" Return "image/x-icon" Case "bmp" Return "image/bmp" EndSwitch Return "application/octet-stream" EndFunc ;==>_MimeOf Func _B64EncodeFile($sPath) Local $hFile = FileOpen($sPath, 16) If $hFile = -1 Then Return SetError(1, 0, "") Local $b = FileRead($hFile) FileClose($hFile) If @error Then Return SetError(1, 0, "") Local $iLen = BinaryLen($b) Local $tIn = DllStructCreate("byte[" & $iLen & "]") DllStructSetData($tIn, 1, $b) Local $iChars = Int($iLen / 3 + 2) * 4 + 8 Local $tOut = DllStructCreate("wchar[" & $iChars & "]") Local $tCount = DllStructCreate("dword") DllStructSetData($tCount, 1, $iChars) ; CRYPT_STRING_BASE64 (1) + CRYPT_STRING_NOCRLF (0x40000000): one line, no breaks to strip. Local $aRet = DllCall("crypt32.dll", "bool", "CryptBinaryToStringW", "struct*", $tIn, "dword", $iLen, "dword", 0x40000001, _ "ptr", DllStructGetPtr($tOut), "ptr", DllStructGetPtr($tCount)) If @error Or Not IsArray($aRet) Or $aRet[0] = 0 Then Return SetError(1, 0, "") Return SetError(0, 0, StringStripWS(DllStructGetData($tOut, 1), 3)) EndFunc ;==>_B64EncodeFile Func _EscapeHTML($sText) Local $sOut = StringReplace($sText, "&", "&") $sOut = StringReplace($sOut, "<", "<") $sOut = StringReplace($sOut, ">", ">") ; The quote is the one that matters here: this string is dropped into title="..." ; and href="...", so a file name containing one quote walks out of the attribute. $sOut = StringReplace($sOut, '"', """) Return $sOut EndFunc ;==>_EscapeHTML ; ============================================================================== ; Dark Chromium Template & Sticky Glass Toolbar ; ============================================================================== Func _BuildHTMLDocument($sBodyContent, $sDisplayPath) Local $sCSS = _ ":root { color-scheme: dark; }" & _ "* { box-sizing: border-box; }" & _ "html, body {" & _ " background-color: #18181b;" & _ " color: #e4e4e7;" & _ " font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;" & _ " font-size: 15px;" & _ " line-height: 1.65;" & _ " margin: 0;" & _ " padding: 0;" & _ "}" & _ "::-webkit-scrollbar { width: 10px; height: 10px; }" & _ "::-webkit-scrollbar-track { background: #18181b; }" & _ "::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 5px; border: 2px solid #18181b; }" & _ "::-webkit-scrollbar-thumb:hover { background: #52525b; }" & _ "::-webkit-scrollbar-corner { background: #18181b; }" & _ ".toolbar {" & _ " position: sticky;" & _ " top: 0;" & _ " z-index: 100;" & _ " display: flex;" & _ " align-items: center;" & _ " gap: 8px;" & _ " padding: 8px 16px;" & _ " background: rgba(24, 24, 27, 0.92);" & _ " backdrop-filter: blur(10px);" & _ " border-bottom: 1px solid #27272a;" & _ " user-select: none;" & _ "}" & _ ".btn {" & _ " background: #27272a;" & _ " color: #e4e4e7;" & _ " border: 1px solid #3f3f46;" & _ " border-radius: 6px;" & _ " padding: 5px 12px;" & _ " font-size: 13px;" & _ " font-family: inherit;" & _ " cursor: pointer;" & _ " transition: background 0.12s, border-color 0.12s;" & _ "}" & _ ".btn:hover { background: #3f3f46; border-color: #52525b; color: #ffffff; }" & _ ".btn:active { background: #52525b; }" & _ ".path-label {" & _ " margin-left: 8px;" & _ " color: #a1a1aa;" & _ " font-size: 12.5px;" & _ " white-space: nowrap;" & _ " overflow: hidden;" & _ " text-overflow: ellipsis;" & _ " flex: 1;" & _ "}" & _ ".content {" & _ " max-width: 920px;" & _ " margin: 0 auto;" & _ " padding: 24px 36px 56px 36px;" & _ "}" & _ "h1, h2, h3, h4, h5, h6 { color: #fafafa; font-weight: 600; margin-top: 1.4em; margin-bottom: 0.5em; }" & _ "h1 { font-size: 1.85em; border-bottom: 1px solid #27272a; padding-bottom: 0.3em; margin-top: 0.2em; }" & _ "h2 { font-size: 1.4em; border-bottom: 1px solid #27272a; padding-bottom: 0.25em; }" & _ "h3 { font-size: 1.18em; color: #f4f4f5; }" & _ "p { margin: 0.65em 0; }" & _ "a { color: #60a5fa; text-decoration: none; }" & _ "a:hover { text-decoration: underline; color: #93c5fd; }" & _ "strong { color: #ffffff; font-weight: 600; }" & _ "img { max-width: 100%; height: auto; border-radius: 6px; background: #ffffff; display: block; margin: 10px 0; }" & _ ".missing-img { color: #f87171; font-style: italic; }" & _ "code {" & _ " background-color: #27272a;" & _ " color: #f472b6;" & _ " padding: 2px 6px;" & _ " border-radius: 4px;" & _ " font-family: 'Cascadia Mono', Consolas, monospace;" & _ " font-size: 0.88em;" & _ "}" & _ ".code-wrap {" & _ " background-color: #111113;" & _ " border: 1px solid #27272a;" & _ " border-radius: 8px;" & _ " margin: 14px 0;" & _ " overflow: hidden;" & _ "}" & _ ".code-lang {" & _ " background-color: #202024;" & _ " color: #a1a1aa;" & _ " font-family: 'Cascadia Mono', Consolas, monospace;" & _ " font-size: 0.78em;" & _ " padding: 5px 14px;" & _ " border-bottom: 1px solid #27272a;" & _ "}" & _ "pre { margin: 0; padding: 14px 16px; overflow-x: auto; }" & _ "pre code { background-color: transparent; color: #d4d4d8; padding: 0; font-size: 0.9em; }" & _ "blockquote {" & _ " margin: 12px 0;" & _ " padding: 8px 16px;" & _ " border-left: 4px solid #3b82f6;" & _ " background-color: #1e293b;" & _ " color: #cbd5e1;" & _ " border-radius: 0 6px 6px 0;" & _ "}" & _ "ul, ol { margin: 0.5em 0 0.8em 0; padding-left: 26px; }" & _ "li { margin: 0.3em 0; }" & _ "li.task { list-style-type: none; margin-left: -20px; }" & _ ".chk { display: inline-block; width: 18px; color: #71717a; font-weight: bold; }" & _ ".chk.checked { color: #22c55e; }" & _ "hr { border: 0; border-top: 1px solid #27272a; margin: 24px 0; }" & _ "table { border-collapse: collapse; width: 100%; margin: 16px 0; border-radius: 6px; overflow: hidden; }" & _ "th, td { border: 1px solid #2e2e32; padding: 9px 13px; text-align: left; }" & _ "th { background-color: #27272a; color: #fafafa; font-weight: 600; }" & _ "tr:nth-child(even) td { background-color: #1f1f23; }" Local $sJS = _ "function sendCmd(cmd) { window.chrome.webview.postMessage(cmd); }" & _ "document.addEventListener('dragover', function(e) { e.preventDefault(); });" & _ "document.addEventListener('drop', function(e) {" & _ " e.preventDefault();" & _ " var f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];" & _ " if (!f) return;" & _ " var r = new FileReader();" & _ " r.onload = function(ev) {" & _ " var s = String(ev.target.result);" & _ " var i = s.indexOf(',');" & _ " window.chrome.webview.postMessage('DROP:' + f.name + '|' + (i >= 0 ? s.slice(i + 1) : ''));" & _ " };" & _ " r.readAsDataURL(f);" & _ "});" Local $sEscaped = _EscapeHTML($sDisplayPath) Local $sToolbar = _ '
            ' & _ ' ' & _ ' ' & _ ' ' & _ ' ' & _ ' ' & _ ' ' & _ '
            ' & $sEscaped & '
            ' & _ '
            ' Return "" & @LF & _ "" & @LF & _ '' & @LF & _ "" & @LF & _ "" & @LF & _ $sToolbar & @LF & _ '
            ' & @LF & _ $sBodyContent & @LF & _ "
            " & @LF & _ "" & @LF & _ "" EndFunc ;==>_BuildHTMLDocument Func _GetWelcomeMarkdown() Return "# MDReader" & @LF & @LF & _ "A **100% offline** dark Markdown reader on AutoIt and the WebView2 UDF." & @LF & @LF & _ "## Keys" & @LF & _ "- `Ctrl+O` open, `F5` reload, `Ctrl++` and `Ctrl+-` zoom" & @LF & _ "- `Ctrl+F` is Chromium's own find bar, it needs no code at all" & @LF & _ "- Drop a `.md` file on the page: it is written to a temp file first, so" & @LF & _ " Edit in Notepad, Reload and the live watcher keep working on it" & @LF & @LF & _ "## Renders" & @LF & _ "1. ordered lists, and a second block starts where you left off" & @LF & _ " - nested items keep their level" & @LF & _ " - `code`, **bold**, *italic*, ~~strike~~" & @LF & _ "2. images sitting next to the document, inlined as data: URIs" & @LF & _ "3. files that are not UTF-8: cp1252, UTF-8 BOM and UTF-16 are sniffed" & @LF & @LF & _ "> A link whose address is `javascript:` shows its label and nothing else." & @LF EndFunc ;==>_GetWelcomeMarkdown ; ============================================================================== ; Windows 10/11 Dark Mode Helpers ; ============================================================================== Func _EnableDarkModeApp() DllCall("uxtheme.dll", "int", 135, "int", 2) ; SetPreferredAppMode(ForceDark) DllCall("uxtheme.dll", "none", 136) ; FlushMenuThemes EndFunc ;==>_EnableDarkModeApp Func _ApplyDarkWindow($hWnd) DllCall("uxtheme.dll", "bool", 133, "hwnd", $hWnd, "bool", True) ; AllowDarkModeForWindow Local $tDark = DllStructCreate("int") DllStructSetData($tDark, 1, 1) DllCall("dwmapi.dll", "long", "DwmSetWindowAttribute", "hwnd", $hWnd, "dword", 20, "ptr", DllStructGetPtr($tDark), "dword", 4) DllCall("dwmapi.dll", "long", "DwmSetWindowAttribute", "hwnd", $hWnd, "dword", 19, "ptr", DllStructGetPtr($tDark), "dword", 4) Local $tBorderCol = DllStructCreate("dword") DllStructSetData($tBorderCol, 1, 0x002A2727) ; DWMWA_BORDER_COLOR DllCall("dwmapi.dll", "long", "DwmSetWindowAttribute", "hwnd", $hWnd, "dword", 34, "ptr", DllStructGetPtr($tBorderCol), "dword", 4) Local $tCapCol = DllStructCreate("dword") DllStructSetData($tCapCol, 1, 0x001B1818) ; DWMWA_CAPTION_COLOR DllCall("dwmapi.dll", "long", "DwmSetWindowAttribute", "hwnd", $hWnd, "dword", 35, "ptr", DllStructGetPtr($tCapCol), "dword", 4) EndFunc ;==>_ApplyDarkWindow