Header logo.
small hallucinations
home • years • tags • about • rss

Untracking files in Git

Normally, you can prevent Git from tracking files by adding them to .gitignore.

But if files are already tracked, Git will continue to track them even after you add them to .gitignore.

We first need to remove the tracked files from Git's index, then add all the files in the current directory—including .gitignore—back to it. Now .gitignore will take effect.

1git rm -r --cached .
2git add .
◆

Trying out Fyne

The idea of building a piece of software once and running it everywhere is certainly charming. Go also supports cross-compilation out of the box.

Fyne is a very promising GUI package for Go that helps you build an app with a graphical interface and cross-compile it for almost any device or operating system.

Heres is a quick example

As a quick example, this code gives you a small window with a label and an input field. Grab the code, initialize a module, run go mod tidy, and you're good to go.

 1package main
 2
 3import (
 4	"fyne.io/fyne/v2/app"
 5	"fyne.io/fyne/v2/widget"
 6	"fyne.io/fyne/v2/container"
 7)
 8
 9func main() {
10	a := app.New() // {1}
11	w := a.NewWindow("Hello, you!") // {2}
12
13	l := widget.NewLabel("Hello! What is your name?") // {3}
14	e := widget.NewEntry()
15	e.SetPlaceHolder("Input your name...")
16
17	w.SetContent(container.NewVBox(l, e)) // {4}
18	w.ShowAndRun()
19}

First, on line {1}, we define an app as a. Then, on line {2}, we define a window, w, with the title “Hello, you!”

Then we create a new label (on line {3}) and a new input field, or entry. Both are “widgets,” so their constructors are in the widget package. We set a placeholder for the input field for good measure.

Next, we create a vertical box, which is a container, and on line {4} set it as the content of window w.

When we build and run the program, it looks like this:

You might have noticed a few quirks. When you resize the window, its contents shiver because Fyne tries to adjust the window size and layout automatically.

When you try to display something that the default typeface cannot render, it appears as a block. CJK characters, Arabic letters, and emojis are all mangled.

Using a custom font

To display Unicode characters correctly, you need to bundle a custom font and include it in your custom theme. The font is then applied globally to your app as part of the theme. From what I read in the issues section, only TTF was supported at the time.

I downloaded Source Han Serif in TTF format and ran this command to bundle it as a resource:

1fyne bundle SourceHanSerifSC.ttf > bundled.go

Next, we get a theme from one of Fyne's demo apps—“Notes.”

The only thing we need to do is change the font name in the Font() method in theme.go:

1func (m *myTheme) Font(s fyne.TextStyle) fyne.Resource {
2	return resourceSourceHanSerifSCTtf
3}

See? Not bad.

Data-binding

I borrowed the theme from Fyne Notes and didn't change its color, which explains the yellow background. The new theme is applied by calling the SetTheme() method.

In this example, whenever the user types in or clears the text field, the greeting changes. This is done by setting up data binding. (If linking two things is “binding,” then the two things are “bound” together. I guess I'll call them “bound variables” and “bound widgets.”)

First, we need to declare bound string variables. Then, we use them to create bound widgets. Note that different methods are used to create the widgets.

At this point, we need to link the two bound variables. Some methods allow bidirectional conversions between a number and a string, and some even support string formats. There's also a method for converting strings to and from URIs.

But it seems that the only way to change greeting whenever userinput changes is to add a listener. To do so, you need to create a data listener from an anonymous function.

With this setup, whenever userinput changes, the callback function runs. Notice how we get and set the value of a bound variable.

 1package main
 2
 3import (
 4	"fmt"
 5	"fyne.io/fyne/v2/app"
 6	"fyne.io/fyne/v2/widget"
 7	"fyne.io/fyne/v2/container"
 8	"fyne.io/fyne/v2/data/binding"
 9)
10
11func main() {
12	a := app.New()
13    // Setting the theme. `myTheme` is defined in `theme.go`
14	a.Settings().SetTheme(&myTheme{})
15	w := a.NewWindow("你好 Hello")
16	
17    // Declare two binding string variables
18	greeting := binding.NewString()
19    userinput := binding.NewString()
20
21    // Adding a listener
22	userinput.AddListener(
23		binding.NewDataListener(func() {
24			if val, ok := userinput.Get(); val == "" || ok != nil {
25				greeting.Set(
26                    "你叫什么名字?\nWhat is your name?"
27                    )
28			} else {
29				greeting.Set(
30                    fmt.Sprintf("你好,%s!\nHello, %s!", val, val)
31                    )
32			}
33		}))
34	
35    // Creating binding widgets
36	l := widget.NewLabelWithData(greeting)
37	e := widget.NewEntryWithData(userinput)
38
39	w.SetContent(container.NewVBox(l, e))
40	w.ShowAndRun()
41}

A few words

I tried two other pure-Go GUI packages: Gio and Nuxui. Both are ingenious projects. (Quite a few other packages help connect Go apps to more established GUI engines.)

Gio seems much more flexible than Fyne, and the way it uses contexts and channels seems more Go-like. But typing Chinese in the input field simply doesn't work. With a CJK IME, you type a sequence of letters and then choose a character or word from a pop-up menu. Ideally, when an IME is active, keydown events should not be registered. The input field should receive data only when a word has been composed by the IME and “committed,” or selected by the user. But when you type in Gio's default input field, both each letter in the sequence and the committed word are entered into the field. This probably has to do with how Gio handles keyboard events.

Nuxui uses a backtick-delimited string to define the UI declaratively. This is far less cumbersome than calling a bunch of nested constructors. I didn't spend too much time on it because versions 0.6 through 0.8 introduced breaking changes to some basic functions, causing some sample projects to fail to compile.

I spent the most time on Fyne because it is the best-documented of the three. There are a few demo videos and even a book!

Fyne is designed with unit testing in mind. It provides helper functions for mocking events and supports snapshot comparisons to verify that the UI renders correctly.

Fyne is also well structured—so much so that it occasionally feels overly boilerplate-heavy.

All in all, Fyne is great and very promising. I had a great time playing with it, and I'm sure you will too.

◆

Creating My Own Static Site Generator

Since I take a lot of notes, I recently thought that I could edit some of them and turn them into a blog. I wasn't satisfied with WordPress because it felt bloated. I didn't feel like familiarizing myself with the settings and configuration of another static site generator, either. So I decided to write my own. I named it “Lysekil” after a place I visited and loved in late March. I published it on GitHub and provided brief documentation.

The template's color scheme was borrowed from the default “Red Graphite” theme in Bear, my favorite note-taking app. (It also happens to be Andy Matuschak's favorite note-taking app. I used to waver between a few other options, including Obsidian, RemNote, and Roam Research. In the end, I always came back to Bear.)

How I made it

I used third-party Python packages to process Markdown*, generate the Atom feed, and apply syntax highlighting to code.

Markdown

Notes in Bear use a plaintext format very similar to Markdown. Writing blog posts in Markdown feels natural.

There are two Python packages available to convert Markdown files into HTML: python-markdown and python-markdown2.

I chose the latter because it integrated more easily with an extra that enables syntax highlighting. It also seemed easier to enable extras (or extensions) in general with python-markdown2 than with python-markdown.

Syntax highlighting

Both packages support syntax highlighting with Pygments. According to the documentation, enabling code highlighting in python-markdown takes a lot more effort.

With markdown2, I could enable the “footnotes”1 and “fenced-code-blocks” extras with ease. To my delight, “fenced-code-blocks” worked exactly like highlighted code blocks in Bear.

Feed

Some people on the web call their personal websites blogs without providing an Atom or RSS feed. (I'm opinionated about this: it's not a blog if it doesn't have a feed.)

The package I used to create the Atom feed was python-feedgen. It supports both Atom and RSS, but there are small differences between the two. I chose Atom over RSS for no particular reason.

One hitch at this step was that Feedgen required timezone information to record when entries were created. To make it work, I needed to pass this information in a timezone object.

This timezone object is defined in the datetime package. Before Python 3.9, one way to specify a timezone was to add an offset to UTC using a timedelta.

Luckily for me, I had upgraded to Python 3.9.5, which provides the zoneinfo package and allows you to specify timezone information with a string, like this: ZoneInfo('Europe/Stockholm'). (The datetime package in Python 3.9.x also makes it easier to manipulate dates and times in ISO-formatted strings.)

Blogs and pages

Blog posts (or “notes”) are presented in chronological order, organized by tags, and grouped by quarter. Pages, on the other hand, are not presented on a timeline.

If you looked at the source code, you would see that this part's implementation is not particularly “DRY.”

Styling

I used SCSS to write the CSS stylesheet. SCSS allows you to nest CSS selectors, making it much easier to organize a CSS file.

I tried adding animated effects to the header using JavaScript. I relied on the window.onscroll event, and it was a bad idea.

It worked on my MacBook Pro, iPhone, and iPad. But because of differences in vertical scroll offsets and the widths of visible elements, it didn't work when the page was not long enough on some screens.

When the page isn't long enough, scrolling up doesn't create a sufficiently large offset to trigger and sustain the width change. There is also the possible performance issue created by too many function calls while the page scrolls. (This topic deserves a more detailed discussion of its own.)

I then tried using { position: sticky } to place the navigation bar at the top of the article page. It looked nice. But since it's already easy to navigate the site, letting the navigation bar take up space at the expense of the reading experience didn't feel right. This was especially true on a smartphone.

What I did wrong

I began working on this SSG on a whim, without clearly planning which features I wanted or how I would implement them.

At first, I thought a single script that converted a bunch of .md files to HTML would suffice. I ended up with a template consisting of HTML snippets sprinkled across six helper functions. (If anyone wants to create a new template for this SSG, I would recommend against it. LOL.)

To organize timelines, tags, and archive groupings, I ended up creating two content classes (notes and pages) and three listing classes (tags, archive groupings, and home-page pagination). Had I planned it in advance and used a better inheritance structure, the classes would have been DRY-er.


  1. Titta! Detta är en fotnot. (Look! This is a footnote.) ↩︎

◆