… to make a drop-in replacement of (now unmaintained) Fira Mono.
"Show me the code"
The development happens in: https://github.com/qbane/FiraCodeNL.
The build is considered stable, but the modifications to Glyphs source code are ad-hoc, not easily mergable with upstream, and should be considered half-done.
Project update: 2025/3/23
Unfortunately, the glyph file format is updated in this commit. If I want to pick up the work, I need to rebase everything I have changed to this commit…
The repo of Fira Mono: https://github.com/mozilla/Fira. The last version is 3.206.
The repo of Fira Code: https://github.com/tonsky/FiraCode. As of writing (2022/6), the latest version number is 6.2, published in late 2021. There is a milestone to v7, but no breaking change has been published.
By the way, Fira Sans had been maintained for a while as FiraGO, whose last commit was in 2018, but no plan was for Fira Mono. So sad.
There is an official branch that has built a variant that removes ligatures called "Fira Code Fixed", but it is only available for v5.2, with no changelog to follow. We use this as a blueprint; see this section for details.
After some reading into the source, all the ingredient to the font is contained in the glyph file FiraCode.glyphs
. It is possibly not the only file we need to work on (more on that later). First, it is in an obscure format called ASCII (old-style) plist format. We had better convert it to JSON to take advantage of existing JSON diffing tools (by the way, my favorite web-based diffing tool is https://jsoneditoronline.org/):
import sys
import json
# pip install openstep_plist
import openstep_plist
with open(sys.argv[1], 'r') as fp:
data = openstep_plist.load(fp, use_numbers=True)
text = json.dumps(data, sort_keys=True, indent=2)
print(text)
Second, the semantics is seemingly proprietary, and the repo even contains a simple decoder/encoder written in Clojure for it to make variants. There is a libary googlefonts/glyphsLib to help us on exploring the format.
Since I am not familar with font mechinary, I am making this document alongside the repository toward the goal.
Now it's time for the cherry picking…
Let us summarize and categorize each and every feature, determining the set of features to be kept. Here is my policy (very subjective):
liga
and calt
both enabled, should be ligature-free.Update 2024/7/17
This is fine if you use the font as a display font, but need more considerations as an editor font, where vertical space tuning can have distracting effects for some people.
Below is a quick reference to each ssXX/cvXX (wiki):
Code | Character(s) |
Image Not Showing
Possible Reasons
|
Notes |
---|---|---|---|
ss01 | r |
Image Not Showing
Possible Reasons
|
|
ss02 | <= → ≤; >= → ≥ |
Image Not Showing
Possible Reasons
|
|
ss03 | & |
Image Not Showing
Possible Reasons
|
|
ss04 | $ |
Image Not Showing
Possible Reasons
|
|
ss05 | @ |
Image Not Showing
Possible Reasons
|
|
ss06 | Thin \ when escaping |
Image Not Showing
Possible Reasons
|
|
ss07 | =~ → ≈; !~ → ≉ |
Image Not Showing
Possible Reasons
|
|
ss08 | Small gaps in == -s |
Image Not Showing
Possible Reasons
|
become void automatically *1 |
ss09 | <<= >>= |= ||= |
Image Not Showing
Possible Reasons
|
|
ss10 | Connected Tl fl fi … |
Image Not Showing
Possible Reasons
Image Not Showing
Possible Reasons
|
keep for bw comp. w/ Fira Mono *2 |
cv01 | a |
Image Not Showing
Possible Reasons
|
|
cv02 | g |
Image Not Showing
Possible Reasons
|
|
cv03..06 | i |
Image Not Showing
Possible Reasons
|
|
cv07..10 | l |
Image Not Showing
Possible Reasons
|
|
cv11..13 | 0 (zero) |
Image Not Showing
Possible Reasons
|
see also zero |
cv14 | 3 |
Image Not Showing
Possible Reasons
|
|
cv15..16 | * |
Image Not Showing
Possible Reasons
|
|
cv17 | ~ |
Image Not Showing
Possible Reasons
|
|
cv18 | % |
Image Not Showing
Possible Reasons
|
*3 |
cv19..20 | <= → (19: ≤; 20: ⇐) |
Image Not Showing
Possible Reasons
|
|
cv21..22 | =< → (21: ⩽; 22: ≤) |
Image Not Showing
Possible Reasons
|
|
cv23 | >= → ≥ |
Image Not Showing
Possible Reasons
|
|
cv24 | /= → ≠ |
Image Not Showing
Possible Reasons
|
|
cv25 | .- |
Image Not Showing
Possible Reasons
|
|
cv26 | :- |
Image Not Showing
Possible Reasons
|
|
cv27 | [] → □ |
Image Not Showing
Possible Reasons
|
|
cv28 | {. .} |
Image Not Showing
Possible Reasons
|
|
cv29 | Curly { } |
Image Not Showing
Possible Reasons
|
|
cv30 | Longer | |
Image Not Showing
Possible Reasons
|
|
cv31 | Curly ( ) |
Image Not Showing
Possible Reasons
|
|
cv32 | .= |
Image Not Showing
Possible Reasons
|
Legend:
Icon | Descriptions |
---|---|
Image Not Showing
Possible Reasons
|
A character variation. Preserve this. |
Image Not Showing
Possible Reasons
|
A ligature. Remove this. |
Image Not Showing
Possible Reasons
|
Can't decide what to do. |
Some explainers:
It "undoes" the ligature for ==
and ===
(→ ≡), just making them look tighter. They are still counted as ligatures, but not enabled by default. I would like to preserve the ==
and ===
ones.
The !=
and !==
ligatures turn into "≠" but longer. These are definitely not preserved.
Fira Mono made two ligatures fi
and fl
available via dlig
, which were surprisingly 1-char wide! Fira Code removes the "feature", and it fine-tunes letter pairs f[ij]+
and [FTI]l+
so that the horizontal strokes are aligned. Ligatures for those letter pairs, still 2-char wide, are available via ss10
. But if you inspect more carefully, ss10
actually contains two extra pairs fl
and ft
. The problem is that ft
's horizontal bar are not aligned (ref.)
fi fii fl fll
Rules can interact with each other, as they are expected to be controlled independently. For instance, there is a ligature for %%
called percent_percent.liga
, and since cv18
substitutes %
, it oughts to provide a ligature variation percent_percent.liga.cv18
when both are in effect. So .liga
may not always appear in the end. There are even duplicates zero.zero.tosf
and zero.tosf.zero
!
calt
)See features/calt/*.fea
in repo:
center
: Verticially align <:>
s, use .center
conj_disj
: /\
→ ∧, \/
→ ∨cross
: Vertically center the x
in i.e., 1920x1080
and 0xFF
– see this threaddashes
: --
→ –, ---
→ —equal_arrows
: connected <===>>
fi_fl
: Verticially align fij
+ ij
; FTIl
+ l
, use .salt_low
greek
: Replace caltGreekUCdiph
and caltGreekUC
. From Fira Mono; Not knowing what exactly they arehyphen_arrows
: connected <--->>
match_cases
: Vertically align -+*
s (w/ lc) and :
s (w/ uc); must go after hyphen_arrows.fea
, use .lc
and .uc
-
and =
not aligning is very annoying! I am disabling it for now…slight
. It only appears in specific point sizes.numbersigns
: Connect ###
sunderscores
: Connect ___
s (shouldn't _
always appear connected? They are invented for this!)
#
s but connecting _
s seems legit.Some other features (not exhaustive; some are automatically generated by Glyphs):
onum
= oldstyle figures, glyphs suffixed .tosf
tnum
= tabular figuresfrac
= fractions (numr
/dnom
are deprecated); Glyphs' implementation is naive, see w3c, also opentypecookbook.com, @sev/frac, for a better receipt, also Iosevka's implementation is the best AFAIK, but its code is cryptic.Warning
Fira Mono's fraction bar's horizontal position is wrong (and so is Fira Code):
1234⁄5678
sinf
= scientific inferiors; note that these are different from subscriptssubs
/sups
= subscripts/superscriptssalt
= stylistic alternates – for balancing symbol height in small capscase
= case-sensitive forms (infinity symbol??)The script script/update_glyphs.sh
invokes clojure -M -m fira-code.main
, updating corresponding portions from other source files. When using the master glyph definitions, the output is (excerpted):
Parsing 'FiraCode.glyphs'...
generated calt: 55 pairs, 26 triples, 3 quadruples, 84 total
replacing class ClosingBracket with 3 entries
replacing class Digit with 13 entries
replacing class DigitTosf with 12 entries
replacing class HexDigit with 12 entries
replacing class OpeningBracket with 3 entries
replacing class Tall with 16 entries
appending to feature calt 270 lines
replacing feature cv01 with 10 lines
...
replacing feature cv32 with 6 lines
replacing feature onum with 12 lines
replacing feature ss01 with 1 lines
...
replacing feature ss10 with 29 lines
replacing feature zero with 2 lines
regenerated NotSpace: 2022 glyphs
Saving 'FiraCode.glyphs'...
The ligature logic is coded in clojure/fira-code/calt.clj
.
See here. A quick reference to script/build.sh
is:
-f
/--features
: Comma-separated list of font features to use. If specified, it calls script/bake_in_features.sh
first to update all specified features to features[name=calt]
, which is by default enabled in most environments.-w
/--weights
: Comma-separated list of weights names to build. Default to build all weights.-n
/--family-name
: Name of this font family, or "features
" to use default font name with features suffixed.-g
/--generate-glyphs-only
: Only generate the updated glyphs file.The targets are:
We are not removing all alternative glyphs from Fira Code, only those not presented in Fira Mono. (So we cannot simply use font feature freezers suggested by Fira Code's official wiki…)
aalt
calt
dlig
onum
/tnum
/zero
frac
/dnom
/numr
subs
/sups
ordn
case
locl
Some more cherry picking is happening.
The repo contains a fixed
branch alongside v5.2. It would save much time if we diff commits and apply changes to the master branch instead of starting over. Essentially, it patches the following:
export=0
to glyphs for ligatures since they become unusedclasses
:
name=notSpace
, *.liga
and *.seq
name=Uppercase
, ?doubleStruck
name=Lowercase
, w_w_w.liga
notSpace
will be auto-updated by script.features
:
aalt
: ss02
, ss07
, hwid
liga
, dlig
calt
: TODO remove lots of lookup {...}
hwid
({ "hwid": {
"automatic": 1,
"code":
"sub cornerbracketleft by cornerbracketleft.half;\n" +
"sub cornerbracketright by cornerbracketright.half;\n" +
""
}})
ss02
:
Less Than/Greater Than with horizontal bar
ss03
: remove the line "sub ampersand_ampersand.liga by ampersand.ss03;
"ss04
: remove ""sub dollar_greater.liga by ...
" *3ss05
: remove "sub asciitilde.spacer' asciitilde_at.liga by asciitilde;
" *2ss06
: remove lookup backslash_thin { ... }
Thin backslash
ss07
Regex matching operator
ss08
Gaps in double-triple equals
glyphs
:
w_w_w.liga
export: 0
to *.liga
glyphs (TODO: .rem
?)familyName
to customParameters[name=note]
to something reasonable.The font for monospaced text of this page is Fira Mono taken from Google Fonts (sorry, but web font has to be base64-encoded in data URI because of the CSP).
Here is a stripped Fira Code Glyphs file converted into JSON for reference: https://jsoneditoronline.org/#left=cloud.a35538d8abb04b11afbf7e86ca50ff1a
pyftsubset
to remove the ligatures.