Skip to main content

Ralsina.Me — Roberto Alsina's website

Software projects are not pets. FINISH YOUR PROJECTS.

I have late­ly been start­ing a ton of project­s. Usu­al­ly that would mean that I would be aban­don­ing a ton of pro­ject­s, but I have been fin­ish­ing them. I have been do­ing this by treat­ing my projects like they are not pet­s.

For ex­am­ple, let's take grafi­to a web fron­tend for sys­tem logs.

I start­ed it. I worked on it for a week or two of spare time, and now it's DONE. To­tal­ly done. It has no miss­ing fea­tures. It has no bugs. It is DONE.

It does what I want it to do, it does it well, it's pou­b­lished in all the ways that make sense, and I don't have to think about it any­more.

Sure, if some­one finds a bug, I will fix it. If some­one asks for a fea­ture, I will con­sid­er it. But I am not go­ing to spend time on it un­less there is a good rea­son to do so.

I am spe­cial­ly not go­ing to spend time on it just be­cause I like it, or be­cause I want to keep work­ing on it. I am not go­ing to pet it, or feed it, or take it for walk­s.

I am not go­ing to think about what fea­tures I could ad­d. It does what it does. It's pret­ty unixy, in that it does one thing and does it well. It is not a pet, it is a tool.

That is how I am han­dling my projects nowa­days. I wrote tar­trazine which is a syn­tax high­light­ing tool in a cou­ple of weeks and it's done. Six­teen? DONE.

Some projects are not fin­ished. Cryc­co is not fin­ished. I will work on it some more. Be­cause it's not done yet. It has miss­ing fea­tures, it has bugs, it has things I want to do with it.

This is spe­cial­ly im­por­tant for free and open source project­s. Main­tain­ers get over­whelmed. Start­ing a project is fun but main­tain­ing it is like hav­ing a pet par­rot, re­quir­ing con­stant at­ten­tion, feed­ing, clean­ing, and who will out­live you.

That at­ti­tude is not sus­tain­able. Imag­ine you were a hob­by car­pen­ter mak­ing ta­bles. Would you say "ta­bles are nev­er fin­ished, they are aban­doned"? No, you would say "this ta­ble is done, I will make an­oth­er one if I want to".

That is a healthy at­ti­tude. This is not re­li­gion, and this is not a job. If I start a new project I will fin­ish it, or I will aban­don it, but I will not cre­ate projects that will re­quire my at­ten­tion for­ev­er.

KV: a remote KVM application

I had been read­ing about re­mote KVMs for a while. There are sev­er­al, like PiKVM, JetKVM, etc. I de­cid­ed I want­ed one to ac­cess my serv­er at home be­cause it has a nasty ten­den­cy to lose net­work con­nec­tiv­i­ty, and I want­ed to be able to fix it with­out hav­ing to go to the of­fice and plug in stuff in­to it.

This is not like run­ning RDP or VNC on the serv­er in that to the serv­er this is hard­ware. It works no mat­ter how crashed or dis­con­nect­ed the serv­er is, as long as it has pow­er. It is like hav­ing a mon­i­tor, key­board and mouse plugged in to the server, but re­mote­ly.

I had all the hard­ware I need­ed:

  • A Radxa Ze­ro, which has OTG sup­port so I could use it as a USB de­vice.
  • A USB cap­ture don­gle, which are cheap and easy to find, to cap­ture the HD­MI out­put of the serv­er.

But I just could not make it work. PiKVM, the most pop­u­lar one, makes it pret­ty dif­fi­cult to make it work on any­thing oth­er than the ex­act hard­ware con­figs they sup­port, and those are ex­act­ly the ones I don't have.

I could not find any im­ple­men­ta­tion that was easy to set­up and sup­port­ed the hard­ware I had, so I de­cid­ed to write my own.

You can get the code at GitHub of course. It com­piles to a sin­gle bi­na­ry. It on­ly re­quires you to have ffm­peg in­stalled, and it should just work as long as your hard­ware sup­ports OT­G, you have a USB cap­ture don­gle, and you plug all the ca­bles cor­rect­ly.

One USB ca­ble and one HD­MI ca­ble go from the serv­er to the Radxa Ze­ro (the HD­MI via cap­ture don­gle) and you run the one bi­na­ry built from this code on the Radxa Ze­ro. It will start a web serv­er on port 3000, and you can ac­cess it from any brows­er.

The web in­ter­face is very sim­ple, but it work­s. You can see the video feed, and you can send key­board and mouse events to the serv­er. You can even pro­vide a disk im­age that the serv­er will think is a USB drive. I sup­pose you can even in­stall the OS in the serv­er that way, but I have not tried it.

If you want to ac­cess it more re­mote­ly, just set­up a VPN in the KVM it­self.

Ideas for programs that don't exist: 4

This is an oc­ca­sion­al se­ries of posts where I will share ideas for pro­grams that don't ex­ist, but should. The goal is to in­spire de­vel­op­ers to cre­ate use­ful tools that can make our lives eas­i­er. Or, more like­ly, to re­mind me about these ideas so I can cre­ate them my­self. Or even more like­ly, to just get them out of my head so I can stop think­ing about them.

Idea 4: A modern version control system that has fewer features

For some things you don't need Git. But for those things you may still need ver­sion con­trol.

Imag­ine a ver­sion con­trol sys­tem just for you. Un­less you are a de­vel­op­er, in that case it's not for you, it's for your dog or some­thing.

I want a ver­sion con­trol sys­tem that is bet­ter than "doc­u­men­t.­doc" ➡️ "doc­u­men­t2.­doc" ➡️ "doc­u­men­t.­fi­nal.­doc" ➡️ "doc­u­men­t.re­al­ly­fi­nal.­doc"

Not a lot bet­ter but bet­ter.

Let's be ul­tra­op­ti­mistic and just dream of what would be ide­al.

  • Us­er cre­ates a file and says "this file is im­por­tant and will change"
  • Mag­i­cal­ly the sys­tem rec­og­nizes the idea and says "ok"
  • Us­er saves a new ver­sion of the file.
  • The sys­tem says "ok"
  • Us­er says "oop­s, where is my old ver­sion?"
  • Sys­tem says "here"

Some­times the sys­tem may al­low the us­er to see the dif­fer­ence be­tween one ver­sion and an­oth­er ver­sion. This re­quires app sup­port in gen­er­al if the file is not an or­di­nary text file.

  • Some­times the us­er may want to make a copy of an old ver­sion.
  • Some­times the us­er may want to "name" an old ver­sion.
  • Some­times the us­er will want to over­write the last ver­sion with an old­er one.
  • Some­times the us­er will want to get back a file he delet­ed.

That's it. That's all the fea­tures.

The sad part is that we had this in the 80s, in VM­S. But any­way, it would be nice.

If I were to im­ple­ment this I would do a sim­ple im­ple­men­ta­tion and then write a user-lev­el filesys­tem to let the us­er ac­cess it trans­par­ent­ly.

Fur­ther, maybe a li­brary that al­lows us­ing this from your own code? So you can save ver­sioned?

Would be nice.

New project: ToCry, a ToDo/Kanban app

I have been us­ing Tasks.md for a while now and ... I like it, but I got the itch to try fix­ing some things and I did­n't re­al­ly want to do JS back­end code on week­end­s, so why not try to build a sim­i­lar ap­p?

Al­so I have been want­ing to do some "vibe cod­ing" with AI and I seem to have un­lim­it­ed Gem­i­ni 2.5 in VS Code for some rea­son, so why not try to do that too?

Well: it worked, you can see the re­sult at ToCry, get the app and use it. It's pret­ty nice!

alt text

Now, I am not say­ing it's per­fec­t, but it works and I learned a lot. Did AI help? Well, yeah. Most of the fron­tend code was AI gen­er­at­ed, most of the back­end code is mine with AI au­to­com­plet­ing stuff.

And this got writ­ten pret­ty fast, I on­ly start­ed it two days ago!

In fac­t, in those two days it was writ­ten twice... be­cause the first time it was ab­so­lute garbage.

It was so bad I not on­ly rewrote it, I re­moved the re­po his­to­ry and pushed the new code over it. I did­n't want to keep that code near, I did not want it to in­flu­ence the re­write.

What Went Wrong The First Time

The first time I went UI-­first. I just prompt­ed and prompt­ed and prompt­ed on a HTML file, try­ing to get Gem­i­ni to gen­er­ate a nice UI that worked with all the da­ta kept on the client side.

This seemed to work well, in a few hours I had a nice UI that was at least func­tion­al. How­ev­er at one point Gem­i­ni hit a wal­l, hard.

Try­ing to add a new fea­ture or fix­ing a bug was al­most im­pos­si­ble, Gem­i­ni would get in loop af­ter loop, do­ing and re­vert­ing the same changes over and over.

When I went to man­u­al­ly check the code, it was a bowl of spa­het­ti, and refac­tor­ing it was be­yond Gem­i­ni's ca­pa­bil­i­ties. Con­nect­ing it to a back­end was doable (and got done) but then state was kept in the client and the back­end, and Gem­i­ni re­fused to move it and con­sis­ten­cy was a night­mare.

It be­came in­creas­ing­ly clear that the code was of no val­ue. So then I nuked it, salt­ed the earth, and start­ed over.

What Went Right The Second Time

The sec­ond time I went back­end-­first. I wrote a sim­ple back­end in Crys­tal, with a sim­ple API that re­turned JSON da­ta for the en­ti­ties I want­ed to man­age: tasks, lanes, board­s, etc.

Af­ter the da­ta struc­tures were clear, prompts like "cre­ate a PUT /note/:id end­point that up­dates the note" worked great. The trick is, of course, that those re­quests are pret­ty much con­tex­t-free, so Gem­i­ni did­n't have to fig­ure out how to con­nect one thing to an­oth­er, it was just writ­ing al­most-boil­er­plate code.

Af­ter sev­er­al of these were cre­at­ed, I in­ter­min­gled refac­tor­ing prompt­s.

  • "Do you see any code that can be moved to a com­mon func­tion?"
  • "Please as­sume this and that are al­ways true and stop check­ing"
  • "Change the names of this and that to some­thing de­scrip­tive and short"

I still am not a huge fan of the code Gem­i­ni writes. It has a ten­den­cy to do mul­ti­ple in­ter­me­di­ate steps that are not need­ed and us­ing in­ter­me­di­ate vari­ables in some sort of over de­scrip­tive no­ta­tion all the time.

Ah, and adds stupid com­ments. Many stupid com­ments. This is an ex­am­ple of its code:

self.notes.each_with_index do |note, index| # Changed to each_with_index to get the index
        note.save                   # This saves the note to data/.notes/note_id.md

        padded_index = index.to_s.rjust(4, '0')
        sanitized_title = note.slug # Note instance method for slug

        symlink_filename = "#{padded_index}_#{sanitized_title}.md"
        source_note_path = File.join("..", ".notes", "#{note.id}.md") # Relative path for symlink
        symlink_target_path = File.join(lane_dir, symlink_filename)
        ...

Usu­al­ly, af­ter a func­tion is "fin­ished", I would go and do a pass "for taste" to make the code more read­able, re­move the un­nec­es­sary com­ments, and make it more con­cise, but the code is func­tion­al and this is CRUD, not a fash­ion show.

The same hap­pened with the fron­tend code. I prompt­ed slow, bit by bit:

  • Add a but­ton to cre­ate a new lane, so when the us­er clicks it they are asked for the lane name. Then it calls POST /lane with the right da­ta
  • Get the lane da­ta from /lanes and dis­play it as a hor­i­zon­tal list of con­tain­ers
  • Add a but­ton to the lane to delete it, which prompts the us­er for con­fir­ma­tion and then calls DELETE /lane/:id

And so on.

Some­times, I tried larg­er prompts where I asked for a whole fea­ture, but those were on­ly oc­ca­sion­al­ly suc­cess­ful. Find­ing the right gran­u­lar­i­ty is key, and I found that the best gran­u­lar­i­ty was "one func­tion" or "one com­po­nen­t".

Again, mix­ing refac­tor­ing as we went along helped a lot to keep the code clean and or­ga­nized, as well as sep­a­rat­ed in rea­son­ably func­tion­s, keep­ing spaghet­ti at bay.

What I Learned

Gem­i­ni codes like a ju­nior. It does not un­der­stand the big pic­ture, it does not un­der­stand the code it writes, It can write code that works ( I as­sume be­cause the In­ter­net is full of work­ing code) but it car­go cults AF.

On the oth­er hand, the code tends to work? And I can fix it pret­ty quick when it does­n't? It's not bad at sim­ple refac­tor­ing, and what it needs more than any­thing is a man­ag­er.

Yes, that seems to be the bad news. Cod­ing with AI felt like be­ing a man­ag­er again. A man­ag­er with a very, very, very ea­ger ju­nior dev who does­n't sleep and feels soooo clever by de­scrib­ing unini­tial­ized vari­ables as "a clas­sic ex­am­ple of a vari­able that has not been ini­tial­ized yet".

Would I Do It Again?

Yeah. I have a ton of things I want to write, and this way I can write them faster. I can al­so put ef­fort in the parts that mat­ter, like da­ta struc­tures and al­go­rithms and let Gem­i­ni do the sil­ly CSS and CRUD.

Yeah, I don't care if the CSS is re­dun­dan­t, as long as it looks ok I am hap­py telling Gem­i­ni to "make the rows tighter" and who cares how.

CRUD? It's a solved prob­lem. I will do a pass to clean up.

Da­ta struc­tures? I will do 90% of the work, be­cause that is what makes the base of the ap­p, and I want it to be sol­id.

Ethical Thoughts

I feel dirty, and like I am cheat­ing. I am prob­a­bly steal­ing code from oth­er peo­ple. Use Gem­i­ni to write a Dock­er­file and it will hap­pi­ly au­to­com­plete things with frag­ments of things in­clud­ing re­po names which ex­ist in the In­ter­net so it's not even a guess, I know it's copy past­ing oth­er peo­ple's code.

OTO­H, I al­ways copy past­ed code from oth­er peo­ple's code.

Writing a simple parser using a finite state machine

I wrote a lit­er­ate pro­gram­ming tool called Cryc­co and at its core is a very sim­ple pars­er which ex­tracts com­ment blocks from source code and or­ga­nizes them in­to a doc­u­men­t.

If you want to see an ex­am­ple, Cryc­co's web­site is made out of its own code by pro­cess­ing it with it­self.

The for­mat is very sim­ple: you write a com­ment block, then some code, then an­oth­er com­ment block, and so on. The com­ment blocks are tak­en as mark­down, and the code blocks are tak­en as source code.

The doc­u­ment is then split in sec­tion­s: 1 chunk of doc, 1 chunk of code, and pro­cessed to make it look nice, with syn­tax high­light­ing and so on.

The pars­er was just a cou­ple dozen lines of code, but when I want­ed to add more fea­tures I ran in­to a wal­l, so I rewrote it us­ing a fi­nite state ma­chine (F­S­M) ap­proach.

Since, again, Cryc­co is lit­er­ate code, you can see the pars­er by just look­ing at the source code of the pars­er it­self, which is in the Doc­u­ment class

Of course, that may not be enough, so let's go in­to de­tail­s.

The state ma­chine has a few states:

    enum State
      CommentBlock
      EnclosingCommentBlock
      CodeBlock
    end

A com­ment block is some­thing like this:

    # This is a comment
    # More comment

An en­clos­ing com­ment block is a "mul­ti­line" com­men­t, like this:

 /* This is a comment
 More comment
 */

Code blocks are lines that are not com­ments :-)

So, suppose you are in the CommentBlock state, and you see a line that starts with # you stay in the same state.

If you see a line that does not start with #, you switch to the CodeBlock state.

When you are in the CommentBlock state, the line you are in is a comment. If you are in the CodeBlock state, the line is code.

Here are the pos­si­ble tran­si­tions in this ma­chine:

    state_machine State, initial: State::CodeBlock do
      event :comment, from: [State::CodeBlock], to: State::CommentBlock
      event :enclosing_comment_start, from: [State::CodeBlock], to: State::EnclosingCommentBlock
      event :enclosing_comment_end, from: [State::EnclosingCommentBlock], to: State::CodeBlock
      event :code, from: [State::CommentBlock], to: State::CodeBlock
    end

Then, to parse the doc­u­men­t, we go line by line, and call the ap­pro­pri­ate event de­pend­ing on the line we are read­ing. That event may change the state or not.

For ex­am­ple:

        if is_comment.match(line) && !NOT_COMMENT.match(line)
          self.comment {
            # These blocks only execute when transitions are successful.
            #
            # So, this block is executed when we are transitioning
            # to a comment block, which means we are starting
            # a new section
            @sections << Section.new(@language)
          }
          # Because the docs section is supposed to be markdown, we need
          # to remove the comment marker from the line.
          processed_line = processed_line.sub(@language.match, "") unless @literate

And that's it! We send the prop­er events to the ma­chine, the ma­chine changes state, we han­dle the line ac­cord­ing to what state we are in, and we end up with a nice­ly parsed doc­u­men­t.

Parsers are some­what scary, but they don't have to be. A fi­nite state ma­chine is a very sim­ple way to write a pars­er that is easy to un­der­stand and main­tain, and of­ten is enough.

Have fun pars­ing!


Contents © 2000-2025 Roberto Alsina