MacBook, defective by design banner

Put the knife down and take a green herb, dude.


One feller's views on the state of everyday computer science & its application (and now, OTHER STUFF) who isn't rich enough to shell out for

Using 89% of the same design the blog had in 2001.

Back-up your data and, when you bike, always wear white.

MarkUpDown is the best Markdown editor for professionals on Windows 10.

It includes two-pane live preview, in-app uploads to imgur for image hosting, and MultiMarkdown table support.

Features you won't find anywhere else include...

You've wasted more than $15 of your time looking for a great Markdown editor.

Stop looking. MarkUpDown is the app you're looking for.

Learn more or head over to the 'Store now!

Friday, February 14, 2020

9to5 quotes Edison, makers of Edison Mail:

To keep our Edison Mail app free, and to protect your privacy by rejecting an advertising-based business model, our company Edison Software, measures e-commerce through a technology that automatically recognizes commercial emails and extracts anonymous purchase information from them. Our technology is designed to ignore personal and work email, which does not help us measure market trends.

Hahahahahahaha! MUHAHAHAHAHAHA!!!

Yeah, right. Okay. I'd like to hear how that "technology" works. If it keys on known marketing email addresses, okay, sure. If my locally running app is sniffing all my emails for marketing-speak (receipts, whatever), I'm suspicious.

And 9to5 does a good job saying why this is wack. I mean, Google is doing this like mad in Gmail, and probably not nearly as safely as Edison. But Gmail alos doesn't do this... (again from 9to5)

And when looking at the big picture, Edison having phrases like “privacy by design” and “privacy first” on its website can feel misleading after learning about how they scrape and sell personal data.

Sell your email client. Charge a subscription. But don't turn on email sniffing by default without telling every customer before they add an account.

That's evil.

Labels: , , , ,

posted by ruffin at 2/14/2020 08:54:00 AM
Thursday, February 13, 2020

I gave away my iPhone to someone who needed it more than me, but also because, admittedly, I hated the XR's Face ID and I'm waiting for the new "SE" with the latest CPU but no Face ID.

In the meanwhile, I've been using an old Android phone -- the first Nokia 6, upgraded to Android 9.

I've also been using my AirPods, which I didn't give away, hooking them up as plain ole Bluetooth headphones to the Nokia.

At first, things were great. But yesterday in the Starbucks while I was trying to drone out the, um, ambient noise, the AirPods weren't giving me any serious volume, and I could barely here ye olde tunes at full blast.

Turns out this is a common problem. The solution is insane, but works.

Cnet has a decent overview, apparently stolen from Reddit. I'll hit the high points.
  • Open Settings.
  • Scroll to the bottom of the page and tap on System.
  • Locate Build Number (may be located under About Phone).
  • Tap on Build Number seven times, after which you will see an alert congratulating you for being a developer. [you'll also see an alert when you're getting close, that you only have X more clicks to become a developer -mfn]
  • Go back to Either the main Settings page or the System page and look for Developer Options and tap on it.
  • Scroll down and find Disable Absolute Volume and turn the switch to the On position.
Now for the step the cnet author misses:
  • Disconnect and reconnect your AirPods from Bluetooth settings (long press the Bluetooth setting icon to access, or however you normally get to settings). 
I don't think I even needed to "forget" them, just dis/re/connect.

Voila. But be careful. They can go pretty loud now.

I wonder why mine started out loud and then went soft. I kinda wonder if using them with my Mac, for instance, set their internal volume low. Without a switch to change volume, maybe the AirPods use the same idea, but do it all in software my Nokia can't access.

But I haven't tried reconnecting them to my MacBook (I use Tooth Fairy, which makes it pretty easy) to see if I can set them high there and then re-pair with the Android phone. I guess I should.

Not nearly as cool as they are with an iPhone, but still by far the easiest headphones to carry with me, and they're with me nearly as often as my keys are at this point, because I hate "ambient" noise almost as much as Face ID.

No, not true. There are few things I hate as much as Face ID. ;^)

Labels: , , , ,

posted by ruffin at 2/13/2020 08:33:00 AM
Wednesday, February 12, 2020

Looks like DuckDuckGo uses Apple Maps now, but not for directions.

I kinda got the feeling they were going all in with Apple from articles like this one from MacWorld...

Today, privacy-minded search engine DuckDuckGo announced that it will use the new MapKit JS tool for all its location-based search results. Previously, the site used OpenStreetMap for its results, with a drop-down box kicking you off to Bing, Google, or Here maps if you asked for directions.

... but I guess that's not exactly the case.

Labels: , ,

posted by ruffin at 2/12/2020 04:24:00 PM
Thursday, February 06, 2020

From The Verge:

Over the years, Apple refused to offer more flexible pricing options to developers. There never has been (and may never be) such a thing as “upgrade pricing” in the App Store, unlike on more open platforms like the Mac. Developers either had to offer major new versions for free, charge current users the same amount as new users, or try to jerry-rig an in-app purchase system.

One, you can't say "jerry-ing". Look it up.

Second, why can't we sell subscriptions to "Vol. 1 of My App"? That is, are subscriptions necessarily time bound?

Luckily, it doesn't matter. It's always seemed to me that there's got to be some slick way of having features unlocked based on your purchase date and which in-app purchases you've purchased later. Why can't you sell "v2 features" as a $10 in-app purchase? The only thing stopping you (and this is a complaint I've seen) is that your app has to support both versions.

That is, if you don't want to change the SKU (TIL: "stock keeping unit" -- add fuel to my earlier The iTunes Warehouse And Dollar Store), you have to have both a version 1 and a version 2 in the same app bundle.

Sorta. If you don't think of it as a weird mutli-headed creature (if no pay, display v1; if yes paid, display v2 <<< bad/dumb), but as your new creature with crucial [new] features behind a paywall, you're done. Who cares if, a few versions in, you save some dev time by allowing the people who paid for v1 get a few v2 features even though they haven't paid for that or your new version 4? They weren't paying, so it's not really a revenue loss.

And that, surprise, exactly what Overcast, Castro, and Fantastical (a few places I've paid and gotten bonus features in new, subscription based versions) have done.

From The Verge's review of the new Fantasical:

If you already own Fantastical 2, though, Flexibits has a pretty cool offer to help mitigate that feeling, in part. If there’s any feature in Fantastical 2 that is now a Fantastical Premium feature, you will still be able to use that feature in the updated app on the platform you own it on, even without a Premium subscription. You won’t need to sign up for a Flexibits account to keep that functionality. And Simmons tells The Verge you’ll still get free bug fix updates as well.

See? Who cares if the people who weren't going to pay the new price get some bug fixes? That's free for them, but it's also essentially free to you -- there's no price to send out that bug fix to more people for app store apps.

And I say this as a Fantasical 2 owner -- for iPhone. I had the iPhone version on my iPads too (no reason to pay twice to get an iPad specific UI, though I considered it), and then, poof!, one day the last week or two it upgraded on my iPad to a full iPad UI! That was a surprise.

At first, I was a little peeved, since I actually liked the old iPhone UI on my iPad, especially my iPad mini (which is old enough I still have to use 2). iPhone UI on iPad was ugly, but tres functional. But now I get the new UI... and liking it after my "free iPad UI exposure" means there's some new advertising too.

Thanks to the "free" upgrade, I know that I could spend more to get a potentially fancier experience. I probably won't, but they've lost almost certainly exactly zero dollars (and lost exactly zero good will; they've probably even built a little) giving me part of v3 in my v2 for free.

Labels: ,

posted by ruffin at 2/06/2020 10:17:00 AM
Tuesday, February 04, 2020

From Michael Tsai's 2016 post, What It’s Like to Take on Venture Capital Investment:

Matt Henderson:

This is fascinating insight into the, I suppose unsurprising, mindset of many venture capital investors. They’re not looking for a profitable business; instead, they’re looking for growth that provides the opportunity for a 100x exit. And their expectation is that you, the founder, will work to achieve that at any cost. And since their investment also brings the expectation of participation and inclusion in the running of the business, any company owner considering taking on investment would be well advised to make sure at the outset that everyone’s on the same page in terms of objectives. [emphasis mine -mfn]

If you don't understand this about venture capital, you're completely missing the boat. It's also why so many startups seem to make really dumb long-term decisions to help tomorrow's bottom line -- they're beholden to people who want to exit and profit personally, not wait around and slowly collect from a profitable company.

If you save and save while you work a 9-5 and open a family restaurant, you hope that investment brings in enough cash that you can live off of that income. You don't want to drain the investment. You want to have it pay for itself and you. That requires profit.

That's not what venture capital does. These investors are looking for a profitable exit, not a long-term investment. They're looking to flip the house so that they can take that capital and flip another, not collect rent for 50 years. They're looking to sell quickly so that they can buy again, not sit and collect dividends.

And that's smart for them. If you can invest $X and get back 10% of that $X at 100x, then you've already got 10x the cash you had before, ready to do it again. That is, if you give someone a dollar, even if they throw 90¢ of it away immediately, and they take that dime and come back to give you $11, you still made $10 in spite of that 90¢ dying a painful death! (Kudos if you see that I mixed numbers there.)

There are very few companies that can appreciate at 10,000% (100x) over seven or so years. And those that do almost certainly can't do it again. Venture capitalists win by finding just one company with mad growth. Then they get the heck out. The bonaza's over.

You don't care about profit. You just care about getting out LOTS more than you put in and keeping the ball rolling. And for companies that aren't going to explode, you want to cut your losses quickly and stop wasting your time "helping" them. That money's gone -- not that venture won't take back 2x, because they will, happily. But if there's no 100x in their future, why keep pushing good money after average?

That's how lots of big money's made, folks. You invest, you find the cash cows, you milk 'em, and you get out to do it again. (And it's also why they require obscene percentages of companies for cash before you've proven yourself to be a good conventional investment.)

Labels: ,

posted by ruffin at 2/04/2020 02:56:00 PM

Noticed something called googlesoftwareupdatedaemon eating up my CPU this morning, and was able to google (well, duck) up this from a MacRumors forum:

The good news [if you are having trouble with googlesoftwareupdatedaemon] is, you can go to the LaunchDaemons folder

(in Finder: Menu Bar > Go > Go to Folder…, enter /Library/LaunchDaemons, click the Go button)

and then delete the offending daemon, probably

Empty Trash, Restart. Done.

I'm not quite yet ready to delete it, but it's interesting to learn where this stuff lives.

Labels: , , ,

posted by ruffin at 2/04/2020 11:42:00 AM
Tuesday, January 28, 2020

Here are the important commands to keep around when logging what SQL statements you've thrown at MySQL when debugging, say, some Entity Framework issues...

-- SET GLOBAL log_output = 'TABLE';
-- SET GLOBAL general_log = 'ON';

SELECT * FROM  mysql.general_log  
WHERE command_type ='Query' 
and event_time > '2020-01-03 11:58:20'
ORDER BY event_time DESC

-- SET GLOBAL general_log = 'OFF';

Labels: ,

posted by ruffin at 1/28/2020 04:06:00 PM
Friday, January 24, 2020

Added Disqus comments to the blog today. I imagine they do all sorts of nasty things behind the scenes with cookies and JavaScript. I apologize for that in advance -- though see below. The way I've done it, you'll only get that nastiness if you go to a single-post page by permalink.

Note also that I use the really old version of Blogger where I have my own html theme with some Blogger variables (like <$BlogItemPermalinkURL$>) sprinkled within. Let's just say that the instructions to add Disqus to Blogger are out of date for dinosaurs like me.

Here's what I added to my template. Neat plus: It's not loaded at all on my front page, and since every post is listed in its entirety there, you shouldn't get any Disqus nastiness unless you go to a specific post. That said, I've always kinda liked Disqus as a comment manager, so I hope you'll find it useful.

<!-- Disqus comments -->
<div id="disqus_thread"></div>
var disqus_config = function () { = "<$BlogItemPermalinkURL$>";  // Replace PAGE_URL with your page's canonical URL variable = "<$BlogItemNumber$>"; // Replace PAGE_IDENTIFIER with your page's unique identifier variable

(function () { // DON'T EDIT BELOW THIS LINE <<< MUAHAHAHAHA!!! I did anyway. Because I needed to.
    if (
        "/" !== window.location.pathname                                // on the home page
        && "" !== window.location.pathname                              // on the home page with IE < 11, _mayhbe_
        && !window.location.pathname.match(/^\/*[0-9]+\/[0-9]+\/*$/)    // archive pages
        && window.location.pathname.indexOf("search") == -1             // tag search pages
    ) {
        var d = document, s = d.createElement('script');
        s.src = '';
        s.setAttribute('data-timestamp', +new Date());
        (d.head || d.body).appendChild(s);
<noscript>Please enable JavaScript to view the <a href="">comments powered by Disqus.</a></noscript>
<!-- eo Disqus comments -->

Key lines: = "<$BlogItemPermalinkURL$>";  // Replace PAGE_URL with your page's canonical URL variable = "<$BlogItemNumber$>"; // Replace PAGE_IDENTIFIER with your page's unique identifier variable

I'm pretty sure those are the Blogger variables you want to use for those two properties.

And then le piece de resistance. Or whatever.

    if (
        "/" !== window.location.pathname                                // on the home page
        && "" !== window.location.pathname                              // on the home page with IE < 11, _mayhbe_
        && !window.location.pathname.match(/^\/*[0-9]+\/[0-9]+\/*$/)    // archive pages
        && window.location.pathname.indexOf("search") == -1             // tag search pages
    ) {   

By adding that check in the shudder area Disqus says not to edit, you make it so that Disqus isn't loaded on your home page (or on archive review pages like this one), which is good, because who wants Disqus on every freaking post on your home page? That'd be lots of wasted space.

Taking the Disqus UI off of posts on the home page of the blog is also good because the UI will only pop up under the first post on the page. That's because Disqus doesn't honor multiple instances of its UI on the same URL. Which leads us into Good Thing #3: Those comments on the first page would forever be for the first post on the home page, which would change from day to day. So it's an important check.

Anyhow, with that info in hand, all you dinosaur Bloggers can add Disqus invasive nastiness to your posts too and enable comments in a manner that's much more mature than all the crap Google and Google+ has tried to pull (and then pull back!) over the years.

Labels: , ,

posted by ruffin at 1/24/2020 11:53:00 AM
Wednesday, January 15, 2020

Here's Today Adventure in Enjoyable AngularJS...

I ran into some code from a project that looked give or take like this...

namespace.directive('caNavigation', function () {
    "use strict";

    return {
        restrict: 'E',
        scope: {
            inLinks: "@",
            collapseNav: "="
        // ...
            ["$scope", /* more jive */],
            function ($scope /* more jive */)
            $scope.$watch('inLinks', function (value) {
                // omgwtfbbq?
                // ...

Well, that's @ and = stuff is pretty cryptic, especially since I hadn't used a directive in AngularJS before (Angular 2+, sure, but still learning ye olde AngularJS -- going backwards, I know. Don't forget I had my first run-in with Perl in 2014 [sic]).

Let's see what ye olde docs say:

[Having to have a separate controller to change direct scopes] is clearly not a great solution.
What we want to be able to do is separate the scope inside a directive from the scope outside, and then map the outer scope to a directive's inner scope. We can do this by creating what we call an isolate scope. To do this, we can use a directive's scope option:

Let's take a closer look at the scope option:
scope: {
customerInfo: '=info'
The scope option is an object that contains a property for each isolate scope binding. In this case it has just one property:
  • Its name (customerInfo) corresponds to the directive's isolate scope property, customerInfo.
  • Its value (=info) tells $compile to bind to the info attribute.

Good heavens, AngularJS. Not the most discoverable. Seems like in AngularJS in general there's lots of magic stringing. Perhaps I shouldn't be surprised, but that doesn't mean I like it. (Here I mean largely inline array notation, which is too hipster by half. I mean, I get that you don't what to lose information when you minimize, but then how about pass real references, not refer to objects by their original names? Sheesh.* )

But let's go a little deeper so that we know what the @ symbol means too.
The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the directive's element. These local properties are useful for aliasing values for templates. The keys in the object hash map to the name of the property on the isolate scope; the values define how the property is bound to the parent scope, via matching attributes on the directive's element:
  • @ or @attr - bind a local scope property to the value of DOM attribute. The result is always a string since DOM attributes are strings. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given <my-component my-attr="hello {{name}}"> and the isolate scope definition scope: { localName:'@myAttr' }, the directive's scope property localName will reflect the interpolated value of hello {{name}}. As the name attribute changes so will the localName property on the directive's scope. The name is read from the parent scope (not the directive's scope).
  • = or =attr - set up a bidirectional binding between a local scope property and an expression passed via the attribute attr. The expression is evaluated in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given <my-component my-attr="parentModel"> and the isolate scope definition scope: { localModel: '=myAttr' }, the property localModel on the directive's scope will reflect the value of parentModel on the parent scope. Changes to parentModel will be reflected in localModel and vice versa. If the binding expression is non-assignable, or if the attribute isn't optional and doesn't exist, an exception ($compile:nonassign) will be thrown upon discovering changes to the local value, since it will be impossible to sync them back to the parent scope.
    By default, the $watch method is used for tracking changes, and the equality check is based on object identity. However, if an object literal or an array literal is passed as the binding expression, the equality check is done by value (using the angular.equals function). It's also possible to watch the evaluated value shallowly with $watchCollection: use =* or =*attr
If you've used Angular 2+ (what a failed naming scheme, btw. Why not "Angular.IO" after the website or just NGular or something so that googling this jive would be easier?), you can already see our steady progression to banana boxes.

But for now, what a mess.
  1. If a scope property is a string that starts with @, then we'll initialize that scope property with the directive's DOM attribute that matches what comes after the @.
    • That is, this is a one-way process. Props down, and set up events if changes need to go back up.
  2. If the prop starts with an =, we're doing the equivalent of Angular.IO's banana boxing, it appears.
    • So if we change whatever's in here, the parent is changing too.
Back to our original example now... We had a cryptic (to me) $scope.$watch('inLinks', function (value)..., and now we know what that means. @ means we've got a prop that $watch can check for changes, which, in this case, means:

  • The listener is called only when the value from the current watchExpression and the previous call to watchExpression are not equal (with the exception of the initial run, see below). Inequality is determined according to reference inequality, strict comparison via the !== Javascript operator, unless objectEquality == true (see next point)
  • When objectEquality == true, inequality of the watchExpression is determined according to the angular.equals function. To save the value of the object for later comparison, the angular.copy function is used. This therefore means that watching complex objects will have adverse memory and performance implications.

Guess that works. Let's ignore the =* shallow comparison jive for now. All we've really got is one prop called inLinks that will be passed... um... "in" that we'll update whenever it changes, probably after the page finishes loading and making some resource calls. And sure enough...

<li ng-repeat="subLink in link.SubLinks|filter: {ShowInNav: true}" ...

That means once we've loaded the correct navigational links for this context, we'll start pushing them into the DOM.

And our code makes sense. Much rejoicing.

* I actually kinda like AngularJS. It's one clean step away from the inefficient, overly-engineered land of enterprise development Bjarnason discusses and that at least the social convention surrounding Angular 2+ seems to require. But there are a few stupid* conventions like this one that really do AngularJS in conceptually.

* Sorry, I've tried to come up with a good synonym, but "stupid" fits here. My earlier use of "hipster" was my kind attept at an alternative, but this stuff really feels a little too much like a first-pass solution to be in a nice, mature templating library.

Labels: , ,

posted by ruffin at 1/15/2020 05:32:00 AM
Tuesday, January 14, 2020

I'm not sure why I didn't look it up earlier, but here's how to exclude files from your Sublime Text 3 projects -- where the real benefit to me is that libraries won't pollute my search results.

Open up your Settings (Preferences menu, then Settings). In the pane that opens, your user settings will appear as a json file on the right.

If you don't already have these properties, add them with your desired values...

"folder_exclude_patterns": ["angular"],
"file_exclude_patterns": ["angular.js", "*.min.js"]

Thanks to here for getting me started with folder_exclude_patterns, and to Sublime for have settings where property names are as easy to guess as they were in this case.

Labels: ,

posted by ruffin at 1/14/2020 01:19:00 PM

Support freedom
All posts can be accessed here:

Just the last year o' posts:

URLs I want to remember:
* Atari 2600 programming on your Mac
* joel on software (tip pt)
* Professional links: resume, github, paltry StackOverflow * Regular Expression Introduction (copy)
* The hex editor whose name I forget
* JSONLint to pretty-ify JSON
* Using CommonDialog in VB 6 * Free zip utils
* git repo mapped drive setup * Regex Tester
* Read the bits about the zone * Find column in sql server db by name
* Giant ASCII Textifier in Stick Figures (in Ivrit) * Quick intro to Javascript
* Don't [over-]sweat "micro-optimization" * Parsing str's in VB6
* .ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); (src) * Break on a Lenovo T430: Fn+Alt+B
email if ya gotta, RSS if ya wanna RSS, (?_?), ¢, & ? if you're keypadless

Powered by Blogger Curmudgeon Gamer badge
The postings on this site are [usually] my own and do not necessarily reflect the views of any employer, past or present, or other entity.