Search code examples
c#consoletaskbarwindow-handles

How to update console window's taskbar progress in Windows Terminal


I have this code that works perfectly in cmd and PowerShell, but does nothing in Windows Terminal.

internal class TaskbarProgress : IDisposable
{
    private IntPtr consoleWindowHandle = IntPtr.Zero;

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetConsoleWindow();

    internal TaskbarProgress()
    {
        consoleWindowHandle = GetConsoleWindow();
        if (consoleWindowHandle != IntPtr.Zero)
        {
            TaskbarProgressCom.SetState(consoleWindowHandle, TaskbarProgressState.Normal);
        }
    }

    internal void SetProgress(ulong currentValue, ulong maximumValue)
    {
        if (consoleWindowHandle != IntPtr.Zero)
        {
            TaskbarProgressCom.SetValue(consoleWindowHandle, currentValue, maximumValue);
        }
    }

    public void Dispose()
    {
        if (consoleWindowHandle != IntPtr.Zero)
        {
            TaskbarProgressCom.SetState(consoleWindowHandle, TaskbarProgressState.NoProgress);
            consoleWindowHandle = IntPtr.Zero;
        }
    }
}

internal enum TaskbarProgressState
{
    NoProgress = 0,
    Indeterminate = 0x1,
    Normal = 0x2,
    Error = 0x4,
    Paused = 0x8
}

internal static class TaskbarProgressCom
{
    ... // Removed for StackOverflow complaint of too much code, but basically the same as https://www.nuget.org/packages/Microsoft-WindowsAPICodePack-Shell
}

I thought maybe the console window is childed, so grab the root window:

[DllImport("user32.dll", ExactSpelling = true)]
private static extern IntPtr GetAncestor(IntPtr hwnd, GetAncestorFlags flags);

// ...

IntPtr rootOwnerHandle = GetAncestor(consoleWindowHandle, GetAncestorFlags.RootOwner);
if (rootOwnerHandle != IntPtr.Zero)
{
    consoleWindowHandle = rootOwnerHandle;
}

But that didn't change anything. What am I missing?

Extra context: https://github.com/dotnet/BenchmarkDotNet/pull/2158


Solution

  • Thanks to folks on the Windows Terminal repo, I got the answer:

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool GetConsoleMode(IntPtr hConsoleHandle, out ConsoleModes lpMode);
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool SetConsoleMode(IntPtr hConsoleHandle, ConsoleModes dwMode);
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr GetStdHandle(int nStdHandle);
    
    const int STD_OUTPUT_HANDLE = -11;
    
    [Flags]
    private enum ConsoleModes : uint
    {
        ENABLE_PROCESSED_INPUT = 0x0001,
        ENABLE_LINE_INPUT = 0x0002,
        ENABLE_ECHO_INPUT = 0x0004,
        ENABLE_WINDOW_INPUT = 0x0008,
        ENABLE_MOUSE_INPUT = 0x0010,
        ENABLE_INSERT_MODE = 0x0020,
        ENABLE_QUICK_EDIT_MODE = 0x0040,
        ENABLE_EXTENDED_FLAGS = 0x0080,
        ENABLE_AUTO_POSITION = 0x0100,
    
        ENABLE_PROCESSED_OUTPUT = 0x0001,
        ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002,
        ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004,
        DISABLE_NEWLINE_AUTO_RETURN = 0x0008,
        ENABLE_LVB_GRID_WORLDWIDE = 0x0010
    }
    
    static void Main(string[] args)
    {
        IntPtr handle = GetStdHandle(STD_OUTPUT_HANDLE);
        ConsoleModes previousConsoleMode;
        GetConsoleMode(handle, out previousConsoleMode);
        SetConsoleMode(handle, ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING | ConsoleModes.ENABLE_PROCESSED_OUTPUT);
    
        for (uint i = 0; i < 100; ++i)
        {
            // Set progress value (0-100).
            Console.Write($"\x1b]9;4;1;{i}\x1b\\");
            Thread.Sleep(100);
        }
    
        // Set state to no progress.
        Console.Write($"\x1b]9;4;0;0\x1b\\");
        SetConsoleMode(handle, previousConsoleMode);
    }
    

    The supported sequences are:

    ESC ] 9 ; 4 ; st ; pr ST
    
    Set progress state on Windows taskbar and tab. When `st` is:
    0: remove progress.
    1: set progress value to pr (number, 0-100).
    2: set the taskbar to the "Error" state
    3: set the taskbar to the "Indeterminate" state
    4: set the taskbar to the "Warning" state
    

    https://github.com/microsoft/terminal/issues/6700