EXEPERTAI LAB
EXEPERT / DIRECTORY

Inside cool-retro-term

cool-retro-term is a terminal emulator by Filippo Scognamiglio that looks like an old cathode-ray-tube monitor. This page runs a web port of it. It uses the app's own shaders, settings maths, built-in profiles and fonts, translated from Qt to WebGL2, so you can see exactly how the effect is built.

The CRT components in the UI gallery's Frames section are EXEPERT's own look-alikes. This is the real renderer, credited to its authors and released under the GPL like the original. The source is linked at the end.

Try it

The terminal runs the real cool-retro-term shaders in WebGL2. Powering on loads its fonts and renderer (about 2 MB); your settings stay in this browser.

Type help once it's on. Right-click for the menu, or open Settings from the title bar.

How a frame is made

Values for Default Amber in a 1024×768 window. Power on the terminal and they follow it instead.

cool-retro-term render passesThe terminal is painted into a text layer. The text feeds the burn-in feedback layer, the bloom blur and the static pass. The static pass, burn-in, frame and noise textures all feed the dynamic pass, which draws the window.Terminal496×368Text layer1024×768Burn-in512×384Bloom512×384, 6 levelsFrame1024×768Static pass1024×768Noise512×512Dynamic pass1024×768 window
Each box is a texture. Sizes are in device pixels for the frame described below. Select a box to jump to its step.

The text is drawn small, then magnified

cool-retro-term does not draw text at screen resolution. It works out a smaller virtual screen and lets the terminal widget (qmltermwidget, a fork of KDE's Konsole) paint into that. Low-resolution fonts are drawn at their native pixel size with antialiasing off, so every glyph pixel stays a hard square.

Terminess is a pixel font, drawn at its native 12 px. The font manager compares how tall a line should be on screen at the current font scaling with the font's own line height. The ratio, 2, is how many screen pixels one virtual pixel becomes. The virtual screen is the window divided by that ratio.

Virtual resolution
512×384
Screen scaling
2×
Margin
8 virtual px
Grid
82 columns × 26 lines
Cell
6×14
Text layer
1024×768
app/qml/PreprocessedTerminal.qml:99–110 The kterminal item sizes itself from the font's scaling.
property int textureResolutionScale: appSettings.lowResolutionFont ? Screen.devicePixelRatio : 1
property int margin: appSettings.margin / screenScaling
property int totalWidth: Math.floor(parent.width / (screenScaling * fontWidth))
property int totalHeight: Math.floor(parent.height / screenScaling)
 
property int rawWidth: totalWidth - 2 * margin
property int rawHeight: totalHeight - 2 * margin
 
textureSize: Qt.size(width / textureResolutionScale, height / textureResolutionScale)
 
width: ensureMultiple(rawWidth, Screen.devicePixelRatio)
height: ensureMultiple(rawHeight, Screen.devicePixelRatio)

A ShaderEffectSource then captures that item, margins included, into the text layer. Its texture is 2× the virtual size, and it is set to repeat at the edges, which the curvature step relies on.

app/qml/PreprocessedTerminal.qml:268–276
ShaderEffectSource{
    id: kterminalSource
    sourceItem: kterminal
    hideSource: true
    wrapMode: ShaderEffectSource.Repeat
    visible: false
    textureSize: Qt.size(kterminal.totalWidth * scaleTexture, kterminal.totalHeight * scaleTexture)
    sourceRect: Qt.rect(-kterminal.margin, -kterminal.margin, kterminal.totalWidth, kterminal.totalHeight)
}

Burn-in: the phosphor remembers

A real CRT's phosphor keeps glowing after the beam moves on. cool-retro-term fakes this with a texture that renders into itself. Whenever the terminal repaints, the burn-in shader keeps the brighter of the remembered image (darkened by how long ago it was last stamped) and the new text. The alpha channel marks where the new text won.

app/shaders/burn_in.frag:21–36
void main() {
    vec2 coords = qt_TexCoord0;
 
    vec3 txtColor = texture(txt_source, coords).rgb;
    vec4 accColor = texture(burnInSource, coords);
 
    float prevMask = accColor.a;
 
    float blurDecay = clamp((burnInLastUpdate - prevLastUpdate) * burnInTime, 0.0, 1.0);
    blurDecay = max(0.0, blurDecay - prevMask);
    vec3 color = max(accColor.rgb - vec3(blurDecay), txtColor);
 
    float currMask = step(rgb2grey(color), rgb2grey(txtColor));
 
    fragColor = vec4(color, currMask) * qt_Opacity;
}

The layer only updates when the terminal emits imagePainted, and that signal reaches QML one frame after the paint. Each update records the time it happened, and the decay is the time between the last two updates multiplied by burnInTime.

app/qml/BurnInEffect.qml:42–56
function completelyUpdate() {
    let newTime = timeManager.time
    if (newTime > lastUpdate) {
        prevLastUpdate = lastUpdate
        lastUpdate = newTime
    }
 
    item.source.scheduleUpdate()
}
 
function restartBlurSource() {
    prevLastUpdate = timeManager.time
    lastUpdate = prevLastUpdate
    completelyUpdate()
}

In the final pass the remembered image is drawn at 65% and fades as time passes since the last update. With burn-in at 0.3 it takes 0.592 s to disappear.

burnInTime
1.689189
Fade time
0.592 s
Burn-in layer
512×384

Bloom: Qt's FastBlur, six levels deep

The glow around bright text comes from Qt's FastBlur. It never runs a wide Gaussian. Instead it copies the text into a padded texture, then halves it five times. Each halving averages four taps placed slightly off the pixel grid. A final pass blends up to five of the six levels.

The weights come from a single number, lod = √(radius ÷ 64) × 1.2 − 0.2. Each level sits at a centre (0.1, 0.3 … 1.1), and a level contributes in proportion to how close its centre is to lod. Here the radius is 40, set by bloom quality.

app/qml/TerminalContainer.qml:57–69
Loader {
    id: bloomEffectLoader
    active: loadBloomEffect
    asynchronous: true
    width: parent.width * appSettings.bloomQuality
    height: parent.height * appSettings.bloomQuality
 
    sourceComponent: FastBlur {
        radius: Utils.lint(16, 64, appSettings.bloomQuality)
        source: terminal.mainSource
        transparentBorder: true
    }
}
Radius
40
lod
0.7487
Weights
0 · 0.041 · 0.201 · 0.361 · 0.279 · 0.119
Levels
640×512 → 320×256 → 160×128 → 80×64 → 40×32 → 20×16

The static pass: curvature, colour fringes, reflections

Everything that does not change from frame to frame happens here, so it only runs again when the text or a setting changes. Barrel curvature pushes each coordinate outward by the square of its distance from the centre. Where the curve lands outside the screen, the shader mirrors the coordinate back in. That mirrored image becomes the faint reflection of the text on the bezel.

app/shaders/terminal_static.frag:50–57
    vec2 txt_coords = qt_TexCoord0;
#if CRT_CURVATURE == 1
    vec2 curvatureCoords = distortCoordinates(qt_TexCoord0);
    shownDraw = max2(step(vec2(0.0), curvatureCoords) - step(vec2(1.0), curvatureCoords));
    isScreen = min2(step(vec2(0.0), curvatureCoords) - step(vec2(1.0), curvatureCoords));
    isReflection = shownDraw - isScreen;
    txt_coords = curvatureCoords * (-1.0 + 2.0 * step(vec2(0.0), curvatureCoords) - 2.0 * step(vec2(1.0), curvatureCoords));
#endif

RGB shift samples the text twice more, a little to the left and right, and mixes the colour channels unevenly. Bloom is added (capped at 0.5 per channel), and the result is divided by 1 + bloom. The dynamic pass multiplies it back up later, so overall brightness stays the same while the glow spreads.

app/shaders/terminal_static.frag:80–95
#if CRT_BLOOM == 1
    vec3 bloomOnScreen = bloomColor * isScreen;
    finalColor += clamp(bloomOnScreen * bloom * bloomAlpha, 0.0, 0.5);
    float bloomScale = 1.0 + max(bloom, 0.0);
    finalColor /= bloomScale;
#endif
 
#if CRT_FRAME_SHININESS == 1
    vec3 reflectionColor = mix(bloomColor * bloomAlpha * 2.0, finalColor, frameShininess * 0.5);
    finalColor = mix(finalColor, reflectionColor, isReflection);
#endif
 
    finalColor *= screen_brightness;
 
    float noise = rand2(qt_TexCoord0) - 0.5;
    finalColor = clamp(finalColor + vec3(noise * 0.025), 0.0, 1.0);
screenCurvature
0.137143
frameSize
0.005714
rgbShift
0
bloom
1.5
screen_brightness
1
Variant
terminal_static_rgb0_bloom1_curve1_shine1

The frame: a rounded rectangle and a little light

The bezel has no image file. A signed-distance function measures how far each pixel is from a rounded rectangle, in pixels. Four diagonal masks split the bezel into top, bottom, left and right, and each gets its own brightness. The bottom edge, which faces up toward the room's light, is brightest; the sides are at two thirds and the top at one third. Inside the screen, ambient light adds a soft glare that is brightest in the middle.

app/shaders/terminal_frame.frag:63–78
 
float distPixels = roundedRectSdfPixels(coords, vec2(0.0), vec2(1.0), screenRadiusPixels);
float frameShadow = (e * 0.66 + w * 0.66 + n * 0.33 + s);
frameShadow *= smoothstep(0.0, edgeSoftPixels * 5.0, distPixels);
 
float frameAlpha = 1.0 - frameShininess * 0.4;
 
float inScreen = smoothstep(0.0, edgeSoftPixels, -distPixels);
float alpha = mix(frameAlpha, mix(0.0, 0.3, ambientLight), inScreen);
float glass = clamp(ambientLight * pow(prod2(coords * (1.0 - coords.yx)) * 25.0, 0.5) * inScreen, 0.0, 1.0);
vec3 frameTint = frameColor.rgb * frameShadow;
float noise = rand2(staticCoords * viewportSize) - 0.5;
frameTint = clamp(frameTint + vec3(noise * 0.04), 0.0, 1.0);
vec3 color = mix(frameTint, vec3(glass), inScreen);
 
fragColor = vec4(color, alpha) * qt_Opacity;
screenRadius
15.6 px
ambientLight
0.3
frameShininess
0.15
frameColor
(0.4166, 0.3727, 0.3277, 1)

The dynamic pass: everything that moves

This pass draws the window. Time advances only every 3 frames, which is 20 updates a second at 60 Hz, so the noise has a filmic, stepped feel. The vertex shader reads one noise texel per update. Its green channel sets the flicker brightness and the frequency of the horizontal-sync wobble. Its red channel decides whether a sync glitch fires at all.

app/shaders/terminal_dynamic.vert:42–49
vec2 coords = vec2(fract(time / 2.048), fract(time / 1048.576));
vec4 initialNoiseTexel = texture(noiseSource, coords);
 
vBrightness = 1.0 + (initialNoiseTexel.g - 0.5) * flickering;
 
float randval = horizontalSyncStrength - initialNoiseTexel.r;
vDistortionScale = step(0.0, randval) * randval * horizontalSyncStrength * horizontalSync;
vDistortionFreq = mix(4.0, 40.0, initialNoiseTexel.g) * step(0.0, horizontalSync);

The fragment shader bends each row sideways for horizontal sync and samples the noise texture, scrolled by time, for static and jitter. It adds the slow bright band of the "glowing line", then layers the burn-in on top. The noise texture is uploaded premultiplied, as Qt uploads every image, so its red channel is already scaled by its alpha. That matters, because red is the value the sync check compares.

app/shaders/terminal_dynamic.frag:138–166
    vec2 staticCoords = distortCoordinates(qt_TexCoord0);
    vec2 coords = qt_TexCoord0;
 
    float dst = sin((coords.y + time) * vDistortionFreq);
    coords.x += dst * vDistortionScale;
 
    vec4 noiseTexel = texture(noiseSource, scaleNoiseSize * coords + vec2(fract(time / 0.051), fract(time / 0.237)));
 
    vec2 txt_coords = coords + (noiseTexel.ba - vec2(0.5)) * jitterDisplacement * jitter;
 
    float color = 0.0001;
    color += noiseTexel.a * staticNoise * (1.0 - distance * 1.3);
    color += randomPass(coords * virtualResolution) * glowingLine;
 
#if CRT_DISPLAY_FRAME == 1
    vec4 frameColor = texture(frameSource, qt_TexCoord0);
    color *= (1.0 - frameColor.a);
#endif
 
    vec3 txt_color = texture(screenBuffer, txt_coords).rgb;
    float bloomScale = 1.0 + max(bloom, 0.0);
    txt_color *= bloomScale;
 
#if CRT_BURN_IN == 1
    vec4 txt_blur = texture(burnInSource, staticCoords);
    float blurDecay = clamp((time - burnInLastUpdate) * burnInTime, 0.0, 1.0);
    vec3 burnInColor = 0.65 * (txt_blur.rgb - vec3(blurDecay)) * (1.0 - txt_blur.a);
    txt_color = max(txt_color, burnInColor);
#endif

Scanline, pixel and sub-pixel rasterization darken the gaps between virtual pixels. They fade in as each virtual pixel grows from 2 to 4 physical pixels, so dense screens don't show moiré. Here the density is 2, so the rasterization intensity is 0.

app/shaders/terminal_dynamic.frag:77–85 Scanlines (mode 1).
#if CRT_RASTER_MODE == 1
    vec3 pixelHigh = ((1.0 + BRIGHTBOOST) - (0.2 * texel)) * texel;
    vec3 pixelLow  = ((1.0 - INTENSITY) + (0.1 * texel)) * texel;
 
    vec2 coords = fract(screenCoords * virtualRes) * 2.0 - vec2(1.0);
    float mask = 1.0 - abs(coords.y);
 
    vec3 rasterizationColor = mix(pixelLow, pixelHigh, mask);
    return mix(texel, rasterizationColor, intensity);
horizontalSyncStrength
0.08
flickering
0.1
staticNoise
0.1
jitterDisplacement
(0.0014, 0.0004)
glowingLine
0.04
Variant
terminal_dynamic_raster0_burn1_frame1_chroma1
app/qml/TimeManager.qml:28–41 The effects clock.
property int framesPerUpdate: Math.max(1, appSettings.effectsFrameSkip)
property int _frameCounter: 0
 
property var frameDriver: FrameAnimation {
    running: enableTimer
    onTriggered: {
        timeManager._frameCounter += 1
 
        if (timeManager._frameCounter >= timeManager.framesPerUpdate) {
            time = elapsedTime
            timeManager._frameCounter = 0
        }
    }
}

Colour: from two hex codes to a phosphor

The terminal paints in plain colours. The tint comes at the very end, from just two inputs: a font colour and a background colour. Saturation first pushes the font colour toward white, and the result is stored as a #rrggbb string. Contrast then mixes the two colours toward each other. One small quirk: strToColor divides by 256, not 255, so even #ffffff comes out slightly below 1.0.

app/qml/ApplicationSettings.qml:66–72
property string _backgroundColor: "#000000"
property string _fontColor: "#ff8100"
property string _frameColor: "#ffffff"
property string saturatedColor: Utils.mix(Utils.strToColor(_fontColor), Utils.strToColor("#FFFFFF"), (saturationColor * 0.5))
property color fontColor: Utils.mix(Utils.strToColor(_backgroundColor), Utils.strToColor(saturatedColor), (0.7 + (contrast * 0.3)))
property color backgroundColor: Utils.mix(Utils.strToColor(saturatedColor), Utils.strToColor(_backgroundColor), (0.7 + (contrast * 0.3)))
property color frameColor: Utils.strToColor(_frameColor)
app/qml/utils.js:65–70
function strToColor(s){
    var r = parseInt(s.substring(1,3), 16) / 256;
    var g = parseInt(s.substring(3,5), 16) / 256;
    var b = parseInt(s.substring(5,7), 16) / 256;
    return Qt.rgba(r, g, b, 1.0);
}

In the shader each pixel's brightness picks a point between the background and font colours. Chroma then lets some of the terminal's own colour through, which is why ls colours survive on the colour profiles.

app/shaders/terminal_dynamic.frag:123–132
vec3 convertWithChroma(vec3 inColor) {
#if CRT_CHROMA == 1
    float grey = rgb2grey(inColor);
    float denom = max(grey, 0.0001);
    vec3 foregroundColor = mix(fontColor.rgb, inColor * fontColor.rgb / denom, chromaColor);
    return mix(backgroundColor.rgb, foregroundColor, grey);
#else
    return mix(backgroundColor.rgb, fontColor.rgb, rgb2grey(inColor));
#endif
}
Font colour
#ff8100
Background
#000000
Saturation, contrast
0.2, 0.8
saturatedColor
#fe8d19
fontColor
#ee8417
backgroundColor
#0f0801

Every setting, as the shaders receive it

Each row is a QML property binding from upstream, evaluated for this frame. Colours arrive premultiplied, and every value is rounded to 32-bit float on the way to the GPU.

Values the bindings share (QML)
NameValueQML bindingSource
normalizedWindowScale1.1428571024 / ((0.5 * width + 0.5 * height))TerminalWindow.qml:47
screenResolution(1024, 768)Qt.size(terminalWindow.width * Screen.devicePixelRatio * appSettings.windowScaling, terminalWindow.height * Screen.devicePixelRatio * appSettings.windowScaling)TerminalContainer.qml:39
_screenDensity2Math.min(screenResolution.width / virtualResolution.width, screenResolution.height / virtualResolution.height)ShaderTerminal.qml:67
burnInFadeTime1.6891891 / Utils.lint(_minBurnInFadeTime, _maxBurnInFadeTime, burnIn)BurnInEffect.qml:34
loadBloomEffect1appSettings.bloom > 0 || appSettings._frameShininess > 0TerminalContainer.qml:31
Dynamic pass (terminal_dynamic.vert/.frag)
NameValueQML bindingSource
qt_Opacity1appSettings.windowOpacity * 0.3 + 0.7TerminalContainer.qml:34
time0timeManager ? timeManager.time : 0ShaderTerminal.qml:107
fontColor(0.932662, 0.517723, 0.091798, 1)parent.fontColor (appSettings.fontColor)ShaderTerminal.qml:79
backgroundColor(0.059525, 0.033051, 0.005859, 1)parent.backgroundColor (appSettings.backgroundColor)ShaderTerminal.qml:80
virtualResolution(512, 384)Qt.size(kterminal.totalWidth, kterminal.totalHeight)ShaderTerminal.qml:101
rasterizationIntensity0Utils.smoothstep(2.0, 4.0, _screenDensity)ShaderTerminal.qml:105
burnInLastUpdate0burnInEffect.lastUpdateShaderTerminal.qml:92
burnInTime1.689189burnInEffect.burnInFadeTimeShaderTerminal.qml:93
burnIn0.3appSettings.burnInShaderTerminal.qml:91
staticNoise0.1appSettings.staticNoiseShaderTerminal.qml:97
screenCurvature0.137143appSettings.screenCurvature * appSettings.screenCurvatureSize * terminalWindow.normalizedWindowScaleShaderTerminal.qml:57
glowingLine0.04appSettings.glowingLine * 0.2ShaderTerminal.qml:88
chromaColor0.2appSettings.chromaColorShaderTerminal.qml:82
jitterDisplacement(0.0014, 0.0004)Qt.size(0.007 * jitter, 0.002 * jitter)ShaderTerminal.qml:96
jitter0.2appSettings.jitterShaderTerminal.qml:95
horizontalSync0.1appSettings.horizontalSyncShaderTerminal.qml:86
horizontalSyncStrength0.08Utils.lint(0.05, 0.35, horizontalSync)ShaderTerminal.qml:87
flickering0.1appSettings.flickeringShaderTerminal.qml:85
scaleNoiseSize(2, 1.5)Qt.size((width * 0.75) / (noiseTexture.width * appSettings.windowScaling * appSettings.totalFontScaling), (height * 0.75) / (noiseTexture.height * appSettings.windowScaling * appSettings.totalFontScaling))ShaderTerminal.qml:98
frameShininess0.15appSettings.frameShininessShaderTerminal.qml:111
frameSize0.005714appSettings.frameSize * terminalWindow.normalizedWindowScaleShaderTerminal.qml:58
bloom1.5parent.bloomSource ? appSettings.bloom * 2.5 : 0ShaderTerminal.qml:112
Static pass (terminal_static.frag)
NameValueQML bindingSource
qt_Opacity11 (rendered into frameBuffer, its own layer)ShaderTerminal.qml:194
screenCurvature0.137143parent.screenCurvatureShaderTerminal.qml:175
rgbShift0appSettings.rgbShift * (4.0 / width) * appSettings.totalFontScalingShaderTerminal.qml:179
frameShininess0.15appSettings.frameShininessShaderTerminal.qml:182
frameSize0.005714parent.frameSizeShaderTerminal.qml:183
screen_brightness1Utils.lint(0.5, 1.5, appSettings.brightness)ShaderTerminal.qml:181
bloom1.5bloomSource ? appSettings.bloom * 2.5 : 0ShaderTerminal.qml:173
Frame (terminal_frame.frag)
NameValueQML bindingSource
qt_Opacity11 (rendered into its own layer)ShaderTerminal.qml:147
screenCurvature0.137143appSettings.screenCurvature * appSettings.screenCurvatureSize * terminalWindow.normalizedWindowScaleTerminalFrame.qml:36
frameColor(0.416556, 0.372717, 0.327703, 1)Utils.mix(Utils.scaleColor(_lightColor, 0.2), _staticFrameColor, 0.125 + 0.750 * ambientLight)TerminalFrame.qml:30
frameSize0.005714appSettings.frameSize * terminalWindow.normalizedWindowScaleTerminalFrame.qml:40
screenRadius15.6appSettings.screenRadiusTerminalFrame.qml:42
viewportSize(1024, 768)Qt.size(width / appSettings.windowScaling, height / appSettings.windowScaling)TerminalFrame.qml:44
ambientLight0.3appSettings.ambientLightTerminalFrame.qml:46
frameShininess0.15appSettings.frameShininessTerminalFrame.qml:38
Burn-in (burn_in.frag)
NameValueQML bindingSource
qt_Opacity11 (rendered into its own layer)BurnInEffect.qml:61
burnInLastUpdate0burnInEffect.lastUpdateBurnInEffect.qml:118
burnInTime1.689189burnInFadeTimeBurnInEffect.qml:117
prevLastUpdate0burnInEffect.prevLastUpdateBurnInEffect.qml:119

Pass inspector

Power on the terminal to look inside it. The inspector shows every texture above as the GPU holds it, lets you stop and step the effects clock, and compiles all 56 shader variants.

How we know it matches

cool-retro-term ships its shaders precompiled as Qt .qsb files, and each one also holds a GLSL ES 1.00 version that Qt generated from the same source. Our test suite renders every variant the app can select both ways: 40 dynamic-pass and 16 static-pass variants, plus the frame and burn-in shaders, at three moments in time. Both versions get identical textures and settings, and every pixel comes out identical.

The settings maths is checked against Qt's own source, down to the details that change pixels:

  • colours are stored at 16 bits per channel;
  • colour uniforms are premultiplied;
  • the noise texture is premultiplied with Qt's rounding;
  • layer sizes are rounded the way Qt rounds them.

Font metrics come from Qt 6.10 itself.

Where the web port differs

  • The shell is simulated. A web page can't start processes, so the terminal runs a small built-in shell with commands chosen to show the effects (top, matrix, testcard). Terminal emulation underneath is @xterm/headless instead of Konsole's engine. Text drawing follows qmltermwidget.
  • Fonts are drawn by the browser.
    • Pixel fonts are drawn eight times larger and reduced to 1-bit masks, because a canvas can't turn off antialiasing.
    • Terminess and Gohu use the bitmap glyphs embedded in their font files, as FreeType does.
    • Fixedsys Excelsior and Cozette are left out because they ship without a licence.
  • Some shortcuts belong to the browser. Ctrl+Shift+T, W, N and Q open and close browser tabs and windows, so here New Tab, Close Tab, New Window and Quit are Alt+Shift+T, W, N and Q. The menus keep the original labels.
  • One window. New Window opens the page again. Copy takes the visible screen, because text selection isn't ported yet.
  • Settings are saved as you change them, in this browser, rather than when the app quits. The ?profile=Name and ?default-settings URL parameters stand in for the command-line flags.

Credits and licence

cool-retro-term is © 2013–2025 Filippo Scognamiglio and 65 other contributors, and is free software under the GNU General Public License, version 3 or later. Its terminal widget, qmltermwidget, builds on KDE's Konsole (Lars Doelle, Robert Knight, Dmitry Zagnoyko) and the qtermwidget project (134 contributors). Bloom uses Qt's FastBlur, © The Qt Company, under the LGPL-3.0 / GPL options Qt offers.

The web port is by EXEPERT. It is released under the same licence, GPL-3.0-or-later, as a modified version of cool-retro-term at 1394ce8. Every file names its origin and what was changed. The terminal's About dialog lists everyone who contributed upstream.

The complete source of this port is a download from this site. It is rebuilt with every deploy, so it always matches what runs above. Inside it, NOTICE.md lists the upstream works, what was changed and where each file came from, and fonts/LICENSES.md covers every font.