Luigit
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 }