A Quickie on New Outlook Search

As I wrote in my first writeup on NOM (New Outlook Mac), I talked about the new search engine, and how it built KQL queries that were compatible with Outlook’s Web version. I just tested it, and yes, it is. Even cooler, if you modify a search in one and paste it into the other, it just works.

For example, I built this search string in NOM:

from:apple.com AND to:jwelch@bynkii.com AND subject:(AppleSeed) AND received>=2018-09-08

And got a listing of four/five emails from AppleSeed relating to various new builds. It pulled in results from multiple folders on that account, including the deleted items folder, which the old search didn’t do, I had to go into that folder to reliably search it.

I then opened my account in OWA, and pasted that string into the search bar there. Booyah! So then, I changed one of the terms from (AppleSeed) to (tvOS) and pasted that back into NOM. Booyah, now I only get emails with tvOS in them.

So that’s pretty cool, but what occurs to me is that this opens up some support avenues for people. If you’re really good with KQL and you have a user trying to do a really specific search and they’re having a hard time with it, you could build that search for them, send them the string, then they can just paste that into the search bar and voila! They find the thing they need.

Now, extending this, were say, the Outlook team to update the scripting dictionary a bit to allow for pasting from the clipboard, then you could provide this to the user as a script and they wouldn’t even have to manually copy and paste. They run the script and voila! They find the thing.

But wait, there’s more. So again, what if the Outlook team updated the scripting dictionary so that you could search more than just contacts with the new KQL search without pasting. And what if they allowed you, via the updated scripting dictionary to save that search as a saved search in the appropriate section (mail/calendar/contacts). That would be kind of handy if you’re having to move someone to a new machine, or you have some standard searches that people in a given group do a lot. Conversely, when they moved to a new machine, you could transfer any existing saved searches via script pretty easily.

You’d be able to give someone a new machine with all the saved searches they built, along with any searches you normally install. That would be kind of nice to be able to do for your users. And you could…if the Outlook Team were to update the scripting dictionary. (This is what I think about on the bus from the show location back to my hotel. Because I’m not quite right.)

Even better, you could use this to help build a maintenance/assistance script that would help someone look for “missing” emails/calendars/etc., create your own search scripts that could then take other actions based on the results of a search, search for specific attachment files and have other applications take actions on those files…if the Outlook Team were to update the scripting dictionary that is.

I feel there’s a theme here, but I can’t quite put my finger on it…

The Outlook is good

Outlook:Mac’s biggest update…well, ever

At Microsoft Ignite 2019, the Office team released an early-ish beta of the new Outlook client. If you’re on the Insider Fast track, you should be able to get it by the time this is posted.

As some background, I’ve been a part of working with and testing Outlook on the Mac since it was called “Entourage” and really, since Entourage was called “Alpaca” because it didn’t yet have a name. And yes, I know, Entourage and Outlook are in fact different products. Relax, it’s all good. This is just establishing timeline of how long I’ve been working with and help test this product.

I don’t think it’s hyperbole to say this is the biggest update to Outlook on the Mac since 2011. Possibly since the short-lived Outlook 98.

At the heart of these changes is the “new” sync engine. I say “new” because it’s not really new, it’s the same sync engine used in other Microsoft products, such as Outlook on iOS and the mail and calendar clients in Windows 10. Yes, that’s right, EWS is dead. Completely dead. It is an ex-protocol.

According to the team, this is really important, because it allows them to radically increase the “improvement velocity” of Outlook. This was a problem when Outlook was using EWS, as that protocol was considered “done” and wasn’t going to get new features anytime soon.

In terms of immediate benefit, I’ve seen two: first, prior to New Outlook Mac (NOM…DON’T YOU JUDGE ME), if I replied to an email in Outlook, that message would show that I’d replied to it. If however I replied say, from iOS’s Mail app, then Outlook wouldn’t show that. Not a huge deal in and of itself, but that kind of linkage in the UI is really convenient. With NOM, even when you reply from iOS Mail, Outlook will reflect that.

I was straight. up. giggling over that. Because it removed such a silly pain point.

In addition, Outlook’s resource usage has dropped off a cliff. I was used to seeing Outlook, regularly, on my 2019 MacBook Air, eating over 100% of available CPU in Activity Monitor. With NOM, I have to search for Outlook in Activity Monitor. At most, I’m seeing around 40% or so. Disk I/O looks to be similarly improved. That’s a big deal for people who care about battery life. Like, you know, everyone on a laptop.

In terms of syncing, and keep in mind, this is a very early release. For example, on-prem Exchange and iCloud accounts still use the old sync engine. I plan to test it once I’m back home and on stable Wi-Fi so I can give y’all some real-world numbers.

UI Changes

The UI has had a fairly extensive change. First, the Ribbon is gone. Like gone. As with EWS, it is an ex-UI element. I personally never had a real problem with the Ribbon, I honestly kind of like it, but I’m a very edge case users. (Long ago, I had some weird stuff using rules kicking off scripts as mailing list folder reapers. I am not an average user.) The new UI is very clean and nicely customizable:

New Toolbar. No More Ribbon in NOM

Obviously, NOM supports Dark Mode. The new toolbar is also fairly customizable:

Toolbar Customization Options

The look is really clean. One thing I’d like to see change in upcoming versions is an ability to increase the font size for toolbar labels. Some of us are not gifted of perfect vision.

Search changes

While Outlook’s search has been solid, there were a lot of cases where it would miss things. As well, being based on Spotlight, if you added a new account, there was a delay before you could use the search effectively while indexing was happening. With NOM, Outlook is now using the MS Online search engine instead of Spotlight. Along with what they say is better overall search performance, there’s a few neat new features this brings:

First, the search is supposedly faster and better. I’ve not had a chance to test this out, not extensively, but it doesn’t feel any slower. It’s also fully usable much faster than the Spotlight-based search was. The search bar itself is now much larger and central in the window. Secondly, they’ve also created a nice in-between UI for when you need a bit more than just typing in a text field, but you don’t need the full-on advanced search.

New search bar, still supports “classic” email search syntax
New search dropdown

The new dropdown is accessed by the funnel icon in the search bar. But I want you to notice the search syntax in the search bar over the dropdown. I’ve pasted it in below:

from:events@floridatheatre.com AND From: "Florida Theatre" AND subject:(yourself) AND received>=2019-10-08

That syntax is the same syntax you’d use in Outlook on the Web. That’s kind of handy. It’s based on MS’s KQL language. This is a fairly rich query language that allows for far richer searching than the traditional searching one did pre-NOM. The way the drop-down builds the query is kind of neat and once the full advanced search UI is attached, (Neither it nor saved searches are currently functional in this version), there’s a potential to do some neat work in building custom searches for people.

Another UI change is related to conversation. Now, when viewing by conversation, it pulls in emails from all folders, not just the current one, a convenience for those with complex folder structures (like me, which is ironic, as I never use conversation view.) There is also an “Ignore Conversation” option, but be aware, “Ignore” in this context moves the conversation into Deleted Items. This is kind of suboptimal, and could be problematic, and I would like to see this behavior change moving forward. In the current release, you can’t not use conversation view, something that again, is a beta issue.

(Note: This really is a beta, not just a “sort of not done” drop. There’s a lot that needs to be done, so NOW is the time to get your feedback in, things are still fluid.)

The Return of My Day

The fogies among the Outlook:Mac users will remember My Day, a related, but separate application that showed you, well, your day. It was a floating window that showed you what was going on in your calendar for that day. I really liked it and have missed that feature since it went away with Outlook 2016. Is it a real pain to alternate between cmd-1 and cmd-2 to switch between mail and calendar views? No, not really, but it’s always been one of those “why is this necessary” things. Like being nibbled to death by baby ducks.

With NOM, My Day is back. It’s not a separate window, it lives on the far right of your mail window and can show you your agenda or your day:

Agenda view (L) and Day View (R)

This is one of those things that isn’t really necessary, but it saves me a lot of time over the course of the day a few seconds at a time, and it means I don’t have to switch out of my current context just to see what’s happening next in my calendars. My Day can show you as many or few of your calendars as you like.

Talking to some of the Outlook folks, they’re looking at, in a future update, making My Day accessible via the Menu Bar, so you could see your appointments and schedule without having to open Outlook. This would be nice, especially when you’re working on something that isn’t email and you don’t want to get drawn into email just because you need to check your schedule.

Another new feature, not yet wired in, is the ability to reply to a message without having to pop a new message window. Calendar invites have a new UI too, giving you the information you need right there in the invite window:

New Event UI

At some point in the near future, this will integrate with Teams, so you can use that as an option for a meeting in addition to or instead of physical rooms. After the session, a few of us asked for more intelligent Out Of Office statuses, like “Working Somewhere Else”, which would allow someone who was working at a different location to, when they get the meeting invite, not even see a physical location for the “location” but rather the “Join Teams Meeting” button. Creating the event would also use this, so that the event creator would know that some of the meeting attendees would not be able to be there in person, and would be using Teams. A bit nicer than the blunt weapon that “out of office” currently is. (Again, if this is something you’d like to see, get your feedback in now.)

Calendar UI changes

Along with the mail UI, the calendar UI has gotten a lot of love. The default toolbar has been greatly simplified:

New Calendar toolbar

Note that the Temperature gets front and center, with the Day/Week/Month view picker in a dropdown to the right. The widget next to that allows you to view My Day in the calendar as well. I’m not sure how amazing that is, but sure, why not. Actually, I accidentally figured out why this is cool. You can have a different set of calendars displayed in My Day than you have displayed in the main calendar window. That’s cool.

Event creation has been greatly simplified in NOM. For simple events, you just drag and drop in the calendar window to select your meeting times on a given day, ala the macOS Calendar app, which then gives you a popup that allows you to quickly set up a simple meeting:

Quick Meeting Popup

A change that I really appreciate is the much, much easier methods NOM has for changing calendars for a new event. It’s literally just a pulldown in the top of the new meeting popup:

SO MUCH NICER

It’s pretty simple and works well for simple needs. But if you need more, the “real” event creation window has also gotten a revamp:

Full Event Creation Window

The easier calendar selection is there as in the popup. Also note that the repeating settings have been moved to a more central location, rather than up at the top where it was a bit easy to miss. Teams meeting selection is right there as well. One thing of note is the calendar column on the left. Note that the current schedule shows as green. If that time conflicted with another event, that would be red, (and I did suggest, given the prevalence of color blindness in the population, that Outlook not solely rely on color for this functionality.) You can change the meeting time and duration in that window to help you find a more compatible time if needed. The traditional scheduling assistant is still available for when you have a lot of attendees with complicated schedules.

One thing I want to point out is the little sun icon in the upper right part of the main window next to the calendar column. That’s in the email window as well, and it allows you to easily toggle the text area of the meeting (or message) from a dark background to a light background and back. A nice touch.

Again, this is a really early release beta. There’s a lot of work to still be done. For example, the Contacts pane doesn’t work at all. You click on it, you get a sheet telling you you have to switch back to old Outlook for that. It is very much not done.

But I’m still really impressed with where NOM is going. The new sync engine will enable a lot of things that just weren’t going to be possible before, like voting. Better syncing with iCloud was brought up and they are aware of the need, and from what the Outlook folks were saying, this should be significantly easier to implement. (Given the Google account improvement updates, I think any political issues went away some time ago.)

There’s some other missing features, like “resend”, but that should be coming back. “View Source” is also missing with accounts using the new sync engine and I pointed out that while not used by a lot of people, for user support/mail support people, being able to see the raw headers of an email is a critical need. Again, if this is important to you, let the team know.

One interesting statement during the session was that matching the Windows client feature for feature is not a goal in and of itself. (Obviously, there’s no ribbon any more.) Before everyone gets all weird, there are windows features, (Like VBA scripting) that not only don’t make a lot of sense on macOS, but are actually dangerous, (like VBA scripting.) “It’s in Windows” is not a great reason to implement a feature. “This is a thing I need and my users need because <reasons>” is a better way to ask for a feature.

I did remind them that Outlook’s scripting implementation has been stagnant since 2011, and it shows and they should update that. Additional feedback on that would be a help in getting them to up the priority for that.

To use NOM now, you currently need to a) be on macOS 10.14 or later and b) be on the Office Insider Fast Track. The Insider Fast requirement will eventually go away, the 10.14 and later, I don’t see that changing.

NOM is not done yet, there’s still a lot of oddities because of its unfinished state, but it’s like the pause between the time a pilot shoves the throttles into afterburner, and the plane starts hauling ass. For the first time in a while, there’s a clear path for some real improvements in Outlook, and I am so here for it.

From AppleScriptObjectiveC to Swift pt. 2

Today kids, we get some REST…

Okay, the joke was right there, and no, there is no joke “too bad” for me to take. So in the previous post, we looked at initial defaults setup, both in ASOC and Swift. We didn’t do anything with them, but we have the initial setup done. There’s a few things we have to deal with outside of that before we actually get into defaults or UI setup.

Also, for everyone who says “just rewrite it in <different language I like better>, note that this series will be rather long before we get to what the ASOC version has today. There’s going to be a lot of work done to get to where we are right now. No new features. Just a lot of work to get to where we are today.

Nagios Manager interacts with Nagios servers via a REST API. So I send URLs with data and get data back. Sometimes I’m reading existing information, (GET), creating or adding information (POST) or removing information (DELETE). But everything revolves around that.

Within Swift, there’s two major components I’m using, at least initially: URLSession and JSONSerialization. The former handles the communication between the app and server(s), the latter handles parsing it out into a useful format, usually a dictionary, or NSDictionary in ObjC-land. While I do use JSONSerialization in ASOC, for the URLSession components, I “cheat” as it were, and use a handy trick from AppleScript: the “do shell script” command, which lets me just execute curl commands from the shell.

I could use URLSession within ASOC, but I never did, “do shell script” is simpler, although probably not easier, and it lets AppleScript handle some things for me automagically. So as before, I’ll split this post up into the ASOC code and the Swift code, with explanations of each. This post will be focused on the GET part of the communications, we’ll take on POST and DELETE later on.

Also, while one downside of Swift is that it does’t have a tool like Script Debugger, it does have playgrounds, which gives me some of the tools Script Debugger has. (There’s no proper debugging in playgrounds, and that is an actual shame, it would make them even more cool than they are.) Playgrounds are awesome for testing out code before putting it in the app.

Nagios REST URLs

Before we get into the code, an overview of how URLs are used within the Nagios REST API. Regardless of what a given URL is doing, they’re all broken down about the same way. There’s a “root” URL, a path that indicates the main section of what the URL is for, an API Key, and then the specific command properties. GET/POST/DELETE are used to indicate the primary action. In this example, I’m using a ‘system info’ URL, as it returns a small amount of data that’s easily used to ensure I’m getting the info I need in the correct form. By and large, this is a very large string that I end up coercing/converting into a Dictionary (Swift) or a record/NSDictionary (ASOC) so I can make better use of key-value pairs.

Here’s a sample of the URL, (sanitized, obvs):

http://<ipaddress or DNS name of server>/nagiosxi/api/v1/system/info?apikey=<very long, complex API key>&pretty=1″

That’s pretty simple, but it has the basics. In terms of application logic, there’s a handful of parts. The first is the IP address or DNS name of the server. I trust I don’t have to explain that one. The second is the /nagios/api/v1/ part. that will go after the IP address or DNS name of the server for every command, and we’ll use that as a constant in Swift, once we get going. The system/info? bit identifies what part of the API the URL is talking to. In this case, basic system info. For user functions, we’d use “system/user”, and for host functions, “objects/host”. The API key is the authentication mechanism for a given server, and why I may add one new feature into this, if it’s not that hard, namely encrypting that data. Maybe. (Security in monitoring software is a mess on a good day. But if I can manage to do the right thing, I try to. I’m not a very good programmer.)

Everything after the API key is the actual command parameters. In this case, there are none, one reason it makes for a useful test case. Finally, the “pretty=1” bit makes the return look like this:

{
“product”: “nagiosxi”,
“version”: “5.6.5”,
“version_major”: “5”,
“version_minor”: “6.5”,
“build_id”: “1563463555”
“release”: 5605
}

Instead of a single undifferentiated string. Much easier to read and use, especially for setting up key-value pairs.

ASOC communications

Here’s the basic code for sending a GET request and putting the results into a record (dictionary) in ASOC:

property theSMTableServerURL : "" --bound to server url column in table
property theSMTableServerAPIKey : "" --bound to server API Key column in table
====================================================================
set theSMSURL to theSelectedServer's theSMTableServerURL as text
set theSMSURL to current application's NSString's stringWithString:theSMSURL
set theSMInfoURL to theSMSURL's stringByAppendingString: "system/info?apikey="
set theSMServerInfoCommand to "/usr/bin/curl -XGET \"" & theSMInfoURL & theSMSelectedAPIKey & "&pretty=1\"" 
set theSMServerInfoJSONDict to my getJSONData:(theSMServerInfoCommand)

====================================================================
on getJSONData:theCurlCommand	
	set theReturnedJSON to do shell script theCurlCommand
	set theReturnedJSON to current application's NSString's stringWithString:theReturnedJSON
	set theReturnedJSONData to theReturnedJSON's dataUsingEncoding:(current application's NSUTF8StringEncoding)
	set {theReturnedJSONDict, theError} to current application's NSJSONSerialization's JSONObjectWithData:theReturnedJSONData options:0 |error|:(reference)
	return theReturnedJSONDict
end getJSONData:

There’s really not a lot going here. The three sections offset by the “===” line are both parts of using curl in ASOC and in the app.

The first part is setting up the two properties we use. The server info, including URL and API key are in the server manager table in the UI. (Really, it’s in an array controller, but there’s no real code to demo that bit. At least not yet.)

theSMTableServerURL property is the IP address or DNS name of the server with “/nagiosxi/api/v1/” appended to it. All Nagios REST commands use that, so there’s no point in not storing it that way. Saves time.

theSMTableServerAPIKey property is the API key used for that server, also in the server manager table.

set theSMSURL to theSelectedServer's theSMTableServerURL as text

Is me doing two things. First, I’m avoiding manipulating the property directly. Secondly, I’m making sure I know theSMSURL is text. (AppleScript’s version, not Cocoa’s.) I found that ASOC has an annoying tendency to sometimes make this particular value text and sometimes it’s an NSSTring. The behaviors of both are rather different within ASOC, so I remove the uncertainty.

set theSMSURL to current application's NSString's stringWithString:theSMSURL

Is me converting AppleScript text to a proper NSString. We’ll need that, and again, this way I know what SMSURL is in terms of type.

set theSMInfoURL to theSMSURL's stringByAppendingString: "system/info?apikey="

Since NSString isn’t mutable, and NSMutableString gives me the irrits and this app is not time-sensitive in terms of execution, meh, create another NSString, but with the bits that make it a “I want basic info on the Nagios Server” command, along with the APIKey lead-in.

set theSMServerInfoCommand to "/usr/bin/curl -XGET \"" & theSMInfoURL & theSMSelectedAPIKey & "&pretty=1\""

Here’s where we build the full curl command. Since the command itself as used requires quotation marks in the command, we add those in by escaping them. the “&” is AppleScript’s string concatenation symbol. So when this is all done, theSMSServerInfoCommand is fully set up to send a request for basic server info to the Nagios server.

set theSMServerInfoJSONDict to my getJSONData:(theSMServerInfoCommand)

Here’s where we call the getJSONData function, or in AppleScript-ese, the handler. We’re passing it theSMServerInfoCommand string, and getting back theSMServerInfoJSONDict, which will be an NSDictionary, or AppleScript Record.

Now for the function. There’s not a lot of code, but there is some stuff happening. ASOC, as a rule, is slaved to Objective-C, so the calling conventions vaguely resemble ObjC, but without all the brackets and @-signs. I think it’s a bit more readable.

on getJSONData:theCurlCommand

The initial function definition. Note that we don’t have to specify the type of data theCurlCommand is. It’s just there, we either use it correctly or we do not. Handy in some ways, less-handy in others. Definitely lets you be lazy, so I like it. Life is too short for a high-level language not to do work for you.

set theReturnedJSON to do shell script theCurlCommand

This is the “do shell script” command, which lets us run curl in the shell environment. It uses sh, not bash or zsh. That can trip you up. It also doesn’t use your .profile or .zshrc files, so you really want to fully path everything. Really. Really.

The result of the command, assuming no error is set into theReturnedJSON. It may not actually be JSON, strictly speaking, but I have my theOwn theNeuroses with variable naming, and I’m the only one having to read this, so meh. What you get back looks like this:

{
“product”: “nagiosxi”,
“version”: “5.6.5”,
“version_major”: “5”,
“version_minor”: “6.5”,
“build_id”: “1563463555”
“release”: 5605
}

(just as a reminder)

set theReturnedJSON to current application's NSString's stringWithString:theReturnedJSON

Since do shell script is an AppleScript command, it returns AppleScript text. We need it to be an NSString, so we convert it to one. Since AppleScript is somewhat unconcerned about many types of coercion, we don’t have to use a different variable. theReturnedJSON used to be AppleScript text, now it is an NSString.

set theReturnedJSONData to theReturnedJSON's dataUsingEncoding:(current application's NSUTF8StringEncoding)

since NSJSONSerialization wants NSData and not NSString, we have to convert it to NSData here via NSString’s dataUsingEncoding method and NSUTF8StringEncoding as the specific encoding process. We do use a new variable here, so I know this is the NSData version of theReturnedJSON.

set {theReturnedJSONDict, theError} to current application's NSJSONSerialization's JSONObjectWithData:theReturnedJSONData options:0 |error|:(reference)

This returns an NSData record of arrays and shoves it in theReturnedJSONDict. Technically it’s an NSJSON object, but really, it functions more like an NSDictionary. Each line is a key-value pair, and this also allows it to work like an AppleScript Record, which is really handy for ASOC.

return theReturnedJSONDict

return theReturnedJSONDict to the calling function.

end getJSONData:

End the function/handler. the “:” functions as a “()” would in other languages. This is inconsistent mind you, sometimes, you’ll see “foo:()” or even “foo()“. The ASOC bridge is sometimes a tad odd.

Swift Communication

So some caveats here. The Swift code is very much playground code. It will look different in the final version, and once I get to where I feel like setting up the github repo for it, you’ll be able to see that. Again: I am not a full-time programmer, nor am I a Swift genius. I am very much a pickup truck programmer. It ain’t fancy, probably slower than it could be, but I can read it and understand it. It’s similar to how I play D&D: I understand the tricks I use as a monk very well. I don’t use a lot, but the ones I use, I really understand. Basically, I’m not clever. At all.

Also, many, many thanks to https://learnappmaking.com/urlsession-swift-networking-how-to/ for that page. It made it so much easier to figure out WTF was going on.

import Cocoa

//many thanks to https://learnappmaking.com/urlsession-swift-networking-how-to/ for this info.

//build the components to get data from nagios
//you can't use URLComponents with this, the encoding causes problems.

//we may be using url = url?.appendingPathComponent("users") kind of thing in the future

//this returns a small number of things from the nagios server
//we'll probably want this to be a var when we build it, made up of different items.

let theURL = URL(string: "http://(ipaddress or DNS name of the server)/nagiosxi/api/v1/system/info?apikey=(really long API key)&pretty=1")!

//create a URL Session object
let theSession = URLSession.shared

//theTask creates a data task. The parts are as follows:
//data - the actual JSON data we get from the server
//response - various response codes, mime types, etc
//error - any errors thrown
let theTask = theSession.dataTask(with: theURL) {data, response, error in
	
	//if we get an error or no data, bad. Probably won't use this in proudction
	if error != nil || data == nil {
		print("Client error!")
		return
	}
	//check for valid response code. Again probably won't use this
	guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
		print("Server error!")
		return
	}
	
	//check for the right mime type. Since we know how nagios returns things, not really needed
	guard let mime = response.mimeType, mime == "application/json" else {
		print("Wrong MIME type!")
		return
	}
	
	//this parses the returned data and turns it into  a proper JSON object
	do {
		//store contents of theURL as a Data variable
		let theData = try Data(contentsOf: theURL)
		
		//get the results as a dictionary, not a string
		let theJSON = try JSONSerialization.jsonObject(with: theData, options: []) as! [String:Any]
		
		
		//note that release version comes as an int from the rest api, not a string:
		/*
		{
			"product": "nagiosxi",
			"version": "5.6.5",
			"version_major": "5",
			"version_minor": "6.5",
			"build_id": "1563463555",
			"release": 5605
		}
		*/
		
		//set up the variables to extract the data from the dictionary
		var theProduct: Any
		var theBuildID: String
		var theVersion: String
		var theRelease: Any
		var theMajorVersion: String
		var theMinorVersion: String
		
		//pull our vars from the dictionary
		//since these could be nil, we force unwrap at theJSON and use as! to force downcast AnyObject to string
		//or Int as needed
		
		//it is probably a good idea to make sure your variables are what you think they are
		//this can avoid problems later. But this does show you have options if you're not sure
		//what the return will actually be
		theProduct = theJSON["product"]! //as! String
		theBuildID = theJSON["build_id"] as! String
		theVersion = theJSON["version"] as! String
		//even though we created the var as an Any, we force it to be an Int here. 
		theRelease = theJSON["release"] as! Int
		theMajorVersion = theJSON["version_major"] as! String
		theMinorVersion = theJSON["version_minor"] as! String
		
		print(theProduct)
		print(theBuildID)
		print(theVersion)
		print(theRelease)
		print(theMajorVersion)
		print(theMinorVersion)
		
		//print(theJSON)

}

theTask.resume()

There’s a lot going on here. Well, not really, but it took me a while to get it. (That was reason why I didn’t use NSURLSession in the ASOC version. It was weird to learn. Well, also, I didn’t have to.)

Some of it, like the setup for the print statements is test code, so I know what’s going on with it. That most likely won’t be that way in the final version, but it is useful here.

let theURL = URL(string: "http://(ipaddress or DNS name of the server)/nagiosxi/api/v1/system/info?apikey=(really long API key)&pretty=1")!

Since this is a constant, we use “let”. This will eventually be built the way it is in the ASOC version. But for now, we just hardcode the whole thing.

let theSession = URLSession.shared

Create a simple URLSession object. Should I need to get more complicated, I will, but not until.

let theTask = theSession.dataTask(with: theURL) {data, response, error in

Create a data task that when run, gives us three things:

1) data, the actual returned JSON data
2) response, the HTTP response info wif error != nil || data == nil {ith things like return codes, etc.
3) error, I feel this is self-explanatory. At best, we’ll only care if it’s not nil.

Maybe not even then. Nagios Manager isn’t the kind of thing you just run casually. I can make more than a few assumptions.

if error != nil || data == nil {
print("Client error!")
return
}


If we get any kind of error or no data, print an error message and return.

(can I just say wordpress’s block editor handling of returns is annoying as hell. No you goddamned schmuck, just because there’s a return when I paste something in, that does NOT mean I want it in a separate block. Ye gods.)

guard let response = response as? HTTPURLResponse, (200…
299).contains(response.statusCode) else {
print("Server error!")
return
}


if we don’t get a “correct” response code, do error things

guard let mime = response.mimeType, mime == "application/json" else {
print("Wrong MIME type!")
return
}


If the MIME type is wrong, do error things.

do {
//store contents of theURL as a Data variable
let theData = try Data(contentsOf: theURL
let theJSON = try JSONSerialization.jsonObject(with: theData, options: []) as! [String:Any]


Here, I’m setting up “Data” as a Data object version of theURL. Since the “contents” of the URL is the returned data, which looks like a big string to swift, we want it to be a dictionary, with the keys being String and the values being Any, as we get multiple return types. This is kind of a pain, since we don’t get ASOC’s “I’ll handle that for you” features, but it’s not awful. (I may be wrong about what this code does. But it does work the way I expect it to work, so close enough. URLSession is weird to me.)

//set up the variables to extract the data from the dictionary
var theProduct: Any
var theBuildID: String
var theVersion: String
var theRelease: Any
var theMajorVersion: String
var theMinorVersion: String


So out of all of these, only theRelease isn’t.a string, it’s an int. This is me playing a bit with Swift, because while I’ve read books and done stuff, this is the first time I’m actually using it.

//it is probably a good idea to make sure your variables are what you think they are
//this can avoid problems later. But this does show you have options if you're not sure what the return will actually be

theProduct = theJSON["product"]! //as! String
theBuildID = theJSON["build_id"] as! String
theVersion = theJSON["version"] as! String
theRelease = theJSON["release"] as! Int
theMajorVersion = theJSON["version_major"] as! String
theMinorVersion = theJSON["version_minor"] as! String


If you can’t tell, I talk to myself a lot in comments. I also walk my way through problems that way. Sometimes I put recipes in my comments. I’m odd like that. This is just me extracting things from a dict and documenting it so when I do it later, I know how. Note me coercing things. Except for theProduct, which was a “I wonder what will happen if I leave it as Any. Turns out, not much. But I won’t do that in the app, seems like a bad idea.

print(theProduct)
print(theBuildID)
print(theVersion)
print(theRelease)
print(theMajorVersion)
print(theMinorVersion)


PRINT ALL THE THINGS

} catch {
//print any errors
print("JSON error: (error.localizedDescription)")
}


Do error things.

}
theTask.resume()


Actually run the task, and get the JSON that lets us do the things. Y’all, I get that it works and it looks the same in ObjC, but this is kind of weird. Just saying.

As I said, this is not how it will look in the app, but it helps me see how swift does things. I’ll probably wrap up the JSON code in its own function and maybe even wrap up the URLSession code in its own function. Or not. Depends on how much of a pain in the ass this is.

The next bit of this will probably delve into the initial UI creation. Unless it doesn’t. And if you read this, thanks!

From AppleScriptObjectiveC to Swift

Oh this will be fun…

Recently, I decided to try to update my Nagios Manager app from AppleScriptObjectiveC to Swift. There’s a few reasons for this, one, it takes a lot less time to type “Swift” than “AppleScriptObjectiveC”. I mean, I can use the more common “ASOC”, but that doesn’t google as well. Secondly, there’s a few things I’d like to do that are a real pain in the ass to do with ASOC, and some that just won’t work.

I don’t want anyone to take “John is dumping ASOC” from this. I’m not. I like ASOC, and it’s a good higher-level language. But there are things it can’t do well, and Swift does those things well.

Note: I will not be using SwiftUI. It would take a month to get Nagios Manager’s UI working in SwiftUI, and in any event, if this were to ever move to iPads/iPhones, I’d have to redesign the UI completely for them anyway. For now, this is a macOS-only app, and as I’ve talked about before, SwiftUI is a really awful choice for macOS apps that aren’t also going to be on iOS/iPadOS

UserDefaults

Nagios Manager stores its settings in User Defaults. It’s a plist that’s an array of dicts with a boolean I use to see if there’s anything there other than the boolean. I’m kind of lazy and if I use UserDefaults, then I don’t have to deal with paths or locations. It may not be the best way to do this, but it lets me write less code. I like writing less code. The initial ASOC version is:

property theDefaults : missing value --referencing outlet for our NSDefaults object
property theSMSettingsList : {} --settings list array
property theSMDefaultsExist : "" --are there currently settings?

set my theDefaults to current application's NSUserDefaults's standardUserDefaults() --make theDefaults the container for defaults operations

my theDefaults's registerDefaults:{serverSettingsList:{}} --sets up "serverSettingsList" as a valid defaults key  changed to more correctly init as array instead of string. It also deals with nils much better

set my theSMSettingsList to (my theDefaults's arrayForKey:"serverSettingsList")'s mutableCopy() --this removes a bit of code by folding the NSMutableArray initialization and keeps it mutable even after copying the contents of serverSettingsList into it.

 set my theSMDefaultsExist to theDefaults's boolForKey:"hasDefaults" --get the boolean value for the hasDefaults key

 if not my theSMDefaultsExist then --if there are no defaults, let the user know this so they can fix that issue.

try --to catch the error -1 

display dialog "there are no default settings existing at launch" --my version of a first run warning. Slick, ain't it.28 when a user hits cancel

on error errorMessage number errorNumber 

if errorNumber is -128 then
--this error is generated when the user hits "cancel" in the display dialog. This on error sinkholes it that so it doesn't cause any actual problems, since the end result for either Cancel or OK is what we want.

 end if
 end try 

set my theSMStatusFieldText to "If you're seeing this, then there's no servers saved in the app's settings. This tab is where you add them.\r\rYou'll need three things - the server's name, URL and API Key. For the URL, only the first part, i.e. https://server.com/ is needed. The \"full\" URL is generated from that.\r\rThe app itself is pretty simple. You can add or remove servers. Those are saved locally on your mac.\rThose servers are used to pull down user info in the User Manager tab. More info is in the application help in the Help Menu."

else if my theSMDefaultsExist then --there's no point in running loadServerTable: if there's no data to load

 my loadServerTable:(missing value) -- initial load of existing data into the server table.

end if

Yeah, there’s a lot going on there, especially if you aren’t used to ASOC or AppleScript. Also, I really like to comment the crap out of my code.

The first three lines are setting up properties, which are…an odd form of global variable that’s not really a global. I probably abuse them, but for ASOC, properties are your very good friend. They’re initially set up as empty strings, one empty list/record which corresponds to an array/dictionary in Swift and one missing value, which probably comes closes to an NSObject. We mostly use them in ASOC for UI binding, but we use it here for creating our initial NSUserDefaults object.

One of the weird things about going from ASOC to Swift is that ASOC is rather tightly bound around ObjC (you can use ObjC in an ASOC app), but not Swift. So there’s some translational work you have to do there, like Array instead of NSArray, etc.

The next statement creates our UserDefaults object:

set my theDefaults to current application's NSUserDefaults's standardUserDefaults() --make theDefaults the container for defaults operations

There’s some ASOC-isms there that need to be dealt with. The “my” keyword is a way of dealing with scope. If I didn’t have the “my”, I’d have to have theDefaults defined in the function it lives in, on applicationWillFinishLaunching:aNotification specifically. (“on” is another ASOC keyword, ala “func” in Swift.) The “my” allows me to use the property version of “theDefaults”. Oh, and “--” is how you do single line comments in ASOC and AppleScript.

current application's” is another ASOCism, which relates to how generic AppleScript does things. AppleScript is really designed to be an application scripting language, where you use different applications as engines to do work. AppleScript in and of itself isn’t really designed to solve all your problems for you. So the “current application” thing is needed so the ASOC runtime knows what you mean. This statment is saying: “Set the value for theDefaults” to be an instance of NSUserDefaults.” This will be how we are able to use NSUserDefaults methods. This is why we defined theDefaults as missing value in the properties statement.

Next is:

my theDefaults's registerDefaults:{serverSettingsList:{}} --sets up "serverSettingsList" as a valid defaults key changed to more correctly init as array instead of string. It also deals with nils much better

This is one of those cases where a higher level language is much nicer to work with. I don’t have to declare much about serverSettingsList other than it’s some form of array-ish object. This will let it work as either a record (dictionary) or list (array). It’s kind of nice. I could have used a string or what have you here, but it turns out I need an array-ish object anyway, and it handles nils better than strings or other variable types.

Next is:

set my theSMSettingsList to (my theDefaults's arrayForKey:"serverSettingsList")'s mutableCopy() --this removes a bit of code by folding the NSMutableArray initialization and keeps it mutable even after copying the contents of serverSettingsList into it.

So within theDefaults, serverSettingsList, as it turns out, is an array of dicts, or in ASOC terms, a list of records. There’s a few dicts in there, one per nagios server and some for the AD Auth Records Nagios Manager needs to use. So what we do here is say “Shove all the data in serverSettingsList into theSMSettingsList array-ish thing, (which then makes theSMSettingsList a list of records), but also make it an NSMutableArray, because we’ll potentially need to modify it at some point.

Next we want to see if there are any defaults currently on-disk:

set my theSMDefaultsExist to theDefaults's boolForKey:"hasDefaults" --get the boolean value for the hasDefaults key

Now, I could have checked to see if the results of the mutableCopy() were nil, but, that’s only going to be the case if there’s nothing. Within Nagios Manager, there is the ability to delete one, or more, or all servers and settings for those servers. However, that doesn’t delete the defaults plist file. So we could have an empty array, which wouldn’t necessarily return nil, even though there’s no useful data in it. By checking the value of the “hasDefaults” boolean, I get a better answer, one of three:

  1. There’s a defaults file, but no servers listed (false)
  2. There’s no defaults file, (false/nil or the ASOC version of nil, missing value.)
  3. There are defaults in the file, (true)

That reduces down to one of two return values, true/false, which make checking easy. The next part is an if-then which handles this:

if not my theSMDefaultsExist then --if there are no defaults, let the user know this so they can fix that issue.

In Swift, this would be “if !theSMDefaultsExist”. If this happens on launch, there’s no defaults available to load, so let’s tell the user that. We’ll do that via a display dialog statement that will let the user know there’s no defaults to read. We’re going to wrap that in a try block because when you hit the “Cancel” button in a display dialog’s dialog, that generates an error (-128) that we want to trap and sinkhole, since we don’t really do anything based on the response. This is jsut for notification purposes to the hu-mon:

try --to catch the error -1

display dialog "there are no default settings existing at launch" --my version of a first run warning. Slick, ain't it. Generates a -128 when a user hits cancel

on error errorMessage number errorNumber

if errorNumber is -128 then
--this error is generated when the user hits "cancel" in the display dialog. This on error sinkholes it that so it doesn't cause any actual problems, since the end result for either Cancel or OK is what we want.

end if
end try


That’s a basic try-on error block. I could have just left the on error part blank, but just in case there’s ever a different error I want to handle differently, I’ve left a structure in place to do that. As I said, this is a big sinkhole for that error.

This next text block:

set my theSMStatusFieldText to "If you're seeing this, then there's no servers saved in the app's settings. This tab is where you add them.\r\rYou'll need three things - the server's name, URL and API Key. For the URL, only the first part, i.e. https://server.com/ is needed. The \"full\" URL is generated from that.\r\rThe app itself is pretty simple. You can add or remove servers. Those are saved locally on your mac.\rThose servers are used to pull down user info in the User Manager tab. More info is in the application help in the Help Menu."

Is used in the UI for Nagios Manager to help explain more of what’s going on. When you see the Swift code, you won’t see any of this or the swift version of the Display Dialog thing, since I haven’t actually started to build the UI yet.

else if my theSMDefaultsExist then --there's no point in running loadServerTable: if there's no data to load

my loadServerTable:(missing value) -- initial load of existing data into the server table.

end if


If the defaults exist, then we call the loadServerTable function, which loads the array controller that manages the list of servers in the UI.

Since we aren’t passing it any values, it gets a (missing value). One of the reasons for using so many properties is there are things that don’t exist in ASOC, like structs, so where you’d normally pass stuff in other languages, I find it much, MUCH easier to use properties instead.

So now, here’s my first pass at the Swift version:

//create the defautls object
var theNMDefaults = UserDefaults.standard
//the var we'll use to actually hold defaults in the app
var serverSettingsList = [String: String]()
 //register serverSettingsList as a valid defaults key
 theNMDefaults.register(defaults: serverSettingsList)
 //pull the defaults, which ends up being an array of dicts
 var theNMSettingsList = theNMDefaults.array(forKey: "serverSettingsList")
 //check to see if defaults exist at all. Even though 
 //theSMSettingsList will be nil if there's NOTHING as in no 
 //plist at all, there's also the case of the file existing, 
 //but we erased the settings or what have you. So we check 
 //the bool key we use for when the file exists but there's 
 //no settings in it. We're also doing some LIGHT variable 
 //renaming. 

//this will be false if there's nothing there or set to 
//false
 var theNMDefaultsExist = theNMDefaults.bool(forKey: "hasDefaults")

//initial if block
 if !theNMDefaultsExist {
 //display alert stating there are no defaults to read
 } else {
 //do initial loading of defaults into the appropriate 
 //controller. That will happen after we start building out 
 //the UI.
 }

So this block isn’t as complete as the ASOC version, but it’s doing the same basic stuff. Creating a user defaults object, registering with the object, checking to see if there’s any defaults, and setting up the actions if there’s anything there.

So that’s the first bit. This will be a long project, and I’m not working on it every day. (For one, it’s about impossible to test from home.)

I also plan on making all kinds of mistakes, because I’m not a full-time coder, I’m certainly not an expert in swift, and really, this app has a design target of me. So there’s things I’m doing here that someone with more of a clue wouldn’t, but meh. This app works by slinging REST commands back and forth and parsing JSON. Algorithm speed is not that essential here. I can also see that I’m going to miss a lot of the things ASOC did for me as a higher level language.

The next step will be the initial UI setup, a window with 4 tabs, and to get the initial setup working for the server manager part. So fun times.


When Bad UI Is Dangerous

CarPlay In iOS 13 has a dangerously distracting issue
(For those who will wonder, I recorded the video in this post while I was stopped.)

As some, or maybe none of you know, I am a bit of a CarPlay fan. I’ve used it since 2016, when I got my Accord EX Coupe, (I went with the EX trim specifically for CarPlay.) I like being able to use the GPS I always have with me, that I don’t need to rig up some mount for my phone, that it’s always there, and it works well. I also find that Siri in CarPlay is a vastly different and better experience than Siri anywhere else. I also love the Irish Woman voice for maps. Because Woman Speaking With Irish Accent.

However, there’s some issues with iOS 13 that I intently dislike, UI issues that should, and would be easy to fix were Apple inclined. I only bring them up because honestly, they’re distracting, and in a car, distracting is a synonym for dangerous.

Contrast Isn’t Just For Televisions Anymore

So the first faux pas, which is related to the other issue is one of contrast in the Maps destination widget, seen below:

That’s in morning sun, under a tree

As the caption states, that’s a picture of my CarPlay screen taken at about 9am under a tree. So fairly dim sunlight. That’s with the sunroof open, which it usually is. Note the lack of contrast between the close widget in the upper right corner and the rest of the widget. (I have never actually been to Peppers. It is a restaurant near the gym I go to, and CarPlay really wants me to go there. I don’t, because piss off CarPlay, I DO WHAT I WANT.)

In midday sunlight, with the sun streaming in the sun roof, that close widget effectively doesn’t exist. So to get rid of it, because if I don’t, it doesn’t let me see the map correctly in the new default view, (which I really like.) However I have to look at it to see where it is. That’s bad. What’s worse is that the tap target is just too small for something like that. No, really, here, watch:

This is entirely too fussy a tap target

Now, admittedly, that first tap kind of sucks, target-wise, but if I had been driving, that’s not going to be that unusual, especially if I’m, you know, looking at the road and not the CarPlay screen as I should be. But no matter how you look at it, that’s not good design. That’s distracting as hell, and that’s dangerous as hell. (The way I really tap is to use the other four fingers as a brace around the edge of the screen so my index finger can hit the widget more precisely. Yay big hands. I can also one-handed work the PRAM-zap combo.)

I have, in fact, reported this as a bug on 27 June. So fairly early, and it’s still open, which is not awful. But still.

This is a case where aesthetics has to take a back seat to safety. The solution is relatively easy. First, make the close target white. That makes it far, far more visible in a variety of lighting situations. Yes, that may be “ugly”, but you know what’s more ugly? Your face after an airbag explodes into it. Because you were in an accident because you were distracted. (Please spare me the “well you shouldn’t…” argument. People are imperfect, we do imperfect things. Part of design is to recognize this and help minimize it where possible. Making a small circle white instead of dark grey is a trivial change that does that.)

Secondly, and this isn’t as easy as changing the color in a widget control, but it is close, increase the size of the tap target. Allow for imperfect taps. It’s not zero-effort, but it’s hardly nuke it from orbit, it’s the only way to be sure either. It’s the kind of thing that could be handled in an update.

Talking to people with other cars and CarPlay implementations, this behavior is not universal. One person I know, their car isn’t a touch screen, so the widget I’m talking about self-dismisses after a few seconds. That’s not the case in my car, that thing will stay there until dismissed as far as I can tell. So that’s fun. Well, it’s FUn. But seriously, this is a distraction issue, and for CarPlay, those need to be high-priority bugs and fixes.

And no, I’m not going to Peppers, I DO WHAT I WANT.

Yeah, SwiftUI too dude!

SwiftUI is just not good at macOS apps

So because I’m stupid, (I know, shocker), I decided to do a thing Apple says is pretty easy: try to make a macOS app using SwiftUI. Setting aside beta issues, let me be clear on this:

  1. It’s not an iOS/iPadOS app that will run on macOS via Catalyst.
  2. It may never run on iOS/iPadOS. I don’t really care about those
  3. This is not some thing with a list picker and a nav view and hey, UI is done.

Here, a screenshot of the non-SwiftUI version:

There’s a lot going on here

So yeah. There’s a lot going on and for what it’s used for, a front end to managing my Nagios servers, it’s pretty good at it. It does thing things I need it to do. That large blank area at the bottom is for status stuff. Also, I don’t want to hear that the reason I had a hard time with this is because I haven’t been a Swift Dev since it was introduced. If the only way an environment is usable requires years of experience to do even simple things, that environment is a failure. Period.

So okay, this is pretty straightforward right? tabs, controls, done. I mean, just to mock up the UI.

Lol.

So after two weeks, spread across much longer, I can say a few things about SwiftUI.

  1. If you’re building a macOS app that is macOS – first or only, stay away from SwiftUI. Like it had the dysentery and was dragging your conestoga towards a school of piranha. It’s honestly awful, as we’ll see
  2. I’m actually really sure at this point, that SwiftUI is an attempt to bring CSS into Apple Device app UI design. In and of itself, that’s not awful. But the current implementation as of the GM 2 drop of Xcode is just awful for macOS. Again, see 1. above.

What I wanted to do was build two tabs and have some stuff in it. So let’s go through this. First, here’s the ContentView of a generic starting macOS project using SwiftUI:

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello World")
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

This gets you this:

Yep. That’s a window.

So now, we want to add tabs. This requires a tab view. So we erase the text saying “Hello world” hit ye olde + button in Xcode and drag in a tab view.

This is my first complaint here: You drag a control into a code window, not onto the window of things you want. Like, I did try, and this may work correctly for i(Pad)OS apps, but for a macOS app, it’s full of fail. I tried to drag a button into a tab view, couldn’t do it. So basically, with SwiftUI, you’re not visually laying out your controls at all.

And this is what the code looks like (in the interest of space, I’m going to not include the content view preview code, it doesn’t change:

struct ContentView: View {
	var body: some View {
		TabView(selection: /*@START_MENU_TOKEN@*/ /*@PLACEHOLDER=Selection@*/.constant(1)/*@END_MENU_TOKEN@*/) {
			/*@START_MENU_TOKEN@*/Text("Tab Content 1").tabItem { Text("Tab Label 1") }.tag(1)/*@END_MENU_TOKEN@*/
			/*@START_MENU_TOKEN@*/Text("Tab Content 2").tabItem { Text("Tab Label 2") }.tag(2)/*@END_MENU_TOKEN@*/
		}
	}
}

Fugly, but it gets us this:

Okay, so far so good

Now already, this is weird. The content of the tabs occurs outside/before the tabItem itself. The label of the tab happens after the tabItem. Sort of. So let’s reformat this a bit, also so it looks more like what you see in examples:

struct ContentView: View {
	@State private var selection = 2
	var body: some View {
		TabView(selection:$selection) {
			Text("Tab Content 1")
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

This gives us the same thing we saw before. A window with two tabs, a single string in each tab, and the tabs themselves have names. I added a line, the “@State” line, so that the first tab, with the .tag(1) property is the start tab.

But this is really weird right? First, the content, the strings “Tab Content 1” and “Tab Content 2” aren’t “in” the .tabItems. They’re before it. Whaaa? This right here is the most counterintuitive shit ever. The content of the .tabItem should be inside the .tabItem block, not outside. The name of the tab should be a property, just like the tag, i.e. .label or some such. Instead, it’s just a text string. Maybe a picture too.

So then, what if you want to put a text field with a label? Oh, then it gets stupid. So first, keeping with “content of thing before <thing>” which took me a WHILE to get, (I kept trying to put it IN the .tabItem, I mean, what kind of IDIOT would think that’s where it goes?) So now, we have this:

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			Text("Text Field Label")
			TextField("Text Field", text: .constant(""))
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

Which gets us the field and the label on top of each other, because this was designed for iOS. Also the text field stretches the width of the window. It looks like this:

Sigh

Actually, that’s not what happened. SwiftUI added a third tab and now it’s all stupid. No warning. What the hell? Well, first, we can try adding a horizontal stack (read “row”) and putting things in that. Sure. let’s try that. According to Apple et al, you just cmd-click the thing you want in the HStack, select “embed in HSTack, and oh look, it also put the .tabItem in the HStack too, which was completely not what we wanted. So NOW, we put our text field inside the HStack, the next line down from the label, and put in a .frame property one line below the TextField declaration to limit the width of the textField because this is not iOS and I don’t want a gigantic text field on a 4K screen and we get closer:

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			HStack {
				Text("Text Field Label")
				TextField("Server URL", text: .constant(""))
					.frame(width:96,height:22)
			}
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

which gets us:

Okay, getting there…

But we want this on the left side of our window, and at the top. So the first is pretty easy. We add a spacer, which shoves it all over to the left, and then we add in some padding so it’s not at the edge of the window. Like seriously, this is some hardcore iOS shit you have to work around. Also, why can’t I just say “hey, in this horizontal row, I would like everything to be aligned left.” That’s a great idea, so of course you can do that. LOL! Nope, in an HStack, you can only adjust the vertical alignment of the things inside it. Because that’s how everyone thinks of aligning horizontal things in a horizontal structure. Vertically.

Sigh.

So now we have:

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			HStack {
				Text("Text Field Label")
				TextField("Server URL", text: .constant(""))
					.frame(width:96,height:22)
			Spacer()
			}
			.padding(.leading)
			
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

Which gives us:

Closer…

So now, what if we want two rows? Another HStack under the other one right?

Oh honey, no, it’s not that simple. That gets you a third tab. But go ahead and try it. Right, so now what? Well, now we embed this in a VStack, (Column) which will also let us handle our vertical positioning via a Spacer on that:

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			VStack{
				HStack {
					Text("Text Field Label 1")
					TextField("Text Field 1", text: .constant(""))
						.frame(width:96,height:22)
					Spacer()
				}
				.padding(.leading)
			Spacer()
			}
				
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

and we get:

Closer….

Of course you’re thinking “why not just set the VStack so everything in it is at the top?” Well, that would be logical, and also have nothing to do with VStack alignment options, which are of course, left, right, and centered. Or leading/trailing/centered. Because when I’m looking at a vertical structure, I only care about horizontal alignment.

Sigh.

So now we just shove another HStack in and we’re set…sort of:

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			VStack{
				HStack {
					Text("Text Field Label 1")
					TextField("Text Field 1", text: .constant(""))
						.frame(width:96,height:22)
					Spacer()
				}
				.padding(.leading)
				HStack {
					Text("Label 2")
					TextField("Text Field 2", text: .constant(""))
						.frame(width:96,height:22)
					Spacer()
				}
				.padding(.leading)
				
			Spacer()
			}
				
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

Which gets us:

Hrm…

I mean, it’s not bad, but the field alignment is kind of messed up. So how do we fix that? I mean, we could add in some padding statements, but that’s a bit much, especially as we may want to have more stuff in the window.

So to do that, we’re going to get medieval on this thing, by which I mean, we’re basically going to have to build a damned table to hold things. (Seriously, I’m having flashbacks to GoLive CyberStudio here.) Because that’s so much better than the old way.

So what we end up with are two VStacks in an HStack in a VStack. To get everything lined up correctly and with the right amount of margin from the left side of the window, we have (Comments added for clarity):

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			VStack {
				HStack {
					//the alignment statement here sets both labels to be left-aligned
					VStack(alignment: .leading) {
						Text("Text Field Label 1")
						Text("Label 2")
					}
					//this padding statment creates space between the edge of the
					//window and the text
					.padding(.leading)
					//this alignment property may not be strictly needed
					VStack(alignment: .leading) {
						TextField("Text Field 1", text: .constant(""))
							.frame(width:96,height:22)
						TextField("Text Field 2", text: .constant(""))
							.frame(width:96,height:22)
					}
				// this is the spacer for the HStack so it's all on the left
				Spacer()
				}
				//this is the spacer for the main VStack so it's at the top
				Spacer()
			}
			//this padding property gives us some space between the tabs and the stuff
			//in the main VStack
			.padding(.top)
				
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

Which looks like:

Alllmost

That’s really close. but the labels and the text field alignment is kind of muffed. You might think “this is where that vertical alignment thing in the HStack comes into play!” You would think that, but you’re wrong. These are two VStacks in the HStack, remember? The way I ended up solving it was to add some padding to the bottom of the one label, so they separate out better:

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			VStack {
				HStack {
					//the alignment statement here sets both labels to be left-aligned
					VStack(alignment: .leading) {
						Text("Text Field Label 1")
							//this separates the labels enough so they seem to
							//line up with the text fields better
							.padding(.bottom)
						Text("Label 2")
					}
					//this padding statment creates space between the edge of the
					//window and the text
						.padding(.leading)
					//this alignment property may not be strictly needed
					VStack(alignment: .leading) {
						TextField("Text Field 1", text: .constant(""))
							.frame(width:96,height:22)
						TextField("Text Field 2", text: .constant(""))
							.frame(width:96,height:22)
					}
				// this is the spacer for the HStack so it's all on the left
				Spacer()
				}
				//this is the spacer for the main VStack so it's at the top
				Spacer()
			}
			//this padding property gives us some space between the tabs and the stuff
			//in the main VStack
			.padding(.top)
				
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

and from this, we get:

Hey, that looks right!

Oh, so if you want your window to be the right size, you add a .frame property to the bottom of the view block:

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			VStack {
				HStack {
					//the alignment statement here sets both labels to be left-aligned
					VStack(alignment: .leading) {
						Text("Text Field Label 1")
							//this separates the labels enough so they seem to
							//line up with the text fields better
							.padding(.bottom)
						Text("Label 2")
					}
					//this padding statment creates space between the edge of the
					//window and the text
						.padding(.leading)
					//this alignment property may not be strictly needed
					VStack(alignment: .leading) {
						TextField("Text Field 1", text: .constant(""))
							.frame(width:96,height:22)
						TextField("Text Field 2", text: .constant(""))
							.frame(width:96,height:22)
					}
				// this is the spacer for the HStack so it's all on the left
				Spacer()
				}
				//this is the spacer for the main VStack so it's at the top
				Spacer()
			}
			//this padding property gives us some space between the tabs and the stuff
			//in the main VStack
			.padding(.top)
				
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	//set the window size here. Beats me why
	.frame(width:706,height:651)
	}
}

And now we have a window that starts life in a precise way:

This is…this is a lot of weird-assed work to get the very simplistic results I wanted. And that’s only on one tab. I have to redo that for every tab. Literally all that code I added is to set up two labels and two text fields, that aren’t doing anything.

I freely admit that my n00bness probably caused a lot of this. But, it was not helped by nothing about using SwiftUI with macOS apps. All of Apple’s documentation and every gods-forsaken screen shot I could find were all for iOS. This may be the longest bit of anything using SwiftUI for macOS available on the internet, and that is sad. Shamefully so. Keep in mind, my end goal has a lot of UI elements. So this should be awful at some point. But I may keep banging on it, because god knows, no one else is, and even documentation from a n00b is better than nothing.

If Apple wants people to use this for macOS dev, and not just i(Pad)OS -> macOS via Catalyst, then they have to, to be blunt, pull their goddamned heads out of their asses on this. Start creating documentation and tutorials and sample code that actually works for macOS targets in and of themselves. Or just admit that SwiftUI is not a good choice for starting on macOS, and that they don’t plan to care about that. That would at least be honest.

As well, there’s some choices here that are just baffling. I refer to SwiftUI as CSS for native UI design, but even CSS isn’t as bad as some of this. Having me put all the contents for a tab before the .tabItem block and not oh, in the .tabItem block is pretty damned weird, and completely counterintuitive.

Forcing macOS devs to specify control location the way you would on i(Pad)OS is way more work than it needs to be. Let me drop a damned button on a window and give me the shell code for that.

But for now, outside of idle curiosity, I cannot see why a macOS dev would ever want to use SwiftUI, it’s just awful.

(If you feel a need to tell me what you think about this, I’m @bynkii on twitter. If you feel the need to tell me i’m stupid for not immediately falling to my knees and loving SwiftUI, you may want to not actually tell me that. It’d be a waste of time for both of us. If you want to give me some pointers from a macOS perspective, DEAR GOD PLEASE!!!!)

“It’s Complicated”

Warning: this is not about tech or D&D comedy. It’s a bit personal, and some of you may not want that here. I understand that more than you think, but it is my site, so yeah.

I have been a reader of Goldie Taylor’s work for some years now, but it wasn’t until I started following her on twitter (@goldietaylor) that I gained a true appreciation for her depth as a writer, her ability to talk about the events of her life, in particular her father, and the mystery surrounding his death, one that may never be cleared up.

The writing she’s done about her family has resonated in me in a big way, but it wasn’t until recently that it came together why: in a very real sense, I never knew my parents. I mean, they were there, my dad until 1991 when the throat cancer he’d worked so hard to get took him, and 1999 when my mom’s lungs did the same for her. I wasn’t there when either of them died. I was in the Air Force in 1991, assigned to Grand Forks, and when my mom died, I was living in Boston, she in St. Petersburg, Fl.

My mom had died some time before I knew about it. For many, that seems strange, but my mom and I would regularly go weeks, sometimes months without talking. We were…contentious…at times. All three of us. We spent a lot of time talking at, or over each other, but rarely with each other. It wasn’t until I’d tried to call her and getting no answer, called the local police to check on her that I found out I was no longer just half an orphan. The way the cop said “Mr. Welch?” told me everything he was about to say.

I was 24 when my father died, and 32 when my mother died. My father died long before my son and my somewhat disastrous first marriage happened, my mom was able to spend sometime with Alex when he was younger (two or so.) My dad was fond of telling stories about himself as a child, and as a young man, most of them…well, the movie “Big Fish” reminded me a lot of him. His stories have a kernel of truth in them, but taking any of them as gospel, probably not a great idea.

My mother said almost nothing about herself. Before she died, I knew she’d been raised in dire times (they were both born at the beginning of The Great Depression), that she had walked away from the Catholic Church after being disgusted by the opulence of a cathedral in Mexico City in 1966 (while she was pregnant with me) while so many of said cathedral’s parishioners were living in the street and starving. “They wouldn’t give them food, but still demanded tithes.” she said, with more disgust in her tone than I’ve heard before or since.

To my knowledge, she never set foot in a Catholic church for services ever again. My mother was unbending about some things.

All I knew about them as a couple was they met in the Boule Miche bar in Chicago, my dad thought she was quite attractive, they dated, they got married and well, then we were a family. They were married in a civil service in Indiana, ostensibly to avoid payback for the many pranks my dad had pulled on his siblings.

It was only later, after she died, that I found out the truth underneath a lot of things. They were married in November of 1966, I was born in March of 1967. Everyone can do that math.

Sitting on the floor of her apartment, reading that wedding license, so many things clicked. Like really why they’d gotten married in Indiana. My dad’s family was very Irish-American and very Catholic. There had always been this tension between my mother and his family. Her antipathy was barely-disguised, and after about 3-4 beers, loudly undisguised. It took me 32 years to find out why.

It took me another few years to forgive them. Which is funny, given my parents and I weren’t that close the last 10-15 years we were on the planet together, but that was us. Other people being unkind to them was not permissible. Weird, isn’t it, how that works.

What also clicked were the reasons behind the nightly, alcohol-fueled screaming matches they had. Well, alcohol-lubricated. They were fueled by anger and resentment. My mom had carved out a rather nice niche for herself in Chicago, and my surprise introduction cratered that niche, rather thoroughly. She never learned how to deal with her resentment at that, and so I was called a remarkable variety of unkind things growing up. Usually at night, with beer being the lubricant that opened the doors to where she mostly kept those feelings locked away.

I’m not angry about it anymore. Getting older does that, it gives you perspective, and with any luck, some empathy.

But when I was younger, it meant that there was this distance between us that never went away, never got better. Had they lived longer, I don’t know if it would have gotten better. I’d like to think it would have, but hard to say.

My mom had radiation poisoning from a job she had with the Air Force in Japan in the late 1940s. Her office was in Hiroshima. I found this out from a cousin long after she died. She’d come home sick, her hair falling out, weakened…then some months later, she was better and she never talked about it to my knowledge. I know she never talked about it with me. All I knew of that time was that she’d been in Japan and there are some pictures of her at parties. She is almost never smiling. There’s a wariness about her, but I don’t know why.

There are things about her life that tell me she may have not been completely straight. I can’t imagine, if that was true, what that was like for a woman in the U.S. in the 30s-60s. But it would explain much. I never knew what her life as a woman, as a person was like, and I doubt I ever will. My mother guarded her privacy more zealously than anyone I have ever known, and even if I could find out, it would almost feel like a betrayal to actively do so. Whatever reasons she had for closing off her past as tightly as she did were hers and I do respect her memory enough to allow them to remain as she preferred.

If abortion had been legal in the late 1960s, I am quite sure I’d not be here. If that would have allowed my mother to live a life with joy instead of resentment as its core, I would almost volunteer to fix that. Every unwanted kid knows that fact. No one has ever hidden that from their children.

note: This does not mean I don’t think that she loved me. I’m sure she did, I know that to be true. But love does not eradicate everything negative about a relationship.

My father was something of an open book, in that he talked a lot. What I learned, decades after his death, was that what he talked about was shallow. It wasn’t until his youngest sister sent me a box of his things that I learned things like how he’d not hit puberty until after high school. I took after him that way, fortunately my son did not. That is a shitty way to go through high school. I learned that he and his father had not gotten along well, and at least up until him being drafted into the Army for Korea, he’d not known why, but desperately wanted to fix that.

I don’t know if he really ever did, but I would like to think so.

He had a host of anger issues that he was never able to manage well. After he was mangled by a car in 1978, he spent the next 13 years kind of waiting for death. He just gave up in many ways, and the beer didn’t help. Sometimes, often times, I think the cruelest thing ever done to him by the universe was allowing him to survive that accident.

I’m regularly surprised that I’m not ardently anti-alcohol, given its centrality in so many of the worst experiences of my childhood, but ultimately, the beer just sits there. It’s the human that causes the problems.

People, not often, but sometimes ask me if I miss my parents. I say “yes” because that is what is expected. In truth, I don’t know. I miss parts of them. They were both, regularly fiercely intelligent. They were obsessive readers, and played Scrabble the way MMA fighters go after belts. They were amazing writers, and my father a just as talented photographer. I learned a lot from their good qualities, as well as their bad.

But I can’t cafeteria them. I can’t just remember parts of the people they were. For better or worse, I’m stuck with all the memories. I am specific about what I will share, but i’m stuck with the range of memories they left me with. So when someone asks me if I miss them…I don’t know. There are parts I absolutely miss, and parts I absolutely do not. Twee tropes like “don’t dwell on the bad” don’t help. They were complicated people, completely unprepared for the ADD-poster child they brought into the world. I daresay most people were unprepared for me.

They did the best they were able to do with the paucity of tools they had available. I managed to not die at a young age, and given I grew up in Miami in the 1970s/1980s, that is kind of an accomplishment.

So yeah. There’s no real point here. There may never be. Other than maybe I feel a certain sympathy/empathy with Goldie Taylor’s lack of knowledge about her father and his death.

What’s The Difference Between An Or-gee And an Orgy?*

Our trio is in Westgate. This is a bit odd, for Paladin at least, because Westgate is not a place for the pure of heart. Outside of Zhentil Keep, and Thay, it’s the worst place for the pure of heart, and yet Paladin likes Westgate. (He tolerates Thay, but Zhentil Keep is a no-go. Even he cannot handle the smells from that city.) Paladin likes Westgate mostly for the restaurants and their smells. Which is a bit odder. (A bitter odd?) He greatly wishes the general level of evil was much lower, but over the years, he has come to realize that larceny, murder, pettifoggery and really good food go together. Even to him, that makes no sense, but it is how things are, and so Paladin likes Westgate.

Monk also likes Westgate, for a related reason: the abundance of amazing coffee shops. Paladin once asked him why coffee shops and criminals seem to exist in nigh-equal quantities and Monk explained that since criminals tend to do criminal stuff at night, they need coffee. Since they have, at least occasionally, gold to spend, they like good coffee. “Oh.” Paladin said, and went back to smelling his latte. (Paladin never smells Monk’s coffee. Paladin tried smelling Monk’s coffee once and was miserable for a week. “It took forever to get that smell out of my head.”)

Barbarian doesn’t really care. There are few places she likes or hates more than any other places. The only real standout is Shadowdale. Because someone there thinks he knows everything and won’t shut up about it, and even she gets tired of the dickwaving with Storm Silverhand. 

What Barbarian doesn’t know is how amused The Simbul (and quite possibly Mystara herself) are by how much a certain archwizard annoys her, (and vice-versa), and how regularly the both of them work to arrange things so she has to go into or near said wizard’s home.

Mages, even good ones, even ones that have ascended to godhood, can be right proper pricks. Remember that.

As they walk about, they have their usual discussion about where to eat. As has become her custom, Barbarian is fine with whatever Paladin wants. She has decided Paladin is her…well, there’s not a proper word for it. She feels kindly towards Paladin, (she also feels things for Monk, but they are Very Different Things) and were Paladin a child or pet, would be said to be spoiling him rotten. Honestly, it’s how Monk also treats Paladin, and well, how just about everyone treats Paladin. 

There’s a rumor that Szass Tam built a kitchen with living staff just for when Paladin comes by for their semiannual checkers match, mostly because Paladin always gives him advance notice so he doesn’t accidentally destroy Szass’s zombies. “He’s the only living creature who is genuinely considerate of my feelings. Of course I’m going to be nice to him.”

She does ask that they not go to The Quivering Thumb, as it is one of the few places that has not only banned her, but spent a fortune in magic to make said ban stick. “They’re more whiny than a gnome without turnips. Something, something you’re not allowed to behead the other gladiators. Bah.” Paladin nods and makes a note of it, Monk, as he always does when she demonstrates her mayhem’ing ability, sighs and falls even more in love with her. 

Monk and Barbarian are a terribly cute couple. In that they are both absolutely enraptured by the other, (the cute part) and what that has involved in terms of surrounding areas is legitimately terrible, (the terribly part. They are literally terribly cute.) There’s a beholder that will never again open its eyes after accidentally espying them en flagrante destructo. That’s a description, not a typo.

They’re walking down a fairly large road in front of Big Edna’s Tavern when Monk pulls up short, gets an annoyed look, holds out his hand and says “Give it back.” Both Barbarian and Paladin are complely confused by this, and said confusion is not helped by Monk’s shadow suddenly appearing to give birth to just over a meter’s worth of halfing dressed all in black. A grumbling halfling. With red hair.

“You are absolutely no fun.” the halfling says. “I am all the fun I need to be, and if you take my money, I cannot buy coffee.” Monk replies. “Fine. Here.” “All of it.” Slitted green eyes roll into the back of Thief’s head. “One day.” she says as she drops another handful of coins into Monk’s hand. “Not yet.” he replies as he weighs the items she’s handed him. At this point, they are both aware of two things. First, Paladin is very happy to see Thief. Not that he approves of stealing or any of Thief’s hobbies, but she is one of his favorite people, for she manages to procure a variety of things for him to smell that no one else can. 

The second thing is the growling and chattering noises coming from Barbarian, who is seeing this strange woman bantering with her Monk. Her eyes are not quite actually glowing red, but it is a near thing. Thief, hearing the noises Barbarian is making, stands fully erect, which means she is slightly taller than Monk’s beltline, and says “DO I HEAR A MIDGET?” The murderous rage that was about to explode out of Barbarian does a reversal into joy so fast it sprains something, and Barbarian says “DO I SEE A GIANT?” with an unusually large smile on her face.

Thief turns, and in an explosion of giggling and squee-ing, jumps into Barbarian’s arms. No one is sure how she avoids all the spikes, but she does. Both Monk and Paladin are agog, for they have never actually seen Barbarian giggle, but giggle she does as she and Thief hug the dickens out of each other. “I didn’t realize you were in Westgate!” Barbarian says once they’ve stopped. “Aye, I’ve been here for a while now. I accidently own an inn” she says, jerking a thumb over her shoulder. “YOU’RE “Big Edna” now?” Barbarian says as she starts to laugh. 

Thief hangs her head. “Technically no, but everyone seems to just love to call me that. At least until they get stabbity’d.” “Well, I will not call you that Thief” Paladin says, “for you clearly do not like it.” The halfling beams up at him. “You are truly the bestest of friends as well as the cheapest of dates. All of you come inside, and Paladin, I have treats for you.” While Paladin is not a jumper, (he can out-jump an Otyugh. Barely), he manages what could be a heel-kicking jump while cheering. Half the passers by are amused by this, the other half throws down money and run away. Thief quickly collects the money, because of course she does.

They all head for the nicest table in the establishment, which is initially occupied by some “of” adventuring company. “The company OF light”, “The guild OF gold”, or whatever. It’s always something OF something. They quickly abandon said table at a high rate OF speed, without any words OF protest. Unsurprising given the simultaneous smiling from Paladin, Monk’s “I wonder if I could drive your elbow hinge through your skull with one finger tap” look, the glare of “IF YOU DO NOT MOVE I WILL END YOU AND EVERYONE WHO HAS EVER MET YOU” from Barbarian, or Thief smiling sweetly at them.

It is probably Thief. The last group she smiled at like that was found with stab wounds in their stab wounds. it’s one of the reasons Monk gets along with her: he appreciates her artistry. Thief likes Monk because while everyone for a league is hating him, they don’t notice her picking their pockets or their houses. (She cleans up during the yearly “Why Won’t Monk Just Die Already” hate parade in Port Llast.) 

Thief snaps her fingers, and drinks appear on the table for all. Paladin’s drink is not actually drinkable, but it’s there for the scents, not the taste, and judging by how Paladin is starting to glow a bit, they are very good smells. There is coffee for Monk, something very alcoholic for Barbarian and Thief has some vaguely vanilla-scented tea that she lets Paladin get a good sniff of before she drinks it. 

“So how…” Barbarian asks, “do you “accidentally” own a tavern?” “She stabbed someone for it.” Monk says. He and Thief get along, but it is a very grumpy kind of friendship. No one is sure why they snipe at each other the way they do, but that is Thief and Monk. A constant stream of insults and grumbling. “I did not. I won it fair and square.” “You have never done anything fair and square. You’ve never even managed a vaguely cheating triangle.” “Well, in this case, I did. Sort of. What I discovered was that if you’re going to stab a fat Sembian merchant in his nethers, make sure before he dies that he’s not under some stupid curse that transfers all his assets to you and keeps them, and you, there.”

“Wait…” Barbarian says, with badly concealed laughter, “…you’re the one that offed old fat Gornhaudar?” Thief’s eyes narrow and her lips grow thin with annoyance. “…yes…that would be me.” “Didn’t you know about his curse? I thought everyone knew about that.” “NO…no I did not. You think I’d have killed him if I had?” “You’re the only one. It’s why I only broke most of his bones when he groped me.” Thief rolls her eyes. “I knew I should have asked you about that. Everyone was dreadfully curious as to why he lived.” 

By now, Monk and Barbarian are staring at the two women, completely baffled. Monk drains his coffee and signals for more by first attempting to get the waiter’s attention, and when that does not work, flinging his mug so that it bounces off of the waiter’s and the barista’s heads before coming to a stop next to the coffee station, upright, undamaged, and ready to be filled. “Can one of you explain this in a way that won’t make my head hurt?” he asks. Paladin thinks about paying attention, but there’s a strata of a lavender variant that he’s only smelled once before buried under layers of efreet sweat and stone giant tears that is demanding all his attention. Also, he knows if it involves Thief, it will be very complicated and probably involve her deep love of shiny things. The only thing that ever changes is how complex the trouble is and how many shiny things are involved. He goes back to smelling.

Thief sighs and nods. “I suppose I should, since I’m now “Big Edna” after all. See, this restaurant? It’s a moneymaker. It always makes money. Been in Westgate since Westgate was big enough to have a restaurant, and it has always made money.” “This is bad?” “Monk…nothing always makes money. I don’t always make money. This place has almost two millennia of non-stop profit. The Time of Troubles? Profitable. Spellplague? Profitable. Dragon swarms? Profit. Lords of Hell running rampant? Profit. Plague? Profit. Swarms of whores? Profit. Nothing. Ever. Stops. The. Profit.” “I can see where that would attract you.” 

Of course it did. My gods, you own this place and you can do whatever you want because you’ll always have money. I was going to finally go back into full time literature acquisitions, which doesn’t pay well at all, but part of my deal is I get to read it before I turn it over.” “And then you found out about the curse.” Thief sighs and stands on her chair so she can properly slam her head into the table. “And then, <wham>, I found out, <wham>, about, <wham>, the curse.<whamwhamwham>” 

“Said curse being…” “That you can’t leave Westgate. When it was first cast, you couldn’t even leave the gods-damned building, but millennia or so of constant attempts at dispelling and circumvention by the victims, and it’s gradually weakened to where you can at least get to the city borders.” “What happens if…” “You try to really leave? You end up back here. Mid-stride. Mid-jump. Mid-jog, mid-skip, mid-slide-on-your-ass-on-a-long-trail-of-grease. If you’re on horseback, the horse keeps going, you pop in here…at full velocity mind you, which was not fun.” “Well, there goes my catapult idea.” “Already tried it. Broke six bones. Stop laughing.”

Monk thinks a moment, and hands his again-empty mug to the waiter who has realized that making this table wait, or even have to vocalize their wishes will be quite painful and so now is the very model of quiet, efficient, servitude. “That…is a remarkably appropriate curse” he says, chuckling a bit at the misery on Thief’s face. “It’s a remarkably something curse.” “So how do you break it?” “So far? Death. The owner’s. That’s the only thing I’ve found. You can’t sell the place, the curse won’t let you put it in writing or even verbally. I tried doing the entire thing in that finger language I learned from a Drow and my hands stopped working. I tried firing the whole staff and closing the place. The next morning, they were all at work, the place was open, and no one remembered me firing them. I’m making more money than I thought possible with less work than I dreamed possible and I am more miserable than a Drow at the beach.”

“Death…hmm…” Barbarian saying those words gets everyone’s attention. It’s rather like a dragon’s inhale. One with bad halitosis in addition to you know, the fire. Everyone in the room is quietly moving towards the exits, somehow casually running for their lives so as not to attract Barbarian’s attention. They’ll all have some very pulled muscles in the morning. Barbarian stands up, walks over to the nearest support column in the room and as gently as possible, removes a small piece from it with her axe. 

She looks over at Thief. “Did you feel that?” “No, it’s a piece of wood two of you away. Why would I feel you hitting it?” “Death…is such an imprecise word.” Barbarian looks over at Monk who has figured out what is going on and is also now standing, putting on the handwraps she says are her favorite, the ones that cause  things to catch on fire when he punches them. “I knew you were special.” she says to him, her eyes aglow with love. 

Love of Monk, love of immenent destruction, it’s hard to tell with her, but, they are indeed aglow. “Paladin dear, would you and Thief mind stepping outside for a few minutes?” she says as she dons her helmet. “Monk and I have a curse to dispel. Make sure you take Thief by her safe, she’ll want to grab as much as she can carry before.” “Before what?” “Before it’s all splinters. Hurry dear, Daddy and I need some special us time.” Monk’s eyes are starting to glow as well.

Somewhere, a blind beholder whimpers.

“But you can’t dispel curses, and Monk is not mymfffff” Thief has jumped on to Paladin’s shoulders and put her hands over his mouth. “Let us do as Mummy asks,” she says, “and make some haste for the nice, safe street. Don’t worry about the safe, Barbarian knows I don’t keep anything I care about in something someone might be able to lay hands on.” With that, they both exit, stage now.

Twenty minutes later, the restaurant is not even splinters. It would be doing well to be ashes. It has not just been destroyed, it has been removed from existence Of the crowd in the streets when the destruction starts, Thief and Paladin are the only ones left when it ends. Thief is the only one who watches the entire thing, and she’s not sure she should have. She makes sure Paladin is facing away from the bar, and has cast a lovely deafness spell upon himself. 

At the edge of Westgate, the four of them pause to let Thief work up the courage to take a step she hasn’t finished properly in over a year. In theory they are being “run out of town by the Eye of Justice”. By which they mean the most junior member of the Eye followed them at sufficient distance that he could see them leave. Barely. The step works, as Barbarian thought it might. “There’s not a huge difference between killing a building and killing a person. Well, I have to sharpen my axe a bit more after killing a building, and it screams less, but other than that, killing is killing. So now, what shall you do that you have your freedom but no more profit machine.” 

Thief once again smiles sweetly, causing Monk to start cursing in three languages no longer spoken on Toril, one of which never was. “Why, hang out with the three of you my dear. None of you are terribly profitable, but you are all a lot of fun.” Monk is the only one not in agreement with this idea, but Barbarian gives him a look and his protests die faster than the last person who told Barbarian girls can’t use axes as well as men. (Oghma is still researching exactly how she managed to do that much damage to one person on every plane of existence at the same time. Mystra doesn’t let him spend too much time on it, it makes him weep about physics.)

As they walk off, Thief suggests they should all go to Shadowdale.

Somewhere an archmage whimpers.

*if you aren’t enough of a Benny Hill fan to get the joke, I’m certainly not going to explain it to you. -Writer

Dinner In Voonlar

It’s early evening in Voonlar. Enough light to see, be seen, but just enough darkness to make it…interesting.

Fortunately, (for those who would make life interesting for others more than any) there is enough light to see why our trio should be allowed to dwell in the depths of boredom evermore. 

As is common in the smaller towns, no one really knows what to make of Paladin. They may have heard of golems, but he doesn’t really fit the bill. For one thing, he radiates happiness at simply being alive. Literally. Paladin’s aura is the kind of pure shiny thing that gives even the foulest demon a strangely happy feeling. In a world of worry, Paladin walks without fear, in search of new smells, or familiar ones that have brought him pleasure. (Paladin neither consumes nor expels food, he is truly the cheapest of dates.) 

He does whirr a bit as he walks, for he’s a warforged. Which is like a golem, except, as Paladin puts it, he doesn’t just murder things. It does make it a bit jarring when he smiles, or gets giggly about butterflies. Well, anything, really. The giggle of a warforged has broken many an evildoer’s courage. The upshot though, is that even the thought of giving Paladin a bad time causes either fear or guilt in even the coldest of hearts, (or cavities that once held hearts.) There’s a lich near Waterdeep who has given Paladin a standing invitation for chess lessons whenever he has the time. People just like Paladin.

People do not just like Monk. People (sometimes) tolerate Monk, people (regularly) hate Monk, people (constantly) want to do awful things to Monk, (especially Storm Silverhand), and would except for the fact that Monk is well, a monk, and a very good one. It doesn’t help that he moves as though he is at the center of the world and it moves around him. Half-buried rocks don’t have the balance Monk has, and the most accomplished dancer in Waterdeep became almost suicidal after observing him rise from his seat to applaud her performance. Monk is very pretty, Monk is beyond graceful, and upon first glance, it is very difficult to understand why people hate Monk so.

Then he opens his mouth. 

Someone once asked Elminster about the hate Monk creates, and Elminster replied, “That child is a being of angelic grace and beauty, with a mouth worse than the foulest middens on three worlds.” Monk is aware of this, but stopped caring some years, perhaps centuries ago. No one is sure how old Monk is, they’re too busy trying to make him go away. Storm Silverhand commented that if you want proof that Paladin is made of purest kindness and mercy, the fact he has not tried to kill Monk is all the proof anyone should need. Storm dreams of turning Monk inside out.

Regularly.

When asked about his effect on people, the most detailed remark Monk ever provided was “Some people think about other people. Their feelings. Their cares. I haven’t seen people as anything more than a collection of places to be hit in longer than I care to remember. You see a butcher, I see someone who I would avoid hitting in the ribs because of the extra suet butchers seem afflicted with.” Somewhere along the way, Monk became rather disassociated from everyone else. If that bothers him, no one can tell. 

He is however, as much as he can be, solicitous towards Paladin. “Just look at the boy, he’s the most innocent thing ever. I mean, he’s a living battering ram incapable of being hurt, but even beholders think he’s cute. It would be an act of pure chaos to be unkind at Paladin.”

Monk, everyone agrees, is totally not chaotic. They do wish his definition of “good” was more “makes other people smile” and less “makes me giggle.” But he will relentlessly follow any and all rules. He just doesn’t always tell others which rules he happens to be following at any given moment.

The upside for Monk is that simply being him makes people think twice about talking to him, much less giving him a hard time. This works out well for them, since Monk can collapse a minotaur’s skull with less effort than a leaf uses to float on the wind.

Sometimes, there have been groups that have braved Monk’s “go away” aura and Paladin’s “Everything is beautiful” aura to give them a hard time. They’re buried in small boxes around Toril.

Ever since Monk and Paladin went through Yulash a week or so ago, even those groups give them a wide berth. Well, not so much Monk or Paladin. They give Barbarian a wide berth. Beholders give Barbarian a wide berth. It is rumored that on its last rampage, the Tarrasque was stopped by Barbarian. She didn’t kill it, she gave it a bad look. Barbarian inspires fear the way Monk inspires hate. Only worse. 

Barbarian has not been a part of the group for very long, but she knows two things: first, she knows that Paladin is both someone who cannot be hurt and someone who she will protect from even the slightest attempt with every fiber of her being and every inch of her axe. Halfway between Yulash and Voonlar, a bird attempted to relieve itself on Paladin. Barbarian didn’t kill the bird as much as she erased its existence. Paladin doesn’t know it, but he is the safest being in existence. Ever. (She is much of the reason why the Voonlarian prejudice towards non-humans is curiously missing when it comes to Paladin.)

The second thing she knows is that Monk stirs feelings in her that make it hard to not kill things even more than she normally does. There is something in the complete and utter discipline surrounding Monk that sets her insides on fire, and she plans on breaking a lot of beds to see if he’s any good at quenching the fire. Or making it worse, she’s not fussy. The entire trip from Yulash, she’s been splitting things with her axe to keep from possibly shocking Paladin with what she’s thinking of doing to Monk.

Trees. Rocks. Random ancient adamantine pillars that survived at least two battles between major deities. Stone bridges. Four bandit companies. That sort of thing.

Barbarian is a human female, six feet tall, with the pinkest hair Monk has ever seen. Monk is well aware of how Barbarian feels about him, he feels much the same about her. In fact, in the week since meeting her, he has tripped three times. Monk has not tripped at all in decades, perhaps centuries. They haven’t discussed it yet, but Monk is fine with seeing if they can break a bed. Or an entire inn, in this instance, he’s not fussy. Monk may be perfectly lawful, but Monk is not an ascetic. Monk likes his beds soft, his coffee hot and Barbarian. He would never admit it, but he actually likes Barbarian more than coffee.

Barbarian is clad in armor which is covered in spikes. Spikes which have spikes, and disturbing stains. “I got the idea from the battlerager company I asked to stop bothering me. Those spikes are proper dwarven craftsmanship.” She smells a bit like the back of a butcher’s shop. Well, her armor does. No one really knows what Barbarian herself smells like, although Paladin has an idea. Paladin is a connoisseur of smells, he thinks Barbarian smells very pretty under all the armor. 

Paladin would never state this out loud, it might hurt Barbarian’s feelings. Paladin likes Barbarian, he thinks she is very nice. Barbarian would say that Paladin is just darling except she doesn’t know that word. At all. She’s caught herself starting to use the word “sweetie” when addressing Paladin, which is quite disturbing since prior to meeting Paladin, she didn’t know that word either.

Barbarian’s weapon of choice is a large double-bladed axe. It may be magical in nature, but the encrusted gore makes it hard to tell. No one wants to get close enough to tell in any case. After dealing with what she called a “minor problem” in Phlan’s main graveyard, (she called it a “minor problem”. The people of Phlan called it “a legion of undead bent on killing everyone in the city.” Perspective, it’s a thing), the mayor of Phlan emptied the city’s treasury into her sack. When asked why he overpaid to that extent, his answer was “I have seen things in the dungeons of Zhentil Keep that came not close to what that woman did to a vampire.” He then waddled off to change his clothing, as the memory of her had caused him to soil himself. 

Thoroughly.

When it comes to killing people, Barbarian is an artist. A gruesome, terrifying artist. Her and Monk are opposites in almost every way, and if they do not get some private time, and soon, they may just break a town.

As neither Monk nor Barbarian are particularly fussy about where they have dinner, they allow Paladin to choose. He initially thought the Sign of the Shield had nice smells, but the lack of a proper dining room puts him off. “I don’t like to smell food by myself, there is no one there to eat it when I am done, and the servants never believe that I didn’t do something to it. I feel bad about wasting food.” Monk and Barbarian note the very private room setup, and resolve to spend the night here. The building seems solid, it should survive.

Paladin nixes the Swords Meet immediately. Aside from his disdain for over-spicing food, (”If  all I wanted to smell was spices, I’d be a caravan guard in Sembia”), the fact the inn is basically a local gathering place for Zhents means that instead of smelling food, he’d be watching Monk and Barbarian slay Zhents. That doesn’t particularly bother him, but it’s been a long walk from Yulash, he really wants to smell some nice, properly-cooked food, and he doesn’t want to have to deal with the smell of dead Zhents. 

They all agree on the Three Elves. It has unlimited food for only five silvers (Barbarian approves), plenty of coffee, (Monk approves) and there is no wall between the kitchen and the dining rooms, so not only can Paladin smell his food, he can smell the cooking, and that is an uncommon joy for Paladin. 

The three sit down and fork over their money. Barbarian pays, she lost a bet with Monk in Yulash. Haundrae, one of the four owners of the in starts to look askance at the money, (understandable given the stains on the gauntlet that drops it into his hand), but sees that it’s attached to Barbarian and decides that well, silver washes just fine. The owners, also the cooks, are soon busy as can be cooking the vast amount of food the trio requests. Barbarian can out-eat a small company, Monk doesn’t eat as much, but he does adore food, and Paladin orders one of everything so he can smell it. 

They’re seated near the kitchen area, and soon the cooks are beaming in the light of the joy emanating from Paladin at the plethora of smells he is bathed in. (Literally. Paladin gets a bit glowy when he’s this happy.) Well, the smells from the food and the kitchen. The smells coming from Monk and Barbarian are not as joyful to him. Fortunately, the food and the kitchen smells outweigh them. Paladin resolves to spend the night in this inn. Hopefully near the kitchen. 

The other three owner-cooks of the inn are a trio of large women, in all dimensions, two are sisters. They’re fantastic cooks, possessed of bubbly flirtatious natures. They are not however, flirting with Monk. This is less due to his personality than the fury they see on Barbarian’s face if they become too familiar with Monk. Monk would normally not be okay with this level of possessiveness, but he’s too enthralled by both Barbarian’s hair and her oft-demonstrated artistry with dismemberment and mayhem. He has found his soulmate. She has found what she hopes is the first man who can keep up with her. Regardless, she makes it clear any extraneous touching of Monk may result in the sprouting of bloody stumps on the toucher. She does this by calmly licking the blade of her axe while glaring.

Perhaps “calmly” isn’t the best word. But the growling is barely audible and her teeth only chatter a little.

The meal is served and devoured with no unnecessary bleeding, so it all works out. This is helped by the large tip Barbarian leaves. (She’s a very good tipper. It’s why she’s welcome in almost every tavern in Toril. Especially the ones she’s destroyed.) As dessert and the empty third keg of Brown Nut Ale are cleared, Paladin says “I like this place, it smells nice. I think I’d like to stay here tonight, what do…” He never finishes the sentence, Monk and Barbarian are already out the door heading for the Sign of the Shield. Fortunately, Monk is slightly faster than Barbarian, so the door to the inn remains intact. Monk only breaks down doors when required, totally not chaotic.

Paladin hears the door to the Shield open and slam shut, then something that sounds like an innkeeper (Mester) starting to refuse to rent a room to Barbarian and Monk followed by…well, imagine the sound pure fury makes as it shoves a handful of platinum into an uncomfortable place and I don’t mean the back of a ffolkewagon. Paladin and everyone in the Three Elves all decide that everything is fine, no need whatsoever to check on the innkeeper over at the Shield. Mester will be fine. Probably. It can absolutely wait until morning.

A few minutes later, there’s the sound of what might have been some of the Shield Trading Company’s veteran guards running into the inn to deal with unruly guests, being tied in a knot and flung into the street through the front door. But everyone who heard the clanging and the thudding and the soft sobbing decided the best course of action was to Mind Their Own Business, and let the clerics at the House of Holy Light know they may want to do something about the guards, the sobbing was somewhat off-putting. Also, no one should be left in the shape of a square knot for too long, it’s a bit hard on the back.

To this day, no one is precisely sure what actually happened to the Sign of the Shield, the other guests, along with the remaining un-knotted staff had fled within minutes of Monk and Barbarian going into their room. None of them would ever speak of what they saw or heard. It took three days before anyone could walk near, much less into the building without instantly sprouting hair all over their bodies. 

This was particularly disturbing to the elves in town. They’re not used to needing depilatory magic. 

One of the minstrels from the Flying Stag was asked by the Bron’s men to describe the noises coming from the Shield that night and responded with “Have you ever heard the sound of a peryton being violated by an orthon while six random demons all sing Rashomeni opera in perfect harmony? No? Neither have I, but I now know exactly what that sounds like, and if it’s all the same to you, I plan on drinking until I can no longer remember it. Or anything else.” In an act of desperation, the Bron sent a letter to Storm Silverhand asking for her help, along with a description of Monk and Barbarian. Her only response was “You let them in, you deal with it.”

The building itself survived after a fashion. The outer walls were mostly intact, the inner structures were almost a total loss. All of the bricks in the second-floor ceiling had to be replaced, they looked like someone had managed to use them as handholds in several places and, somehow, footholds in several others. The tack and other gear in the stables were either found destroyed or so defiled that they had to be burned. At least two rooms required the services of every cleric in the town to get the stones in the floor to stop whimpering. 

There was some thought to mustering up a posse to arrest the trio, but firstly, no one in the town was even slightly willing to take part. The un-knotted members of the Shield Trading Company all lined up at the dismemberment crosses when ordered to ride after them under pain of slow death, stating “this will hurt less.”  

The second discouraging factor was the very large mound of platinum found in one of the bathing rooms, (when counted, enough to buy a new inn and half of Voonlar), the only undamaged room in the building. On top of the platinum was a note in precise, perfect script that read “Thank you so much for your hospitality and professionalism during our stay. We hope this payment will cover any accidental damages we may have caused.” Underneath were three words in less neat script, quite possibly using some form of blood for ink which read: “This. Never. Happened.”

The entire town agreed on that.

How It Happened That Monk And Paladin Met Barbarian

Six Feet of Violence in both hair color and interaction with the world

Monk and Paladin are just outside of Yulash. This isn’t a scenic area with trees and squirrels. Yulash is a pit, little more than a battleground between Hillsfar and Zhentil Keep with the occasional escaping slime god. At this point, the only real difference between the factions is the color of their helmet brushes. That, and the Zhents don’t have helmet brushes, they’ve always won the style war on Toril. Completely evil, but fabulous on the runway.

Monk and Paladin are standing outside of what would be an open road into Yulash were it not for the barricades. And the slime. Yulash generates slime the way Monk generates hate.

Monk: “Paladin, I can’t remember, are those mercenaries Hillsfarian or Zhents?

Paladin: “Zhents. They don’t have the silly helmets”

Monk: “Right, I always forget about that. Do they look like they have any pet beholders then? The Zhents love those for some reason.”

Paladin: “Well, they do make winning fights easier, but I don’t see any, it’s hard to hide a beholder.”

Monk: “Good point. Still, it will be a fight, and…”

Just then, the largest woman either Monk or Paladin have ever seen walks up. She’s about as tall as Monk, but where he’s lithe, she’s built like…well, a brick shithouse. She is also carrying an axe of considerable size and sharpness, decorated with what looks like years’ and entire towns’ worth of dried blood. She also has pink hair. Very pink hair. Lathanderites weep in jealous rage over how pink Barbarian’s hair is. This is important to Monk, he has a thing for pink.

Barbarian: “Are these Zhents causing you trouble?”

Monk: “Not as such. They might try in a few minutes, we’re trying to decide if Yulash is worth the trouble of walking through it, or just avoiding the entire mess.”

Barbarian: “Yulash is never worth it, it’s a pit of a pit. Latrines smell better. The only reason people go through Yulash is walking around Yulash takes longer and it smells so bad even Otyughs stay away. But, since they are Zhents and they might possibly get in my way or otherwise vex me…”

With that, Barbarian dons the rather impressive helmet she’d been wearing on her hip, unshoulders her axe, and with a loud and rather profane battlecry, runs towards the barricade, intent on bisecting Zhents.

Paladin: “Monk, I have never seen a helmet like that. Even the spikes have spikes.”

Monk: “Yes…isn’t she dreamy?”

Paladin: “What?”

Monk: “Um, nothing, I said nothing. Come on, let’s see how she does. This could be fun if she’s any good.”

Barbarian hits the Zhent’s barriers at full speed. That’s less impressive than the fact she doesn’t seem to actually notice the barriers. They notice her, in a “turning to flying scrap” kind of way.

Monk: “Paladin, you have a decent memory. Do you ever remember seeing people killed by someone by running into a wooden barrier so hard, they were impaled by the scraps?”

Paladin: “There was that dragon just south of Anuroch that did that to a bunch of Purple Dragon Knights.”

Monk: “Does that really count? I mean, he flattened a town with one swing of his tale, I think that’s accidental. Look, she hit that second barrier just right to put the scraps into that crossbow-type in the shape of a “Z”. That’s either artistry or showing off.”

Paladin: “You asked if we’d ever seen that before. If you want a different answer, you have to ask a different question.”

Monk: “Fair. Oh my, a beheading that kills the guy behind the decapitee with the flying head. See Paladin, you just don’t see artistry like that any more. Nowadays it’s all hack, hack, slash, slash. Style. that’s what we are missing, style.”

Barbarian: “Are either of you two going to help or just provide bad commentary?”

Monk: “Our commentary is not bad, it is witty and wise. Besides, you need help less than any army I’ve ever seen. The Tarrasque needs help more than you do. Oh wait, you have a scratch on your arm. Here, let me heal that.”

Monk walks over to where Barbarian is doing something very rude to a wizard with the handle of her greataxe and heals a cut that is maybe an inch long.

Monk: “There, I have helped…hm, I never thought about cleaning weapon hilts with arterial spray. That is a neat trick, I’ll have to remember it.”

Barbarian stops mutilating the now quite dead wizard, and stares at Monk with the normal response to his version of “humor”, that is, exasperation and barely-masked annoyance.

Barbarian: “I should let you handle the rest of this and make funny comments of my own.”

Monk: “Will you buy me dinner if I can handle the rest of this lot in under a minute without moving my feet?”

Barbarian gives Monk the once-over.

Barbarian: “if you can do it in under forty-five seconds, you might just get more than dinner, you’re more pretty than your mouth is big. Besides, they’re kind of boring.”

Monk doesn’t waste time agreeing, he simply bends down, grabs an armful of kindling that used to be part of a barrier and turns to Barbarian.

Monk: “The clock starts once you walk back over to where Paladin is standing. You’re blocking part of my view.”

Barbarian gives Monk a wry smile, puts her axe over her shoulder and walks over to Paladin. Some of the Zhents act like they were thinking about using their crossbows on her, then they look at the wizard she’s turned into some kind of very wrong sponge art with the haft of the greataxe and decide that Monk would be a better target.

Barbarian: “Okay, ready…GO”

Monk, without moving his feet, because that’s the bet, and keeping the terms of a bet is totally not chaotic, does a pivot that shouldn’t be possible for creatures with a spine. The crossbowmen who made the decision to shoot at him realize they decided poorly as he catches their bolts with one hand and returns them. At high speeds. Into rude places, and we don’t mean the back of a Ffolkwagon. The remaining wizard casts various things at him, none of which work.

Paladin: “Hi, lady with the very large ax, are you our new friend?”

Barbarian looks at Paladin for a minute. She’s never seen a warforged, much less one as smilely as Paladin. It would look creepy, but he reeks of sincerity, goodness, and a less-than-bright view of life she, and everyone who is not evil find charming. (To be honest, even the evil lot like Paladin. They’d like it if he didn’t cause them so many problems, but it’s really hard to genuinely hate Paladin. That, they reserve for Monk. There’s a town just outside of Neverwinter that has a yearly hate parade in dedication to Monk.)

Barbarian: “Well, I’m definitely yours, I like you, you’re nice. Him, we’ll see.”

Paladin: “We get that a lot…oh, they shouldn’t use magic on Monk.”

Barbarian: “It doesn’t seem to work well.”

Paladin: “It also makes him itchy. He hates that.”

Barbarian: “Understandable.”

As the other two talk, Monk bounces the kindling in his hands a few times, then with a sweep of both arms, launches it at the remaining Zhents. Every one of them goes down with a piece of wood in the throat. Monk looks around at the now-twitching corpses, shaking his head.

Monk: “The Zhents just hate throat guards. I’ll never understand why.”

Barbarian: “They like choking, and I don’t mean in combat. It’s a thing with them.”

Monk rolls his eyes at that.

Monk: “Zhents. Anyway, you owe me dinner.”

Barbarian: “I do indeed, but not in Yulash.”

Monk: “No, eating here is awful. Even Otyughs know that. This town gets slime on everything somehow. Even sealed ale barrels somehow have globs of slime. It’d be an impressive trick if it wasn’t making the food so rancid.”

Barbarian: “Hmm…not Shadowdale, they annoy me.”

Monk: “Ditto, someone there thinks he knows everything, and never shuts up about it.”

Barbarian: “And how. How about you my new…whatever you are friend?”

Paladin: “I’m a warforged. I’m like a golem, but I’m a person and I don’t just murder people at someone’s command. I’m a paladin because I like being good. But I don’t eat, I just like to smell things.”

Barbarian: “You are truly the cheapest of dates, and possibly quite handy in preventing awkward social faux pas. Okay my fine whirring friend, where is your favorite place to smell food.”

Paladin thinks about this. Fortunately, smelling food is one of his favorite things, and he can actually think about that well. Otherwise, they might grow old waiting for an answer.

Paladin: “Voonlar is nice. Ashabenford is better, but we have to go through Shadowdale, and it’s full of people who know everything and like to tell me about it. Constantly. That much talking makes it hard to smell my dinner. Also, Storm Silverhand threatened to turn Monk inside-out if she ever saw him again.”

Barbarian: “Why am I not surprised. Voonlar it is then, dinner’s on me.”

Monk: “I certainly hope so.”

Barbarian: “What was that?”

Monk: “Um nothing, I said nothing. Let us be off to Voonlar then.”