repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
nugu/nugu.nu
Raw# === WCAG 2.1 contrast ratio ===
def hex-to-rgb [hex: string] {
let clean = ($hex | str trim | str substring 1..)
[
((($clean | str substring 0..<2) | into int --radix 16) / 255.0),
((($clean | str substring 2..<4) | into int --radix 16) / 255.0),
((($clean | str substring 4..<6) | into int --radix 16) / 255.0),
]
}
def srgb-linearize [c: float] {
if $c <= 0.04045 { $c / 12.92 } else { (($c + 0.055) / 1.055) ** 2.4 }
}
def relative-luminance [hex: string] {
let rgb = (hex-to-rgb $hex)
let r = (0.2126 * (srgb-linearize $rgb.0))
let g = (0.7152 * (srgb-linearize $rgb.1))
let b = (0.0722 * (srgb-linearize $rgb.2))
[$r, $g, $b] | math sum
}
def wcag-contrast [fg: string, bg: string] {
let l1 = (relative-luminance $fg)
let l2 = (relative-luminance $bg)
(([$l1 $l2] | math max) + 0.05) / (([$l1 $l2] | math min) + 0.05)
}
# minimum contrast thresholds based on Nugu's stated intentions.
# error: "nonnegotiable and urgent. very aggressive and visible." → WCAG AA text
# warning: "likely to be problems, but not urgent. distracting but ignorable." → large text
# info: "context relevant suggestions. relaxed and easy to ignore." → present but not demanding
# debug: "easy to spot" → large text
# content.normal: default text → WCAG AA text
# content.accent: theme identity, prominent → WCAG AA text
# content.minor: receded structural text → large text
# content.focus: user is actively editing → WCAG AA text
# content.unfocus: not in focus, receded → present but not demanding
# content.important_local: system demands attention → WCAG AA text
# content.important_global: important but lighter variant → between AA and large text
# ui.*: same semantics but on ui.backdrop instead of content.backdrop
# content.backdrop on semantic colors: semantic colors used as highlight backgrounds
const contrast_thresholds = [
{ color: "error", bg: "content.backdrop", min: 4.5 },
{ color: "warning", bg: "content.backdrop", min: 3.0 },
{ color: "info", bg: "content.backdrop", min: 2.5 },
{ color: "content.backdrop", bg: "error", min: 4.5 },
{ color: "content.backdrop", bg: "warning", min: 4.5 },
{ color: "content.backdrop", bg: "info", min: 4.5 },
{ color: "debug", bg: "content.backdrop", min: 3.0 },
{ color: "content.normal", bg: "content.backdrop", min: 4.5 },
{ color: "content.accent", bg: "content.backdrop", min: 4.5 },
{ color: "content.minor", bg: "content.backdrop", min: 3.0 },
{ color: "content.focus", bg: "content.backdrop", min: 4.5 },
{ color: "content.unfocus", bg: "content.backdrop", min: 2.5 },
{ color: "content.important_local", bg: "content.backdrop", min: 4.5 },
{ color: "content.important_global", bg: "content.backdrop", min: 2.5 },
{ color: "ui.normal", bg: "ui.backdrop", min: 4.5 },
{ color: "ui.accent", bg: "ui.backdrop", min: 4.5 },
{ color: "ui.minor", bg: "ui.backdrop", min: 3.0 },
{ color: "ui.focus", bg: "ui.backdrop", min: 4.5 },
{ color: "ui.unfocus", bg: "ui.backdrop", min: 2.5 },
{ color: "ui.important_local", bg: "ui.backdrop", min: 4.5 },
{ color: "ui.important_global", bg: "ui.backdrop", min: 2.5 },
]
def validate-theme [theme: record, theme_name: string] {
let pal = $theme.palette
let results = ($contrast_thresholds | each {|t|
let cparts = ($t.color | split row ".")
let bparts = ($t.bg | split row ".")
let color_val = if ($cparts | length) == 1 {
$pal | get $t.color
} else {
$pal | get $cparts.0 | get $cparts.1
}
let bg_val = if ($bparts | length) == 1 {
$pal | get $t.bg
} else {
$pal | get $bparts.0 | get $bparts.1
}
let ratio = (wcag-contrast $color_val $bg_val)
{
color: $t.color,
hex: $color_val,
bg_hex: $bg_val,
ratio: ($ratio | math round --precision 1),
min: $t.min,
pass: ($ratio >= $t.min),
}
})
let failures = ($results | where pass == false)
print $"(ansi yellow_bold)WCAG AA: ($theme_name) theme(ansi reset)"
let failure_count = ($failures | length)
if $failure_count > 0 {
for f in $failures {
print $" (ansi red)✗(ansi reset) ($f.color) ($f.hex) on ($f.bg_hex) — ($f.ratio):1, need ($f.min):1"
}
print $" (ansi red_bold)($failure_count) failures(ansi reset)"
} else {
print $" (ansi green)✓ all colors pass(ansi reset)"
}
print ""
$failure_count
}
export def check [] {
print ""
let failure_count = ([
(validate-theme (new) "dark")
(validate-theme (new --light) "light")
] | math sum)
if $failure_count > 0 {
error make { msg: $"($failure_count) Nugu contrast checks failed" }
}
}
def readable-textcolor [bg: string] {
pastel textcolor $bg | pastel format hex | ansi strip
}
export def print-colors [theme: record] {
let mat = [
[
{ name: "debug", color: $theme.palette.debug }
{ name: "error", color: $theme.palette.error }
{ name: "warning", color: $theme.palette.warning }
{ name: "info", color: $theme.palette.info }
null
null
null
null
]
[
{ name: "normal", color: $theme.palette.content.normal }
{ name: "backdrop", color: $theme.palette.content.backdrop }
{ name: "accent", color: $theme.palette.content.accent }
{ name: "minor", color: $theme.palette.content.minor }
{ name: "focus", color: $theme.palette.content.focus }
{ name: "unfocus", color: $theme.palette.content.unfocus }
{ name: "important_local", color: $theme.palette.content.important_local }
{ name: "important_global", color: $theme.palette.content.important_global }
]
[
{ name: "normal", color: $theme.palette.ui.normal }
{ name: "backdrop", color: $theme.palette.ui.backdrop }
{ name: "accent", color: $theme.palette.ui.accent }
{ name: "minor", color: $theme.palette.ui.minor }
{ name: "focus", color: $theme.palette.ui.focus }
{ name: "unfocus", color: $theme.palette.ui.unfocus }
{ name: "important_local", color: $theme.palette.ui.important_local }
{ name: "important_global", color: $theme.palette.ui.important_global }
]
]
let num_x = ($mat | get 0 | length)
let num_y = ($mat | length)
let bg = "#000000"
let cols = (term size | get columns)
let pad = 2
let block_w = (($cols - (($num_x - 1) * $pad)) // $num_x // 2) * 2
let block_h = ($block_w // 2)
let name_row = (($block_h // 2) - 1)
let hex_row = ($block_h // 2)
for by in 0..<$num_y {
for py in 0..<$block_h {
for bx in 0..<$num_x {
let item = $mat | get $by | get $bx
let color = if ($item | is-not-empty) { $item.color } else { $bg }
let has_text = ($item | is-not-empty) and ($block_w >= 8) and ($py == $name_row or $py == $hex_row)
if $has_text {
let fg_color = (readable-textcolor $color)
let label = if $py == $name_row { $item.name } else { $color }
let padded = ($label | fill --alignment c --character ' ' --width $block_w)
let line = ($padded | str substring 0..<$block_w)
print --no-newline $"(ansi --escape { bg: $color, fg: $fg_color })($line)(ansi reset)"
} else {
let solid = ("" | fill --alignment l --character '█' --width $block_w)
print --no-newline $"(ansi --escape { bg: $color, fg: $color })($solid)(ansi reset)"
}
if $bx < ($num_x - 1) {
print --no-newline $"(ansi --escape { bg: $bg, fg: $bg })██(ansi reset)"
}
}
print ""
}
if $by < ($num_y - 1) {
let total_w = ($num_x * $block_w) + (($num_x - 1) * $pad)
let solid = ("" | fill --alignment l --character '█' --width $total_w)
print --no-newline $"(ansi --escape { bg: $bg, fg: $bg })($solid)(ansi reset)"
print ""
}
}
}
def make-palette [
debug: string,
error: string,
warning: string,
info: string,
content_normal: string,
content_backdrop: string,
content_accent: string,
content_minor: string,
content_focus: string,
content_unfocus: string,
content_important_local: string,
content_important_global: string,
ui_normal: string,
ui_backdrop: string,
ui_accent: string,
ui_minor: string,
ui_focus: string,
ui_unfocus: string,
ui_important_local: string,
ui_important_global: string
] {
{
palette: {
debug: $debug
error: $error
warning: $warning
info: $info
content: {
normal: $content_normal
backdrop: $content_backdrop
accent: $content_accent
minor: $content_minor
focus: $content_focus
unfocus: $content_unfocus
important_local: $content_important_local
important_global: $content_important_global
}
ui: {
normal: $ui_normal
backdrop: $ui_backdrop
accent: $ui_accent
minor: $ui_minor
focus: $ui_focus
unfocus: $ui_unfocus
important_local: $ui_important_local
important_global: $ui_important_global
}
}
}
}
# modifies a color by some cool operation -> flux.
# default flux is to darken.
def flux [
compensator: float # power of the flux light
deescalator: float # weaken the flux charisma
--light # use the light variant of flux color modification
] {
pastel desaturate $deescalator |
pastel ( if $light { "darken" } else { "lighten" } ) $compensator |
pastel format hex |
ansi strip
}
def mix [
color: string # color to mix into
--fraction: float = 0.42 # OkLab blend fraction
] {
pastel mix --colorspace OkLab --fraction $fraction $color
}
def new [
--light # make the light variant
] {
let base = (open base.toml | get palette)
def normal_flux [ --c: float=0.069 --d:float=0.42 ] { if $light { flux $c $d --light } else { flux $c $d } }
def inverted_flux [ --c: float=0.069 --d:float=0.42 ] { if $light { flux $c $d } else { flux $c $d --light } }
let normal = if $light { $base.backdrop } else { $base.normal }
let backdrop = if $light { $base.normal } else { $base.backdrop }
let error_seed = ($base.error | mix $base.accent --fraction 0.08)
let warning_seed = ($base.warning | mix $base.accent --fraction 0.03)
let info_seed = ($base.info | mix $base.important --fraction 0.25 | mix $base.backdrop --fraction 0.08)
let error = ($error_seed | normal_flux --c 0.12 --d 0.22)
let warning = if $light {
$warning_seed | flux 0.35 0.25 --light
} else {
$warning_seed | flux 0.08 0.18
}
let info = if $light {
$info_seed | flux 0.18 0.25 --light
} else {
$info_seed | flux 0.12 0.25
}
let debug = (
$base.debug
| normal_flux --c 0.15 --d 0.2
)
let content_normal = (
$normal
| inverted_flux
)
let content_backdrop = (
$backdrop
| normal_flux
)
let content_accent = (
$base.accent
| normal_flux
)
let content_minor = (
$normal
| mix $backdrop
| normal_flux
)
let content_focus = (
$base.focus
| mix $content_normal
| mix $base.important
| normal_flux
)
let content_unfocus = (
$base.focus
| mix $content_backdrop
| mix $base.important
| normal_flux --d 1
)
let content_important_local = (
$base.important
| mix $content_normal
| mix $base.important
| mix $base.accent
| normal_flux
)
let content_important_global = (
$base.important
| mix $content_normal
| mix $base.important
| mix $base.accent
| normal_flux --c (0.42 * 0.5)
)
let ui_normal = (
$content_normal
| inverted_flux
)
let ui_backdrop = (
$content_backdrop
| normal_flux
)
let ui_accent = (
$content_accent
| normal_flux --d 0
)
let ui_minor = (
$content_minor
| normal_flux
)
let ui_focus = (
$content_focus
| normal_flux --d (0.42 * 0.25)
)
let ui_unfocus = (
$content_unfocus
| normal_flux
)
let ui_important_local = (
$content_important_local
| normal_flux --d 0
)
let ui_important_global = (
$content_important_global
| normal_flux --d 0
)
(make-palette $debug $error $warning $info
$content_normal $content_backdrop
$content_accent
$content_minor
$content_focus $content_unfocus
$content_important_local $content_important_global
$ui_normal $ui_backdrop
$ui_accent
$ui_minor
$ui_focus $ui_unfocus
$ui_important_local $ui_important_global)
}
export def dark [] { new }
export def light [] { new --light }