Querying information from an Explorer window

Date:July 20, 2004 / year-entry #284
Tags:code
Orig Link:https://blogs.msdn.microsoft.com/oldnewthing/20040720-00/?p=38393
Comments:    37
Summary:Sometimes software development is inventing new stuff. But often, it's just putting together the stuff you already have. Today's puzzle is one of the latter type of problem. Given a window handle, you can you determine (1) whether it is an Explorer window, and if so (2) what folder it is viewing, and (3) what item is currently...

Sometimes software development is inventing new stuff. But often, it's just putting together the stuff you already have. Today's puzzle is one of the latter type of problem.

Given a window handle, you can you determine (1) whether it is an Explorer window, and if so (2) what folder it is viewing, and (3) what item is currently focused.

This is not an inherently difficult task. You just have to put together lots of small pieces.

Start with the ShellWindows object which represents all the open shell windows. You can enumerate through them all with the Item property. This is rather clumsy from C++ because the ShellWindows object was designed for use by a scripting language like JScript or Visual Basic.

 IShellWindows *psw;
 if (SUCCEEDED(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL,
                                IID_IShellWindows, (void**)&psw))) {
  VARIANT v;
  V_VT(&v) = VT_I4;
  IDispatch  *pdisp;
  BOOL fFound = FALSE;
  for (V_I4(&v) = 0; !fFound && psw->Item(v, &pdisp) == S_OK;
       V_I4(&v)++) {
    ...
    pdisp->Release();
  }
  psw->Release();
 }

From each item, we can ask it for its window handle and see if it's the one we want.

   IWebBrowserApp *pwba;
   if (SUCCEEDED(pdisp->QueryInterface(IID_IWebBrowserApp, (void**)&pwba))) {
     HWND hwndWBA;
     if (SUCCEEDED(pwba->get_HWND((LONG_PTR*)&hwndWBA)) &&
       hwndWBA == hwndFind) {
       fFound = TRUE;
       ...
     }
     pwba->Release();
   }

Okay, now that we have found the folder via its IWebBrowserApp, we need to get to the top shell browser. This is done by querying for the SID_STopLevelBrowser service and asking for the IShellBrowser interface.

       IServiceProvider *psp;
       if (SUCCEEDED(pwba->QueryInterface(IID_IServiceProvider, (void**)&psp))) {
         IShellBrowser *psb;
         if (SUCCEEDED(psp->QueryService(SID_STopLevelBrowser,
                              IID_IShellBrowser, (void**)&psb))) {
           ...
           psb->Release();
         }
         psp->Release();
       }

From the IShellBrowser, we can ask for the current shell view via the QueryActiveShellView method.

           IShellView *psv;
           if (SUCCEEDED(psb->QueryActiveShellView(&psv))) {
             ...
             psv->Release();
           }

Of course, what we really want is the IFolderView interface, which is the automation object that contains all the real goodies.

             IFolderView *pfv;
             if (SUCCEEDED(psv->QueryInterface(IID_IFolderView,
                                               (void**)&pfv))) {
               ...
               pfv->Release();
             }

Okay, now we're golden. What do you want to get from the view? How about the location of the IShellFolder being viewed. To do that, we need to use IPersistFolder2::GetCurFolder. The GetFolder method will give us access to the shell folder, from which we ask for IPersistFolder2. (Most of the time you want the IShellFolder interface, since that's where most of the cool stuff hangs out.)

               IPersistFolder2 *ppf2;
               if (SUCCEEDED(pfv->GetFolder(IID_IPersistFolder2,
                                            (void**)&ppf2))) {
                 LPITEMIDLIST pidlFolder;
                 if (SUCCEEDED(ppf2->GetCurFolder(&pidlFolder))) {
                   ...
                   CoTaskMemFree(pidlFolder);
                 }
                 ppf2->Release();
               }

Let's convert that pidl into a path, for display purposes.

                   if (!SHGetPathFromIDList(pidlFolder, g_szPath)) {
                     lstrcpyn(g_szPath, TEXT("<not a directory>"), MAX_PATH);
                   }
                   ...

What else can we do with what we've got? Oh right, let's see what the currently-focused object is.

                   int iFocus;
                   if (SUCCEEDED(pfv->GetFocusedItem(&iFocus))) {
                     ...
                   }

Let's display the name of the focused item. To do that we need the item's pidl and the IShellFolder. (See, I told you the IShellFolder is where the cool stuff is.) The item comes from the Item method (surprisingly enough).

                     LPITEMIDLIST pidlItem;
                     if (SUCCEEDED(pfv->Item(iFocus, &pidlItem))) {
                       ...
                       CoTaskMemFree(pidlItem);
                     }

(If we had wanted a list of selected items we could have used the Items method, passing SVGIO_SELECTION.)

After we get the item's pidl, we also need the IShellFolder:

                       IShellFolder *psf;
                       if (SUCCEEDED(ppf2->QueryInterface(IID_IShellFolder,
                                                          (void**)&psf))) {
                         ...
                         psf->Release();
                       }

Then we put the two together to get the item's display name, with the help of the GetDisplayNameOf method.

                         STRRET str;
                         if (SUCCEEDED(psf->GetDisplayNameOf(pidlItem,
                                                   SHGDN_INFOLDER,
                                                   &str))) {
                           ...
                         }

We can use the helper function StrRetToBuf to convert the kooky STRRET structure into a boring string buffer. (The history of the kooky STRRET structure will have to wait for another day.)

                           StrRetToBuf(&str, pidlItem, g_szItem, MAX_PATH);

Okay, let's put this all together. It looks rather ugly because I put everything into one huge function instead of breaking them out into subfunctions. In "real life" I would have broken things up into little helper functions to make things more manageable.

Start with the scratch program and add this new function:

#include <shlobj.h>
#include <exdisp.h>

TCHAR g_szPath[MAX_PATH];
TCHAR g_szItem[MAX_PATH];

void CALLBACK RecalcText(HWND hwnd, UINT, UINT_PTR, DWORD)
{
 HWND hwndFind = GetForegroundWindow();
 g_szPath[0] = TEXT('\0');
 g_szItem[0] = TEXT('\0');

 IShellWindows *psw;
 if (SUCCEEDED(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL,
                                IID_IShellWindows, (void**)&psw))) {
  VARIANT v;
  V_VT(&v) = VT_I4;
  IDispatch  *pdisp;
  BOOL fFound = FALSE;
  for (V_I4(&v) = 0; !fFound && psw->Item(v, &pdisp) == S_OK;
       V_I4(&v)++) {
   IWebBrowserApp *pwba;
   if (SUCCEEDED(pdisp->QueryInterface(IID_IWebBrowserApp, (void**)&pwba))) {
     HWND hwndWBA;
     if (SUCCEEDED(pwba->get_HWND((LONG_PTR*)&hwndWBA)) &&
       hwndWBA == hwndFind) {
       fFound = TRUE;
       IServiceProvider *psp;
       if (SUCCEEDED(pwba->QueryInterface(IID_IServiceProvider, (void**)&psp))) {
         IShellBrowser *psb;
         if (SUCCEEDED(psp->QueryService(SID_STopLevelBrowser,
                              IID_IShellBrowser, (void**)&psb))) {
           IShellView *psv;
           if (SUCCEEDED(psb->QueryActiveShellView(&psv))) {
             IFolderView *pfv;
             if (SUCCEEDED(psv->QueryInterface(IID_IFolderView,
                                               (void**)&pfv))) {
               IPersistFolder2 *ppf2;
               if (SUCCEEDED(pfv->GetFolder(IID_IPersistFolder2,
                                            (void**)&ppf2))) {
                 LPITEMIDLIST pidlFolder;
                 if (SUCCEEDED(ppf2->GetCurFolder(&pidlFolder))) {
                   if (!SHGetPathFromIDList(pidlFolder, g_szPath)) {
                     lstrcpyn(g_szPath, TEXT("<not a directory>"), MAX_PATH);
                   }
                   int iFocus;
                   if (SUCCEEDED(pfv->GetFocusedItem(&iFocus))) {
                     LPITEMIDLIST pidlItem;
                     if (SUCCEEDED(pfv->Item(iFocus, &pidlItem))) {
                       IShellFolder *psf;
                       if (SUCCEEDED(ppf2->QueryInterface(IID_IShellFolder,
                                                          (void**)&psf))) {
                         STRRET str;
                         if (SUCCEEDED(psf->GetDisplayNameOf(pidlItem,
                                                   SHGDN_INFOLDER,
                                                   &str))) {
                           StrRetToBuf(&str, pidlItem, g_szItem, MAX_PATH);
                         }
                         psf->Release();
                       }
                       CoTaskMemFree(pidlItem);
                     }
                   }
                   CoTaskMemFree(pidlFolder);
                 }
                 ppf2->Release();
               }
               pfv->Release();
             }
             psv->Release();
           }
           psb->Release();
         }
         psp->Release();
       }
     }
     pwba->Release();
   }
    pdisp->Release();
  }
  psw->Release();
 }
 InvalidateRect(hwnd, NULL, TRUE);
}

Now all we have to do is call this function periodically and print the results.

BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
    SetTimer(hwnd, 1, 1000, RecalcText);
    return TRUE;
}

void
PaintContent(HWND hwnd, PAINTSTRUCT *pps)
{
  TextOut(pps->hdc, 0, 0, g_szPath, lstrlen(g_szPath));
  TextOut(pps->hdc, 0, 20, g_szItem, lstrlen(g_szItem));
}

We're ready to roll. Run this program and set it to the side. Then launch an Explorer window and watch the program track the folder you're in and what item you have focused.

Okay, so I hope I made my point: Often, the pieces you need are already there; you just have to figure out how to put them together. Notice that each of the pieces is in itself not very big. You just had to recognize that they could be put together in an interesting way.

Exercise: Change this program so it takes the folder and switches it to details view.

[Raymond is currently on vacation; this message was pre-recorded.]


Comments (37)
  1. Dr. Jekyll says:

    If everything is so nicely scoped, why not use CComPtr<>s / CComQIPtr<>s ? This very same code would be so much more readable (no ugly QueryInterfaces, Releases, uninitialized variables)…

    Guess it’s a habit thing.

  2. Raymond Chen says:

    I try to avoid using any extra libraries in these articles. If you like those libraries, you can translate raw C++ into your library; but it’s harder for others to translate a library into raw C++. (For example, I could’ve used a library to auto-free the pidlFolder, but that would have confused anybody who wasn’t familiar with that library.)

  3. A. Reader says:

    Hideous. That screams "potential memory leaks!"

  4. Ben Cooke says:

    That code is hilarious. It’s stuff like that which really puts me off learning about COM. Fortunately, I’ve managed to basically ignore COM so far, and if all this .NET stuff becomes popular maybe I can just pretend it never happened. :)

    Pleaaase release me, let me gooooooo……

  5. Jack Mathews says:

    Well, you could easily flatten out that code with a CComPtr class, a CComMemoryHandle class, and a function called GuaranteeSuccess that throws an exception on a bad HRESULT and catch that in the function.

  6. Raymond Chen says:

    Any reader who wants to make an ATL version of this function is free to do so. It would probably be a lot prettier. But I write in pure C++ in order to avoid arguing over which template library is best.

  7. dude says:

    cant compile : error C2065: ‘StrRetToBuf’ : undeclared identifier

  8. Ben Hutchings says:

    Does Explorer lock windows that external clients are looking at (or indeed <em>all</em> of them while the windows are being enumerated) or is this code vulnerable to race conditions?

  9. Michael says:

    I try to avoid using any extra libraries in these articles. <

    I for one am glad you approach your problems this way. It shows me what is "really going on". Keep up the good work!!

  10. Cedric Beust says:

    I believe the only point you’ve made is that only VB or C# should ever be used to access COM :-)



    Cedric

  11. drebin says:

    I have a burning sensation in my eyes, is that normal??

  12. Never use goto statements? I LOVE goto statements :)

  13. Jack Mathews says:

    Zach: Ummm, except now you can’t use C++ construction/destruction semantics with yoru code…

  14. Nicole DesRosiers says:

    Having had to actually code up a program using a raw dispatch interface in C++, I really appreciate that you wrote this example without libraries. When I was trying to design my program I nearly tore my hair out trying to translate the things written on MSDN with the wrapper code into raw function calls that were actually useful to me.

  15. MadHungarian :) says:

    The thing that bugs me is the use of Hungarian notation. ‘psf’ doesn’t say much, ‘shellFolder’ does.

  16. Raymond Chen says:

    Zachary: Note however that some of your code goto’s over variable initialization – for example if the initial CoCreateInstance fails. This results in the RELEASE trying to use an uninitialized variable.

  17. Joe says:

    Zachary, you have to make sure that when you make macros you take care to write them correctly. For instance, this hypothetical code is wrong:

    if(–i == 0) // we have no more use for the object

    RELEASE(pObj);

    else

    COM_CALL(pObj->foo(), finish);

    First, because the RELEASE() macro has a semi-colon, you end up with two semi-colons and that’ll terminate the if statement before the else. If you remove the extra semi-colon, then the else clause gets attached to the if statement in the macro, and not the if(–i == 0).

    I think the usual way of handling this is to use:

    #define RELEASE(ptr)

    if (ptr) ptr->Release(); else

    So that the semi-colon at the end of RELEASE(foo); will close the else.

  18. Jack Mathews says:

    Or instead of a macro, you use something like a CComPtr, and to fix the nesting, you do like this:

    enum EBadResult

    {

    kBadResult

    };

    void Succeed( HRESULT hr )

    {

    if ( !SUCCEEDED(hr) )

    {

    throw kBadResult;

    }

    }

    … then in your code, you can simply do …

    try

    {

    Object obj;

    Succeed( incoming->DoThis( obj.GetPtrPtr() ) );

    Succeed( obj->DoThisNow() );

    }

    catch ( EBadResult )

    {

    // Do what you need to in order to fail

    }

    … and you will get automatic destruction of the intermediate objects that you didn’t use. You write a small amount more code, you get similar ease of use as goto, and much MUCH more robust error handling in the language.

  19. John Schroedl says:

    Great post. Keep ’em coming with more good stuff for the shell!

  20. Zachary Turner says:

    Even in pure C++, better to just stick a label at the bottom and have a goto statement if the COM call fails, rather than use deeper nesting if the call succeeds.

    To the poster who said it’s things like that make you not want to learn COM, it’s very simple and easy to read as long as you provide an appropriate framework for making it easy to read. For example, consider the following two macros:

    #define COM_CALL(pfn, lbl)

    if (FAILED(pfn)) goto lbl;

    #define RELEASE(ptr)

    if (ptr) ptr->Release();

    Now the RecalcText function is as follows:

    void CALLBACK RecalcText(HWND hwnd, UINT, UINT_PTR, DWORD)

    {

    HWND hwndFind = GetForegroundWindow();

    g_szPath[0] = TEXT(‘’);

    g_szItem[0] = TEXT(‘’);

    IShellWindows *psw;

    COM_CALL(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL, IID_IShellWindows, (void**)&psw), finish);

    VARIANT v;

    V_VT(&v) = VT_I4;

    IDispatch *pdisp;

    BOOL fFound = FALSE;

    for (V_I4(&v) = 0; !fFound && psw->Item(v, &pdisp) == S_OK; V_I4(&v)++)

    {

    }

    IWebBrowserApp *pwba = NULL;

    IServiceProvider *psp = NULL;

    IShellBrowser *psb = NULL;

    IShellView *psv = NULL;

    IFolderView *pfv = NULL;

    IPersistFolder2 *ppf2 = NULL;

    LPITEMIDLIST pidlFolder = NULL;

    LPITEMIDLIST pidlItem = NULL;

    IShellFolder *psf = NULL;

    STRRET str;

    COM_CALL(pdisp->QueryInterface(IID_IWebBrowserApp, (void**)&pwba), finish);

    if (hwndWBA != hwndFind)

    goto finish;

    COM_CALL(pwba->QueryInterface(IID_IServiceProvider, (void**)&psp), finish);

    COM_CALL(psp->QueryService(SID_STopLevelBrowser, IID_IShellBrowser, (void**)&psb), finish);

    COM_CALL(psb->QueryActiveShellView(&psv), finish);

    COM_CALL(psv->QueryInterface(IID_IFolderView, (void**)&pfv), finish);

    COM_CALL(pfv->GetFolder(IID_IPersistFolder2, (void**)&ppf2), finish);

    COM_CALL(ppf2->GetCurFolder(&pidlFolder), finish);

    if (!SHGetPathFromIDList(pidlFolder, g_szPath))

    lstrcpyn(g_szPath, TEXT("<not a directory>"), MAX_PATH);

    int iFocus;

    COM_CALL(pfv->GetFocusedItem(&iFocus), finish);

    COM_CALL(pfv->Item(iFocus, &pidlItem), finish);

    COM_CALL(ppf2->QueryInterface(IID_IShellFolder, (void**)&psf), finish);

    COM_CALL(psf->GetDisplayNameOf(pidlItem, SHGDN_INFOLDER, &str), finish);

    StrRetToBuf(&str, pidlItem, g_szItem, MAX_PATH);

    finish:

    RELEASE(psf);

    TASK_FREE(pidlItem);

    TASK_FREE(pidlFolder);

    RELEASE(ppf2);

    RELEASE(pfv);

    RELEASE(psv);

    RELEASE(psb);

    RELEASE(psp);

    RELEASE(pwba);

    RELEASE(pdisp);

    RELEASE(psw);

    InvalidateRect(hwnd, NULL, TRUE);

    }

    I probably introduced a few bugs here, but I just typed this in notepad and didn’t bother compiling. The point is obvious, just change the logic flow and COM is much easier to follow. Don’t listen to what they say, that you should "never" use goto statements.

  21. For writing anything but the simplest macros, I typically recommend enclosing in a do { … } while(0) block as follows:

    #define DoStuff()

    do {

    // statement 1;

    // statement 2;

    // …;

    } while (0)

    The if (…) else trick should work as well.

    I also explained my C/C++ error-handling style which uses gotos, along with its limitations, in:

    http://www.livejournal.com/users/sengelha/35640.html

    http://www.livejournal.com/users/sengelha/37087.html

    http://www.livejournal.com/users/sengelha/37182.html

  22. Tim Robinson says:

    I try to avoid using any extra libraries in these articles.

    Even the Microsoft C++ runtime library includes COM support classes. For most Windows developers, that’s hardly an ‘extra library’.

  23. Stewart Tootill says:

    I tend to use the exception concept and ATL com smart pointers. I know the example wasn’t written in ATL, but even if thats the only ATL functionality you use its worth it in my opinion.

    As for the macro, I typically use

    inline HRESULT TestComCall( HRESULT hr ) throw (_com_error)

    {

    if ( FAILED( hr ) )

    _com_issue_error( hr );

    return hr;

    }

    it returns hr so you can use it with the Next method on IEnum interfaces. You can replace the _com_error with your favourite exception. Also its a shame, but the microsoft compiler whinges about the exception specification.

  24. Tudor Tihan says:

    Hy,

    Is there any way to similarly get a handle of the text inputs in the Internet Explorer ?

    If it could be a write-only way (for setting texts inside) that would be great, because it would mean that I could write a program to fill my credentials on my visited sites without sharing them with password manager and such.

    So, is it possible?

    Could you give me some guidelines, please, as I am new to advanced COM programming.

    Thank you!

  25. Raymond Chen says:

    mshtml.h contains the interfaces you need. I leave filling in the details as an exercise. (Besides, this is an IE question and that’s not my area.)

  26. Steve, I’m curious about the use of do…while(0) in your macro. I’ve written scads of multiline code macros, and all I ever do is enclose them in braces. So your example would be:

    #define DoStuff()

    {

    // statement 1;

    // statement 2;

    // …;

    }

    Is there a situation where that would fail and do…while(0) would fix it?

  27. Michael Geary: The #define DoStuff() { … } style works in most cases but not all. See http://www.livejournal.com/users/sengelha/38124.html for details.

  28. Zachary Turner says:

    Well as I said, I typed it in notepad and didn’t compile anything ;-) The point was just to illustrate a general concept, which when applied would simplify readability. Thanks for pointing out the errors though. To respond to the individual comments (I’m sure everyone knows everything I’m about to say, but I might as well say it anyway for the sake of completeness):

    Raymond: Skipping over variable initialization is easily handled by moving all declarations and setting them equal to 0. The release macro checks whether or not the value of the pointer is 0, and if it is it does nothing. So you never release garbage that way. Obviously you know that already, but like I said just pointing it out for completeness’ sake.

    Jack: Are you referring to construction/destruction semantics with regards to COM smart pointers auto add-refing and releasing? You can use whatever you want, this was just an attempt to simplify readability in a way that didn’t use any smart pointer template library, which was Raymond’s initial goal as well. Since some readers said "it sucks, you can’t read it without smart pointers", I was just providing a way by which you could ;-)

    Joe: Good point, thanks :)

    Jack (2nd message): Raymond’s original goal was specifically NOT to use any library such as that, so that people didn’t have to argue over which template library was best.

    Michael: Yes, the do while macro allows you to put a semicolon at the end Consider your macro to be called DOMICHAEL, and steve’s to be called DOSTEVE. Now consider the following code:

    if (1)

    DOMIKE();

    else

    DOSTEVE();

    if (1)

    DOSTEVE();

    else

    DOMIKE();

    The first if/else block generates a compiler error, because a ; after DOMIKE() terminates the if block, as Joe pointed out earlier. The second if block does NOT generate a compiler error, however, as you are required to put a ; to terminate the do while.

    That’s the only difference that I see. This only applies for single line if blocks as well, if you already had enclosing braces for the if block there would be no difference.

  29. Michael Moore says:

    Seems to me you might as well stick with the simple braces method since a do { … } while (0) approach also results in a syntax error if you leave off the semicolon.

    i.e. this won’t compile either:

    if ( expression )

    do { a(); b(); } while (0)

    else

    c();

    so its really just a question of whether you think semicolon’s should be used to terminate macro usage.

    I’ll grant you the lack of a semicolon is a bit more obvious a syntax error for the macro than a mismatched else.

    -Michael

  30. I was once reminded of the constant controversy that surrounds the use of goto by the two following posts: Raymond Chen’s article on a COM trick to track the Explorer…

  31. Stephen and Zachary, many thanks for the explanation on the do…while(0) bit. All makes sense now, and I’ll start doing my own macros that way.

    Michael M., when I write a code macro like this, I like to do it in such a way that I can use the semicolon when I use the macro. The idea is that the code that calls the macro should look the same whether it’s a macro or a function.

    Taking a simpler example, I would write a macro like this:

    #define DoStuff() CallSomething()

    and not like this:

    #define DoStuff() CallSomething();

    because when I call DoStuff, I want to call it like this:

    DoStuff();

    and not like this:

    DoStuff()

    The latter style means that the caller has to know it’s a macro, which I wouldn’t want.

  32. Raymond Chen says:

    The semicolon-less style also messes up most code auto-formatters / syntax colorers.

  33. sir says:

    buncha jealous heytah’s ;-)

  34. Looks like this turned into a discussion about code readability and the use of gotos.

    Gotos are not good, but suggest the need for better error handling than the inadequate try-catch semantics. I suggest Structured Cascaded Exception Handling solves this problem and should be adopted in the next generation of languages.

  35. Steve Donie says:

    You forgot to mention the Accessibilty Interfaces! Go straight to the focused item, get notified when it changes rather than polling…

    Run the magnifier (Start->Programs->Accessories->Accessibility->Magnifier) or Narrator (same place) to see what those can do. Then see http://microsoft.com/enable/ for a ling to the developer docs.

  36. The ShellWindows object was designed for scripting.

Comments are closed.


*DISCLAIMER: I DO NOT OWN THIS CONTENT. If you are the owner and would like it removed, please contact me. The content herein is an archived reproduction of entries from Raymond Chen's "Old New Thing" Blog (most recent link is here). It may have slight formatting modifications for consistency and to improve readability.

WHY DID I DUPLICATE THIS CONTENT HERE? Let me first say this site has never had anything to sell and has never shown ads of any kind. I have nothing monetarily to gain by duplicating content here. Because I had made my own local copy of this content throughout the years, for ease of using tools like grep, I decided to put it online after I discovered some of the original content previously and publicly available, had disappeared approximately early to mid 2019. At the same time, I present the content in an easily accessible theme-agnostic way.

The information provided by Raymond's blog is, for all practical purposes, more authoritative on Windows Development than Microsoft's own MSDN documentation and should be considered supplemental reading to that documentation. The wealth of missing details provided by this blog that Microsoft could not or did not document about Windows over the years is vital enough, many would agree an online "backup" of these details is a necessary endeavor. Specifics include:

<-- Back to Old New Thing Archive Index