From f7bd72184b7ca402a29a5ca2f6cdb0d9bb0f4500 Mon Sep 17 00:00:00 2001 From: David Ibia Date: Sun, 9 Nov 2025 14:17:03 +0100 Subject: [PATCH] fix(colorscheme): update theme name from "catppuccin-mocha" to "Catppuccin Frappe" feat(shaders): add CRT-styled shader with public domain license and adjustments for Ghostty, including parameter definitions and tonal control functions. refactor(shader): optimize color conversion and output to SRGB for better visual quality feat(shaders): add flicker, glow, and starfield shader effects for terminal ambiance. --- .config/ghostty/colorscheme | 2 +- .config/ghostty/shaders/crt.glsl | 310 +++++++++++++++++++++++++ .config/ghostty/shaders/flicker.glsl | 42 ++++ .config/ghostty/shaders/glow.glsl | 22 ++ .config/ghostty/shaders/starfield.glsl | 136 +++++++++++ 5 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 .config/ghostty/shaders/crt.glsl create mode 100644 .config/ghostty/shaders/flicker.glsl create mode 100644 .config/ghostty/shaders/glow.glsl create mode 100644 .config/ghostty/shaders/starfield.glsl diff --git a/.config/ghostty/colorscheme b/.config/ghostty/colorscheme index 08bcba7..a1c78a7 100644 --- a/.config/ghostty/colorscheme +++ b/.config/ghostty/colorscheme @@ -1 +1 @@ -theme = "catppuccin-mocha" \ No newline at end of file +theme = "Catppuccin Frappe" \ No newline at end of file diff --git a/.config/ghostty/shaders/crt.glsl b/.config/ghostty/shaders/crt.glsl new file mode 100644 index 0000000..fa69f49 --- /dev/null +++ b/.config/ghostty/shaders/crt.glsl @@ -0,0 +1,310 @@ +// source: https://gist.github.com/qwerasd205/c3da6c610c8ffe17d6d2d3cc7068f17f +// credits: https://github.com/qwerasd205 +//============================================================== +// +// [CRTS] PUBLIC DOMAIN CRT-STYLED SCALAR by Timothy Lottes +// +// [+] Adapted with alterations for use in Ghostty by Qwerasd. +// For more information on changes, see comment below license. +// +//============================================================== +// +// LICENSE = UNLICENSE (aka PUBLIC DOMAIN) +// +//-------------------------------------------------------------- +// This is free and unencumbered software released into the +// public domain. +//-------------------------------------------------------------- +// Anyone is free to copy, modify, publish, use, compile, sell, +// or distribute this software, either in source code form or as +// a compiled binary, for any purpose, commercial or +// non-commercial, and by any means. +//-------------------------------------------------------------- +// In jurisdictions that recognize copyright laws, the author or +// authors of this software dedicate any and all copyright +// interest in the software to the public domain. We make this +// dedication for the benefit of the public at large and to the +// detriment of our heirs and successors. We intend this +// dedication to be an overt act of relinquishment in perpetuity +// of all present and future rights to this software under +// copyright law. +//-------------------------------------------------------------- +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +// KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +// AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +// OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +//-------------------------------------------------------------- +// For more information, please refer to +// +//============================================================== + +// This shader is a modified version of the excellent +// FixingPixelArtFast by Timothy Lottes on Shadertoy. +// +// The original shader can be found at: +// https://www.shadertoy.com/view/MtSfRK +// +// Modifications have been made to reduce the verbosity, +// and many of the comments have been removed / reworded. +// Additionally, the license has been moved to the top of +// the file, and can be read above. I (Qwerasd) choose to +// release the modified version under the same license. + +// The appearance of this shader can be altered +// by adjusting the parameters defined below. + +// "Scanlines" per real screen pixel. +// e.g. SCALE 0.5 means each scanline is 2 pixels. +// Recommended values: +// o High DPI displays: 0.33333333 +// - Low DPI displays: 0.66666666 +#define SCALE 0.33333333 + +// "Tube" warp +#define CRTS_WARP 1 + +// Darkness of vignette in corners after warping +// 0.0 = completely black +// 1.0 = no vignetting +#define MIN_VIN 0.5 + +// Try different masks +// #define CRTS_MASK_GRILLE 1 +// #define CRTS_MASK_GRILLE_LITE 1 +// #define CRTS_MASK_NONE 1 +#define CRTS_MASK_SHADOW 1 + +// Scanline thinness +// 0.50 = fused scanlines +// 0.70 = recommended default +// 1.00 = thinner scanlines (too thin) +#define INPUT_THIN 0.75 + +// Horizonal scan blur +// -3.0 = pixely +// -2.5 = default +// -2.0 = smooth +// -1.0 = too blurry +#define INPUT_BLUR -2.75 + +// Shadow mask effect, ranges from, +// 0.25 = large amount of mask (not recommended, too dark) +// 0.50 = recommended default +// 1.00 = no shadow mask +#define INPUT_MASK 0.65 + +float FromSrgb1(float c) { + return (c <= 0.04045) ? c * (1.0 / 12.92) : + pow(c * (1.0 / 1.055) + (0.055 / 1.055), 2.4); +} +vec3 FromSrgb(vec3 c) { + return vec3( + FromSrgb1(c.r), FromSrgb1(c.g), FromSrgb1(c.b)); +} + +vec3 CrtsFetch(vec2 uv) { + return FromSrgb(texture(iChannel0, uv.xy).rgb); +} + +#define CrtsRcpF1(x) (1.0/(x)) +#define CrtsSatF1(x) clamp((x),0.0,1.0) + +float CrtsMax3F1(float a, float b, float c) { + return max(a, max(b, c)); +} + +vec2 CrtsTone( + float thin, + float mask) { + #ifdef CRTS_MASK_NONE + mask = 1.0; + #endif + + #ifdef CRTS_MASK_GRILLE_LITE + // Normal R mask is {1.0,mask,mask} + // LITE R mask is {mask,1.0,1.0} + mask = 0.5 + mask * 0.5; + #endif + + vec2 ret; + float midOut = 0.18 / ((1.5 - thin) * (0.5 * mask + 0.5)); + float pMidIn = 0.18; + ret.x = ((-pMidIn) + midOut) / ((1.0 - pMidIn) * midOut); + ret.y = ((-pMidIn) * midOut + pMidIn) / (midOut * (-pMidIn) + midOut); + + return ret; +} + +vec3 CrtsMask(vec2 pos, float dark) { + #ifdef CRTS_MASK_GRILLE + vec3 m = vec3(dark, dark, dark); + float x = fract(pos.x * (1.0 / 3.0)); + if (x < (1.0 / 3.0)) m.r = 1.0; + else if (x < (2.0 / 3.0)) m.g = 1.0; + else m.b = 1.0; + return m; + #endif + + #ifdef CRTS_MASK_GRILLE_LITE + vec3 m = vec3(1.0, 1.0, 1.0); + float x = fract(pos.x * (1.0 / 3.0)); + if (x < (1.0 / 3.0)) m.r = dark; + else if (x < (2.0 / 3.0)) m.g = dark; + else m.b = dark; + return m; + #endif + + #ifdef CRTS_MASK_NONE + return vec3(1.0, 1.0, 1.0); + #endif + + #ifdef CRTS_MASK_SHADOW + pos.x += pos.y * 3.0; + vec3 m = vec3(dark, dark, dark); + float x = fract(pos.x * (1.0 / 6.0)); + if (x < (1.0 / 3.0)) m.r = 1.0; + else if (x < (2.0 / 3.0)) m.g = 1.0; + else m.b = 1.0; + return m; + #endif +} + +vec3 CrtsFilter( + vec2 ipos, + vec2 inputSizeDivOutputSize, + vec2 halfInputSize, + vec2 rcpInputSize, + vec2 rcpOutputSize, + vec2 twoDivOutputSize, + float inputHeight, + vec2 warp, + float thin, + float blur, + float mask, + vec2 tone +) { + // Optional apply warp + vec2 pos; + #ifdef CRTS_WARP + // Convert to {-1 to 1} range + pos = ipos * twoDivOutputSize - vec2(1.0, 1.0); + + // Distort pushes image outside {-1 to 1} range + pos *= vec2( + 1.0 + (pos.y * pos.y) * warp.x, + 1.0 + (pos.x * pos.x) * warp.y); + + // TODO: Vignette needs optimization + float vin = 1.0 - ( + (1.0 - CrtsSatF1(pos.x * pos.x)) * (1.0 - CrtsSatF1(pos.y * pos.y))); + vin = CrtsSatF1((-vin) * inputHeight + inputHeight); + + // Leave in {0 to inputSize} + pos = pos * halfInputSize + halfInputSize; + #else + pos = ipos * inputSizeDivOutputSize; + #endif + + // Snap to center of first scanline + float y0 = floor(pos.y - 0.5) + 0.5; + // Snap to center of one of four pixels + float x0 = floor(pos.x - 1.5) + 0.5; + + // Inital UV position + vec2 p = vec2(x0 * rcpInputSize.x, y0 * rcpInputSize.y); + // Fetch 4 nearest texels from 2 nearest scanlines + vec3 colA0 = CrtsFetch(p); + p.x += rcpInputSize.x; + vec3 colA1 = CrtsFetch(p); + p.x += rcpInputSize.x; + vec3 colA2 = CrtsFetch(p); + p.x += rcpInputSize.x; + vec3 colA3 = CrtsFetch(p); + p.y += rcpInputSize.y; + vec3 colB3 = CrtsFetch(p); + p.x -= rcpInputSize.x; + vec3 colB2 = CrtsFetch(p); + p.x -= rcpInputSize.x; + vec3 colB1 = CrtsFetch(p); + p.x -= rcpInputSize.x; + vec3 colB0 = CrtsFetch(p); + + // Vertical filter + // Scanline intensity is using sine wave + // Easy filter window and integral used later in exposure + float off = pos.y - y0; + float pi2 = 6.28318530717958; + float hlf = 0.5; + float scanA = cos(min(0.5, off * thin) * pi2) * hlf + hlf; + float scanB = cos(min(0.5, (-off) * thin + thin) * pi2) * hlf + hlf; + + // Horizontal kernel is simple gaussian filter + float off0 = pos.x - x0; + float off1 = off0 - 1.0; + float off2 = off0 - 2.0; + float off3 = off0 - 3.0; + float pix0 = exp2(blur * off0 * off0); + float pix1 = exp2(blur * off1 * off1); + float pix2 = exp2(blur * off2 * off2); + float pix3 = exp2(blur * off3 * off3); + float pixT = CrtsRcpF1(pix0 + pix1 + pix2 + pix3); + + #ifdef CRTS_WARP + // Get rid of wrong pixels on edge + pixT *= max(MIN_VIN, vin); + #endif + + scanA *= pixT; + scanB *= pixT; + + // Apply horizontal and vertical filters + vec3 color = + (colA0 * pix0 + colA1 * pix1 + colA2 * pix2 + colA3 * pix3) * scanA + + (colB0 * pix0 + colB1 * pix1 + colB2 * pix2 + colB3 * pix3) * scanB; + + // Apply phosphor mask + color *= CrtsMask(ipos, mask); + + // Tonal control, start by protecting from /0 + float peak = max(1.0 / (256.0 * 65536.0), + CrtsMax3F1(color.r, color.g, color.b)); + // Compute the ratios of {R,G,B} + vec3 ratio = color * CrtsRcpF1(peak); + // Apply tonal curve to peak value + peak = peak * CrtsRcpF1(peak * tone.x + tone.y); + // Reconstruct color + return ratio * peak; +} + +float ToSrgb1(float c) { + return (c < 0.0031308 ? c * 12.92 : 1.055 * pow(c, 0.41666) - 0.055); +} +vec3 ToSrgb(vec3 c) { + return vec3( + ToSrgb1(c.r), ToSrgb1(c.g), ToSrgb1(c.b)); +} + +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + float aspect = iResolution.x / iResolution.y; + fragColor.rgb = CrtsFilter( + fragCoord.xy, + vec2(1.0), + iResolution.xy * SCALE * 0.5, + 1.0 / (iResolution.xy * SCALE), + 1.0 / iResolution.xy, + 2.0 / iResolution.xy, + iResolution.y, + vec2(1.0 / (50.0 * aspect), 1.0 / 50.0), + INPUT_THIN, + INPUT_BLUR, + INPUT_MASK, + CrtsTone(INPUT_THIN, INPUT_MASK) + ); + + // Linear to SRGB for output. + fragColor.rgb = ToSrgb(fragColor.rgb); +} diff --git a/.config/ghostty/shaders/flicker.glsl b/.config/ghostty/shaders/flicker.glsl new file mode 100644 index 0000000..c6183a1 --- /dev/null +++ b/.config/ghostty/shaders/flicker.glsl @@ -0,0 +1,42 @@ +// https://github.com/12jihan/ghostty_shaders/blob/main/flicker.glsl + +float rand(vec2 co) { + return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); +} + +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord/iResolution.xy; + + // Random timing for glitches + float timeScale = floor(iTime * 2.0); + float randomTrigger = step(0.52, rand(vec2(timeScale, 0.80))); + + // RGB Split with random intensity + float splitStrength = randomTrigger * 0.20 * rand(vec2(timeScale, 2.0)); + float r = texture(iChannel0, vec2(uv.x + splitStrength, uv.y)).r; + float g = texture(iChannel0, uv).g; + float b = texture(iChannel0, vec2(uv.x - splitStrength, uv.y)).b; + + // Random vertical glitch blocks + float blockNoise = rand(vec2(floor(uv.y * 32.0), timeScale)); + float blockOffset = (step(0.996, blockNoise) * 2.0 - 1.0) * 0.02; + uv.x = uv.x + blockOffset * randomTrigger; + + // CRT scanlines + float scanline = sin(uv.y * 1000.0) * 0.04 + 0.96; + + // Vertical sync glitch + float vSync = sin(uv.y * 50.0 + iTime * 5.0) * randomTrigger * 0.01; + uv.x += vSync; + + vec3 color = vec3(r, g, b); + + // Apply scanlines and noise + color *= scanline; + color *= (0.95 + rand(uv + timeScale) * 0.05); + + // Random color noise at glitch moments + color += randomTrigger * rand(uv + iTime) * 0.01; + + fragColor = vec4(color, 0.95); +} diff --git a/.config/ghostty/shaders/glow.glsl b/.config/ghostty/shaders/glow.glsl new file mode 100644 index 0000000..d7b701b --- /dev/null +++ b/.config/ghostty/shaders/glow.glsl @@ -0,0 +1,22 @@ +// https://github.com/12jihan/ghostty_shaders/blob/main/glow.glsl + +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord/iResolution.xy; + + // Base color from terminal + vec3 color = texture(iChannel0, uv).rgb; + + // Add bloom/glow + float bloom = 0.05; + vec3 glow = vec3(0.0); + for(float i = 0.0; i < 4.0; i++) { + vec2 offset = vec2(i) / iResolution.xy; + glow += texture(iChannel0, uv + offset).rgb; + glow += texture(iChannel0, uv - offset).rgb; + } + + // Combine glow with original color + color += glow * bloom; + + fragColor = vec4(color, 1.0); +} diff --git a/.config/ghostty/shaders/starfield.glsl b/.config/ghostty/shaders/starfield.glsl new file mode 100644 index 0000000..b8a221d --- /dev/null +++ b/.config/ghostty/shaders/starfield.glsl @@ -0,0 +1,136 @@ +// https://github.com/hackr-sh/ghostty-shaders/blob/main/starfield.glsl +// transparent background +const bool transparent = false; + +// terminal contents luminance threshold to be considered background (0.0 to 1.0) +const float threshold = 0.15; + +// divisions of grid +const float repeats = 30.; + +// number of layers +const float layers = 21.; + +// star colors +const vec3 white = vec3(1.0); // Set star color to pure white + +float luminance(vec3 color) { + return dot(color, vec3(0.2126, 0.7152, 0.0722)); +} + +float N21(vec2 p) { + p = fract(p * vec2(233.34, 851.73)); + p += dot(p, p + 23.45); + return fract(p.x * p.y); +} + +vec2 N22(vec2 p) { + float n = N21(p); + return vec2(n, N21(p + n)); +} + +mat2 scale(vec2 _scale) { + return mat2(_scale.x, 0.0, + 0.0, _scale.y); +} + +// 2D Noise based on Morgan McGuire +float noise(in vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + + // Four corners in 2D of a tile + float a = N21(i); + float b = N21(i + vec2(1.0, 0.0)); + float c = N21(i + vec2(0.0, 1.0)); + float d = N21(i + vec2(1.0, 1.0)); + + // Smooth Interpolation + vec2 u = f * f * (3.0 - 2.0 * f); // Cubic Hermite Curve + + // Mix 4 corners percentages + return mix(a, b, u.x) + + (c - a) * u.y * (1.0 - u.x) + + (d - b) * u.x * u.y; +} + +float perlin2(vec2 uv, int octaves, float pscale) { + float col = 1.; + float initScale = 4.; + for (int l; l < octaves; l++) { + float val = noise(uv * initScale); + if (col <= 0.01) { + col = 0.; + break; + } + val -= 0.01; + val *= 0.5; + col *= val; + initScale *= pscale; + } + return col; +} + +vec3 stars(vec2 uv, float offset) { + float timeScale = -(iTime + offset) / layers; + float trans = fract(timeScale); + float newRnd = floor(timeScale); + vec3 col = vec3(0.); + + // Translate uv then scale for center + uv -= vec2(0.5); + uv = scale(vec2(trans)) * uv; + uv += vec2(0.5); + + // Create square aspect ratio + uv.x *= iResolution.x / iResolution.y; + + // Create boxes + uv *= repeats; + + // Get position + vec2 ipos = floor(uv); + + // Return uv as 0 to 1 + uv = fract(uv); + + // Calculate random xy and size + vec2 rndXY = N22(newRnd + ipos * (offset + 1.)) * 0.9 + 0.05; + float rndSize = N21(ipos) * 100. + 200.; + + vec2 j = (rndXY - uv) * rndSize; + float sparkle = 1. / dot(j, j); + + // Set stars to be pure white + col += white * sparkle; + + col *= smoothstep(1., 0.8, trans); + return col; // Return pure white stars only +} + +void mainImage(out vec4 fragColor, in vec2 fragCoord) +{ + // Normalized pixel coordinates (from 0 to 1) + vec2 uv = fragCoord / iResolution.xy; + + vec3 col = vec3(0.); + + for (float i = 0.; i < layers; i++) { + col += stars(uv, i); + } + + // Sample the terminal screen texture including alpha channel + vec4 terminalColor = texture(iChannel0, uv); + + if (transparent) { + col += terminalColor.rgb; + } + + // Make a mask that is 1.0 where the terminal content is not black + float mask = 1 - step(threshold, luminance(terminalColor.rgb)); + + vec3 blendedColor = mix(terminalColor.rgb, col, mask); + + // Apply terminal's alpha to control overall opacity + fragColor = vec4(blendedColor, terminalColor.a); +}