MacBook, defective by design banner

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


descrip:

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 www.myfreakinfirst-andlast-name.com

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

FOR ENTERTAINMENT PURPOSES ONLY!!!
Back-up your data and, when you bike, always wear white.

As an Amazon Associate, I earn from qualifying purchases. Affiliate links in green.

x

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!

Thursday, May 07, 2026

Note to self:

for /f "tokens=*" %i in ('git branch --format="%(refname:short)" ^| findstr /C:"feature/"') do @echo git branch -m "%i" "old/%i"

Then remove @echo to make it "really happen".

(That adds old/ to the front of anything that starts with feature/.)

Labels: , ,


posted by Jalindrine at 5/07/2026 11:38:00 AM
Wednesday, April 29, 2026

Note to self: To look through your git history to find files by name (and to have the files edited in each commit listed)...

git log --all --name-only -- "**/*Base*"

Though note that it's case sensitive, and not because I used a capital B. It's always case sensitive. Looks like there are weird workaround depending on your git version, like not including a glob for filename and piping EVERYTHING to grep to figure out or using git's really unwieldy pathspec.

Labels: ,


posted by Jalindrine at 4/29/2026 11:35:00 AM
Wednesday, January 07, 2026

I mean, I already knew (if only based on the hard flattening of my own reputation graph) that StackOverlow was dead, but sheesh. Today I learned...

Prompt: how to mark xunit test as do not run but still allow test to be debugged

xUnit v3 introduces an [Explicit] property on the [Fact] and [Theory] attributes specifically for this purpose. Explicit tests are not run by default unless specifically requested via a runner command line switch or by an IDE gesture (like right-clicking and running an individual test).

... or, later...

Apply a Trait: Ensure your tests are decorated with the [Trait] attribute, for example:

[Trait("Category", "Integration")]

Enter Filter Syntax: In the search box at the top of the Test Explorer, type the exclusion filter using the following format:

-Trait:"Category [Integration]"

That second, in my case, does, in fact, work well.

Why bother with clicking into SO to search for a good answer when I can have a conversation with AI and prompt my way to perfection?


I posted to Mastodon a while back that anyone who thinks we ยญaren't going to have humanoid robots sooner than later is missing the boat. Like different sized railroad gauges determining the size of the trains that use them, or that cars are roughly horse-buggy-sized, or that mobile homes are constrained by the width of our interstate lanes, or... or any other practical standard -- our world is largely human-sized! Not just that chair, but doors, tools, the things that tools work on (eg, nuts and bolts), the cabin of trucks/tractors/fire buckets, If you want to plug a multi-use device into something that can also be used by a human, you should have (among your stable) a humanish-sized model of your robot.

Same for LLMs! If you want "compute" to mesh well with humans, you need to create an interface that we already use if you're aiming for quick adoption. Keyboards & mice are good examples -- human hands are insanely dexterous if the keys and devices fit them. And poof, I have an 123 key (give or take) device to create code.

And so, no surprise, we quickly went from 0s and 1s to hex to assembler to coding languages that are decent compromises between written language and charted logic.

But that's for minutiae in code. If we want to describe at a higher level, we talk about it!

You get the point. Anyone who bet against LLMs better not use language themselves either, or they only have themselves to blame.

/soapbox

Labels: , , , , ,


posted by Jalindrine at 1/07/2026 10:56:00 AM
Friday, December 12, 2025

It's neat that you can program logic on most any computer that boots nowadays. Maybe not crazy UI libraries, but wrap properly factored middleware logic in a console interface or just write some unit tests and you can get a lot done without much compute at all.

What's important in a working laptop?

I used to look for stuff like replaceable batteries, but with USB-C power packs, that's not an issue if it has a USB-C port that allows power. Now my list is, give or take...

  1. Power & input in, video out via one port
  2. Expandable RAM
  3. Replaceable M.2 SSD
  4. Fingerprint reader (so I'm not typing PINs and passwords in public)
  5. Fits in my laptop bag (a nebulous requirement for the reader, I realize)
  6. A backlight for the keyboard (any color is fine)

Nice to haves:

  • A good keyboard (usually I'm plugged to a dock, but sometimes laptops on the lap (or coffeeshop table) are nice)
  • Better than 1920x1080 screen
  • Enough nits to see the screen outside
  • A TrackPoint nubbin'

My old ThinkPad P51, a serious monster that had a high-def screen and USB-C video output, finally gave up the Windows 11 ghost. The version I'd gotten (used) was, at one point, good enough to run Windows 11, but then was taken off the list! At some point, that was actually enforced and it stopped updating, no security updates, nothing.

I got a few more months out of Insider Preview, but then my keyboard and mouse died due to drivers, I spent hours debugging, and I finally gave up.

So back in the market. In the meanwhile, I've been using a gaming laptop from 2021 as my dedicated workstation in the home office (and at coworking a little), a super cheap ThinkPad E490s for mobile dev (coffeehouses, trips, and coworking), and my M1 MacBook Air when I need to macOS.

These museum pieces do well, and I've never regretted buying gaming laptops, as their CPUs alone give them years of headroom as development boxes.

But as it's time to replace both the high-end (gaming laptop is old) and the mobile workstation (P51 is dead), I am stressing too much about processors. I usually check out PassMark scores to get an idea of how fast they are. I don't know if it's accurate or useful at all, but it seems to give a pretty good relative number.

Unfortunately I keep forgetting what my current boxes' scores are for comparison. So hard right on this post's topic as we swerve into "note to self" land and record them. (I have a vague recollection I've done this before. Apologies.)

Box

CPU

Single-thread

Multithread

ThinkPad P51

i7-7820HX

2115

7185

ThinkPad E590s

i5-8265U

2019

5810

IdeaPad Gaming (3? from 2021)

Ryzen 5 4600H

2416

14178

M1 MacBook Air

M1

3678

14145

Lenovo LOQ 15 (2025)

i7-13650HX

3756

30479

ThinkPad E14 Gen 7

Ryzen 7 250

3761

25508

The last two are ones I'm looking at now. LOQ is for sale now for $850, and the E14 as I'd want it is $823.65.

Getting back on topic, the neat part is that the E590s I got off of eBay for under $100 a while back and inserted some extra RAM into has been my mobile box for a few months. It's occasionally a little slow, but for logic work it's... just fine.

If you've got $100, internet, and someplace to plug in with the ability and drive to learn to develop software, you've got a livelihood.

That's kind of amazing.

Labels: , ,


posted by ruffin at 12/12/2025 10:22:00 AM
Tuesday, June 17, 2025

Okay, was looking back through WCF earlier this week for a prospective client, and figured I'd leave some notes.

Here's the stock code, give or take:

public interface IService
{
    [OperationContract]
    string GetData(int value);
    // ...
}
public class Service : IService
{
    public string GetData(int value)
    {
    	return string.Format("You entered: {0}", value);
    }

    // ...
}

And all that's in Service.svc is <%@ ServiceHost Language="C#" Debug="true" Service="Service" CodeBehind="~/App_Code/Service.cs" %>

Again, this is the stock WCF project so far. Not overly fancy.

I wanted to be able to hit the stock endpoint from a WCF project in Visual Studio without using a WCF client. Getting to the WSDL file was easy: You can steal that from the page the WCF Test Client refers to if nothing else.

For me, that stock page was:

http://localhost:60817/Service.svc

Here's some of the startup page's contents:

You have created a service.

To test this service, you will need to create a client and use it to call the service. You can do this using the svcutil.exe tool from the command line with the following syntax:

svcutil.exe http://localhost:60817/Service.svc?wsdl

You can also access the service description as a single file:

http://localhost:60817/Service.svc?singleWsdl

Then the WSDL itself:

http://localhost:60817/Service.svc?wsdl

<?xml version="1.0" encoding="utf-8"?>
<wsdl:definitions name="Service" targetNamespace="http://tempuri.org/"
	xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
	xmlns:wsx="http://schemas.xmlsoap.org/ws/2004/09/mex"
	xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
	xmlns:wsa10="http://www.w3.org/2005/08/addressing"
	xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
	xmlns:wsap="http://schemas.xmlsoap.org/ws/2004/08/addressing/policy"
	xmlns:msc="http://schemas.microsoft.com/ws/2005/12/wsdl/contract"
	xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
	xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
	xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata"
	xmlns:xsd="http://www.w3.org/2001/XMLSchema"
	xmlns:tns="http://tempuri.org/"
	xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
	xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl"
	xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
	<wsdl:types>
		<xsd:schema targetNamespace="http://tempuri.org/Imports">
			<xsd:import schemaLocation="http://localhost:60817/Service.svc?xsd=xsd0" namespace="http://tempuri.org/"/>
			<xsd:import schemaLocation="http://localhost:60817/Service.svc?xsd=xsd1" namespace="http://schemas.microsoft.com/2003/10/Serialization/"/>
			<xsd:import schemaLocation="http://localhost:60817/Service.svc?xsd=xsd2" namespace="http://schemas.datacontract.org/2004/07/"/>
		</xsd:schema>
	</wsdl:types>
	<wsdl:message name="IService_GetData_InputMessage">
		<wsdl:part name="parameters" element="tns:GetData"/>
	</wsdl:message>
	<wsdl:message name="IService_GetData_OutputMessage">
		<wsdl:part name="parameters" element="tns:GetDataResponse"/>
	</wsdl:message>
	<wsdl:message name="IService_GetDataUsingDataContract_InputMessage">
		<wsdl:part name="parameters" element="tns:GetDataUsingDataContract"/>
	</wsdl:message>
	<wsdl:message name="IService_GetDataUsingDataContract_OutputMessage">
		<wsdl:part name="parameters" element="tns:GetDataUsingDataContractResponse"/>
	</wsdl:message>
	<wsdl:portType name="IService">
		<wsdl:operation name="GetData">
			<wsdl:input wsaw:Action="http://tempuri.org/IService/GetData" message="tns:IService_GetData_InputMessage"/>
			<wsdl:output wsaw:Action="http://tempuri.org/IService/GetDataResponse" message="tns:IService_GetData_OutputMessage"/>
		</wsdl:operation>
		<wsdl:operation name="GetDataUsingDataContract">
			<wsdl:input wsaw:Action="http://tempuri.org/IService/GetDataUsingDataContract" message="tns:IService_GetDataUsingDataContract_InputMessage"/>
			<wsdl:output wsaw:Action="http://tempuri.org/IService/GetDataUsingDataContractResponse" message="tns:IService_GetDataUsingDataContract_OutputMessage"/>
		</wsdl:operation>
	</wsdl:portType>
	<wsdl:binding name="BasicHttpBinding_IService" type="tns:IService">
		<soap:binding transport="http://schemas.xmlsoap.org/soap/http"/>
		<wsdl:operation name="GetData">
<!-- ================================================================================= -->
<!-- This part is reasonably important later.                                                     -->
<!-- ================================================================================= -->
<soap:operation soapAction="http://tempuri.org/IService/GetData" style="document"/>
<!-- ================================================================================= -->
<!-- ================================================================================= -->
			<wsdl:input>
				<soap:body use="literal"/>
			</wsdl:input>
			<wsdl:output>
				<soap:body use="literal"/>
			</wsdl:output>
		</wsdl:operation>
		<wsdl:operation name="GetDataUsingDataContract">
			<soap:operation soapAction="http://tempuri.org/IService/GetDataUsingDataContract" style="document"/>
			<wsdl:input>
				<soap:body use="literal"/>
			</wsdl:input>
			<wsdl:output>
				<soap:body use="literal"/>
			</wsdl:output>
		</wsdl:operation>
	</wsdl:binding>
	<wsdl:service name="Service">
		<wsdl:port name="BasicHttpBinding_IService" binding="tns:BasicHttpBinding_IService">
			<soap:address location="http://localhost:60817/Service.svc"/>
		</wsdl:port>
	</wsdl:service>
</wsdl:definitions>

What you want to be able to do to invoke a service endpoint directly is, of course, find its URL. In my case, with a little help from SoapUI, I figured out it was the same URL that started up initially:

http://localhost:60817/Service.svc

The first key is that you have to set up two headers to the request:

  • Content-Type
    • In my case, text/xml
  • SOAPAction
    • So for me, based on the WSDL above, that's http://tempuri.org/IService/GetData for the GetData action.

But you seem to have to send it a body in a POST -- GET gives you that stock opening page again. (Since GET and POST are REST conventions, I half-way expected the WCF service not to care, especially since we know GET can have a body now.)

I am not yet going to claim I know how the WSDL tells you what your input parameters should have looked like.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tem="http://tempuri.org/">
   <soapenv:Header/>
   <soapenv:Body>
      <tem:GetData>
         <!--Optional:-->
         <tem:value>5</tem:value>
      </tem:GetData>
   </soapenv:Body>
</soapenv:Envelope>

Again, do this all in a Postman POST request with a body that's raw with XML as its type and you're golden. Change tem:value to another int to see more insanely interesting messages like...

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
    <s:Body>
        <GetDataResponse xmlns="http://tempuri.org/">
            <GetDataResult>You entered: 5</GetDataResult>
        </GetDataResponse>
    </s:Body>
</s:Envelope>

I do not miss XML.


Update: Ha! Wish I'd talked to myself from 13 years ago first!

Labels: , ,


posted by Jalindrine at 6/17/2025 07:27:00 PM
Monday, June 16, 2025

Looking into updating a slew of VB.NET API to C# for a client, and figured I should brush off my VB.NET before getting too serious.

And I figured that means I should make a console app, connect to a sample API URI at JsonPlaceholder, grab a collection of entities, serialize them, loop through, and access something on each individual entity instance. That's half (or more) of what any API does, right?

Oh wow. Slightly larger rabbit hole than I figured.

First, you can't make the top-level Main sub Async in VB.NET. DotNetPerls makes it sound like you can wait out an Async Sub from the Sub Main, but you can't.

From stackoverflow.com:

Subs should not be async. Event handlers are the only exception to that rule. You awaitย Taskย which can only be returned from aย Function. If the intention is to make that interface async then all the members need to be functions that return aย Taskย or its derivative.

Can confirm.

The right answer is to set up an awaiter in a synchronous [sic] Sub Main.

From stackoverflow.com:

You won't be able to do top-levelย Awaitย in a console program. You can still make it work (preserving theย Asyncย onย LoadData) with the following changes:

  1. Change the signature ofย LoadDataย toย Private Async Function LoadData() As Task
  2. Change the call inย Mainย toย LoadData.GetAwaiter().GetResult()

It is somewhat amazing how few upvotes these have gotten at SO. Aka "VB.NET was never super popular, relatively speaking [to C#], was it?"

Here's some working code.

Imports Newtonsoft.Json

Module Program

    Public Sub Main()
        Console.WriteLine("Hello World!")

        DoStuff.GetAwaiter().GetResult()

        Console.WriteLine("Done")
    End Sub

    Private Async Function DoStuff() As Task
        Dim httpClient As New Net.Http.HttpClient
        httpClient.BaseAddress = New Uri("https://jsonplaceholder.typicode.com/")

        Try
            Console.WriteLine("Starting")
            ' BaseAddress is strangely draconian:
            ' https://stackoverflow.com/a/23438417/1028230
            'Dim result1 As String = Await webClient.GetStringAsync("https://jsonplaceholder.typicode.com/users/1/todos") ' if you don't use BaseAddress
            Dim result1 As String = Await httpClient.GetStringAsync("users/1/todos")
            Console.WriteLine("Ending")

            Dim allTodos = JsonConvert.DeserializeObject(Of IEnumerable(Of Todo))(result1)

            For Each element In allTodos
                Console.Write(element.Id & "--")
            Next
        Catch ex As Exception
            Console.WriteLine(ex.Message)
        End Try
    End Function
End Module

and the Todo class looks like this:

Public Class Todo
    '{
    '  "userId": 1,
    '  "id": 1,
    '  "title": "delectus aut autem",
    '  "completed": false
    '},
    Public Property UserId As Integer
    Public Property Id As Integer
    Public Property Title As String
    Public Property Completed As Boolean
End Class

As I think I've mentioned before, a prof told me in college, "They're all different dialects of the same language." Or as a buddy on the ultimate team said, "It's all zeroes and ones." Nothing fancy going on here, but it does take a while to get your ears tuned to the new (ancient?) dialect.

Labels: ,


posted by Jalindrine at 6/16/2025 11:24:00 AM
Friday, May 30, 2025

Note to self: It's pretty easy to get a Stripe CheckoutSession in .NET.

I talked about writing a Chrome extension and integrating payment through Stripe a few months ago. Unfortunately the "what appears to be an amazingly well-written howto" wasn't, quite.

So here's a quick bit of play with the Stripe API using .NET. This looks up information according to a "Checkout Session" id, which is what you'll get sent if you set up a "After Payment" processor URL, which is what I'm doing -- for now in an Azure Function, just to be kewl.

(No, you shouldn't use hard-coded paths in your code. No, you shouldn't pass around anonymous classed entities in C#. You're right, there's one instance of insanely not-DRY-ness in this code that almost bugs me. And I hate kludging ! after a nullable and would refactor that. This is pretty serious "proof of concept" territory here.)

Steps:

  1. Create a .NET console app.
    • For bonus points, use the newer template without a Main method version, I guess. I got this and was too lazy to start over.
  2. Add stripe.net to your project via your preferred NuGet manager.
  3. Get your Stripe secret key and put it in C:\temp\stripe.key
  4. Paste in the below junk, wiping out the Main method that's in there by default.
  5. Get a CheckoutSession id from, idk, say a Payment Link "After Payment" URL, and insert it into the code.
  6. Profit?
static void Main(string[] args)
{
    Console.WriteLine("Hello, World!");

    StripeConfiguration.ApiKey = File.ReadAllText(@"C:\temp\stripe.key").Trim();
    Service = new Stripe.Checkout.SessionService();

    var csId = "cs_test_a1RRCS1VWdOwOwvpJfdZCtjb9jSVOxp9ZhQibAI1Hv3dsvi6BfCPIWwdZP";
    var info = LookupStripeCheckoutSession(csId);

    Console.WriteLine(JsonConvert.SerializeObject(info, Formatting.Indented));
    File.WriteAllText(
        @"C:\temp\importantStuff.txt",
        JsonConvert.SerializeObject(info, Formatting.Indented)
    );
}

// Stripe Fees:
// https://stripe.com/pricing
// "Starts at 2.9% + 30ยข [per successful transaction for domestic cards]"
// Fudge.
public static object LookupStripeCheckoutSession(string checkoutSessionId)
{
    Session session = Service!.Get(checkoutSessionId);

    if (!session.Metadata.TryGetValue("appId", out var appId))
    {
        throw new MissingFieldException("Checkout session does not contain a product id");
    }

    System.IO.File.WriteAllText(
        @"C:\temp\session.txt",
        JsonConvert.SerializeObject(session, Formatting.Indented)
    );

    return new {
        CheckoutSessionId = session.Id,
        RufworkAppId = appId,
        session.AmountTotal,
        session.Created,

        session.PaymentStatus, // One of: <c>no_payment_required</c>, <c>paid</c>, or <c>unpaid</c>.

        // Note that TotalDetails (as with any child object brought over)
        // has its own json settings, so it'll, eg, serialize to snake case
        // if you don't munge it.
        session.TotalDetails,
        session.CustomerDetails.Email,
    };
}

I'd been thinking about having a $3/year [sic] subscription for an extension. After a test, I learned what the comments, above, mention: That'll cost me ($3 x .029 + 30ยข somehow equals...) 41ยข [???] per transaction.

Okay, apparently there's a "Usage fee" in addition to the charge?

Amount

Fees

Total

Type

Description

Created

Available on

-$0.02

โ€”

-$0.02

Stripe fee

Billing - Usage Fee (2025-05-26)

May 27

May 27

$3.00

-$0.39

$2.61

Charge

Subscription creation

May 26

Jun 2

From togai.com:

Stripe allows you to perform end-to-end financial transactions with its suite of integrated offerings:

Stripe Billing - This lets you create recurring subscriptions and invoices. Pricing starts at 2.9% + 30๏ฟ for every successful transaction.

Usage pricing for pay-as-you-go is 0.7% of billing volume.

If you need a Custom billing domain it is $10 per month.

Stripe Invoicing - Allows you to invoice upto 25 customers for free every month. Thereafter an overage of 0.4% is applied per invoice paid.

Stripe Tax - Offers you tools for tax calculation priced at 0.5% for each transaction.

Stripe Atlas - Assists you with setting up a company at a one-time setup fee of $500.

Stripe Sigma - Offers reporting and analytics using SQL. Pricing starts at 2๏ฟ per transaction and includes a $10 fee towards infrastructure. (emphasis mine -mfn)

From bossrevolution.com:

  • Billing.ย For businesses with a subscription-based or usage-based model, Stripe enables automated billing on a pay-as-you-go or monthly payment basis. Fees start at 0.7% of the billing volume or $620 per month for a one-year contract.

Cool cool. 13 2/3% of $3. Just below Apple's small dev 15%.

Good times. Least the API is simple. More to come.


From stripe.com:

Debit card purchases in the US are more extreme: there are no returns on interchange fees on any US debit card transaction.

If your business often processes refunds shortly after a payment, you can combat these potential lost fees by leaving the transaction authorization open rather than settling the sale right away. This is possible because you only pay interchange fees once a transaction is settled. If you leave the authorization open and a customer makes a return, you can simply reverse the authorization and avoid losing any extra interchange fees (since you never paid those fees to begin with).

For example, if you captured and settled a $100 debit card transaction and a customer requested a return, you could lose $0.42. However, if you had left the authorization open, you could only lose slightly less than $0.04.

You can generally leave an authorization open for up to two days before you pay additional fees, so this approach is most relevant for industries with the immediate delivery of goods (such as food delivery services).

What you can do:ย Configure the Stripe Payment Intents API toย separate authorization and capture.

good heavens.

Labels: , , , ,


posted by Jalindrine at 5/30/2025 01:26:00 PM
Monday, May 26, 2025

Sometimes, I surprise myself:

@Cthutu I wish "It's like the difference of [declaring] an address vsย [commanding] directions. The address is useful no matter you are. Whereas directions are invalid if you start somewhere else," was the selected answer all by itself.ย ;^)ย 

Labels: ,


posted by Jalindrine at 5/26/2025 03:32:00 PM
Wednesday, October 23, 2024

From stackoverflow.com:

Taken from MSDN's page on InvalidOperationException: "InvalidOperationException is used in cases when the failure to invoke a method is caused by reasons other than invalid arguments."ย 

โ€“ย STW
ย Commented Apr 21, 2009 at 19:44

I often forget what the "right" exception is to throw when it's not an argument issue -- and linters are getting better at reminding me not to be lazy and to stop using Exception with no subtype. "Code is evidence of the beliefs of its authors" after all.

I suppose InvalidOperationException is as good a fallback as any.

Labels: ,


posted by Jalindrine at 10/23/2024 10:31:00 AM
Tuesday, October 01, 2024

Okay, I've had this open in drafts too long. I think it's got most of the info I wanted, so let's cut it loose for when I need it in the future.


I often take a different laptop with me when I'm travelling than whatever the "prime" development box is for a project, often to ensure I don't lose sensitive information if the laptop "disappears" while I'm out. When doing this, I usually copy the folder I'm working in, remotes (so personal access tokens, VPN setup, etc) be darned, and work from that.

The issue is often getting that work back onto the "prime" boxen. That usually means remembering how to make and apply git patches.

Look, here's the deal... ;)

If you want to copy over and preserve individual commits, you want to use "email" formatted patches. You can envision why. If you came before the time when everyone had shared remotes or if your workforce is distributed and most simply don't have remote access, it's easy to schlep around code via email. And so git has email support built-int! Though do note we're only using the format, as it carefully preserves each commit separately; we're not actually emailing anything. Unless you really want to.

On the travelling box:

Let's say I wanted the last 5 commits. I'd use this command to create an email-formatted patch file:

git format-patch -k --stdout HEAD~5 > patch.patch

Open up the text file and take a look! It's actually kinda interesting, begging for an SMTP server to send it on its way.

On the "prime" development box:

git am -3 patch.patch

Now look, if you used git apply here, it would apply EVERYTHING IN THE FILE AS A SINGLE ACTION and not commit anything. It's like rolling all the changes into a single worksession that needs to be committed. Using git apply for an email versioned patch reduces to the same operation as creating a diff with git diff and git applying it.

We DON'T want that. You have to use git am to get the email action going.

The -3 is for three-way merge if there's a conflict git can't resolve, and is the way I (and several other StackOverflow users, apparently) best prefer to manage conflicting patch applications. But you really shouldn't run into that much if you just worked on an existing branch.

Do make sure you're on the right branches on both boxes.


TODO: How do you get the patch to include staged files?

Labels: ,


posted by Jalindrine at 10/01/2024 11:21:00 AM
Friday, August 23, 2024

Okay, look, if there's one thing I'm tired of, it's half-baked example code that doesn't anticipate changes needed to push it into production.

Like the good ole WeatherForecastController from the .NET Core WebAPI template.

using Microsoft.AspNetCore.Mvc;

namespace MyApp .Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", 
            "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet(Name = "GetWeatherForecast")]
        public IEnumerable<WeatherForecast> Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

I mean, Visual Studio immediately complains:

Remove this unread private field '_logger' or refactor the code to use its value.

Well, duh. We have an endpoint with no logging. When would we need to log? Probably when we're doing something more complicated than creating random 8-ball style forecasts.

So let's pretend it's more difficult, throw in a try... catch, and actually log the exception.

[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
    try
    {
        return new ActionResult<IEnumerable<WeatherForecast>>(Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }));
    }
    catch (Exception e)
    {
        _logger.LogError(e, "Something failed");
        return BadRequest("that didn't work");
    }
}

Guess what? Now we got TWO errors! YAY!!

  1. CS0029 Cannot implicitly convert type 'Microsoft.AspNetCore.Mvc.ActionResult<System.Collections.Generic.IEnumerable<MyApp.WeatherForecast>>' to 'System.Collections.Generic.IEnumerable<MyApp.WeatherForecast>'
  2. CS0266 Cannot implicitly convert type 'Microsoft.AspNetCore.Mvc.BadRequestObjectResult' to 'System.Collections.Generic.IEnumerable<MyApp.WeatherForecast>'. An explicit conversion exists (are you missing a cast?)

Dare you to tell me what to do next. Heck, I don't know. I do know WebAPIs have been around so long there are tons of wrong answers on the net.

Let's just show one example that does work and call it a day.

[HttpGet(Name = "GetWeatherForecast")]
public ActionResult<IEnumerable<WeatherForecast>> Get()
{
    try
    {
        return new ActionResult<IEnumerable<WeatherForecast>>(Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }));
    }
    catch (Exception e)
    {
        _logger.LogError(e, "Something failed");
        return BadRequest("that didn't work");
    }
}

Why do I need to wrap the return type with ActionResult to be able to return BadRequest REST code? I don't know man. It should implicitly cast imo into any hard type return value. I mean it's not like we're literally returning IEnumerable<WeatherForecast> in the original code. We are doing some magic behind the scenes to make that a JSON return value for our consumers. Why not do the same for any REST code convenience type too?

Anyhow, I just want to remember this trick for the next time it happens so to the blog it goes.

:sigh:

Labels: , , ,


posted by Jalindrine at 8/23/2024 05:50:00 PM
Friday, July 05, 2024

I've been using BBEdit as my compare tool on macOS for a few years now, but recently noticed that it keeps opening a window that's maybe 90% of my screen's width and height. That might be useful on a large monitor, but my 13" MacBook Air felt especially cramped.

Welp, to change the default size of a window in BBEdit, you apparently use the menu! Feels very OS 9-. /nostalgia

From "BBEdit > New Window Size & Location > Set Default" on ArsTechnica:

When I began my search for how to set the default location for new windows in BBEdite, I was certain that to set this required a "write defaults" from the command line.

Not so -- no need for BBEdit Expert Preferences for this ... it's in the menu:

Menubar > Window > Save Default <type of> Window

The Save Default Window command stores the position and size of the front window in BBEditโ€™s preferences, and BBEdit will create all new windows of the same type with the stored position and size.

In my case, the "type of" is "Difference".

So, to be overly clear, first open a difference window, size and position it to taste, and then run the "Menubar > Window > Save Default Window" jive.


I'm sure I've mentioned I've been using BBEdit since the year of its birth (not sure exactly, but certainly in 1992. I still fondly remember [a few years later] using it in tandem with Transmit). I'd wandered away from BBEdit for years, using Ultra-Edit on Windows for a while, then VIm, Visual Studio, and a number of language-specific editors (sort of like (and including) PhpStorm), Coda (super briefly), Sublime Text, and now largely (and largely happily) VS Code crossplatform.

It's kinda neat to have a daily use for the "old grey lady" of text editing again and to continue not to be disappointed in its feature-set. BBEdit doesn't suck.

Labels: ,


posted by ruffin at 7/05/2024 06:27:00 PM
Tuesday, March 12, 2024

If you want to release a .NET MAUI app on macOS (or even iOS), I'm going to assume you want to develop at least part time there.

Visual Studio on macOS is dying, which leaves us with either using a dying app or using VSCode (which I wish everyone would write like that... VSCode. Then it'd actually be easily googleable).

Using VSCode for .NET MAUI isn't straightforward, imo, but I found a pretty danged good tutorial on it from James Montemagno, a long-time Xamarin user and MS employee.

MAUI on VSCode lacks some stuff like XAML designers, iirc, but he actually decided to use VSCode and straight-code the UI in C# for this video, which makes it perhaps the best, "How to use MAUI on macOS" tutorial out there right now.

So here you go...

Recreating Threads App with .NET MAUI & NEW VS Code Extension!

aka "The best tutorial for coding MAUI on macOS available today".

No thanks [for me for showing you this] necessary. ;^D


A few other .NET MAUI links to remember (might edit as I add them here):

Labels: , , , ,


posted by ruffin at 3/12/2024 04:13:00 PM
Wednesday, June 14, 2023

How to merge lots of videos into one with ffmpeg on Windows...

Put this into a .bat file and smoke it:

REM https://stackoverflow.com/a/41387530/1028230
:: Create File List
for %%i in (*.mp4) do echo file '%%i'>> mylist.txt

:: Concatenate Files
ffmpeg -f concat -safe 0 -i mylist.txt -c copy output.mov

Obviously use a full path to ffmpeg if it's not in your PATH.

I got errors when trying to save out to an mp4, but not when saving to .mov.

Actually I got errors for .mov, too, when combining Wyze cam videos like this, but the resulting file still opened fine.

Labels: ,


posted by ruffin at 6/14/2023 01:43:00 PM
Wednesday, January 11, 2023

Remember when I said this?

but if I want something quick that can run anywhere [someone's on Windows], I use a batch file, and over the years, kinda like VIm, I've slowly become if not proficient then at least competent.

What a naรฏve time that was.


Take this seemingly innocuous question on SO:

How can you you insert a newline from your batch file output?

And then check this answer:

Here you go, create a .bat file with the following in it :

@echo off
REM Creating a Newline variable (the two blank lines are required!)
set NLM=^


set NL=^^^%NLM%%NLM%^%NLM%%NLM%
REM Example Usage:
echo There should be a newline%NL%inserted here.

echo.
pause

Wha? set NL=^^^%NLM%%NLM%^%NLM%%NLM%??? What in the world? Huh?

Luckily (?) one of the commenters asked the same question.

Here's part of the answer:

The caret is an escape character for the next character, or at the line end it is used as multiline character, but this is nearly the same.

At the line end it simply escapes the next character, in this case theย <Linefeed>, but there is a hidden feature, so if the escaped character is aย <LF>ย it is ignored and the next character is read and escaped, but this charater will be always escaped, even if it is also aย <LF>.

...

So you need to add an escaped linefeed into the line and that is the NL-Variable. The NL-Variable consists of only three characters.
NL=^<LF><LF>ย And if this is expanded, it creates only one escapedย <LF>ย as the firstย <LF>ย after the caret will be ignored.

I mean, um, that requires Inception level cognition to understand. ๐Ÿ˜ (Okay, actually it's pretty straightforward, but talk about a lack of discoverability.)

What's even more interesting is that dev's answer to the original question, which is this:

Like the answer of Ken, but with the use of the delayed expansion.

setlocal EnableDelayedExpansion
(set \n=^
%=Do not remove this line=%
)

echo Line1!\n!Line2
echo Works also with quotes "!\n!line2"

Aside

Okay, before I point out what I want to point out, note that %=_____=% is an inline comment format for batch.

From ss64.com, a common domain when I'm in batchland:

Batch variableย namesย can contain spaces and punctuation, we can take advantage of this fact by typing our comment as a non-existent variable. Because it doesnโ€™t exist, this variable will expand to nothing and so will have no effect when the script is run.

n.b. This only works in batch files, not directly at the command prompt.

To be sure that we donโ€™t accidently choose a name which is in fact a real variable, start and end the comment with "="

@Echo off
Echo This is an example of an %= Inline Comment =% in the middle of a line.

(Variable names starting with "=" are reserved for undocumented dynamicย variables. Those dynamic variables never end with "=", so by using an "=" at both the start and end of our comment, there isย noย possibility of a name clash.)


And we're back

Okay, now what I'd like to point out here is the setlocal EnableDelayedExpansion, something I think I had to use recently in a script I wrote. It's a bizarre new world.

Here's some explanation from ss64.com:

EnableDelayedExpansion

Delayed Expansion will cause variables within a batch file to be expanded at execution time rather than at parse time[;] this option is turned on with the SETLOCAL EnableDelayedExpansionย command.

Variable expansion means replacing a variable (e.g.ย %windir%)ย with its valueย C:\WINDOWS

  • By default expansion will happen just once, before each line is executed.
    In this case, a "line" includes bracketed expressions even if they are formatted to run over several lines.

  • Whenย !delayed!ย expansion is turned on, the variables will be expanded each time the line is executed, or for each loop in a FOR looping command.

And here's a decent example from that page showing how it works in practice.

@echo off
SETLOCALย EnableDelayedExpansion
Set "_var=first"
Set "_var=second"ย & Echo %_var% !_var!

The output is

first second

Note that the above example has to be in a batch file to work properly.

Anyhow, it's an insane rabbit hole that I didn't schedule for this sprint and fell into anyhow. I'm going to cut my losses now that I've warned others who may have been as naรฏve about batch scripts as I was, oh so long (less than three weeks ๐Ÿ˜‰) ago.


Oh good heavens. I guess this makes sense. The first example can be taken by itself as a brain teaser, I think.

From ss64.com, again on EnableDelayedExpansion:

@echo off
Setlocal
Set _html=Hello^>World
Echo %_html%

In the above, the Echo command will create a text file called 'world' - not quite what we wanted!

Got that figured out? If not, sit and think about it for a second before reading more.

Got it? Good. Clever, huh?

This is because the variable is expanded at parse time, so the last line is executing Echo Hello > World and the > character is interpreted as a redirection operator.

If we now try the same thing with EnableDelayedExpansion:

Setlocal EnableDelayedExpansion
Set _html=Hello^>World
Echo !_html!

With delayed expansion, the variable (including the > ) is only expanded at execution time so the > character is never interpreted as a redirection operator.

This makes it possible to work with HTML and XML formatted strings in a variable.

Labels: , , ,


posted by ruffin at 1/11/2023 12:27:00 PM
Thursday, December 22, 2022

The longer I work in Windows, the more I find myself using cmd.exe. I use PowerShell plenty too, but if I want something quick that can run anywhere [someone's on Windows], I use a batch file, and over the years, kinda like VIm, I've slowly become if not proficient then at least competent.

Nearly (and maybe even over at this point) thirty years ago I had a guy wisely tell me, when I was considering buying a new Mac or Windows PC, "It's all zeros and ones." Same for script languages, mostly. It might be a pain to learn batch scripting on Windows sometimes, but there's very little you can't do if you set your mind to it.

But this is a really neat trick to create "arrays" in batch that I've never seen. I've edited a bit to allow running in a .bat cleanly:

REM https://stackoverflow.com/questions/18462169/how-to-loop-through-array-in-batch

echo off


set Arr[0]=apple
set Arr[1]=banana
set Arr[2]=cherry
set Arr[3]=donut

set "x=0"

:SymLoop
if defined Arr[%x%] (
    call echo %%Arr[%x%]%%
    set /a "x+=1"
    GOTO :SymLoop

Clever.


While I'm at it, here's a PowerShell script I've been using to approximate grep there. I've dabbled in this problem before, but it's usually a good idea to reduce it to script instead of leaving only human-readable lessons learned:

param (
    [Parameter(Position=0)]
    [string]$needle,
    [Parameter(Position=1)]
    [string]$filepathToSearch,
    [Parameter(Position=2)]
    [string]$optionalFileForOutput
)

if ($optionalFileForOutput) {
    Get-ChildItem -Path $filepathToSearch |Select-String -Pattern $needle |Out-File -width 99999 $optionalFileForOutput
} else {
    select-string -path $filepathToSearch -pattern $needle
}

That captures the Out-File wackiness from the previous post but also wraps Select-Stringing a file, making it easier to remember, not that it's difficult. I should fix the casing.

Labels: , , , ,


posted by ruffin at 12/22/2022 10:21:00 AM
Tuesday, November 01, 2022

I always forget where this is, and had another iPhone die from a fall yesterday, which often requires the SIM card moving to my backup Android until a replacement iPhone is secured.
 
That site where you get sent an SMS on the new phone and Apple removes your phone number from Message's SMS black hole is this:
 
 
Remember when this didn't exist? :shudder:

Labels: ,


posted by Jalindrine at 11/01/2022 10:45:00 AM
Friday, October 28, 2022

VIm remains one of my most useful tools.

:v/pattern/d

From a somewhat useful SO comment:

Fwiw -- Looks like g [in a command like :g/pattern/d] is for "global" and v [like we have here] is for "inverse" (if you believe what you read on vim.famdom).

Labels: , ,


posted by ruffin at 10/28/2022 05:05:00 PM
Thursday, October 27, 2022

A note to self I might have already noted... myself.

angular.module('MyApp')['_invokeQueue'].forEach(function(value){ 
    console.log(value[1] + ": " + value[2][0]);
});

Sauce.

So look, I'm at a company where much of the app is still in AngularJS. The downside is that this reminds me of a post I'd shared before that said:

If your list of things to develop is really a list of things that you wonโ€™t have to do in a more functional environment, none of which will make you more employable elsewhereโ€ฆ itโ€™s time to walk away.

That's not wrong. More experience in AngularJS isn't making me more employable at this point unless I want to work in the small subset of positions that plan to continue using it well past its dead-on date. Which, again, was January of this year.

That admission aside, as I've mentioned before, there is, however, a practical advantage: Most of the questions I have when I'm developing have nearly canonical answers. I mean, have you seen StackOverflow's blog on the Next.JS conference? The blog's title is "Goodbye Webpack, hello Turbopack!". With just a little bit of exaggeration, webpack, we barely knew ye.

I was looking to get us to a React stack, and I'd initially liked this "minimal" build tool suite over at 2ality. Okay, okay, it's been over three years, but it's now kaput. Gone. Snowpack is dead. I'd heard a bit about ViteJS already a bit from my RSS sub to Shawn Wildermuth's blog, and sure enough that's what Snowpack says to use now. If I'd learned Snowpack inside and out (not that it was tough to learn, and I've been playing with it on and off for a few years), that'd be nearly wasted time now.

Look, it's fine to need to keep up a little, it's not like .NET 6 doesn't have a decent amount of new stuff (does it though?), and maybe it's just that I've been around long enough to see things change several times, but it doesn't really have to, does it? There are database admins who haven't needed to learn anything exceptional for their whole career. I mean, look, it's not like our buddy Mr. Dave hasn't kept things moving, but it ain't client-side JavaScript. He's had a chance to get good at what he does, to really drill-down and understand rdbms engines.

Okay, yes. I'm a little jealous. ๐Ÿ˜

And if I haven't mentioned it before, I'm pretty sure it's because of conferences, YouTube, and Twitter. Everybody who wants to be a bleeding edge type can be, and they all race each other to be the new expert of something nobody else can be yet because it was just released. God made humans, but constantly evolving tech stacks made humans equal[ly clueless].

That said, I do need to write my follow-up to my intro to why we're using transpilation-free Preact. Preact really is a wonderful bridge from a hopelessly out of date client-side app to, well, at least a TypeScript-powered React refactor. Need to develop something new now but can't, for whatever reason, create all new tooling?

If you can limit support to IE 9+ or so, Preact (with hooks) is your stack. We've used Preact with Enzyme to create a tested, cradle-to-grave page with a reasonably complex UI and have it running well right smack in the middle of an AngularJS app. Set down new work in Preact now, start eating from the top with React, and when they meet in the middle, it's a very quick port.

So maybe I won't need to remember that cheat to get all directives, constants, etc out of an AngularJS site for too long. But when I forget again, at least I'll know I've got it sitting here.

Labels: , , , , ,


posted by ruffin at 10/27/2022 04:44:00 PM
Thursday, August 04, 2022

I've run into there being two different sets of collections in .NET before, generic vs. well, vs. not (?), but never really sat down to understand it.

They are very different. From stackoverflow.com:

ICollection<T> and ICollection are actually very different interfaces that unfortunately share a name and not much else.

Ugh.

From an MSDN link in that answer:

Collection<T> seems like ICollection, but itโ€™s actually a very different abstraction. We found that ICollection was not very useful. At the same time, we did not have an abstraction that represented an read/write non-indexed collection. ICollection<T> is such abstraction and you could say that ICollection does not have an exact corresponding peer in the generic world; IEnumerable<T> is the closest.

Here they are:

This means LINQ works on the latter but not the former. Luckily ICollection (no T) is still enumerable, so foreach away! (Are there considerations about iteration, like can you do it twice? Maybe? I haven't run into one yet.)

There are similar issues with many collection types, reviewed in some detail on that page, including IDictionary...

From microsoft.com:

IDictionary<TKey, TValue> is roughly equivalent to IDictionary.

Which includes a quick follow-up on DictionaryBase:

Important

We don't recommend that you use the DictionaryBase class for new development. Instead, we recommend that you use the generic Dictionary<TKey,TValue> or KeyedCollection<TKey,TItem> class . For more information, see Non-generic collections shouldn't be used on GitHub.

That said, DictionaryBase's entry gives us a nice example implementation so we don't have to dig up a concrete implementation somewhere else, like Exception.Data (which is what created this rabbit hole for me).

Using that example ShortstringDictionary we can illustrate our issue this way:

ShortstringDictionary oldStyleDictionary = new ShortstringDictionary();

oldStyleDictionary.Add("One", "a");
oldStyleDictionary.Add("Two", "ab");
oldStyleDictionary.Add("Three", "abc");
oldStyleDictionary.Add("Four", "abcd");
oldStyleDictionary.Add("Five", "abcde");

//var filtered = oldStyleDictionary.Where( // <<<<< doesn't exist; no LINQ

So notice that there's no LINQ here, so we can't Where our ShortstringDictionary.

But the dictionary IS enumerable. Well, kinda. The Keys and Values collections are.

So we can foreach through Keys and, here, insert each key/value into a System.Collections.Generic.Dictionary<TKey,TValue> (here, specifically, a Dictionary<string, string>).

Note that the Keys collections' values, though the implementation forces them to be strings, are NOT typed!

Keys is a System.Collections.ICollection (from the humourously named System.Collections.NonGeneric.dll).

Dictionary<string, string> newStyleDictionary = new Dictionary<string, string>();
foreach (object key in oldStyleDictionary.Keys)
{
    string keyAsString = key.ToString();
    Console.WriteLine($"{key}: {oldStyleDictionary[keyAsString]}");

    newStyleDictionary.Add(keyAsString, oldStyleDictionary[keyAsString]);
}

Console.WriteLine("\n==========================\n");

Now our generic Dictionary<TKey,TValue> can use LINQ's Where to filter.

var filtered = newStyleDictionary.Where(x => x.Value.Contains("c"));

foreach (var kvp in filtered)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}

Here endeth the lesson.

And begineth another -- quick update from this SO answer:

The most painful difference is that for the generic Dictionary<string, string>, when I call this[key] for a key that does not exist, I get an exception stating the key does not exist.

On a DictionaryBase I get back null with no exception. This was painful in my case because the system was full of code that did not check that the dictionary ContainsKey before trying to get the keys value. It was made more painful by me assuming I messed something up with serialization.

That appears to be true!

Console.WriteLine($"{bogusKey}: {oldStyleDictionary[bogusKey]}"); // writes "bogus: "
Console.WriteLine($"{bogusKey}: {newStyleDictionary[bogusKey]}"); // throws System.Collections.Generic.KeyNotFoundException 

Labels: , ,


posted by ruffin at 8/04/2022 02:34:00 PM

<< Older | Newer >>


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 etree.org 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.