add win-gui-cli post, work on axes of fantasy a little more

This commit is contained in:
Joseph Montanaro 2024-06-17 06:24:38 -04:00
parent 918791baf6
commit b38f9f426a
2 changed files with 47 additions and 1 deletions

View File

@ -15,7 +15,7 @@ For a while now, I've had a private taxonomy of fantasy books, based on the dist
* *Low Fantasy* is set in a fantasy world that is separate from ours, but that can be reached (at least some of the time) by some means, such as a portal, magic object, ritual, etc.
* *Urban Fantasy* is fantasy in which the fantasy world is contained _within_ our world, but typically hidden from the prying eyes of mere mortals in some fashion.
I refer to this as a "personal" taxonomy because as far as I can tell, nobody else shares it. The terms are well-known, of course, and there's some overlap--what most people call "High Fantasy" certainly does tend to be set in fantasy worlds with no connection to our own - but "Urban Fantasy" in particular means a whole lot more to most people than just "fantasy set in our world." I imagine that most people would agree that urban fantasy should be, well, urban - not that it has to take place in _cities_ stricly speaking, but it should at least portray the fantastical elements in and around the real world, and pay some attention to the question of how it stays out of sight of Muggles.
I refer to this as a "personal" taxonomy because as far as I can tell, nobody else shares it. The terms are well-known, of course, and there's some overlap--what most people call "High Fantasy" certainly does tend to be set in fantasy worlds with no connection to our own - but "Urban Fantasy" in particular means a whole lot more to most people than just "fantasy set in our world." Most people seem to agree that urban fantasy should be, well, urban - not that it has to take place in _cities_ stricly speaking, but it should at least portray the fantastical elements in and around the real world, and pay some attention to the question of how it stays out of sight of Muggles.
Obviously, my personal classification system is much simpler and stricter than this. To be honest, it's not terribly useful on its own - while the relationship between a fantasy world and our own is certainly _an_ attribute worth considering for classification purposes, it's far from the only one.
@ -94,4 +94,20 @@ For the most part, though, the people who know about magic are the people who ha
Other notable examples of the genre include _The Dresden Files_, _Percy Jackson and the Olympians_, _The Laundry Files_, _Neverwhere_, _American Gods_, the _Artemis Fowl_ books, the _Mither Mages_ series, the _Iron Druid_ series, _Monster Hunter International_, and of course _Harry Potter_.
## Quasi-Historical Era
Another good dimension for differentiating fantasy stories is their "era," so to speak: What real-world historical period provides the basis for the level of technology, social structures, etc?<Sidenote>This is necessarily bound up with the question of _where_ the fantasical cultures get their inspiration, but unfortunately that side of it isn't nearly as easy to map onto a single numerical scale, so I'm going to mostly ignore it for now.</Sidenote>
I don't think it's worth trying to be too precise with the exact historical placement of most fantasy works because most are filled with anachronisms and/or cobbled together from patchworks of different specific times and places,<Sidenote>E.g. the _Belgariad_, which has one country filled with more-or-less French knights of the late Medieval era, and another country populated by not-quite Vikings, in the same world.</Sidenote> so they don't really belong to _one_ precise era.<Sidenote>Exceptions, of course, being things like the _Traitor Son Cycle_, which is _very_ clearly set in an analogue of the late 14th century.</Sidenote> But you can usually get a rough sense of the aesthetic that the author is going for, in broad strokes.
### Antiquity
This is pretty rare, but there are a few examples. The _Codex Alera_ is set in something approximating Imperial Rome, and though I haven't read them I've heard that David Gemmell has written some in an Ancient-Greece-ish setting. I've heard rumors of some ancient Egyptian ones as well.
### Dark Ages
If anything, even rarer than the above. The only ones of which I am aware are the _Belisarius_ series,<Sidenote>Wikipedia terms this "Alternate history science fiction" but it isn't very science-y at all, from what I remember, and it _is_ more than a little magic-y, so I'll call it fantasy.</Sidenote> and (although I haven't read this one) the _Sarantine Mosaic_, both set around 500-600 AD.<Sidenote>So post-fall-of-Rome, which I think counts as Dark Ages.</Sidenote>
### Middle Ages
The _vast_ majority of fantasy is set in something with approximately the aesthetics of Medieval Europe. Tolkien, obviously, was the trend-setter here,<Sidenote>Although interestingly, Tolkien's work seems to clock in rather earlier than your Standard Formulaic Fantasy Setting 1A. Tolkien's world is closest to the _early_ middle ages (circa 1050 or so), from what I understand - e.g. his characters consistently use chain mail rather than plate armor; presumably plate armor doesn't exist in Middle Earth. The obvious reason for this, of course, is that this was the era Tolkien himself had studied most intensively - mostly its literature, from what I understand, but you can't become an expert in the literature of a period without developing at least _some_ sense of what life was like then. So naturally, this was what he drew on when crafting his fantasy settings. Later fantasy, on the other hand, seems to draw most heavily on the _late_ middle ages, circa 1300-1500, with plate armor, chivalry, the occasional joust, etc.</Sidenote> but practically everyone

View File

@ -0,0 +1,30 @@
---
title: 'Mixing GUIs and CLIs on Windows: A Cautionary Tale'
date: 2024-06-17
---
<script>import Sidenote from '$lib/Sidenote.svelte';</script>
If you've used desktop Linux, then I'm sorry for you.<Sidenote>I also use desktop Linux. I'm sorry for me, too.</Sidenote> You will, however, most likely be familiar with the practice of using the same app from either the CLI or the GUI, depending on how you invoke it and what you want to do with it. In some cases, the CLI merely replicates the functionality of the GUI, but (due to being, you know, a CLI) is much easier to incorporate in scripts and such. In other cases ([Wezterm](https://wezfurlong.org/wezterm/) is a good example) the GUI app acts as a "server" with which the CLI communicates to cause it to do various things while it runs.
On Linux, this is as natural as breathing. There's nothing in particular that distinguishes a "GUI app" from a "CLI app", other than that the GUI app _happens_ to ultimately call whatever system APIs are involved in creating windows, drawing text, and so on. Moreover, even when running its GUI, a Linux app always has stdout, stderr, etc. In most day-to-day usage, these get stashed in some inscrutable location at the behest of Gnome or XFCE or whatever ends up being responsible for spawning and babysitting GUI apps most of the time, but you can always see them if you launch the app from a terminal instead. In fact, this is a common debugging step when an app is misbehaving: launch it from a terminal so you can see whether it's spitting out errors to console that might help diagnose the problem.
Since Windows also has both GUIs and CLIs, you might naively expect the same sort of thing to work there, but woe betide you if you try to do this. Windows thinks every app must be _either_ a GUI app or a CLI app, and never the twain shall meet, or at lest never the twain shall meet without quite a significant degree of jank.
Every Windows executable is flagged somehow<Sidenote>I don't know how precisely, probably a magic bit somewhere in the executable header or something like that.</Sidenote> as "GUI app" or "CLI app", and this results in different behavior on launch. CLI apps are allocated a [console](https://learn.microsoft.com/en-us/windows/console/definitions), on which concept I'm not _entirely_ clear but which seems somewhat similar to a [pty](https://man7.org/linux/man-pages/man7/pty.7.html) on Linux. GUI apps, on the other hand, are not expected to produce console output and so are not allocated a console at all, which means that if they try to e.g. write to stdout they just... don't. I'm not sure what exactly happens when they try: in my experience e.g. `println!()` in Rust just becomes a no-op, but it's possible that this is implemented on the Rust side because writing to stdout from a GUI app would crash your program otherwise, or something.
Aha, says the clever Windows developer, but I am a practitioner of the Deep Magicks, and I know of APIs such as [`AllocConsole`](https://learn.microsoft.com/en-us/windows/console/allocconsole) and [`FreeConsole`](https://learn.microsoft.com/en-us/windows/console/freeconsole) which allow an app to control the existence and attached-ness of its Windows consoles. But not so fast, my wizardly acquaintance. Yes, you can do this, but it's still _janky as hell_. There are two basic approaches: you can either a) flag the executable as a GUI app, then call `AllocConsole` and `AttachConsole` to get a console which can then be used for stdout/err/etc, or you can b) flag the executable as a CLI app, so it gets allocated a console by default, then call `FreeConsole` to get rid of it if you decide you don't want it.
If you do a), the problem is that the app doesn't have a console at its inception, so `AllocConsole` creates an entirely _new_ console, with no connection to the console from which you invoked the app. So it pops up in a new window, which is typically the default terminal emulator<Sidenote>On Windows 10 and earlier, this defaults to `conhost.exe`, which is the terminal emulator equivalent of a stone knife chipped into chape by bashing it against other stones.</Sidenote> rather than whatever you have set up, and - even worse - _it disappears as soon as your app exits_, because of course its lifecycle is tied to that of the app. So the _extremely standard_ CLI behavior of "execute, print some output, then exit" doesn't work, because there's no time to _read_ that output before the app exits and the window disappears.
Alternatively, you can call `AttachConsole` with the PID of the parent process, or you can just pass -1 instead of a real PID to say "use the console of the parent process". But this almost as terrible, because - again - the app _doesn't have a console when it launches_, so whatever shell you used to launch it will just assume that it doesn't need to wait for any output and blithely continue on its merry way. If you then attempt to write to stdout, you _will_ see the output, but it will be interleaved with your shell prompt, keyboard input, and so on, so again, not really usable.
Ok, so you do b) - flag your app as a CLI app, then call `FreeConsole` as soon as it launches to detach from the console that gets automatically assigned to it. Unfortunately this doesn't work either. When you launch a CLI app in a context that expects a GUI, such as the Start menu, it gets assigned a brand-new console window, again using whatever is the default terminal emulator. In my experience, it isn't consistently possible (from within the process at least) to call `FreeConsole` quickly enough to prevent this window from at least flashing briefly on the desktop. Livable? Sure, I guess, but it would be a sad world indeed if we never aimed higher than just _livable_.
Up until now, my solution has been to simply create two copies of my executable, one GUI and one CLI, put them in different directories, and add the directory of the CLI executable to my `PATH` so that it's the one that gets invoked when I run `mycommand` in a terminal. This works ok, despite being fairly inelegant, but just today I discovered a better way via [this rant](https://www.devever.net/~hl/win32con).<Sidenote>With whose sentiments I must agree in every particular.</Sidenote> Apparently you can specify the `CREATE_NO_WINDOW` flag when creating the process, which prevents it from creating a new window. Unfortunately, as that page notes, this requires you to control the invoation of the process, so the only way to make proper use of it is to create a "shim" executable that calls your main executable (which will be, in this case, marked as a CLI app) with the `CREATE_NO_WINDOW` flag. That post also points out that if you have a `.exe` file and a `.com` file alongside each other, Windows will prefer the `.com` file when the app is invoked via the CLI, so your shim can be a `.com` file and your main executable a `.exe`. I haven't tried this yet myself, but it sounds like it would work, and it's a general enough solution that may app frameworks such as [Tauri](https://tauri.app/)<Sidenote>Building a Tauri app is how I encountered this problem in the first place.</Sidenote> might eventually handle it for you.
Another solution that was suggested to me recently (I think this is the more "old school" way of handling this problem) is to create a new virtual desktop, then ensure that the spurious console window gets created there so that it's out of sight. I haven't tried this myself, so I'm not familiar with the details, but my guess is that like the above it would require you to control the invocation of the process, so there isn't really any advantage over the other method, and it's still hacky as hell.
Things like this really drive home to me how thoroughly Windows relegates the CLI to being a second-class citizen. In some ways it almost feels like some of the products of overly-optimistic 1960s-era futurism, like [designing a fighter jet without a machine gun](https://en.wikipedia.org/wiki/McDonnell_Douglas_F-4_Phantom_II?useskin=vector) because we have guided missiles now, and _obviously_ those are better, right? But no, in fact it turns out that sometimes a gun was _actually_ preferable to a guided missile, because surprise! Different tools have different strengths and weaknesses.
Of course, if Microsoft had been in charge of the F-4 it would have [taken them 20 years](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/) finally add the machine gun, and when they did it would haved fired at a 30-degree angle off from the heading of the jet, so I guess we can be thankful that we aren't engaging in air-to-air dogfights with our terminal emulators, or something.