theming
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
/target/
|
/target/
|
||||||
/.tools/
|
/.tools/
|
||||||
/result
|
/result
|
||||||
|
/.worktrees
|
||||||
|
|||||||
+20
-3
@@ -7,9 +7,26 @@ font_size = 16
|
|||||||
scrollback = 10000
|
scrollback = 10000
|
||||||
window_width = 1100
|
window_width = 1100
|
||||||
window_height = 720
|
window_height = 720
|
||||||
background = "#17191c"
|
|
||||||
foreground = "#e2e4e8"
|
# Any theme from `slerm --list-themes`: "slerm" plus every theme Helix ships.
|
||||||
accent = "#58c4ad"
|
theme = "slerm"
|
||||||
|
|
||||||
|
# Shorthand for the [colors] entries of the same name.
|
||||||
|
# background = "#17191c"
|
||||||
|
# foreground = "#e2e4e8"
|
||||||
|
# accent = "#58c4ad"
|
||||||
|
|
||||||
|
# Colors overriding the theme, all optional. Alongside the three above:
|
||||||
|
# cursor, selection_background, selection_foreground, border,
|
||||||
|
# chrome_background, chrome_foreground, chrome_active_background,
|
||||||
|
# popup_background,
|
||||||
|
# and the sixteen ANSI colors:
|
||||||
|
# black, red, green, yellow, blue, magenta, cyan, white,
|
||||||
|
# and each of those again as bright_black ... bright_white.
|
||||||
|
# [colors]
|
||||||
|
# cursor = "#58c4ad"
|
||||||
|
# red = "#cc6666"
|
||||||
|
# bright_red = "#d54e53"
|
||||||
|
|
||||||
[kitty]
|
[kitty]
|
||||||
image_storage_mb = 64
|
image_storage_mb = 64
|
||||||
|
|||||||
@@ -0,0 +1,699 @@
|
|||||||
|
//! Converts the themes Helix ships in `runtime/themes` into `src/themes.rs`.
|
||||||
|
//!
|
||||||
|
//! Helix themes describe an editor: they name syntax scopes, not the sixteen
|
||||||
|
//! ANSI colors a terminal needs. The conversion therefore reads the UI scopes
|
||||||
|
//! directly (`ui.background`, `ui.statusline`, ...) and *infers* the ANSI
|
||||||
|
//! palette by scoring every color the theme uses against the hue each slot is
|
||||||
|
//! expected to hold, preferring the scopes that conventionally carry it.
|
||||||
|
//!
|
||||||
|
//! cargo run --example gen_themes -- <helix>/runtime/themes src/themes.rs
|
||||||
|
|
||||||
|
use std::{collections::BTreeMap, fmt::Write as _, path::Path};
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
|
type Rgb = [u8; 3];
|
||||||
|
|
||||||
|
/// A color as written in a theme: its RGB value, plus the ANSI slot it named if
|
||||||
|
/// it was written as one of Helix's terminal color names rather than as hex.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
struct Color {
|
||||||
|
rgb: Rgb,
|
||||||
|
ansi: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helix's terminal color names and the palette slots they select. `gray` is
|
||||||
|
/// the dim one and `light-gray` the bright one, matching how Helix hands them
|
||||||
|
/// to crossterm (`Gray` -> dark grey, `LightGray` -> grey).
|
||||||
|
const NAMED: &[(&str, u8)] = &[
|
||||||
|
("black", 0),
|
||||||
|
("red", 1),
|
||||||
|
("green", 2),
|
||||||
|
("yellow", 3),
|
||||||
|
("blue", 4),
|
||||||
|
("magenta", 5),
|
||||||
|
("cyan", 6),
|
||||||
|
("light-gray", 7),
|
||||||
|
("gray", 8),
|
||||||
|
("light-red", 9),
|
||||||
|
("light-green", 10),
|
||||||
|
("light-yellow", 11),
|
||||||
|
("light-blue", 12),
|
||||||
|
("light-magenta", 13),
|
||||||
|
("light-cyan", 14),
|
||||||
|
("white", 15),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The xterm defaults, used both for themes that name terminal colors outright
|
||||||
|
/// and as the last resort for a slot nothing in a theme fits.
|
||||||
|
const XTERM: [Rgb; 16] = [
|
||||||
|
[0x00, 0x00, 0x00],
|
||||||
|
[0xcd, 0x00, 0x00],
|
||||||
|
[0x00, 0xcd, 0x00],
|
||||||
|
[0xcd, 0xcd, 0x00],
|
||||||
|
[0x00, 0x00, 0xee],
|
||||||
|
[0xcd, 0x00, 0xcd],
|
||||||
|
[0x00, 0xcd, 0xcd],
|
||||||
|
[0xe5, 0xe5, 0xe5],
|
||||||
|
[0x7f, 0x7f, 0x7f],
|
||||||
|
[0xff, 0x00, 0x00],
|
||||||
|
[0x00, 0xff, 0x00],
|
||||||
|
[0xff, 0xff, 0x00],
|
||||||
|
[0x5c, 0x5c, 0xff],
|
||||||
|
[0xff, 0x00, 0xff],
|
||||||
|
[0x00, 0xff, 0xff],
|
||||||
|
[0xff, 0xff, 0xff],
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The hue each chromatic slot aims for, and the scopes that conventionally
|
||||||
|
/// carry it. Earlier scopes are preferred, but only as a tie-break: a theme
|
||||||
|
/// whose `function` is teal still gets a teal blue rather than a stray hue.
|
||||||
|
const SLOTS: [(u8, f32, &[&str]); 6] = [
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
0.0,
|
||||||
|
&[
|
||||||
|
"error",
|
||||||
|
"diagnostic.error",
|
||||||
|
"diff.minus",
|
||||||
|
"keyword.control.exception",
|
||||||
|
"markup.list.unchecked",
|
||||||
|
"variable.builtin",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
2,
|
||||||
|
120.0,
|
||||||
|
&[
|
||||||
|
"string",
|
||||||
|
"diff.plus",
|
||||||
|
"markup.raw",
|
||||||
|
"hint",
|
||||||
|
"diagnostic.hint",
|
||||||
|
"variable.other.member",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
3,
|
||||||
|
48.0,
|
||||||
|
&[
|
||||||
|
"warning",
|
||||||
|
"diagnostic.warning",
|
||||||
|
"constant.numeric",
|
||||||
|
"attribute",
|
||||||
|
"type",
|
||||||
|
"markup.heading",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
4,
|
||||||
|
230.0,
|
||||||
|
&[
|
||||||
|
"function",
|
||||||
|
"info",
|
||||||
|
"diagnostic.info",
|
||||||
|
"constructor",
|
||||||
|
"markup.link.url",
|
||||||
|
"diff.delta",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
5,
|
||||||
|
300.0,
|
||||||
|
&[
|
||||||
|
"keyword",
|
||||||
|
"constant",
|
||||||
|
"special",
|
||||||
|
"namespace",
|
||||||
|
"markup.link.text",
|
||||||
|
"type.enum.variant",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
6,
|
||||||
|
185.0,
|
||||||
|
&[
|
||||||
|
"operator",
|
||||||
|
"constant.character.escape",
|
||||||
|
"markup.quote",
|
||||||
|
"string.special",
|
||||||
|
"punctuation.special",
|
||||||
|
"label",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// One theme file, before inheritance is resolved.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Raw {
|
||||||
|
inherits: Option<String>,
|
||||||
|
palette: BTreeMap<String, String>,
|
||||||
|
/// Scope -> (fg, bg), each already flattened out of Helix's several
|
||||||
|
/// spellings (bare string, `{ fg, bg }`, `{ underline = { color } }`).
|
||||||
|
scopes: BTreeMap<String, (Option<String>, Option<String>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(path: &Path) -> Raw {
|
||||||
|
let text = std::fs::read_to_string(path).expect("cannot read theme file");
|
||||||
|
let table: toml::Table = toml::from_str(&text).expect("theme is not valid TOML");
|
||||||
|
let mut raw = Raw::default();
|
||||||
|
for (key, value) in table {
|
||||||
|
match (key.as_str(), value) {
|
||||||
|
("inherits", Value::String(name)) => raw.inherits = Some(name),
|
||||||
|
("palette", Value::Table(entries)) => {
|
||||||
|
for (name, value) in entries {
|
||||||
|
if let Value::String(color) = value {
|
||||||
|
raw.palette.insert(name, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(_, Value::String(fg)) => {
|
||||||
|
raw.scopes.insert(key, (Some(fg), None));
|
||||||
|
}
|
||||||
|
(_, Value::Table(style)) => {
|
||||||
|
let text = |v: Option<&Value>| v.and_then(Value::as_str).map(str::to_owned);
|
||||||
|
let fg = text(style.get("fg")).or_else(|| {
|
||||||
|
style
|
||||||
|
.get("underline")
|
||||||
|
.and_then(Value::as_table)
|
||||||
|
.and_then(|u| text(u.get("color")))
|
||||||
|
});
|
||||||
|
raw.scopes.insert(key, (fg, text(style.get("bg"))));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raw
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A theme with its `inherits` chain folded in: the child's palette entries and
|
||||||
|
/// whole scope entries win over the parent's, exactly as Helix merges them.
|
||||||
|
struct Theme {
|
||||||
|
palette: BTreeMap<String, String>,
|
||||||
|
scopes: BTreeMap<String, (Option<String>, Option<String>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flatten(name: &str, raws: &BTreeMap<String, Raw>) -> Theme {
|
||||||
|
let mut chain = vec![];
|
||||||
|
let mut current = Some(name.to_owned());
|
||||||
|
while let Some(name) = current {
|
||||||
|
if chain.contains(&name) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = raws.get(&name).and_then(|r| r.inherits.clone());
|
||||||
|
chain.push(name);
|
||||||
|
}
|
||||||
|
let mut theme = Theme {
|
||||||
|
palette: BTreeMap::new(),
|
||||||
|
scopes: BTreeMap::new(),
|
||||||
|
};
|
||||||
|
// Ancestors first so that descendants overwrite them.
|
||||||
|
for name in chain.iter().rev() {
|
||||||
|
let Some(raw) = raws.get(name) else { continue };
|
||||||
|
theme.palette.extend(raw.palette.clone());
|
||||||
|
theme.scopes.extend(raw.scopes.clone());
|
||||||
|
}
|
||||||
|
theme
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Theme {
|
||||||
|
fn color(&self, text: &str) -> Option<Color> {
|
||||||
|
let text = self.palette.get(text).map(String::as_str).unwrap_or(text);
|
||||||
|
if let Some((_, index)) = NAMED.iter().find(|(name, _)| *name == text) {
|
||||||
|
return Some(Color {
|
||||||
|
rgb: XTERM[*index as usize],
|
||||||
|
ansi: Some(*index),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let digits = text.strip_prefix('#')?;
|
||||||
|
let expand = |c: u8| (c << 4) | c;
|
||||||
|
let nibble = |i: usize| u8::from_str_radix(&digits[i..i + 1], 16).ok();
|
||||||
|
let byte = |i: usize| u8::from_str_radix(&digits[i..i + 2], 16).ok();
|
||||||
|
let rgb = match digits.len() {
|
||||||
|
3 => [expand(nibble(0)?), expand(nibble(1)?), expand(nibble(2)?)],
|
||||||
|
6 | 8 => [byte(0)?, byte(2)?, byte(4)?],
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(Color { rgb, ansi: None })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fg(&self, scope: &str) -> Option<Color> {
|
||||||
|
self.color(self.scopes.get(scope)?.0.as_deref()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bg(&self, scope: &str) -> Option<Color> {
|
||||||
|
self.color(self.scopes.get(scope)?.1.as_deref()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every color the theme mentions anywhere, with the number of scopes that
|
||||||
|
/// use it. This is the pool the ANSI slots are chosen from; the count
|
||||||
|
/// separates a theme's working colors from one-off accents such as the
|
||||||
|
/// color of a jump label.
|
||||||
|
fn all(&self) -> Vec<(Color, usize)> {
|
||||||
|
let mut counts: BTreeMap<Rgb, (Color, usize)> = BTreeMap::new();
|
||||||
|
let named = self.palette.values().map(String::as_str);
|
||||||
|
// `ui.*` carries backgrounds, greys and one-off markers such as the
|
||||||
|
// color of a jump label, none of which belong in a terminal palette.
|
||||||
|
let scoped = self
|
||||||
|
.scopes
|
||||||
|
.iter()
|
||||||
|
.filter(|(scope, _)| !scope.starts_with("ui."))
|
||||||
|
.flat_map(|(_, (fg, bg))| [fg, bg])
|
||||||
|
.filter_map(|c| c.as_deref());
|
||||||
|
for color in named.chain(scoped).filter_map(|c| self.color(c)) {
|
||||||
|
counts.entry(color.rgb).or_insert((color, 0)).1 += 1;
|
||||||
|
}
|
||||||
|
counts.into_values().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn luma(c: Rgb) -> f32 {
|
||||||
|
let channel = |v: u8| {
|
||||||
|
let v = v as f32 / 255.0;
|
||||||
|
if v <= 0.04045 {
|
||||||
|
v / 12.92
|
||||||
|
} else {
|
||||||
|
((v + 0.055) / 1.055).powf(2.4)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
0.2126 * channel(c[0]) + 0.7152 * channel(c[1]) + 0.0722 * channel(c[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contrast(a: Rgb, b: Rgb) -> f32 {
|
||||||
|
let (a, b) = (luma(a) + 0.05, luma(b) + 0.05);
|
||||||
|
if a > b { a / b } else { b / a }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hue in degrees, saturation and lightness in 0..1.
|
||||||
|
fn hsl(c: Rgb) -> (f32, f32, f32) {
|
||||||
|
let [r, g, b] = [
|
||||||
|
c[0] as f32 / 255.0,
|
||||||
|
c[1] as f32 / 255.0,
|
||||||
|
c[2] as f32 / 255.0,
|
||||||
|
];
|
||||||
|
let max = r.max(g).max(b);
|
||||||
|
let min = r.min(g).min(b);
|
||||||
|
let (span, sum) = (max - min, max + min);
|
||||||
|
let lightness = sum / 2.0;
|
||||||
|
if span < f32::EPSILON {
|
||||||
|
return (0.0, 0.0, lightness);
|
||||||
|
}
|
||||||
|
let saturation = span / (1.0 - (sum - 1.0).abs());
|
||||||
|
let hue = if max == r {
|
||||||
|
60.0 * (((g - b) / span) % 6.0)
|
||||||
|
} else if max == g {
|
||||||
|
60.0 * ((b - r) / span + 2.0)
|
||||||
|
} else {
|
||||||
|
60.0 * ((r - g) / span + 4.0)
|
||||||
|
};
|
||||||
|
(hue.rem_euclid(360.0), saturation, lightness)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hue_distance(a: f32, b: f32) -> f32 {
|
||||||
|
let d = (a - b).abs() % 360.0;
|
||||||
|
d.min(360.0 - d)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mix(a: Rgb, b: Rgb, t: f32) -> Rgb {
|
||||||
|
let channel = |i: usize| (a[i] as f32 + (b[i] as f32 - a[i] as f32) * t).round() as u8;
|
||||||
|
[channel(0), channel(1), channel(2)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const BLACK: Rgb = [0, 0, 0];
|
||||||
|
const WHITE: Rgb = [255, 255, 255];
|
||||||
|
|
||||||
|
/// The converted theme, mirroring the struct in `src/theme.rs`.
|
||||||
|
struct Out {
|
||||||
|
dark: bool,
|
||||||
|
background: Rgb,
|
||||||
|
foreground: Rgb,
|
||||||
|
cursor: Rgb,
|
||||||
|
selection_bg: Rgb,
|
||||||
|
selection_fg: Rgb,
|
||||||
|
accent: Rgb,
|
||||||
|
chrome_bg: Rgb,
|
||||||
|
chrome_fg: Rgb,
|
||||||
|
chrome_active_bg: Rgb,
|
||||||
|
border: Rgb,
|
||||||
|
popup_bg: Rgb,
|
||||||
|
ansi: [Rgb; 16],
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert(theme: &Theme) -> Out {
|
||||||
|
let text = theme
|
||||||
|
.fg("ui.text")
|
||||||
|
.or_else(|| theme.fg("ui.background"))
|
||||||
|
.or_else(|| theme.fg("ui.statusline"));
|
||||||
|
let background = theme
|
||||||
|
.bg("ui.background")
|
||||||
|
.or_else(|| theme.bg("ui.popup"))
|
||||||
|
.or_else(|| theme.bg("ui.statusline"));
|
||||||
|
// A theme that never states a background (the `term16` family leaves it to
|
||||||
|
// the terminal) is placed by the brightness of its text instead.
|
||||||
|
let dark = match (background, text) {
|
||||||
|
(Some(bg), _) => luma(bg.rgb) < 0.22,
|
||||||
|
(None, Some(fg)) => luma(fg.rgb) > 0.22,
|
||||||
|
(None, None) => true,
|
||||||
|
};
|
||||||
|
let background = background.map_or(if dark { BLACK } else { WHITE }, |c| c.rgb);
|
||||||
|
let mut foreground = text.map_or(if dark { WHITE } else { BLACK }, |c| c.rgb);
|
||||||
|
// A theme that states neither (peachpuff leaves both to the terminal) can
|
||||||
|
// resolve to text the same shade as its background, which renders nothing.
|
||||||
|
if contrast(foreground, background) < 2.0 {
|
||||||
|
foreground = if dark { WHITE } else { BLACK };
|
||||||
|
}
|
||||||
|
let far = if dark { WHITE } else { BLACK };
|
||||||
|
|
||||||
|
let ansi = palette(theme, background, foreground, dark);
|
||||||
|
let cursor = theme
|
||||||
|
.bg("ui.cursor.primary")
|
||||||
|
.or_else(|| theme.bg("ui.cursor"))
|
||||||
|
.or_else(|| theme.fg("ui.cursor.primary"))
|
||||||
|
.or_else(|| theme.fg("ui.cursor"))
|
||||||
|
.map_or(foreground, |c| c.rgb);
|
||||||
|
let selection_bg = theme
|
||||||
|
.bg("ui.selection.primary")
|
||||||
|
.or_else(|| theme.bg("ui.selection"))
|
||||||
|
.map_or(mix(background, far, 0.22), |c| c.rgb);
|
||||||
|
let selection_fg = theme
|
||||||
|
.fg("ui.selection.primary")
|
||||||
|
.or_else(|| theme.fg("ui.selection"))
|
||||||
|
.map(|c| c.rgb)
|
||||||
|
.filter(|c| contrast(*c, selection_bg) >= 2.5)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
if contrast(foreground, selection_bg) >= 2.5 {
|
||||||
|
foreground
|
||||||
|
} else {
|
||||||
|
mix(
|
||||||
|
selection_bg,
|
||||||
|
if luma(selection_bg) > 0.3 {
|
||||||
|
BLACK
|
||||||
|
} else {
|
||||||
|
WHITE
|
||||||
|
},
|
||||||
|
0.85,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let chrome_bg = theme
|
||||||
|
.bg("ui.statusline")
|
||||||
|
.or_else(|| theme.bg("ui.popup"))
|
||||||
|
.map(|c| c.rgb)
|
||||||
|
.filter(|c| *c != background)
|
||||||
|
.unwrap_or_else(|| mix(background, far, 0.09));
|
||||||
|
let chrome_fg = theme
|
||||||
|
.fg("ui.statusline")
|
||||||
|
.map(|c| c.rgb)
|
||||||
|
.filter(|c| contrast(*c, chrome_bg) >= 2.0)
|
||||||
|
.unwrap_or_else(|| mix(chrome_bg, far, 0.7));
|
||||||
|
// The accent marks the focused pane: it is drawn both on the title bar and
|
||||||
|
// as a border on the terminal background, so it has to read on both.
|
||||||
|
let readable = |c: Rgb| contrast(c, chrome_bg) >= 2.5 && contrast(c, background) >= 2.5;
|
||||||
|
let vivid = [theme.fg("ui.cursor.match").map(|c| c.rgb), Some(cursor)]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.find(|c| readable(*c) && hsl(*c).1 > 0.30);
|
||||||
|
let accent = vivid
|
||||||
|
// A theme whose cursor is a plain grey still deserves a colored focus
|
||||||
|
// ring, so fall back to its own blue, cyan or magenta.
|
||||||
|
.or_else(|| {
|
||||||
|
[4, 6, 5, 3]
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| ansi[i])
|
||||||
|
.find(|c| readable(*c))
|
||||||
|
})
|
||||||
|
.or(Some(cursor).filter(|c| readable(*c)))
|
||||||
|
.unwrap_or(foreground);
|
||||||
|
Out {
|
||||||
|
dark,
|
||||||
|
background,
|
||||||
|
foreground,
|
||||||
|
cursor,
|
||||||
|
selection_bg,
|
||||||
|
selection_fg,
|
||||||
|
accent,
|
||||||
|
chrome_bg,
|
||||||
|
chrome_fg,
|
||||||
|
// The focused pane's title bar steps away from the unfocused ones,
|
||||||
|
// whichever direction that is for this theme's chrome.
|
||||||
|
chrome_active_bg: mix(
|
||||||
|
chrome_bg,
|
||||||
|
if luma(chrome_bg) > 0.18 { BLACK } else { WHITE },
|
||||||
|
0.14,
|
||||||
|
),
|
||||||
|
// Unfocused borders are a hint, not a frame, so a theme with a loud
|
||||||
|
// window color is pulled back towards its background.
|
||||||
|
border: theme
|
||||||
|
.fg("ui.window")
|
||||||
|
.or_else(|| theme.fg("ui.virtual.indent-guide"))
|
||||||
|
.map_or(mix(background, far, 0.28), |c| mix(background, c.rgb, 0.65)),
|
||||||
|
popup_bg: theme.bg("ui.popup").map_or(chrome_bg, |c| c.rgb),
|
||||||
|
ansi,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Picks the sixteen ANSI colors. The six chromatic slots score every color the
|
||||||
|
/// theme uses against the slot's hue; the greys are derived from the background
|
||||||
|
/// and foreground so that they always stay legible against the theme's own
|
||||||
|
/// background.
|
||||||
|
fn palette(theme: &Theme, background: Rgb, foreground: Rgb, dark: bool) -> [Rgb; 16] {
|
||||||
|
let pool = theme.all();
|
||||||
|
let mut ansi = XTERM;
|
||||||
|
// A theme whose palette names terminal colors outright is already
|
||||||
|
// describing a palette; those entries are taken as written.
|
||||||
|
let named: BTreeMap<u8, Rgb> = theme
|
||||||
|
.palette
|
||||||
|
.keys()
|
||||||
|
.filter_map(|name| Some((slot_named(name)?, theme.color(name)?.rgb)))
|
||||||
|
.collect();
|
||||||
|
let mut taken: Vec<Rgb> = vec![];
|
||||||
|
let mut assigned = [false; 7];
|
||||||
|
for (index, rgb) in &named {
|
||||||
|
ansi[*index as usize] = *rgb;
|
||||||
|
if let Some(slot) = assigned.get_mut(*index as usize) {
|
||||||
|
*slot = true;
|
||||||
|
taken.push(*rgb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score every slot against every color the theme uses, then hand each color
|
||||||
|
// to the slot that wants it most. Assigning greedily one slot at a time
|
||||||
|
// would let an early slot take a color a later slot needed far more, which
|
||||||
|
// is how palettes with no true blue end up with two cyans.
|
||||||
|
let mut pairs: Vec<(f32, usize, Rgb)> = vec![];
|
||||||
|
for (index, hue, scopes) in SLOTS {
|
||||||
|
if assigned[index as usize] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let preferred: Vec<Rgb> = scopes
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| Some(theme.fg(s)?.rgb))
|
||||||
|
.collect();
|
||||||
|
for (candidate, uses) in &pool {
|
||||||
|
let (h, s, l) = hsl(candidate.rgb);
|
||||||
|
// A grey, an unreadable color, the plain text color and a color the
|
||||||
|
// theme only spelled as a terminal name (which says nothing about
|
||||||
|
// what that terminal color should be) are never palette entries.
|
||||||
|
if s < 0.10
|
||||||
|
|| contrast(candidate.rgb, background) < 1.8
|
||||||
|
|| candidate.ansi.is_some()
|
||||||
|
|| candidate.rgb == foreground
|
||||||
|
|| candidate.rgb == background
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let rank = preferred
|
||||||
|
.iter()
|
||||||
|
.position(|p| *p == candidate.rgb)
|
||||||
|
.map_or(20.0, |r| r as f32 * 2.5);
|
||||||
|
// Whichever slot's hue this color sits closest to has the better
|
||||||
|
// claim on it, so that a theme's red is not spent on magenta just
|
||||||
|
// because it happens to be the color of `keyword`.
|
||||||
|
let closest = SLOTS
|
||||||
|
.iter()
|
||||||
|
.map(|(_, target, _)| *target)
|
||||||
|
.min_by(|a, b| hue_distance(h, *a).total_cmp(&hue_distance(h, *b)));
|
||||||
|
let score = hue_distance(h, hue) * 1.5
|
||||||
|
+ rank
|
||||||
|
+ (1.0 - s) * 38.0
|
||||||
|
+ if closest == Some(hue) { 0.0 } else { 45.0 }
|
||||||
|
// A color used once is a one-off accent, not a working color.
|
||||||
|
+ match uses {
|
||||||
|
0 | 1 => 22.0,
|
||||||
|
2 => 10.0,
|
||||||
|
_ => 0.0,
|
||||||
|
}
|
||||||
|
+ if l > 0.85 { 40.0 } else { 0.0 };
|
||||||
|
pairs.push((score, index as usize, candidate.rgb));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pairs.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
|
||||||
|
for (_, index, rgb) in &pairs {
|
||||||
|
if !assigned[*index] && !taken.contains(rgb) {
|
||||||
|
assigned[*index] = true;
|
||||||
|
ansi[*index] = *rgb;
|
||||||
|
taken.push(*rgb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A theme with fewer than six usable colors leaves slots over; those share
|
||||||
|
// whichever color came closest rather than falling back to a stock xterm
|
||||||
|
// primary that belongs to no theme at all.
|
||||||
|
for (_, index, rgb) in &pairs {
|
||||||
|
if !assigned[*index] {
|
||||||
|
assigned[*index] = true;
|
||||||
|
ansi[*index] = *rgb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (index, _, _) in SLOTS {
|
||||||
|
if !assigned[index as usize] {
|
||||||
|
ansi[index as usize] = mix(XTERM[index as usize], foreground, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The bright variant is the theme's own lighter shade of the same color
|
||||||
|
// where it has one, and a lightened copy otherwise. Matching the hue is not
|
||||||
|
// enough: a pale grey-blue is a poor bright blue, so the candidate has to
|
||||||
|
// keep most of the base color's chroma, and it cannot be a color another
|
||||||
|
// slot already owns.
|
||||||
|
let chroma = |c: Rgb| (*c.iter().max().unwrap() - *c.iter().min().unwrap()) as f32;
|
||||||
|
for (index, _, _) in SLOTS {
|
||||||
|
if named.contains_key(&(index + 8)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let base = ansi[index as usize];
|
||||||
|
let (base_hue, _, lightness) = hsl(base);
|
||||||
|
let brighter = pool
|
||||||
|
.iter()
|
||||||
|
.map(|(c, _)| c)
|
||||||
|
.filter(|c| {
|
||||||
|
let (h, _, l) = hsl(c.rgb);
|
||||||
|
hue_distance(h, base_hue) <= 30.0
|
||||||
|
&& chroma(c.rgb) >= chroma(base) * 0.7
|
||||||
|
&& (0.05..=0.40).contains(&(l - lightness))
|
||||||
|
&& contrast(c.rgb, background) >= 2.0
|
||||||
|
&& !taken.contains(&c.rgb)
|
||||||
|
})
|
||||||
|
.min_by(|a, b| hsl(a.rgb).2.total_cmp(&hsl(b.rgb).2));
|
||||||
|
ansi[index as usize + 8] = brighter.map_or_else(
|
||||||
|
|| mix(base, WHITE, if lightness > 0.7 { 0.12 } else { 0.26 }),
|
||||||
|
|c| c.rgb,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Greys: black through bright white always span from the theme's darker end
|
||||||
|
// to its lighter end, so ANSI text stays readable in light themes too.
|
||||||
|
let comment = theme
|
||||||
|
.fg("comment")
|
||||||
|
.map(|c| c.rgb)
|
||||||
|
.filter(|c| contrast(*c, background) >= 2.0);
|
||||||
|
let greys = if dark {
|
||||||
|
[
|
||||||
|
mix(background, WHITE, 0.13),
|
||||||
|
foreground,
|
||||||
|
comment.unwrap_or_else(|| mix(background, WHITE, 0.42)),
|
||||||
|
mix(foreground, WHITE, 0.35),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
[
|
||||||
|
mix(foreground, BLACK, 0.25),
|
||||||
|
mix(background, BLACK, 0.13),
|
||||||
|
comment.unwrap_or_else(|| mix(foreground, background, 0.45)),
|
||||||
|
background,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
for (slot, grey) in [0, 7, 8, 15].into_iter().zip(greys) {
|
||||||
|
ansi[slot as usize] = *named.get(&slot).unwrap_or(&grey);
|
||||||
|
}
|
||||||
|
ansi
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The palette slot a `[palette]` key names outright, so that a theme defining
|
||||||
|
/// `red`/`bright-blue` and friends keeps its own values.
|
||||||
|
fn slot_named(name: &str) -> Option<u8> {
|
||||||
|
let name = name.to_ascii_lowercase().replace('_', "-");
|
||||||
|
let (bright, base) = match name
|
||||||
|
.strip_prefix("bright-")
|
||||||
|
.or_else(|| name.strip_prefix("light-"))
|
||||||
|
{
|
||||||
|
Some(base) => (8, base.to_owned()),
|
||||||
|
None => (0, name),
|
||||||
|
};
|
||||||
|
let index = match base.as_str() {
|
||||||
|
"black" => 0,
|
||||||
|
"red" => 1,
|
||||||
|
"green" => 2,
|
||||||
|
"yellow" => 3,
|
||||||
|
"blue" => 4,
|
||||||
|
"magenta" => 5,
|
||||||
|
"cyan" => 6,
|
||||||
|
"white" => 7,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(index + bright)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
let dir = args
|
||||||
|
.next()
|
||||||
|
.expect("usage: gen_themes <themes dir> <out.rs>");
|
||||||
|
let out = args
|
||||||
|
.next()
|
||||||
|
.expect("usage: gen_themes <themes dir> <out.rs>");
|
||||||
|
|
||||||
|
let mut raws = BTreeMap::new();
|
||||||
|
for entry in std::fs::read_dir(&dir).expect("cannot read themes directory") {
|
||||||
|
let path = entry.expect("cannot read theme").path();
|
||||||
|
if path.extension().is_some_and(|e| e == "toml") {
|
||||||
|
let name = path.file_stem().unwrap().to_string_lossy().into_owned();
|
||||||
|
raws.insert(name, read(&path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut text = String::from(
|
||||||
|
"//! The themes Helix ships with, converted by `cargo run --example gen_themes`.\n\
|
||||||
|
//! Generated file: edit the generator, not this.\n\
|
||||||
|
//!\n\
|
||||||
|
//! Upstream: <https://github.com/helix-editor/helix/tree/master/runtime/themes>.\n\
|
||||||
|
//! The individual themes keep their own authors and licenses; see `themes/LICENSES`.\n\n\
|
||||||
|
use crate::theme::Theme;\n\n\
|
||||||
|
/// Every built-in theme, sorted by name so that lookup can binary search.\n\
|
||||||
|
pub static THEMES: &[(&str, Theme)] = &[\n",
|
||||||
|
);
|
||||||
|
for name in raws.keys() {
|
||||||
|
let out = convert(&flatten(name, &raws));
|
||||||
|
let hex = |c: Rgb| format!("0x{:02x}{:02x}{:02x}", c[0], c[1], c[2]);
|
||||||
|
let ansi = out
|
||||||
|
.ansi
|
||||||
|
.iter()
|
||||||
|
.map(|c| hex(*c))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
writeln!(
|
||||||
|
text,
|
||||||
|
" (\n {name:?},\n Theme {{\n \
|
||||||
|
dark: {dark},\n background: {bg},\n foreground: {fg},\n \
|
||||||
|
cursor: {cursor},\n \
|
||||||
|
selection_bg: {sel_bg},\n selection_fg: {sel_fg},\n \
|
||||||
|
accent: {accent},\n chrome_bg: {chrome_bg},\n \
|
||||||
|
chrome_fg: {chrome_fg},\n chrome_active_bg: {chrome_active},\n \
|
||||||
|
border: {border},\n popup_bg: {popup},\n \
|
||||||
|
ansi: [\n {ansi},\n ],\n }},\n ),",
|
||||||
|
dark = out.dark,
|
||||||
|
bg = hex(out.background),
|
||||||
|
fg = hex(out.foreground),
|
||||||
|
cursor = hex(out.cursor),
|
||||||
|
sel_bg = hex(out.selection_bg),
|
||||||
|
sel_fg = hex(out.selection_fg),
|
||||||
|
accent = hex(out.accent),
|
||||||
|
chrome_bg = hex(out.chrome_bg),
|
||||||
|
chrome_fg = hex(out.chrome_fg),
|
||||||
|
chrome_active = hex(out.chrome_active_bg),
|
||||||
|
border = hex(out.border),
|
||||||
|
popup = hex(out.popup_bg),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
text.push_str("];\n");
|
||||||
|
std::fs::write(&out, text).expect("cannot write output");
|
||||||
|
eprintln!("wrote {} themes to {out}", raws.len());
|
||||||
|
}
|
||||||
+67
-16
@@ -3,10 +3,10 @@ use crate::{
|
|||||||
input,
|
input,
|
||||||
layout::{Axis, Direction, Layout, PaneId, Tab},
|
layout::{Axis, Direction, Layout, PaneId, Tab},
|
||||||
pane::{self, Pane},
|
pane::{self, Pane},
|
||||||
render::color,
|
theme::{self, Theme},
|
||||||
};
|
};
|
||||||
use anyhow::{Result, ensure};
|
use anyhow::{Result, ensure};
|
||||||
use egui::{Color32, CornerRadius, Rect, Stroke, vec2};
|
use egui::{CornerRadius, Rect, Stroke, vec2};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashSet},
|
collections::{BTreeMap, HashSet},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
@@ -14,6 +14,7 @@ use std::{
|
|||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
config: Config,
|
config: Config,
|
||||||
|
theme: Theme,
|
||||||
config_path: PathBuf,
|
config_path: PathBuf,
|
||||||
panes: BTreeMap<PaneId, Pane>,
|
panes: BTreeMap<PaneId, Pane>,
|
||||||
tabs: Vec<Tab>,
|
tabs: Vec<Tab>,
|
||||||
@@ -76,11 +77,13 @@ impl App {
|
|||||||
config: Config,
|
config: Config,
|
||||||
config_path: PathBuf,
|
config_path: PathBuf,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
cc.egui_ctx.set_visuals(egui::Visuals::dark());
|
let theme = config.theme()?;
|
||||||
|
cc.egui_ctx.set_visuals(theme.visuals());
|
||||||
cc.egui_ctx.set_fonts(fonts(&config)?);
|
cc.egui_ctx.set_fonts(fonts(&config)?);
|
||||||
let pane = Pane::new(&config, &cc.egui_ctx, None)?;
|
let pane = Pane::new(&config, &theme, &cc.egui_ctx, None)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
config,
|
config,
|
||||||
|
theme,
|
||||||
config_path,
|
config_path,
|
||||||
panes: BTreeMap::from([(1, pane)]),
|
panes: BTreeMap::from([(1, pane)]),
|
||||||
tabs: vec![Tab::new(1, 1)],
|
tabs: vec![Tab::new(1, 1)],
|
||||||
@@ -102,13 +105,27 @@ impl App {
|
|||||||
.get(self.current)
|
.get(self.current)
|
||||||
.and_then(|tab| self.panes.get(&tab.active))
|
.and_then(|tab| self.panes.get(&tab.active))
|
||||||
.and_then(Pane::cwd);
|
.and_then(Pane::cwd);
|
||||||
let pane = Pane::new(&self.config, ctx, cwd.as_deref())?;
|
let pane = Pane::new(&self.config, &self.theme, ctx, cwd.as_deref())?;
|
||||||
let id = self.next_id;
|
let id = self.next_id;
|
||||||
self.next_id += 1;
|
self.next_id += 1;
|
||||||
self.panes.insert(id, pane);
|
self.panes.insert(id, pane);
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Switches theme for this session. Every open pane is retinted, since a
|
||||||
|
/// terminal keeps its palette rather than asking for it each frame.
|
||||||
|
fn set_theme(&mut self, name: &str, ctx: &egui::Context) -> Result<()> {
|
||||||
|
let previous = std::mem::replace(&mut self.config.theme, name.to_owned());
|
||||||
|
self.theme = self.config.theme().inspect_err(|_| {
|
||||||
|
self.config.theme = previous;
|
||||||
|
})?;
|
||||||
|
for pane in self.panes.values_mut() {
|
||||||
|
pane::apply_config(&mut pane.terminal, &self.config, &self.theme)?;
|
||||||
|
}
|
||||||
|
ctx.set_visuals(self.theme.visuals());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn action(&mut self, action: Action, ctx: &egui::Context) -> Result<()> {
|
fn action(&mut self, action: Action, ctx: &egui::Context) -> Result<()> {
|
||||||
let active = self.tabs[self.current].active;
|
let active = self.tabs[self.current].active;
|
||||||
match action {
|
match action {
|
||||||
@@ -216,10 +233,12 @@ impl App {
|
|||||||
Action::ReloadConfig => {
|
Action::ReloadConfig => {
|
||||||
let config = Config::load(&self.config_path)?;
|
let config = Config::load(&self.config_path)?;
|
||||||
let fonts = fonts(&config)?;
|
let fonts = fonts(&config)?;
|
||||||
|
self.theme = config.theme()?;
|
||||||
for pane in self.panes.values_mut() {
|
for pane in self.panes.values_mut() {
|
||||||
pane::apply_config(&mut pane.terminal, &config)?;
|
pane::apply_config(&mut pane.terminal, &config, &self.theme)?;
|
||||||
}
|
}
|
||||||
ctx.set_fonts(fonts);
|
ctx.set_fonts(fonts);
|
||||||
|
ctx.set_visuals(self.theme.visuals());
|
||||||
self.config = config;
|
self.config = config;
|
||||||
}
|
}
|
||||||
Action::Copy => {
|
Action::Copy => {
|
||||||
@@ -382,6 +401,7 @@ impl eframe::App for App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut action = None;
|
let mut action = None;
|
||||||
|
let mut picked = None;
|
||||||
egui::TopBottomPanel::top("tabs").show(ctx, |ui| {
|
egui::TopBottomPanel::top("tabs").show(ctx, |ui| {
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
if cfg!(target_os = "macos") {
|
if cfg!(target_os = "macos") {
|
||||||
@@ -429,6 +449,20 @@ impl eframe::App for App {
|
|||||||
action = Some(Action::ReloadConfig);
|
action = Some(Action::ReloadConfig);
|
||||||
ui.close();
|
ui.close();
|
||||||
}
|
}
|
||||||
|
ui.menu_button("Theme", |ui| {
|
||||||
|
ui.label("Applies now; set `theme` in the config to keep it.");
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.max_height(420.0)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for name in theme::names() {
|
||||||
|
let current = self.config.theme == name;
|
||||||
|
if ui.selectable_label(current, name).clicked() && !current {
|
||||||
|
picked = Some(name);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if cfg!(target_os = "macos") {
|
if cfg!(target_os = "macos") {
|
||||||
@@ -438,14 +472,14 @@ impl eframe::App for App {
|
|||||||
if let Some(error) = self.error.clone() {
|
if let Some(error) = self.error.clone() {
|
||||||
egui::TopBottomPanel::bottom("error").show(ctx, |ui| {
|
egui::TopBottomPanel::bottom("error").show(ctx, |ui| {
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
ui.colored_label(Color32::LIGHT_RED, error);
|
ui.colored_label(theme::color(self.theme.ansi[9]), error);
|
||||||
if ui.button("Dismiss").clicked() {
|
if ui.button("Dismiss").clicked() {
|
||||||
self.error = None;
|
self.error = None;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let bg = color(config::rgb(&self.config.background).unwrap());
|
let bg = theme::color(self.theme.background);
|
||||||
let tab = &mut self.tabs[self.current];
|
let tab = &mut self.tabs[self.current];
|
||||||
let mut tiled = vec![];
|
let mut tiled = vec![];
|
||||||
let mut workspace = Rect::NOTHING;
|
let mut workspace = Rect::NOTHING;
|
||||||
@@ -464,7 +498,7 @@ impl eframe::App for App {
|
|||||||
if !tab.zoomed {
|
if !tab.zoomed {
|
||||||
pane.last_rect = *rect;
|
pane.last_rect = *rect;
|
||||||
}
|
}
|
||||||
if show_pane(ui, *id, pane, *rect, &self.config, active) {
|
if show_pane(ui, *id, pane, *rect, &self.config, &self.theme, active) {
|
||||||
tab.active = *id;
|
tab.active = *id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -487,7 +521,7 @@ impl eframe::App for App {
|
|||||||
let rect = ui.available_rect_before_wrap();
|
let rect = ui.available_rect_before_wrap();
|
||||||
ui.allocate_rect(rect, egui::Sense::hover());
|
ui.allocate_rect(rect, egui::Sense::hover());
|
||||||
pane.last_rect = rect;
|
pane.last_rect = rect;
|
||||||
if show_pane(ui, id, pane, rect, &self.config, active) {
|
if show_pane(ui, id, pane, rect, &self.config, &self.theme, active) {
|
||||||
tab.active = id;
|
tab.active = id;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -510,6 +544,11 @@ impl eframe::App for App {
|
|||||||
pane.error = Some(e.to_string());
|
pane.error = Some(e.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(name) = picked
|
||||||
|
&& let Err(e) = self.set_theme(name, ctx)
|
||||||
|
{
|
||||||
|
self.error = Some(e.to_string());
|
||||||
|
}
|
||||||
if !self.tabs.is_empty()
|
if !self.tabs.is_empty()
|
||||||
&& let Some(action) = action
|
&& let Some(action) = action
|
||||||
&& let Err(e) = self.action(action, ctx)
|
&& let Err(e) = self.action(action, ctx)
|
||||||
@@ -552,9 +591,10 @@ fn show_pane(
|
|||||||
pane: &mut Pane,
|
pane: &mut Pane,
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
|
theme: &Theme,
|
||||||
active: bool,
|
active: bool,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let accent = color(config::rgb(&config.accent).unwrap());
|
let accent = theme::color(theme.accent);
|
||||||
let title_rect = Rect::from_min_size(rect.min, vec2(rect.width(), 25.0_f32.min(rect.height())));
|
let title_rect = Rect::from_min_size(rect.min, vec2(rect.width(), 25.0_f32.min(rect.height())));
|
||||||
// The title bar only rounds off the top of the pane, unless the pane is so
|
// The title bar only rounds off the top of the pane, unless the pane is so
|
||||||
// short that the bar is all there is of it.
|
// short that the bar is all there is of it.
|
||||||
@@ -571,7 +611,11 @@ fn show_pane(
|
|||||||
sw: bottom,
|
sw: bottom,
|
||||||
se: bottom,
|
se: bottom,
|
||||||
},
|
},
|
||||||
Color32::from_gray(if active { 43 } else { 31 }),
|
theme::color(if active {
|
||||||
|
theme.chrome_active_bg
|
||||||
|
} else {
|
||||||
|
theme.chrome_bg
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
ui.painter()
|
ui.painter()
|
||||||
.with_clip_rect(title_rect.shrink(3.0).intersect(ui.clip_rect()))
|
.with_clip_rect(title_rect.shrink(3.0).intersect(ui.clip_rect()))
|
||||||
@@ -580,7 +624,11 @@ fn show_pane(
|
|||||||
egui::Align2::LEFT_TOP,
|
egui::Align2::LEFT_TOP,
|
||||||
pane_title(id, pane),
|
pane_title(id, pane),
|
||||||
egui::FontId::proportional(13.0),
|
egui::FontId::proportional(13.0),
|
||||||
if active { accent } else { Color32::GRAY },
|
if active {
|
||||||
|
accent
|
||||||
|
} else {
|
||||||
|
theme::color(theme.chrome_fg)
|
||||||
|
},
|
||||||
);
|
);
|
||||||
// Click-only leaves dragging to the floating window's existing move handler.
|
// Click-only leaves dragging to the floating window's existing move handler.
|
||||||
let header = ui.interact(
|
let header = ui.interact(
|
||||||
@@ -594,7 +642,7 @@ fn show_pane(
|
|||||||
// The pane rectangle is already allocated; a layout scope would grow its parent.
|
// The pane rectangle is already allocated; a layout scope would grow its parent.
|
||||||
let mut content_ui = ui.new_child(egui::UiBuilder::new().id_salt(id).max_rect(content));
|
let mut content_ui = ui.new_child(egui::UiBuilder::new().id_salt(id).max_rect(content));
|
||||||
content_ui.set_clip_rect(content.intersect(ui.clip_rect()));
|
content_ui.set_clip_rect(content.intersect(ui.clip_rect()));
|
||||||
match pane.show(&mut content_ui, content, config, active) {
|
match pane.show(&mut content_ui, content, config, theme, active) {
|
||||||
Ok(clicked) => focused |= clicked,
|
Ok(clicked) => focused |= clicked,
|
||||||
Err(e) => pane.error = Some(e.to_string()),
|
Err(e) => pane.error = Some(e.to_string()),
|
||||||
}
|
}
|
||||||
@@ -606,7 +654,7 @@ fn show_pane(
|
|||||||
if active {
|
if active {
|
||||||
accent
|
accent
|
||||||
} else {
|
} else {
|
||||||
Color32::from_gray(54)
|
theme::color(theme.border)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
egui::StrokeKind::Inside,
|
egui::StrokeKind::Inside,
|
||||||
@@ -649,9 +697,11 @@ mod tests {
|
|||||||
shell: vec!["/bin/sh".into()],
|
shell: vec!["/bin/sh".into()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let pane = Pane::new(&config, ctx, None).unwrap();
|
let theme = config.theme().unwrap();
|
||||||
|
let pane = Pane::new(&config, &theme, ctx, None).unwrap();
|
||||||
App {
|
App {
|
||||||
config,
|
config,
|
||||||
|
theme,
|
||||||
config_path: PathBuf::new(),
|
config_path: PathBuf::new(),
|
||||||
panes: BTreeMap::from([(1, pane)]),
|
panes: BTreeMap::from([(1, pane)]),
|
||||||
tabs: vec![Tab::new(1, 1)],
|
tabs: vec![Tab::new(1, 1)],
|
||||||
@@ -687,6 +737,7 @@ mod tests {
|
|||||||
app.panes.get_mut(&1).unwrap(),
|
app.panes.get_mut(&1).unwrap(),
|
||||||
rect,
|
rect,
|
||||||
&app.config,
|
&app.config,
|
||||||
|
&app.theme,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
+40
-9
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::theme::{self, Theme};
|
||||||
use anyhow::{Context, Result, bail, ensure};
|
use anyhow::{Context, Result, bail, ensure};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -17,9 +18,13 @@ pub struct Config {
|
|||||||
pub scrollback: usize,
|
pub scrollback: usize,
|
||||||
pub window_width: f32,
|
pub window_width: f32,
|
||||||
pub window_height: f32,
|
pub window_height: f32,
|
||||||
pub background: String,
|
pub theme: String,
|
||||||
pub foreground: String,
|
/// Shorthand for the `[colors]` entries of the same name.
|
||||||
pub accent: String,
|
pub background: Option<String>,
|
||||||
|
pub foreground: Option<String>,
|
||||||
|
pub accent: Option<String>,
|
||||||
|
/// Colors overriding the theme, keyed by the names in `theme::FIELDS`.
|
||||||
|
pub colors: BTreeMap<String, String>,
|
||||||
pub kitty: Kitty,
|
pub kitty: Kitty,
|
||||||
pub multiplexer: Multiplexer,
|
pub multiplexer: Multiplexer,
|
||||||
pub keys: BTreeMap<String, Action>,
|
pub keys: BTreeMap<String, Action>,
|
||||||
@@ -102,9 +107,11 @@ impl Default for Config {
|
|||||||
scrollback: 10000,
|
scrollback: 10000,
|
||||||
window_width: 1100.0,
|
window_width: 1100.0,
|
||||||
window_height: 720.0,
|
window_height: 720.0,
|
||||||
background: "#17191c".into(),
|
theme: theme::DEFAULT_NAME.into(),
|
||||||
foreground: "#e2e4e8".into(),
|
background: None,
|
||||||
accent: "#58c4ad".into(),
|
foreground: None,
|
||||||
|
accent: None,
|
||||||
|
colors: BTreeMap::new(),
|
||||||
kitty: Kitty::default(),
|
kitty: Kitty::default(),
|
||||||
multiplexer: Multiplexer::default(),
|
multiplexer: Multiplexer::default(),
|
||||||
keys: [
|
keys: [
|
||||||
@@ -196,14 +203,38 @@ impl Config {
|
|||||||
"floating maximums must be 160..16384 points"
|
"floating maximums must be 160..16384 points"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for color in [&config.background, &config.foreground, &config.accent] {
|
config.theme()?;
|
||||||
rgb(color)?;
|
|
||||||
}
|
|
||||||
for key in config.keys.keys() {
|
for key in config.keys.keys() {
|
||||||
shortcut(key)?;
|
shortcut(key)?;
|
||||||
}
|
}
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The configured theme with the `[colors]` overrides applied. Validated by
|
||||||
|
/// `parse`, so a config that loaded is a config whose theme resolves.
|
||||||
|
pub fn theme(&self) -> Result<Theme> {
|
||||||
|
let mut theme = theme::find(&self.theme)
|
||||||
|
.with_context(|| format!("unknown theme: {} (try --list-themes)", self.theme))?;
|
||||||
|
let shorthand = [
|
||||||
|
("background", &self.background),
|
||||||
|
("foreground", &self.foreground),
|
||||||
|
("accent", &self.accent),
|
||||||
|
];
|
||||||
|
let named = shorthand
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(name, value)| Some((*name, value.as_ref()?)))
|
||||||
|
.chain(self.colors.iter().map(|(k, v)| (k.as_str(), v)));
|
||||||
|
for (name, value) in named {
|
||||||
|
theme.set(name, packed(value)?)?;
|
||||||
|
}
|
||||||
|
Ok(theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `#RRGGBB` string as the packed integer a theme stores.
|
||||||
|
pub fn packed(text: &str) -> Result<theme::Rgb> {
|
||||||
|
let color = rgb(text)?;
|
||||||
|
Ok((color.r as u32) << 16 | (color.g as u32) << 8 | color.b as u32)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rgb(text: &str) -> Result<libghostty_vt::style::RgbColor> {
|
pub fn rgb(text: &str) -> Result<libghostty_vt::style::RgbColor> {
|
||||||
|
|||||||
+9
-1
@@ -7,6 +7,8 @@ mod pty;
|
|||||||
mod render;
|
mod render;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
mod theme;
|
||||||
|
mod themes;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
|
|
||||||
@@ -20,13 +22,19 @@ fn main() -> Result<()> {
|
|||||||
"--config" => path = args.next().context("--config needs a path")?.into(),
|
"--config" => path = args.next().context("--config needs a path")?.into(),
|
||||||
"--check-config" => check = true,
|
"--check-config" => check = true,
|
||||||
"--init-config" => init = true,
|
"--init-config" => init = true,
|
||||||
|
"--list-themes" => {
|
||||||
|
for name in theme::names() {
|
||||||
|
println!("{name}");
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
"--version" => {
|
"--version" => {
|
||||||
println!("slerm {}", env!("CARGO_PKG_VERSION"));
|
println!("slerm {}", env!("CARGO_PKG_VERSION"));
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
"--help" | "-h" => {
|
"--help" | "-h" => {
|
||||||
println!(
|
println!(
|
||||||
"slerm [--config PATH] [--check-config | --init-config]\n\nDefault config: {}",
|
"slerm [--config PATH] [--check-config | --init-config | --list-themes]\n\nDefault config: {}",
|
||||||
path.display()
|
path.display()
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
+37
-14
@@ -1,8 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, rgb},
|
config::Config,
|
||||||
input,
|
input,
|
||||||
pty::Pty,
|
pty::Pty,
|
||||||
render::{Renderer, cell_size},
|
render::{Grid, Renderer, cell_size},
|
||||||
|
theme::{self, Theme},
|
||||||
};
|
};
|
||||||
use anyhow::{Result, ensure};
|
use anyhow::{Result, ensure};
|
||||||
use egui::{FontId, Rect, Sense, vec2};
|
use egui::{FontId, Rect, Sense, vec2};
|
||||||
@@ -91,14 +92,14 @@ fn percent_decode(path: &str) -> String {
|
|||||||
String::from_utf8_lossy(&out).into_owned()
|
String::from_utf8_lossy(&out).into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn terminal(config: &Config) -> Result<Terminal<'static, 'static>> {
|
pub fn terminal(config: &Config, theme: &Theme) -> Result<Terminal<'static, 'static>> {
|
||||||
let mut term = Terminal::new(TerminalOptions {
|
let mut term = Terminal::new(TerminalOptions {
|
||||||
cols: 80,
|
cols: 80,
|
||||||
rows: 24,
|
rows: 24,
|
||||||
max_scrollback: config.scrollback,
|
max_scrollback: config.scrollback,
|
||||||
})?;
|
})?;
|
||||||
term.resize(80, 24, 8, 16)?;
|
term.resize(80, 24, 8, 16)?;
|
||||||
apply_config(&mut term, config)?;
|
apply_config(&mut term, config, theme)?;
|
||||||
term.on_xtversion(|_| Some(concat!("slerm ", env!("CARGO_PKG_VERSION"))))?;
|
term.on_xtversion(|_| Some(concat!("slerm ", env!("CARGO_PKG_VERSION"))))?;
|
||||||
term.on_device_attributes(|_| {
|
term.on_device_attributes(|_| {
|
||||||
Some(terminal::DeviceAttributes {
|
Some(terminal::DeviceAttributes {
|
||||||
@@ -114,14 +115,21 @@ pub fn terminal(config: &Config) -> Result<Terminal<'static, 'static>> {
|
|||||||
tertiary: Default::default(),
|
tertiary: Default::default(),
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
term.on_color_scheme(|_| Some(terminal::ColorScheme::Dark))?;
|
let scheme = if theme.dark {
|
||||||
|
terminal::ColorScheme::Dark
|
||||||
|
} else {
|
||||||
|
terminal::ColorScheme::Light
|
||||||
|
};
|
||||||
|
term.on_color_scheme(move |_| Some(scheme))?;
|
||||||
Ok(term)
|
Ok(term)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_config(term: &mut Terminal<'_, '_>, config: &Config) -> Result<()> {
|
pub fn apply_config(term: &mut Terminal<'_, '_>, config: &Config, theme: &Theme) -> Result<()> {
|
||||||
term.set_default_fg_color(Some(rgb(&config.foreground)?))?
|
let palette = theme.palette(term.default_color_palette()?);
|
||||||
.set_default_bg_color(Some(rgb(&config.background)?))?
|
term.set_default_fg_color(Some(theme::rgb_color(theme.foreground)))?
|
||||||
.set_default_cursor_color(Some(rgb(&config.accent)?))?
|
.set_default_bg_color(Some(theme::rgb_color(theme.background)))?
|
||||||
|
.set_default_cursor_color(Some(theme::rgb_color(theme.cursor)))?
|
||||||
|
.set_default_color_palette(Some(palette))?
|
||||||
.set_kitty_image_storage_limit(config.kitty.image_storage_mb * 1024 * 1024)?
|
.set_kitty_image_storage_limit(config.kitty.image_storage_mb * 1024 * 1024)?
|
||||||
.set_kitty_image_from_file_allowed(config.kitty.allow_file)?
|
.set_kitty_image_from_file_allowed(config.kitty.allow_file)?
|
||||||
.set_kitty_image_from_temp_file_allowed(config.kitty.allow_temp_file)?
|
.set_kitty_image_from_temp_file_allowed(config.kitty.allow_temp_file)?
|
||||||
@@ -130,8 +138,13 @@ pub fn apply_config(term: &mut Terminal<'_, '_>, config: &Config) -> Result<()>
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Pane {
|
impl Pane {
|
||||||
pub fn new(config: &Config, ctx: &egui::Context, cwd: Option<&Path>) -> Result<Self> {
|
pub fn new(
|
||||||
let mut terminal = terminal(config)?;
|
config: &Config,
|
||||||
|
theme: &Theme,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
cwd: Option<&Path>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let mut terminal = terminal(config, theme)?;
|
||||||
let renderer = Renderer::new()?;
|
let renderer = Renderer::new()?;
|
||||||
let mouse_encoder = mouse::Encoder::new()?;
|
let mouse_encoder = mouse::Encoder::new()?;
|
||||||
let mouse_event = mouse::Event::new()?;
|
let mouse_event = mouse::Event::new()?;
|
||||||
@@ -261,6 +274,7 @@ impl Pane {
|
|||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
|
theme: &Theme,
|
||||||
active: bool,
|
active: bool,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
let font = FontId::monospace(config.font_size);
|
let font = FontId::monospace(config.font_size);
|
||||||
@@ -289,15 +303,24 @@ impl Pane {
|
|||||||
}
|
}
|
||||||
let response = ui.interact(grid, ui.id().with("terminal"), Sense::click_and_drag());
|
let response = ui.interact(grid, ui.id().with("terminal"), Sense::click_and_drag());
|
||||||
self.pointer(ui, &response, grid, cell)?;
|
self.pointer(ui, &response, grid, cell)?;
|
||||||
self.renderer
|
self.renderer.paint(
|
||||||
.paint(&self.terminal, ui.painter(), grid, cell, &font, active)?;
|
&self.terminal,
|
||||||
|
ui.painter(),
|
||||||
|
&Grid {
|
||||||
|
rect: grid,
|
||||||
|
cell,
|
||||||
|
font: &font,
|
||||||
|
theme,
|
||||||
|
active,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
if let Some(error) = &self.error {
|
if let Some(error) = &self.error {
|
||||||
ui.painter().text(
|
ui.painter().text(
|
||||||
grid.left_bottom() - vec2(0.0, ch),
|
grid.left_bottom() - vec2(0.0, ch),
|
||||||
egui::Align2::LEFT_TOP,
|
egui::Align2::LEFT_TOP,
|
||||||
error,
|
error,
|
||||||
FontId::proportional(12.0),
|
FontId::proportional(12.0),
|
||||||
egui::Color32::LIGHT_RED,
|
theme::color(theme.ansi[9]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(response.clicked()
|
Ok(response.clicked()
|
||||||
|
|||||||
+62
-13
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::theme::{self, Theme};
|
||||||
use anyhow::{Result, ensure};
|
use anyhow::{Result, ensure};
|
||||||
use egui::{
|
use egui::{
|
||||||
Color32, ColorImage, FontId, Painter, Rect, Stroke, TextureHandle, TextureOptions, pos2, vec2,
|
Color32, ColorImage, FontId, Painter, Rect, Stroke, TextureHandle, TextureOptions, pos2, vec2,
|
||||||
@@ -15,6 +16,17 @@ use std::{
|
|||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Where a pane's terminal is drawn this frame, and the colors that are slerm's
|
||||||
|
/// own rather than the terminal's.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Grid<'a> {
|
||||||
|
pub rect: Rect,
|
||||||
|
pub cell: egui::Vec2,
|
||||||
|
pub font: &'a FontId,
|
||||||
|
pub theme: &'a Theme,
|
||||||
|
pub active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn color(c: RgbColor) -> Color32 {
|
pub fn color(c: RgbColor) -> Color32 {
|
||||||
Color32::from_rgb(c.r, c.g, c.b)
|
Color32::from_rgb(c.r, c.g, c.b)
|
||||||
}
|
}
|
||||||
@@ -145,6 +157,7 @@ pub struct Renderer {
|
|||||||
glyphs: HashMap<String, Arc<egui::Galley>>,
|
glyphs: HashMap<String, Arc<egui::Galley>>,
|
||||||
font_marker: Option<Arc<egui::Galley>>,
|
font_marker: Option<Arc<egui::Galley>>,
|
||||||
cached_colors: Option<Colors>,
|
cached_colors: Option<Colors>,
|
||||||
|
cached_selection: (Color32, Color32),
|
||||||
cached_cell: egui::Vec2,
|
cached_cell: egui::Vec2,
|
||||||
cached_cols: u16,
|
cached_cols: u16,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -163,6 +176,7 @@ impl Renderer {
|
|||||||
glyphs: HashMap::new(),
|
glyphs: HashMap::new(),
|
||||||
font_marker: None,
|
font_marker: None,
|
||||||
cached_colors: None,
|
cached_colors: None,
|
||||||
|
cached_selection: (Color32::PLACEHOLDER, Color32::PLACEHOLDER),
|
||||||
cached_cell: egui::Vec2::ZERO,
|
cached_cell: egui::Vec2::ZERO,
|
||||||
cached_cols: 0,
|
cached_cols: 0,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -174,11 +188,15 @@ impl Renderer {
|
|||||||
&mut self,
|
&mut self,
|
||||||
term: &Terminal<'static, '_>,
|
term: &Terminal<'static, '_>,
|
||||||
painter: &Painter,
|
painter: &Painter,
|
||||||
rect: Rect,
|
grid: &Grid<'_>,
|
||||||
cell: egui::Vec2,
|
|
||||||
font: &FontId,
|
|
||||||
active: bool,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let &Grid {
|
||||||
|
rect,
|
||||||
|
cell,
|
||||||
|
font,
|
||||||
|
theme,
|
||||||
|
active,
|
||||||
|
} = grid;
|
||||||
let painter = painter.with_clip_rect(rect.intersect(painter.clip_rect()));
|
let painter = painter.with_clip_rect(rect.intersect(painter.clip_rect()));
|
||||||
// Keep one egui galley alive in its cache each frame. A new identity means
|
// Keep one egui galley alive in its cache each frame. A new identity means
|
||||||
// fonts/DPI/the atlas were reset, invalidating every cached glyph's UVs.
|
// fonts/DPI/the atlas were reset, invalidating every cached glyph's UVs.
|
||||||
@@ -199,12 +217,18 @@ impl Renderer {
|
|||||||
let colors = snapshot.colors()?;
|
let colors = snapshot.colors()?;
|
||||||
let cols = snapshot.cols()?;
|
let cols = snapshot.cols()?;
|
||||||
let row_count = snapshot.rows()? as usize;
|
let row_count = snapshot.rows()? as usize;
|
||||||
|
let selected_colors = (
|
||||||
|
theme::color(theme.selection_bg),
|
||||||
|
theme::color(theme.selection_fg),
|
||||||
|
);
|
||||||
let rebuild_all = snapshot.dirty()? == Dirty::Full
|
let rebuild_all = snapshot.dirty()? == Dirty::Full
|
||||||
|| self.cached_colors.as_ref() != Some(&colors)
|
|| self.cached_colors.as_ref() != Some(&colors)
|
||||||
|
|| self.cached_selection != selected_colors
|
||||||
|| self.cached_cols != cols
|
|| self.cached_cols != cols
|
||||||
|| self.row_cache.len() != row_count;
|
|| self.row_cache.len() != row_count;
|
||||||
self.row_cache.resize_with(row_count, CachedRow::default);
|
self.row_cache.resize_with(row_count, CachedRow::default);
|
||||||
self.cached_colors = Some(colors.clone());
|
self.cached_colors = Some(colors.clone());
|
||||||
|
self.cached_selection = selected_colors;
|
||||||
self.cached_cols = cols;
|
self.cached_cols = cols;
|
||||||
painter.rect_filled(rect, 0, color(colors.background));
|
painter.rect_filled(rect, 0, color(colors.background));
|
||||||
let mut rows = self.rows.update(&snapshot)?;
|
let mut rows = self.rows.update(&snapshot)?;
|
||||||
@@ -237,9 +261,14 @@ impl Renderer {
|
|||||||
let explicit_bg = c.bg_color()?;
|
let explicit_bg = c.bg_color()?;
|
||||||
let mut bg = color(explicit_bg.unwrap_or(colors.background));
|
let mut bg = color(explicit_bg.unwrap_or(colors.background));
|
||||||
let selected = c.is_selected()?;
|
let selected = c.is_selected()?;
|
||||||
if style.inverse ^ selected {
|
if style.inverse {
|
||||||
std::mem::swap(&mut fg, &mut bg);
|
std::mem::swap(&mut fg, &mut bg);
|
||||||
}
|
}
|
||||||
|
// A selection takes the theme's own colors rather than inverting
|
||||||
|
// the cell, which would be invisible over inverted text.
|
||||||
|
if selected {
|
||||||
|
(bg, fg) = selected_colors;
|
||||||
|
}
|
||||||
if explicit_bg.is_some() || style.inverse || selected {
|
if explicit_bg.is_some() || style.inverse || selected {
|
||||||
backgrounds.push(egui::Shape::rect_filled(cell_rect, 0, bg));
|
backgrounds.push(egui::Shape::rect_filled(cell_rect, 0, bg));
|
||||||
}
|
}
|
||||||
@@ -461,7 +490,17 @@ mod tests {
|
|||||||
ctx.run(egui::RawInput::default(), |ctx| {
|
ctx.run(egui::RawInput::default(), |ctx| {
|
||||||
let painter = ctx.layer_painter(egui::LayerId::background());
|
let painter = ctx.layer_painter(egui::LayerId::background());
|
||||||
renderer
|
renderer
|
||||||
.paint(term, &painter, rect, cell_size(&painter, font), font, true)
|
.paint(
|
||||||
|
term,
|
||||||
|
&painter,
|
||||||
|
&Grid {
|
||||||
|
rect,
|
||||||
|
cell: cell_size(&painter, font),
|
||||||
|
font,
|
||||||
|
theme: &theme::DEFAULT,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
})
|
||||||
.shapes
|
.shapes
|
||||||
@@ -475,7 +514,9 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let ctx = egui::Context::default();
|
let ctx = egui::Context::default();
|
||||||
let mut term = crate::pane::terminal(&crate::config::Config::default()).unwrap();
|
let mut term =
|
||||||
|
crate::pane::terminal(&crate::config::Config::default(), &crate::theme::DEFAULT)
|
||||||
|
.unwrap();
|
||||||
term.resize(20, 5, 8, 16).unwrap();
|
term.resize(20, 5, 8, 16).unwrap();
|
||||||
term.vt_write("\x1b[31mNOR\u{e0b4}\x1b[0m Android\r\nsecond line".as_bytes());
|
term.vt_write("\x1b[31mNOR\u{e0b4}\x1b[0m Android\r\nsecond line".as_bytes());
|
||||||
let rect = Rect::from_min_size(pos2(12.0, 24.0), vec2(200.0, 120.0));
|
let rect = Rect::from_min_size(pos2(12.0, 24.0), vec2(200.0, 120.0));
|
||||||
@@ -548,7 +589,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn cached_glyphs_rebuild_after_font_or_scale_changes() {
|
fn cached_glyphs_rebuild_after_font_or_scale_changes() {
|
||||||
let ctx = egui::Context::default();
|
let ctx = egui::Context::default();
|
||||||
let mut term = crate::pane::terminal(&crate::config::Config::default()).unwrap();
|
let mut term =
|
||||||
|
crate::pane::terminal(&crate::config::Config::default(), &crate::theme::DEFAULT)
|
||||||
|
.unwrap();
|
||||||
term.resize(20, 5, 8, 16).unwrap();
|
term.resize(20, 5, 8, 16).unwrap();
|
||||||
term.vt_write(b"font cache");
|
term.vt_write(b"font cache");
|
||||||
let rect = Rect::from_min_size(egui::Pos2::ZERO, vec2(200.0, 120.0));
|
let rect = Rect::from_min_size(egui::Pos2::ZERO, vec2(200.0, 120.0));
|
||||||
@@ -595,7 +638,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let ctx = egui::Context::default();
|
let ctx = egui::Context::default();
|
||||||
ctx.set_fonts(crate::app::fonts(&config).unwrap());
|
ctx.set_fonts(crate::app::fonts(&config).unwrap());
|
||||||
let mut term = crate::pane::terminal(&config).unwrap();
|
let mut term = crate::pane::terminal(&config, &theme::DEFAULT).unwrap();
|
||||||
term.resize(160, 50, 9, 18).unwrap();
|
term.resize(160, 50, 9, 18).unwrap();
|
||||||
let screen = (1..=50)
|
let screen = (1..=50)
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
@@ -629,10 +672,16 @@ mod tests {
|
|||||||
.paint(
|
.paint(
|
||||||
&term,
|
&term,
|
||||||
&ctx.layer_painter(egui::LayerId::background()),
|
&ctx.layer_painter(egui::LayerId::background()),
|
||||||
Rect::from_min_size(egui::Pos2::ZERO, vec2(1440.0, 900.0)),
|
&Grid {
|
||||||
vec2(9.0, 18.0),
|
rect: Rect::from_min_size(
|
||||||
&FontId::monospace(16.0),
|
egui::Pos2::ZERO,
|
||||||
true,
|
vec2(1440.0, 900.0),
|
||||||
|
),
|
||||||
|
cell: vec2(9.0, 18.0),
|
||||||
|
font: &FontId::monospace(16.0),
|
||||||
|
theme: &theme::DEFAULT,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
},
|
},
|
||||||
|
|||||||
+15
-12
@@ -1,4 +1,4 @@
|
|||||||
use crate::{config::Config, input, pane, render};
|
use crate::{config::Config, input, pane, render, theme};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use egui::{Event, Key, Modifiers};
|
use egui::{Event, Key, Modifiers};
|
||||||
use libghostty_vt::{
|
use libghostty_vt::{
|
||||||
@@ -30,7 +30,7 @@ fn key(key: Key, modifiers: Modifiers) -> Event {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn terminal_preserves_unicode_colors_and_application_input_modes() {
|
fn terminal_preserves_unicode_colors_and_application_input_modes() {
|
||||||
let mut term = pane::terminal(&Config::default()).unwrap();
|
let mut term = pane::terminal(&Config::default(), &theme::DEFAULT).unwrap();
|
||||||
term.vt_write("hello \x1b[31mred\x1b[0m \u{754c}e\u{301}\r\n".as_bytes());
|
term.vt_write("hello \x1b[31mred\x1b[0m \u{754c}e\u{301}\r\n".as_bytes());
|
||||||
assert!(plain(&term).contains("hello red \u{754c}e\u{301}"));
|
assert!(plain(&term).contains("hello red \u{754c}e\u{301}"));
|
||||||
let events = [
|
let events = [
|
||||||
@@ -59,7 +59,7 @@ fn terminal_preserves_unicode_colors_and_application_input_modes() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn queries_return_to_the_pty_and_alternate_screen_restores() {
|
fn queries_return_to_the_pty_and_alternate_screen_restores() {
|
||||||
let mut term = pane::terminal(&Config::default()).unwrap();
|
let mut term = pane::terminal(&Config::default(), &theme::DEFAULT).unwrap();
|
||||||
let response = Rc::new(RefCell::new(vec![]));
|
let response = Rc::new(RefCell::new(vec![]));
|
||||||
let writer = response.clone();
|
let writer = response.clone();
|
||||||
term.on_pty_write(move |_, bytes| writer.borrow_mut().extend_from_slice(bytes))
|
term.on_pty_write(move |_, bytes| writer.borrow_mut().extend_from_slice(bytes))
|
||||||
@@ -74,7 +74,7 @@ fn queries_return_to_the_pty_and_alternate_screen_restores() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn kitty_images_decode_render_reuse_and_delete() {
|
fn kitty_images_decode_render_reuse_and_delete() {
|
||||||
graphics::set_png_decoder(Some(Box::new(render::PngDecoder))).unwrap();
|
graphics::set_png_decoder(Some(Box::new(render::PngDecoder))).unwrap();
|
||||||
let mut term = pane::terminal(&Config::default()).unwrap();
|
let mut term = pane::terminal(&Config::default(), &theme::DEFAULT).unwrap();
|
||||||
let replies = Rc::new(RefCell::new(vec![]));
|
let replies = Rc::new(RefCell::new(vec![]));
|
||||||
let output = replies.clone();
|
let output = replies.clone();
|
||||||
term.on_pty_write(move |_, data| output.borrow_mut().extend_from_slice(data))
|
term.on_pty_write(move |_, data| output.borrow_mut().extend_from_slice(data))
|
||||||
@@ -137,10 +137,13 @@ fn kitty_images_decode_render_reuse_and_delete() {
|
|||||||
.paint(
|
.paint(
|
||||||
&term,
|
&term,
|
||||||
&painter,
|
&painter,
|
||||||
rect,
|
&render::Grid {
|
||||||
egui::vec2(8.0, 16.0),
|
rect,
|
||||||
&egui::FontId::monospace(12.0),
|
cell: egui::vec2(8.0, 16.0),
|
||||||
true,
|
font: &egui::FontId::monospace(12.0),
|
||||||
|
theme: &theme::DEFAULT,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
})
|
||||||
@@ -173,7 +176,7 @@ fn pty_executes_input_and_bracketed_paste_is_sanitized() {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let ctx = egui::Context::default();
|
let ctx = egui::Context::default();
|
||||||
let mut pane = pane::Pane::new(&config, &ctx, None).unwrap();
|
let mut pane = pane::Pane::new(&config, &theme::DEFAULT, &ctx, None).unwrap();
|
||||||
pane.send(b"printf '\\033[31mpty-%s\\033[0m\\n' 'success'\r".to_vec())
|
pane.send(b"printf '\\033[31mpty-%s\\033[0m\\n' 'success'\r".to_vec())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let deadline = Instant::now() + Duration::from_secs(5);
|
let deadline = Instant::now() + Duration::from_secs(5);
|
||||||
@@ -216,7 +219,7 @@ fn new_panes_start_in_the_directory_of_the_pane_they_came_from() {
|
|||||||
let dir = std::env::temp_dir().join(format!("slerm cwd {}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("slerm cwd {}", std::process::id()));
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
let dir = dir.canonicalize().unwrap();
|
let dir = dir.canonicalize().unwrap();
|
||||||
let mut pane = pane::Pane::new(&config, &ctx, None).unwrap();
|
let mut pane = pane::Pane::new(&config, &theme::DEFAULT, &ctx, None).unwrap();
|
||||||
pane.send(format!("cd '{}'\r", dir.display()).into_bytes())
|
pane.send(format!("cd '{}'\r", dir.display()).into_bytes())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// /bin/sh reports no OSC 7, so the directory has to come from the process.
|
// /bin/sh reports no OSC 7, so the directory has to come from the process.
|
||||||
@@ -227,7 +230,7 @@ fn new_panes_start_in_the_directory_of_the_pane_they_came_from() {
|
|||||||
}
|
}
|
||||||
assert_eq!(pane.cwd().as_deref(), Some(dir.as_path()));
|
assert_eq!(pane.cwd().as_deref(), Some(dir.as_path()));
|
||||||
|
|
||||||
let mut child = pane::Pane::new(&config, &ctx, pane.cwd().as_deref()).unwrap();
|
let mut child = pane::Pane::new(&config, &theme::DEFAULT, &ctx, pane.cwd().as_deref()).unwrap();
|
||||||
child
|
child
|
||||||
.send(b"printf 'at:%s\\n' \"$PWD\"\r".to_vec())
|
.send(b"printf 'at:%s\\n' \"$PWD\"\r".to_vec())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -251,7 +254,7 @@ fn osc7_wins_over_the_process_directory_but_only_for_this_machine() {
|
|||||||
let reported = std::env::temp_dir().join(format!("slerm osc7 {}", std::process::id()));
|
let reported = std::env::temp_dir().join(format!("slerm osc7 {}", std::process::id()));
|
||||||
std::fs::create_dir_all(&reported).unwrap();
|
std::fs::create_dir_all(&reported).unwrap();
|
||||||
let reported = reported.canonicalize().unwrap();
|
let reported = reported.canonicalize().unwrap();
|
||||||
let mut pane = pane::Pane::new(&config, &ctx, None).unwrap();
|
let mut pane = pane::Pane::new(&config, &theme::DEFAULT, &ctx, None).unwrap();
|
||||||
let process_cwd = pane.cwd().unwrap();
|
let process_cwd = pane.cwd().unwrap();
|
||||||
assert_ne!(process_cwd, reported);
|
assert_ne!(process_cwd, reported);
|
||||||
|
|
||||||
|
|||||||
+261
@@ -0,0 +1,261 @@
|
|||||||
|
use crate::themes::THEMES;
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use egui::Color32;
|
||||||
|
use libghostty_vt::style::{Palette, PaletteIndex, RgbColor};
|
||||||
|
|
||||||
|
/// A packed `0xRRGGBB` color. Themes are compiled in, so they hold plain
|
||||||
|
/// integers rather than anything that needs allocating or parsing at startup.
|
||||||
|
pub type Rgb = u32;
|
||||||
|
|
||||||
|
/// Every color slerm draws with. A theme covers the terminal grid, the sixteen
|
||||||
|
/// ANSI colors programs address by index, and the chrome around them, so that
|
||||||
|
/// picking a light theme does not leave a dark tab bar behind.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Theme {
|
||||||
|
/// Whether the chrome should be drawn as a dark or a light widget set.
|
||||||
|
pub dark: bool,
|
||||||
|
pub background: Rgb,
|
||||||
|
pub foreground: Rgb,
|
||||||
|
pub cursor: Rgb,
|
||||||
|
pub selection_bg: Rgb,
|
||||||
|
pub selection_fg: Rgb,
|
||||||
|
/// The focused pane: its border, its title and the cursor.
|
||||||
|
pub accent: Rgb,
|
||||||
|
/// The tab bar, menus and pane title bars.
|
||||||
|
pub chrome_bg: Rgb,
|
||||||
|
pub chrome_fg: Rgb,
|
||||||
|
/// The title bar of the focused pane.
|
||||||
|
pub chrome_active_bg: Rgb,
|
||||||
|
/// The border of an unfocused pane.
|
||||||
|
pub border: Rgb,
|
||||||
|
pub popup_bg: Rgb,
|
||||||
|
/// ANSI 0..=15: black, red, green, yellow, blue, magenta, cyan, white, then
|
||||||
|
/// the same eight again in their bright form.
|
||||||
|
pub ansi: [Rgb; 16],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The name of slerm's own theme, which is what a config without a `theme` gets.
|
||||||
|
pub const DEFAULT_NAME: &str = "slerm";
|
||||||
|
|
||||||
|
/// slerm's own colors. The palette is libghostty's built-in one, so the default
|
||||||
|
/// look is exactly what the terminal renders with no theme applied at all.
|
||||||
|
pub const DEFAULT: Theme = Theme {
|
||||||
|
dark: true,
|
||||||
|
background: 0x17191c,
|
||||||
|
foreground: 0xe2e4e8,
|
||||||
|
cursor: 0x58c4ad,
|
||||||
|
selection_bg: 0x2f3d44,
|
||||||
|
selection_fg: 0xe2e4e8,
|
||||||
|
accent: 0x58c4ad,
|
||||||
|
chrome_bg: 0x1f2124,
|
||||||
|
chrome_fg: 0xa8adb5,
|
||||||
|
chrome_active_bg: 0x2b2e33,
|
||||||
|
border: 0x363636,
|
||||||
|
popup_bg: 0x1f2124,
|
||||||
|
ansi: [
|
||||||
|
0x1d1f21, 0xcc6666, 0xb5bd68, 0xf0c674, 0x81a2be, 0xb294bb, 0x8abeb7, 0xc5c8c6, 0x666666,
|
||||||
|
0xd54e53, 0xb9ca4a, 0xe7c547, 0x7aa6da, 0xc397d8, 0x70c0b1, 0xeaeaea,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The theme with this name, if there is one. slerm's own comes first; the rest
|
||||||
|
/// are the Helix themes, which `THEMES` keeps sorted for the binary search.
|
||||||
|
pub fn find(name: &str) -> Option<Theme> {
|
||||||
|
if name == DEFAULT_NAME {
|
||||||
|
return Some(DEFAULT);
|
||||||
|
}
|
||||||
|
let index = THEMES.binary_search_by_key(&name, |(name, _)| name).ok()?;
|
||||||
|
Some(THEMES[index].1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every theme name, in the order a picker should list them.
|
||||||
|
pub fn names() -> impl Iterator<Item = &'static str> {
|
||||||
|
std::iter::once(DEFAULT_NAME).chain(THEMES.iter().map(|(name, _)| *name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color(rgb: Rgb) -> Color32 {
|
||||||
|
Color32::from_rgb((rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rgb_color(rgb: Rgb) -> RgbColor {
|
||||||
|
RgbColor {
|
||||||
|
r: (rgb >> 16) as u8,
|
||||||
|
g: (rgb >> 8) as u8,
|
||||||
|
b: rgb as u8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where one of the names a config may override writes to.
|
||||||
|
type Field = fn(&mut Theme) -> &mut Rgb;
|
||||||
|
|
||||||
|
/// The names a config may override, paired with the field each one writes.
|
||||||
|
/// Ordered so that `[colors]` reads the way a palette is usually written.
|
||||||
|
const FIELDS: [(&str, Field); 27] = [
|
||||||
|
("background", |t| &mut t.background),
|
||||||
|
("foreground", |t| &mut t.foreground),
|
||||||
|
("cursor", |t| &mut t.cursor),
|
||||||
|
("selection_background", |t| &mut t.selection_bg),
|
||||||
|
("selection_foreground", |t| &mut t.selection_fg),
|
||||||
|
("accent", |t| &mut t.accent),
|
||||||
|
("chrome_background", |t| &mut t.chrome_bg),
|
||||||
|
("chrome_foreground", |t| &mut t.chrome_fg),
|
||||||
|
("chrome_active_background", |t| &mut t.chrome_active_bg),
|
||||||
|
("border", |t| &mut t.border),
|
||||||
|
("popup_background", |t| &mut t.popup_bg),
|
||||||
|
("black", |t| &mut t.ansi[0]),
|
||||||
|
("red", |t| &mut t.ansi[1]),
|
||||||
|
("green", |t| &mut t.ansi[2]),
|
||||||
|
("yellow", |t| &mut t.ansi[3]),
|
||||||
|
("blue", |t| &mut t.ansi[4]),
|
||||||
|
("magenta", |t| &mut t.ansi[5]),
|
||||||
|
("cyan", |t| &mut t.ansi[6]),
|
||||||
|
("white", |t| &mut t.ansi[7]),
|
||||||
|
("bright_black", |t| &mut t.ansi[8]),
|
||||||
|
("bright_red", |t| &mut t.ansi[9]),
|
||||||
|
("bright_green", |t| &mut t.ansi[10]),
|
||||||
|
("bright_yellow", |t| &mut t.ansi[11]),
|
||||||
|
("bright_blue", |t| &mut t.ansi[12]),
|
||||||
|
("bright_magenta", |t| &mut t.ansi[13]),
|
||||||
|
("bright_cyan", |t| &mut t.ansi[14]),
|
||||||
|
("bright_white", |t| &mut t.ansi[15]),
|
||||||
|
];
|
||||||
|
|
||||||
|
impl Theme {
|
||||||
|
/// Overrides one color by the name a config file uses for it.
|
||||||
|
pub fn set(&mut self, name: &str, value: Rgb) -> Result<()> {
|
||||||
|
let Some((_, field)) = FIELDS.iter().find(|(known, _)| *known == name) else {
|
||||||
|
bail!("unknown color: {name}");
|
||||||
|
};
|
||||||
|
*field(self) = value;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The 256-color palette a terminal renders indexed colors through. Only
|
||||||
|
/// the first sixteen are themed; the color cube and greyscale ramp above
|
||||||
|
/// them are fixed by the protocol, so they keep libghostty's values.
|
||||||
|
pub fn palette(&self, default: Palette) -> Palette {
|
||||||
|
const SLOTS: [PaletteIndex; 16] = [
|
||||||
|
PaletteIndex::BLACK,
|
||||||
|
PaletteIndex::RED,
|
||||||
|
PaletteIndex::GREEN,
|
||||||
|
PaletteIndex::YELLOW,
|
||||||
|
PaletteIndex::BLUE,
|
||||||
|
PaletteIndex::MAGENTA,
|
||||||
|
PaletteIndex::CYAN,
|
||||||
|
PaletteIndex::WHITE,
|
||||||
|
PaletteIndex::BRIGHT_BLACK,
|
||||||
|
PaletteIndex::BRIGHT_RED,
|
||||||
|
PaletteIndex::BRIGHT_GREEN,
|
||||||
|
PaletteIndex::BRIGHT_YELLOW,
|
||||||
|
PaletteIndex::BRIGHT_BLUE,
|
||||||
|
PaletteIndex::BRIGHT_MAGENTA,
|
||||||
|
PaletteIndex::BRIGHT_CYAN,
|
||||||
|
PaletteIndex::BRIGHT_WHITE,
|
||||||
|
];
|
||||||
|
let mut palette = default;
|
||||||
|
for (slot, rgb) in SLOTS.into_iter().zip(self.ansi) {
|
||||||
|
palette.set(slot, rgb_color(rgb));
|
||||||
|
}
|
||||||
|
palette
|
||||||
|
}
|
||||||
|
|
||||||
|
/// egui's widget colors, so that the tab bar, menus and buttons follow the
|
||||||
|
/// theme rather than staying on egui's own dark palette.
|
||||||
|
pub fn visuals(&self) -> egui::Visuals {
|
||||||
|
let mut visuals = if self.dark {
|
||||||
|
egui::Visuals::dark()
|
||||||
|
} else {
|
||||||
|
egui::Visuals::light()
|
||||||
|
};
|
||||||
|
let (chrome, accent, text) = (
|
||||||
|
color(self.chrome_bg),
|
||||||
|
color(self.accent),
|
||||||
|
color(self.chrome_fg),
|
||||||
|
);
|
||||||
|
let widgets = &mut visuals.widgets;
|
||||||
|
widgets.noninteractive.bg_fill = chrome;
|
||||||
|
widgets.noninteractive.weak_bg_fill = chrome;
|
||||||
|
widgets.noninteractive.fg_stroke = egui::Stroke::new(1.0_f32, text);
|
||||||
|
widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0_f32, color(self.border));
|
||||||
|
for widget in [
|
||||||
|
&mut widgets.inactive,
|
||||||
|
&mut widgets.hovered,
|
||||||
|
&mut widgets.active,
|
||||||
|
&mut widgets.open,
|
||||||
|
] {
|
||||||
|
widget.bg_fill = color(self.chrome_active_bg);
|
||||||
|
widget.weak_bg_fill = color(self.chrome_active_bg);
|
||||||
|
widget.fg_stroke = egui::Stroke::new(1.0_f32, color(self.foreground));
|
||||||
|
widget.bg_stroke = egui::Stroke::new(1.0_f32, color(self.border));
|
||||||
|
}
|
||||||
|
widgets.hovered.weak_bg_fill = color(self.selection_bg);
|
||||||
|
widgets.active.weak_bg_fill = color(self.selection_bg);
|
||||||
|
widgets.active.fg_stroke = egui::Stroke::new(1.0_f32, accent);
|
||||||
|
visuals.panel_fill = chrome;
|
||||||
|
visuals.window_fill = color(self.popup_bg);
|
||||||
|
visuals.extreme_bg_color = color(self.background);
|
||||||
|
visuals.selection.bg_fill = color(self.selection_bg);
|
||||||
|
visuals.selection.stroke = egui::Stroke::new(1.0_f32, accent);
|
||||||
|
visuals.hyperlink_color = accent;
|
||||||
|
visuals.window_stroke = egui::Stroke::new(1.0_f32, color(self.border));
|
||||||
|
visuals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_helix_theme_is_present_sorted_and_legible() {
|
||||||
|
assert!(THEMES.len() > 150, "expected the full Helix theme set");
|
||||||
|
assert!(
|
||||||
|
THEMES.windows(2).all(|w| w[0].0 < w[1].0),
|
||||||
|
"THEMES must be sorted for find() to binary search it"
|
||||||
|
);
|
||||||
|
for name in ["catppuccin_mocha", "gruvbox", "nord", "onedark", "dracula"] {
|
||||||
|
assert!(find(name).is_some(), "missing {name}");
|
||||||
|
}
|
||||||
|
assert_eq!(find(DEFAULT_NAME), Some(DEFAULT));
|
||||||
|
assert_eq!(find("no such theme"), None);
|
||||||
|
assert_eq!(names().count(), THEMES.len() + 1);
|
||||||
|
|
||||||
|
// A theme whose text matches its background, or whose palette collapses
|
||||||
|
// onto one color, would render an unusable terminal.
|
||||||
|
for (name, theme) in THEMES {
|
||||||
|
let luma = |rgb: Rgb| {
|
||||||
|
let channel = |shift: u32| ((rgb >> shift) & 0xff) as f32 / 255.0;
|
||||||
|
0.2126 * channel(16) + 0.7152 * channel(8) + 0.0722 * channel(0)
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
(luma(theme.foreground) - luma(theme.background)).abs() > 0.15,
|
||||||
|
"{name}: text is not readable on its background"
|
||||||
|
);
|
||||||
|
assert_eq!(theme.dark, luma(theme.background) < luma(theme.foreground));
|
||||||
|
// Themes built on three or four colors (acme, vesper) reuse them
|
||||||
|
// across slots; anything flatter than that has lost its palette.
|
||||||
|
let distinct: std::collections::BTreeSet<_> = theme.ansi.iter().collect();
|
||||||
|
assert!(distinct.len() >= 9, "{name}: palette is too flat");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overrides_are_named_and_reach_the_palette() {
|
||||||
|
let mut theme = DEFAULT;
|
||||||
|
theme.set("red", 0x123456).unwrap();
|
||||||
|
theme.set("bright_white", 0xfedcba).unwrap();
|
||||||
|
theme.set("accent", 0x00ff00).unwrap();
|
||||||
|
assert_eq!(theme.ansi[1], 0x123456);
|
||||||
|
assert_eq!(theme.ansi[15], 0xfedcba);
|
||||||
|
assert_eq!(theme.accent, 0x00ff00);
|
||||||
|
for unknown in ["dark", "purple", "ansi", ""] {
|
||||||
|
assert!(theme.set(unknown, 0).is_err(), "accepted {unknown}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let palette = theme.palette(Palette([RgbColor { r: 0, g: 0, b: 0 }; 256]));
|
||||||
|
assert_eq!(palette.get(PaletteIndex::RED), rgb_color(0x123456));
|
||||||
|
assert_eq!(palette.get(PaletteIndex::BRIGHT_WHITE), rgb_color(0xfedcba));
|
||||||
|
// Everything above the sixteen named slots is left as it was.
|
||||||
|
assert_eq!(palette.0[16], RgbColor { r: 0, g: 0, b: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
+3623
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Daniel Fichtinger
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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 OR COPYRIGHT HOLDERS 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.
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 James Simpson
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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 OR COPYRIGHT HOLDERS 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.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016 CloudCannon
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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 OR COPYRIGHT HOLDERS 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.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Aaron Young
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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 OR COPYRIGHT HOLDERS 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.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 NTBBloodbath
|
||||||
|
|
||||||
|
This theme has been ported by spentbliss <zyhkekrz.hatbox475@passinbox.com>.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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 OR COPYRIGHT HOLDERS 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.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 sainnhe
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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 OR COPYRIGHT HOLDERS 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.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Strash
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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 OR COPYRIGHT HOLDERS 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.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Alex Barnett
|
||||||
|
|
||||||
|
This theme has been ported by spentbliss <zyhkekrz.hatbox475@passinbox.com>.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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 OR COPYRIGHT HOLDERS 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.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Copyright (C) 2020-2023 Free Software Foundation, Inc.
|
||||||
|
|
||||||
|
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, with the Front-Cover Texts being “A GNU Manual,” and with the Back-Cover Texts as in (a) below. A copy of the license is included in the section entitled “GNU Free Documentation License.”
|
||||||
|
|
||||||
|
(a) The FSF’s Back-Cover Text is: “You have the freedom to copy and modify this GNU manual.”
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Copyright (C) 2020-2023 Free Software Foundation, Inc.
|
||||||
|
|
||||||
|
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, with the Front-Cover Texts being “A GNU Manual,” and with the Back-Cover Texts as in (a) below. A copy of the license is included in the section entitled “GNU Free Documentation License.”
|
||||||
|
|
||||||
|
(a) The FSF’s Back-Cover Text is: “You have the freedom to copy and modify this GNU manual.”
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Copyright (C) 2020-2023 Free Software Foundation, Inc.
|
||||||
|
|
||||||
|
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, with the Front-Cover Texts being “A GNU Manual,” and with the Back-Cover Texts as in (a) below. A copy of the license is included in the section entitled “GNU Free Documentation License.”
|
||||||
|
|
||||||
|
(a) The FSF’s Back-Cover Text is: “You have the freedom to copy and modify this GNU manual.”
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Copyright (C) 2020-2023 Free Software Foundation, Inc.
|
||||||
|
|
||||||
|
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, with the Front-Cover Texts being “A GNU Manual,” and with the Back-Cover Texts as in (a) below. A copy of the license is included in the section entitled “GNU Free Documentation License.”
|
||||||
|
|
||||||
|
(a) The FSF’s Back-Cover Text is: “You have the freedom to copy and modify this GNU manual.”
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 drcmda
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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 OR COPYRIGHT HOLDERS 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.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 arturoalviar
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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 OR COPYRIGHT HOLDERS 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.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 sainnhe
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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 OR COPYRIGHT HOLDERS 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.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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 <https://unlicense.org>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Rauno Freiberg
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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 OR COPYRIGHT HOLDERS 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.
|
||||||
Reference in New Issue
Block a user