Compare commits

...

10 commits

Author SHA1 Message Date
soup 116dc232a8
[pritty] Initial commit 2025-01-17 11:41:35 -05:00
soup 4f589a07af
. 2025-01-16 20:05:09 -05:00
soup ce38e751ac
[klout] initial runtime data version 2024-12-22 01:05:25 -05:00
soup 8d838e3751
[klout] before rewrite with runtime data 2024-12-22 00:08:14 -05:00
soup f303ddad1a
[klout] the generatorrr 2024-12-21 19:22:49 -05:00
soup b665f2edf7
Able to generate layouts 2024-12-21 18:32:18 -05:00
soup 863ace8c2c
[klout] . 2024-12-21 18:08:52 -05:00
soup 73d86358d5
[klout] . 2024-12-21 18:03:12 -05:00
soup eb0ef2c4b2
[bake] initial commit 2024-12-21 01:17:25 -05:00
soup c9d964b20d
add .gitignore 2024-12-21 01:17:17 -05:00
32 changed files with 53573 additions and 63 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
**/build/*
**/target/*
pritty/outputs/*

32
bake/Cargo.lock generated Normal file
View file

@ -0,0 +1,32 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bake"
version = "0.0.1"
dependencies = [
"eyre",
]
[[package]]
name = "eyre"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
dependencies = [
"indenter",
"once_cell",
]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"

14
bake/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "bake"
version = "0.0.1"
edition = "2024"
[lib]
path = "lib/lib.rs"
[[bin]]
name = "bake"
path = "bin/main.rs"
[dependencies]
eyre = "0.6.12"

35
bake/bin/main.rs Normal file
View file

@ -0,0 +1,35 @@
use std::process::ExitCode;
use eyre::Context;
const USAGE: &str = r#"
bake [target...] [-- [args-for-last-target...]]
"#;
fn print_usage() {
println!("{USAGE}");
}
fn go() -> eyre::Result<()> {
let contents = match std::fs::read_to_string("build.bake") {
Ok(v) => v,
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => {
println!("build.bake not present in current directory");
return Ok(());
},
_ => {
return Err(e).wrap_err("Failed to read contents of build.bake")
},
},
};
let mut graph = bake::build_graph_from_str(&contents)?;
bake::execute_graph(&graph)?;
Ok(())
}
fn main() -> eyre::Result<()> {
go()
}

1
bake/build.bake Normal file
View file

@ -0,0 +1 @@
(-> :name run (phony) (phony) "cargo run")

25
bake/lib/lib.rs Normal file
View file

@ -0,0 +1,25 @@
mod syn;
mod wald;
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug)]
pub struct Error;
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
impl std::error::Error for Error {
}
pub struct Graph;
pub fn build_graph_from_str(s: &str) -> Result<Graph> {
todo!()
}
pub fn execute_graph(graph: &Graph) -> Result<()> {
todo!()
}

23
bake/lib/syn/ast.rs Normal file
View file

@ -0,0 +1,23 @@
use super::{
cst::{self, NodeKind},
tok::{self, TokenKind},
};
pub struct Atom<'a>(&'a cst::Node);
impl cst::Node {
pub fn as_atom(&self) -> Option<Atom> {
if *self.kind() != NodeKind::Atom {
return None;
}
Some(Atom(self))
}
}
impl Atom<'_> {
pub fn value(&self) -> &tok::Atom {
match &self.0.token().unwrap().kind {
TokenKind::Atom(a) => a,
_ => unreachable!(),
}
}
}

167
bake/lib/syn/cst.rs Normal file
View file

@ -0,0 +1,167 @@
use super::{
ast,
tok::{Token, TokenKind, Tokens},
};
#[derive(Debug)]
pub struct Tree {
nodes: Vec<Node>,
}
impl Tree {
pub fn new() -> Self {
let mut out = Self { nodes: vec![] };
out.register({
let mut node = Node::unregistered();
node.kind = NodeKind::Root;
node
});
out
}
pub fn register(&mut self, mut node: Node) -> NodeRef {
node.id = self.nodes.len();
let out = NodeRef(node.id);
self.nodes.push(node);
out
}
pub fn root(&self) -> NodeRef {
NodeRef(0)
}
pub fn add_child(&mut self, parent: NodeRef, child: NodeRef) {
parent.resolve_mut(self).children.push(child);
child.resolve_mut(self).parent = Some(parent);
}
pub fn nth_child(
&self,
parent: NodeRef,
child_index: usize,
) -> Option<NodeRef> {
parent.resolve(self).children.get(child_index).copied()
}
pub fn children_of(
&self,
parent: NodeRef,
) -> impl Iterator<Item = NodeRef> {
parent.resolve(self).children.iter().copied()
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
struct NodeRef(usize);
impl NodeRef {
fn resolve<'a>(&self, tree: &'a Tree) -> &'a Node {
&tree.nodes[self.0]
}
fn resolve_mut<'a>(&self, tree: &'a mut Tree) -> &'a mut Node {
&mut tree.nodes[self.0]
}
}
#[derive(Default, Debug)]
pub struct Node {
id: usize,
parent: Option<NodeRef>,
children: Vec<NodeRef>,
kind: NodeKind,
whitespace: Option<Token>,
token: Option<Token>,
}
impl Node {
fn unregistered() -> Self {
Self {
id: usize::MAX,
parent: None,
children: vec![],
kind: NodeKind::Unknown,
whitespace: None,
token: None,
}
}
pub fn kind(&self) -> &NodeKind {
&self.kind
}
pub fn token(&self) -> Option<&Token> {
self.token.as_ref()
}
}
#[derive(Default, Debug, Eq, PartialEq)]
pub enum NodeKind {
#[default]
Unknown,
List,
Token,
Atom,
Root,
}
pub fn parse(corpus: &str) -> Tree {
let mut tree = Tree::new();
let mut tokens = Tokens::new(corpus);
while let Some(nr) = parse_one(&mut tree, &mut tokens) {
let root = tree.root();
tree.add_child(root, nr);
}
tree
}
pub fn parse_one(tree: &mut Tree, tokens: &mut Tokens) -> Option<NodeRef> {
let tok = tokens.next()?;
let (ws, tok) = match tok.kind {
TokenKind::Whitespace(_) => (Some(tok), tokens.next()),
_ => (None, Some(tok)),
};
let mut node = Node::unregistered();
node.whitespace = ws;
let tok = match tok {
Some(tok) => tok,
None => {
node.kind = NodeKind::Token;
return Some(tree.register(node));
},
};
match &tok.kind {
TokenKind::Atom(_) => {
node.kind = NodeKind::Atom;
},
_ => todo!(),
}
node.token = Some(tok);
Some(tree.register(node))
}
#[cfg(test)]
mod test {
use crate::syn::{
cst::NodeKind,
tok::{Atom, TokenKind},
};
use super::parse;
#[test]
fn atom1() {
let tree = parse("32");
let mut children =
tree.children_of(tree.root()).map(|n| n.resolve(&tree));
let first = children.next().unwrap().as_atom().unwrap().value();
}
}

238
bake/lib/syn/mod.rs Normal file
View file

@ -0,0 +1,238 @@
use crate::wald::{NodeRef, NodeStorage, Text};
#[derive(Copy, Clone, Eq, PartialEq)]
#[repr(u16)]
pub enum NodeKind {
// Tokens
// These are the "leaf" nodes
LPar,
RPar,
Whitespace,
Word,
// Composite
/// A generic node that wraps multiple tokens. This is useful for e.g.
/// attaching whitespace to a Word.
Multi,
List,
Root,
#[doc(hidden)]
_ErrFirst,
ErrUnexpectedEOF,
#[doc(hidden)]
_ErrLast,
#[doc(hidden)]
_Last,
}
impl From<u16> for NodeKind {
fn from(value: u16) -> Self {
assert!(value < NodeKind::_Last as u16);
unsafe { core::mem::transmute::<u16, NodeKind>(value) }
}
}
impl From<NodeKind> for u16 {
fn from(val: NodeKind) -> Self {
val as u16
}
}
pub fn breaks_word(b: u8) -> bool {
b.is_ascii_whitespace() || b == b')' || b == b'('
}
pub struct Parser<'a> {
input: &'a str,
at: usize,
nodes: &'a mut NodeStorage,
}
impl<'a> Parser<'a> {
pub fn new(nodes: &'a mut NodeStorage, input: &'a str) -> Self {
Self {
nodes,
input,
at: 0,
}
}
pub fn head(&self) -> Option<u8> {
self.input[self.at..].bytes().next()
}
pub fn skip(&mut self, amt: usize) {
self.at += amt;
}
pub fn skip_while(&mut self, mut f: impl FnMut(u8) -> bool) {
while let Some(head) = self.head() {
if !f(head) {
break;
}
self.skip(1);
}
}
pub fn parse_whitespace(&mut self) -> Option<NodeRef> {
let start = self.at;
self.skip_while(|b| b.is_ascii_whitespace());
let end = self.at;
let span = start..end;
if span.is_empty() {
return None;
}
let node = self.nodes.new_node(NodeKind::Whitespace.into());
self.nodes.set_text(node, Text::Span(span));
Some(node)
}
pub fn parse_word(&mut self, whitespace: Option<NodeRef>) -> NodeRef {
let start = self.at;
self.skip_while(|b| !breaks_word(b));
let end = self.at;
let node_word = self.nodes.new_node(NodeKind::Word.into());
self.nodes.set_text(node_word, Text::Span(start..end));
let node_multi = self.nodes.new_node(NodeKind::Multi.into());
if let Some(node_whitespace) = whitespace {
self.nodes.append_child(node_multi, node_whitespace);
}
self.nodes.append_child(node_multi, node_word);
node_multi
}
pub fn parse_list(&mut self, whitespace: Option<NodeRef>) -> NodeRef {
assert!(self.head().unwrap() == b'(');
let node_list = self.nodes.new_node(NodeKind::List.into());
if let Some(node_whitespace) = whitespace {
self.nodes.append_child(node_list, node_whitespace);
}
// LPar
let start = self.at;
self.skip(1);
let end = self.at;
let node_lpar = self.nodes.new_node(NodeKind::LPar.into());
self.nodes.set_text(node_lpar, Text::Span(start..end));
self.nodes.append_child(node_list, node_lpar);
loop {
let head = match self.head() {
None => {
let node_err =
self.nodes.new_node(NodeKind::ErrUnexpectedEOF.into());
self.nodes.append_child(node_list, node_err);
break;
},
Some(h) => h,
};
if head == b')' {
let start = self.at;
self.skip(1);
let end = self.at;
let node_rpar = self.nodes.new_node(NodeKind::RPar.into());
self.nodes.set_text(node_rpar, Text::Span(start..end));
self.nodes.append_child(node_list, node_rpar);
break;
}
let node_child = self.parse_one().unwrap();
self.nodes.append_child(node_list, node_child);
}
node_list
}
pub fn parse_one(&mut self) -> Option<NodeRef> {
let whitespace = self.parse_whitespace();
let head = self.head()?;
let node = match head {
b'(' => self.parse_list(whitespace),
_ => self.parse_word(whitespace),
};
Some(node)
}
pub fn parse(&mut self) {
assert!(self.nodes.nodes().next().is_none());
let node_root = self.nodes.new_node(NodeKind::Root.into());
while let Some(node) = self.parse_one() {
self.nodes.append_child(node_root, node);
}
}
}
pub fn parse(storage: &mut NodeStorage, input: &str) {
let mut parser = Parser::new(storage, input);
parser.parse();
}
#[cfg(test)]
mod test {
use crate::{
syn::{parse, NodeKind},
wald::NodeStorage,
};
#[test]
fn simple_print_input_exactly() {
let input = r#"(+ 3 4)"#;
let mut storage = NodeStorage::new();
parse(&mut storage, input);
assert!(!storage
.nodes()
.any(|n| (NodeKind::_ErrFirst.into()..NodeKind::_ErrLast.into())
.contains(&storage.tag(n))));
let root = storage
.nodes()
.find(|&n| storage.tag(n) == NodeKind::Root.into())
.unwrap();
let display = storage.display_syntax(input, root);
let output = format!("{display}");
assert_eq!(input, output);
}
#[test]
fn traversal() {
let input = r#"(+ 3 4)"#;
let mut nodes = NodeStorage::new();
parse(&mut nodes, input);
let three = nodes
.nodes()
.find(|&n| nodes.text(n).as_str(input) == "3")
.unwrap();
let three_multi = nodes.parent(three).unwrap();
let four_multi = nodes.sibling_next(three_multi).unwrap();
let four = nodes
.children(four_multi)
.find(|&n| nodes.tag(n) == NodeKind::Word.into())
.unwrap();
assert_eq!(nodes.text(four).as_str(input), "4");
let list = nodes.parent(three_multi).unwrap();
assert_eq!(list, nodes.parent(four_multi).unwrap());
}
}

200
bake/lib/syn/tok.rs Normal file
View file

@ -0,0 +1,200 @@
use std::ops::Range;
#[derive(Eq, PartialEq, Debug)]
pub struct Span(Range<usize>);
impl From<Range<usize>> for Span {
fn from(value: Range<usize>) -> Self {
Self(value)
}
}
#[derive(Eq, PartialEq, Debug)]
pub enum TokenError {
InvalidByteInNumericLiteral,
}
#[derive(PartialEq, Debug)]
pub enum TokenKind {
Whitespace(String),
LPar(u8),
RPar(u8),
Atom(Atom),
Error(TokenError),
}
#[derive(PartialEq, Debug)]
pub struct Token {
pub kind: TokenKind,
pub span: Span,
}
impl Token {
}
#[derive(PartialEq, Debug)]
pub enum Atom {
Keyword(String),
Identifier(String),
String(String),
Integer(i64),
Float(f64),
}
fn is_numlit(b: u8) -> bool {
b.is_ascii_digit() || b == b'.'
}
fn is_identifier(b: u8) -> bool {
!ends_literal(b)
}
fn ends_literal(b: u8) -> bool {
[b'(', b')'].contains(&b) || b.is_ascii_whitespace()
}
pub struct Tokens<'a> {
at: usize,
corpus: &'a str,
}
impl<'a> Tokens<'a> {
pub fn new(corpus: &'a str) -> Self {
Self { corpus, at: 0 }
}
fn head(&self) -> Option<u8> {
if self.at >= self.corpus.len() {
return None;
}
self.corpus[self.at..].bytes().next()
}
fn pop_head(&mut self) -> Option<u8> {
let out = self.head()?;
self.at += 1;
Some(out)
}
fn chomp_while(&mut self, mut f: impl FnMut(u8) -> bool) {
loop {
let ch = match self.pop_head() {
None => return,
Some(ch) => ch,
};
if !f(ch) {
self.at -= 1;
return;
}
}
}
fn next(&mut self) -> Option<Token> {
let start = self.at;
let ch = self.pop_head()?;
let tk = match ch {
b'(' => TokenKind::LPar(ch),
b')' => TokenKind::RPar(ch),
_ if ch.is_ascii_whitespace() => {
self.chomp_while(|b| b.is_ascii_whitespace());
TokenKind::Whitespace(self.corpus[start..self.at].to_string())
},
_ if ch.is_ascii_digit() => {
let mut is_float = false;
self.chomp_while(|b| {
if b == b'.' {
is_float = true;
}
is_numlit(b)
});
if !self.head().map(ends_literal).unwrap_or(true) {
TokenKind::Error(TokenError::InvalidByteInNumericLiteral)
} else {
TokenKind::Atom(if is_float {
Atom::Float(
self.corpus[start..self.at].parse().unwrap(),
)
} else {
Atom::Integer(
self.corpus[start..self.at].parse().unwrap(),
)
})
}
},
b':' => {
self.chomp_while(is_identifier);
TokenKind::Atom(Atom::Keyword(
self.corpus[start..self.at].to_string(),
))
},
_ => {
self.chomp_while(is_identifier);
TokenKind::Atom(Atom::Identifier(
self.corpus[start..self.at].to_string(),
))
},
};
Some(Token {
kind: tk,
span: Span::from(start..self.at),
})
}
}
impl Iterator for Tokens<'_> {
type Item = Token;
fn next(&mut self) -> Option<Self::Item> {
Self::next(self)
}
}
#[cfg(test)]
mod test_tokenize {
use super::{Atom, TokenKind};
use super::Tokens;
#[test]
fn simple1() {
let tokens: Vec<_> =
Tokens::new("(:hello)").map(|tk| tk.kind).collect();
assert_eq!(
tokens,
[
TokenKind::LPar(b'('),
TokenKind::Atom(Atom::Keyword(":hello".to_string())),
TokenKind::RPar(b')'),
]
)
}
#[test]
fn simple2() {
let tokens: Vec<_> =
Tokens::new("(-> 1 2.4)").map(|t| t.kind).collect();
assert_eq!(
tokens,
[
TokenKind::LPar(b'('),
TokenKind::Atom(Atom::Identifier("->".to_string())),
TokenKind::Whitespace(" ".to_string()),
TokenKind::Atom(Atom::Integer(1)),
TokenKind::Whitespace(" ".to_string()),
TokenKind::Atom(Atom::Float(2.4)),
TokenKind::RPar(b')'),
]
)
}
}

164
bake/lib/wald/mod.rs Normal file
View file

@ -0,0 +1,164 @@
use std::{fmt::Display, ops::Range};
pub enum Text {
Span(Range<usize>),
Static(&'static str),
String(String),
}
impl Default for Text {
fn default() -> Self {
Self::Static("")
}
}
impl Text {
pub fn empty() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
match self {
Self::Span(s) => s.is_empty(),
Self::Static(s) => s.is_empty(),
Self::String(s) => s.is_empty(),
}
}
pub fn as_str<'a>(&'a self, text: &'a str) -> &'a str {
match self {
Self::Span(s) => &text[s.clone()],
Self::Static(s) => s,
Self::String(s) => s.as_str(),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct NodeRef(usize);
#[derive(Default)]
pub struct NodeStorage {
tags: Vec<u16>,
parents: Vec<Option<NodeRef>>,
children: Vec<Vec<NodeRef>>,
siblings_prev: Vec<Option<NodeRef>>,
siblings_next: Vec<Option<NodeRef>>,
texts: Vec<Text>,
}
impl NodeStorage {
pub fn nodes(&self) -> impl Iterator<Item = NodeRef> {
(0..self.tags.len()).map(NodeRef)
}
pub fn new_node(&mut self, tag: u16) -> NodeRef {
let node = NodeRef(self.tags.len());
self.tags.push(tag);
self.parents.push(None);
self.children.push(vec![]);
self.siblings_prev.push(None);
self.siblings_next.push(None);
self.texts.push(Text::default());
node
}
pub fn tag(&self, node: NodeRef) -> u16 {
self.tags[node.0]
}
pub fn parent(&self, node: NodeRef) -> Option<NodeRef> {
self.parents[node.0]
}
pub fn set_parent(&mut self, child: NodeRef, parent: NodeRef) {
self.parents[child.0] = Some(parent);
}
pub fn children(&self, node: NodeRef) -> impl Iterator<Item = NodeRef> {
self.children[node.0].iter().copied()
}
pub fn append_child(&mut self, parent: NodeRef, child: NodeRef) {
self.set_parent(child, parent);
let children = &mut self.children[parent.0];
let child_index = children.len();
children.push(child);
if child_index > 0 {
let prev_index = child_index - 1;
let prev_sibling = children[prev_index];
self.set_sibling_next(prev_sibling, child);
self.set_sibling_prev(child, prev_sibling);
}
}
pub fn sibling_next(&mut self, node: NodeRef) -> Option<NodeRef> {
self.siblings_next[node.0]
}
pub fn set_sibling_next(&mut self, this: NodeRef, next: NodeRef) {
self.siblings_next[this.0] = Some(next);
}
pub fn sibling_prev(&mut self, node: NodeRef) -> Option<NodeRef> {
self.siblings_prev[node.0]
}
pub fn set_sibling_prev(&mut self, this: NodeRef, prev: NodeRef) {
self.siblings_prev[this.0] = Some(prev);
}
pub fn text(&self, node: NodeRef) -> &Text {
&self.texts[node.0]
}
pub fn set_text(&mut self, node: NodeRef, text: Text) {
self.texts[node.0] = text;
}
pub fn display_syntax<'a>(
&'a self,
text: &'a str,
node: NodeRef,
) -> impl Display + 'a {
struct DisplaySyntax<'a> {
nodes: &'a NodeStorage,
node: NodeRef,
text: &'a str,
}
impl Display for DisplaySyntax<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self.nodes.text(self.node) {
Text::Span(s) => &self.text[s.clone()],
Text::Static(s) => s,
Text::String(s) => s.as_str(),
};
write!(f, "{s}")?;
for child in self.nodes.children(self.node) {
write!(
f,
"{}",
self.nodes.display_syntax(self.text, child)
)?;
}
Ok(())
}
}
DisplaySyntax {
nodes: self,
node,
text,
}
}
}
impl NodeStorage {
pub fn new() -> Self {
Self::default()
}
}

View file

@ -24,12 +24,12 @@
},
"locked": {
"lastModified": 1,
"narHash": "sha256-PVtFcvxh3Aqgel46BBFzxN0IvEVDzw/n/hWJ76mVThQ=",
"path": "/nix/store/0d80mkh7c7904wghfbvy3rpd395lriny-source/nix/deno-flake",
"narHash": "sha256-YOJheOuchbi3vU4jlQ9hMcyDU+bK9tzi+4dskNeE6Ww=",
"path": "./nix/deno-flake",
"type": "path"
},
"original": {
"path": "/nix/store/0d80mkh7c7904wghfbvy3rpd395lriny-source/nix/deno-flake",
"path": "./nix/deno-flake",
"type": "path"
}
},

View file

@ -2,7 +2,7 @@
inputs.nixpkgs.url = "nixpkgs";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.deno-flake = {
url = "./nix/deno-flake";
url = "path:./nix/deno-flake";
inputs.nixpkgs.follows = "nixpkgs";
};
inputs.fenix = {

359
klout/Cargo.lock generated
View file

@ -2,6 +2,24 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "eyre"
version = "0.6.12"
@ -12,17 +30,78 @@ dependencies = [
"once_cell",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "indexmap"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "klout"
version = "0.0.0"
dependencies = [
"eyre",
"num_cpus",
"rand",
"rustc-hash",
"serde",
"toml",
"walkdir",
]
[[package]]
name = "libc"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
@ -30,3 +109,283 @@ name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "rustc-hash"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "syn"
version = "2.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "unicode-ident"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View file

@ -3,5 +3,19 @@ name = "klout"
version = "0.0.0"
edition = "2024"
[[bin]]
name = "klout"
path = "src/klout.rs"
[[bin]]
name = "klout-gen-data"
path = "src/gen_data.rs"
[dependencies]
eyre = "0.6.12"
eyre = "0.6.12"
rand = "0.8.5"
num_cpus = "1.16.0"
serde = { version = "1.0.216", features = ["derive"] }
toml = "0.8.19"
walkdir = "2.5.0"
rustc-hash = "2.1.0"

15851
klout/corpora/dracula.txt Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

10591
klout/corpora/walden.txt Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

11
klout/data/matrices.txt Normal file
View file

@ -0,0 +1,11 @@
q w f p b j l u y '
a r s t g m n e i o
x c d v z k h , . /
LPinky LRing LMiddle LIndex LIndex RIndex RIndex RMiddle RRing RPinky
LPinky LRing LMiddle LIndex LIndex RIndex RIndex RMiddle RRing RPinky
LRing LMiddle LIndex LIndex LIndex RIndex RIndex RMiddle RRing RPinky
100 4 2 4 30 30 4 2 4 100
50 1 0.1 0.1 5 5 0.1 0.1 1 50
2 3 2 10 30 10 2 3 2 100

10
klout/settings.toml Normal file
View file

@ -0,0 +1,10 @@
[paths]
layout = "data/initial_layout.txt"
matrices = "data/matrices.txt"
[workers]
n_workers = 1
prefer_numcpus = true
[parameters]

172
klout/src/gen_data.rs Normal file
View file

@ -0,0 +1,172 @@
use std::{
cmp::Ordering,
ops::{Add, Div, Mul, Sub},
path::Path,
};
use eyre::{eyre, Result};
use rustc_hash::FxHashMap;
type GramMap<const N: usize, T> = FxHashMap<[u8; N], T>;
#[derive(Default, Debug)]
struct Grams<T> {
grams1: GramMap<1, T>,
grams2: GramMap<2, T>,
grams3: GramMap<3, T>,
grams4: GramMap<4, T>,
}
impl<T> Grams<T>
where
T: Div<Output = T> + Copy,
{
fn divide_by(&mut self, n: T) {
divide_by(&mut self.grams1, n);
divide_by(&mut self.grams2, n);
divide_by(&mut self.grams3, n);
divide_by(&mut self.grams4, n);
}
}
impl<T> Grams<T>
where
T: Copy
+ PartialOrd
+ Default
+ Sub<Output = T>
+ Add<Output = T>
+ Div<Output = T>
+ Mul<Output = T>,
{
fn normalize(&mut self, omin: T, omax: T) {
normalize(&mut self.grams1, omin, omax);
normalize(&mut self.grams2, omin, omax);
normalize(&mut self.grams3, omin, omax);
normalize(&mut self.grams4, omin, omax);
}
}
fn divide_by<const N: usize, T: Div<Output = T> + Copy>(
grams: &mut GramMap<N, T>,
n: T,
) {
for v in grams.values_mut() {
*v = *v / n;
}
}
fn normalize<const N: usize, T>(grams: &mut GramMap<N, T>, omin: T, omax: T)
where
T: Copy
+ PartialOrd
+ Default
+ Sub<Output = T>
+ Add<Output = T>
+ Div<Output = T>
+ Mul<Output = T>,
{
let max = grams
.values()
.copied()
.max_by(|&a, &b| {
if a > b {
Ordering::Greater
} else {
Ordering::Less
}
})
.unwrap_or(Default::default());
let min = grams
.values()
.copied()
.min_by(|&a, &b| {
if a > b {
Ordering::Greater
} else {
Ordering::Less
}
})
.unwrap_or(Default::default());
for v in grams.values_mut() {
*v = map_to_range(*v, min, max, omin, omax);
}
}
#[test]
fn test_normalize() {
let mut input = GramMap::<1, f64>::default();
input.insert([b'a'], 500.);
input.insert([b'b'], 300.);
input.insert([b'c'], 100.);
input.insert([b'd'], 125.);
normalize(&mut input, 0., 100.);
assert_eq!(input[b"a"], 100.);
assert_eq!(input[b"b"], 50.);
assert_eq!(input[b"c"], 0.);
assert_eq!(input[b"d"], 6.25);
}
// maps a number from range [amin, amax] to range [bmin, bmax]
fn map_to_range<V>(v: V, amin: V, amax: V, bmin: V, bmax: V) -> V
where
V: Sub<Output = V>
+ Add<Output = V>
+ Mul<Output = V>
+ Div<Output = V>
+ Copy,
{
bmin + (((v - amin) * (bmax - bmin)) / (amax - amin))
}
#[test]
fn test_map_to_range() {
assert_eq!(map_to_range(40, 0, 100, 0, 10), 4);
assert_eq!(map_to_range(60, 50, 100, 5, 10), 6);
assert_eq!(map_to_range(55.5, 55., 56., 0., 1.), 0.5);
}
type GramsCounts = Grams<usize>;
type GramsFreqs = Grams<usize>;
fn gen_data_file(path: &Path) -> Result<GramsCounts> {
let data = std::fs::read_to_string(path)?;
let mut grams = Grams::default();
for win in data.as_bytes().windows(4) {
*grams.grams1.entry([win[0]]).or_insert(0) += 1;
*grams.grams2.entry([win[0], win[1]]).or_insert(0) += 1;
*grams.grams3.entry([win[0], win[1], win[2]]).or_insert(0) += 1;
*grams
.grams4
.entry([win[0], win[1], win[2], win[3]])
.or_insert(0) += 1;
}
// TODO: We lose a few N<4 grams here, but it's probably not that big of a deal
Ok(grams)
}
fn gen_data(inputs: Vec<String>) -> Result<GramsCounts> {
let mut grams = Grams::default();
for dir in inputs {
for de in walkdir::WalkDir::new(dir).into_iter() {
let de = de?;
if de.file_type().is_file() {
grams = grams.combine(gen_data_file(de.path())?);
}
}
}
Ok(grams)
}
fn main() -> Result<()> {
let mut dirs: Vec<String> = std::env::args().skip(1).collect();
let grams = gen_data(dirs)?;
Ok(())
}

269
klout/src/klout.rs Normal file
View file

@ -0,0 +1,269 @@
use std::{collections::HashMap, hash::Hash, ops::Deref};
use eyre::{eyre, Context, Result};
use serde::Deserialize;
#[derive(Debug)]
struct Matrix<T> {
width: usize,
height: usize,
data: Vec<T>,
}
#[derive(Debug)]
struct BiMatrix<T> {
m: Matrix<T>,
to_coord: HashMap<T, MatrixCoord>,
}
impl<T> Deref for BiMatrix<T> {
type Target = Matrix<T>;
fn deref(&self) -> &Self::Target {
&self.m
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
struct MatrixCoord {
x: usize,
y: usize,
}
impl MatrixCoord {
fn new(x: usize, y: usize) -> Self {
Self { x, y }
}
}
impl<T: Copy + Clone> Matrix<T> {
fn new(width: usize, height: usize) -> Self
where
T: Default,
{
let data = vec![T::default(); width * height];
Self {
width,
height,
data,
}
}
fn from_vec(width: usize, height: usize, data: Vec<T>) -> Result<Self> {
if width * height != data.len() {
return Err(eyre!("Invalid data len"));
}
let out = Self {
width,
height,
data,
};
Ok(out)
}
fn val_at_coord(&self, c: MatrixCoord) -> &T {
self.val_at_index(self.coord_to_index(c))
}
fn val_at_index(&self, i: usize) -> &T {
&self.data[i]
}
fn index_to_coord(&self, i: usize) -> MatrixCoord {
let x = i % self.width;
let y = i / self.width;
MatrixCoord { x, y }
}
fn coord_to_index(&self, c: MatrixCoord) -> usize {
c.y * self.width + c.x
}
fn set(&mut self, v: T, c: MatrixCoord) {
let i = self.coord_to_index(c);
self.data[i] = v;
}
fn size(&self) -> usize {
self.width * self.height
}
}
impl<T: Hash + Eq + Copy + Clone> BiMatrix<T> {
fn new(width: usize, height: usize) -> Self
where
T: Default,
{
Self {
m: Matrix::new(width, height),
to_coord: Default::default(),
}
}
fn from_vec(width: usize, height: usize, data: Vec<T>) -> Result<Self> {
let m = Matrix::from_vec(width, height, data)?;
let mut out = Self {
m,
to_coord: Default::default(),
};
for (i, v) in out.m.data.iter().enumerate() {
out.to_coord
.insert(*v, MatrixCoord::new(i % width, i / width));
}
Ok(out)
}
fn val_to_coord(&self, v: &T) -> MatrixCoord {
*self.to_coord.get(v).unwrap()
}
fn val_to_index(&self, v: &T) -> usize {
self.coord_to_index(self.val_to_coord(v))
}
fn set(&mut self, v: T, c: MatrixCoord) {
self.m.set(v, c);
self.to_coord.insert(v, c);
}
}
type Layout = BiMatrix<char>;
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
enum Hand {
Left,
Right,
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
enum Digit {
Pinky,
Ring,
Middle,
Index,
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
struct Finger {
digit: Digit,
hand: Hand,
}
fn load_matrices(
paths: &Paths,
) -> Result<(Layout, Matrix<Finger>, Matrix<f64>)> {
let data = std::fs::read_to_string(&paths.matrices)?;
let mut sections = data.split("\n\n");
let section_layout =
sections.next().ok_or(eyre!("Missing section: layout"))?;
let data: Vec<Vec<_>> = section_layout
.lines()
.map(|l| {
l.split_whitespace()
.flat_map(|s| s.chars().next())
.collect()
})
.collect();
let width = data
.iter()
.map(|l| l.len())
.max()
.ok_or(eyre!("Initial layout was empty"))?;
let height = data.len();
let mut layout = Layout::new(width, height);
for (y, row) in data.iter().enumerate() {
for (x, ch) in row.iter().enumerate() {
layout.set(*ch, MatrixCoord::new(x, y));
}
}
let section_fingers =
sections.next().ok_or(eyre!("Missing section: fingers"))?;
let lines_fingers = section_fingers.lines();
let fingers: Vec<Finger> = lines_fingers
.flat_map(|l| {
let words = l.split_whitespace();
words
})
.map(|w| {
let (l_or_r, digit) = w.split_at(1);
let hand = match l_or_r.to_lowercase().as_str() {
"l" => Hand::Left,
"r" => Hand::Right,
_ => return Err(eyre!("Invalid finger in data/matrices.txt")),
};
let digit = match digit.to_lowercase().as_str() {
"pinky" => Digit::Pinky,
"ring" => Digit::Ring,
"middle" => Digit::Middle,
"index" => Digit::Index,
_ => return Err(eyre!("Invalid finger in data/matrices.txt")),
};
Ok(Finger { hand, digit })
})
.collect::<Result<Vec<_>, _>>()?;
let m_finger = Matrix::from_vec(layout.width, layout.height, fingers)
.wrap_err("When loading fingers from data/matrices.txt")?;
let section_effort =
sections.next().ok_or(eyre!("Missing section: effort"))?;
let lines_effort = section_effort.lines();
let efforts: Vec<f64> = lines_effort
.flat_map(|l| l.split_whitespace())
.map(|w| w.parse())
.collect::<Result<Vec<_>, _>>()
.wrap_err("When loading efforts from data/matrices.txt")?;
let m_effort = Matrix::from_vec(layout.width, layout.height, efforts)
.wrap_err("When loading efforts from data/matrices.txt")?;
Ok((layout, m_finger, m_effort))
}
#[derive(Deserialize, Debug)]
struct Paths {
matrices: String,
}
#[derive(Deserialize, Debug)]
struct Workers {
n_workers: usize,
prefer_numcpus: bool,
}
#[derive(Deserialize, Debug)]
struct Parameters {}
#[derive(Deserialize, Debug)]
struct Settings {
paths: Paths,
workers: Workers,
parameters: Parameters,
}
fn main() -> Result<()> {
let settings = std::fs::read_to_string("./settings.toml")?;
let mut settings: Settings = toml::from_str(&settings)?;
if settings.workers.prefer_numcpus {
settings.workers.n_workers = num_cpus::get();
}
let (m_layout, m_fingers, m_effort) = load_matrices(&settings.paths)?;
Ok(())
}

View file

@ -1,12 +1,19 @@
use std::collections::HashMap;
use core::f64;
use eyre::Result;
use rand::{rngs::ThreadRng, Rng};
use std::{
collections::HashMap,
io::{self, Write},
sync::mpsc::{Receiver, Sender},
};
#[derive(Copy, Clone, Eq, PartialEq)]
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum Hand {
Left,
Right,
}
#[derive(Copy, Clone, Eq, PartialEq)]
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum Digit {
Pinky,
Ring,
@ -14,7 +21,7 @@ enum Digit {
Index,
}
#[derive(Copy, Clone, Eq, PartialEq)]
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
struct Finger {
hand: Hand,
digit: Digit,
@ -42,9 +49,9 @@ const RINDEX: Finger = F!(Right, Index);
#[rustfmt::skip]
const MATRIX_EFFORT: &[f64; MATRIX_COUNT] = &[
15., 3.0, 1.5, 3.0, 10., 10., 3.0, 1.5, 3.0, 15.,
8., 1., 0.5, 0.5, 4., 4., 0.5, 0.5, 1., 8.,
1.5, 2.0, 1., 1., 7., 5., 1., 2.0, 1.5, 10.,
100., 6., 10., 6., 40., 40., 10., 10., 10., 100.,
50., 2., 0.1, 0.1, 15., 15., 0.1, 0.1, 2., 50.,
10., 14., 10., 20., 40., 30., 6., 14., 6., 100.,
];
#[rustfmt::skip]
@ -75,11 +82,11 @@ const fn char_data_max_index() -> usize {
// https://www3.nd.edu/~busiforc/handouts/cryptography/Letter%20Frequencies.html#Relative_frequencies_of_letters
const CHAR_FREQ_LOOKUP: [f64; char_data_max_index()] = {
let mut data: [f64; char_data_max_index()] = [1.; char_data_max_index()];
let mut data: [f64; char_data_max_index()] = [0.; char_data_max_index()];
macro_rules! d {
($ch:expr, $val:expr) => {{
data[$ch as usize] = $val * 100.;
data[$ch as usize] = $val * 1000.;
}};
}
@ -113,6 +120,7 @@ const CHAR_FREQ_LOOKUP: [f64; char_data_max_index()] = {
data
};
#[derive(Copy, Clone)]
struct Layout {
key_indices: [usize; char_data_max_index()],
}
@ -129,18 +137,71 @@ impl Layout {
Self { key_indices }
}
fn to_key_matrix(self) -> [char; MATRIX_COUNT] {
let mut out = ['0'; MATRIX_COUNT];
for ch in CHARACTERS.chars() {
out[self.key_indices[ch as usize]] = ch;
}
out
}
fn key_to_xy(&self, ch: char) -> (u8, u8) {
let index = self.key_indices[ch as usize];
let y = index / 10;
let x = index % 10;
(x as u8, y as u8)
}
fn balance_penalty(&self) -> f64 {
let km = self.to_key_matrix();
let mut left_freq = 0.;
for x in 0..5 {
for y in 0..3 {
let i = y * 10 + x;
let ch = km[i];
left_freq += CHAR_FREQ_LOOKUP[ch as usize];
}
}
let mut right_freq = 0.;
for x in 5..10 {
for y in 0..3 {
let i = y * 10 + x;
let ch = km[i];
right_freq += CHAR_FREQ_LOOKUP[ch as usize];
}
}
(left_freq - right_freq).abs() * 10.
}
}
#[test]
fn test_key_to_xy() {
assert_eq!(INITIAL_LAYOUT.key_to_xy('g'), (4, 1));
}
fn key_effort(ch: char, l: &Layout) -> f64 {
CHAR_FREQ_LOOKUP[ch as usize] * MATRIX_EFFORT[l.key_indices[ch as usize]]
}
/*
#[rustfmt::skip]
const INITIAL_LAYOUT: Layout = Layout::from_key_matrix(&[
'q', 'w', 'f', 'p', 'b', 'j', 'l', 'u', 'y', '\'',
'a', 'r', 's', 't', 'g', 'm', 'n', 'e', 'i', 'o',
'x', 'c', 'd', 'v', 'z', 'k', 'h', ',', '.', '/',
]);
*/
#[rustfmt::skip]
const INITIAL_LAYOUT: Layout = Layout::from_key_matrix(&[
'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p',
'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '\'',
'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/',
]);
type NGramFreqs = HashMap<&'static str, f64>;
@ -169,7 +230,7 @@ unsafe fn init_bigram_freqs() {
("ng", 0.01053385),
]
.into_iter()
.map(|(s, v)| (s, v * 100.))
.map(|(s, v)| (s, v * 4000.))
.collect::<NGramFreqs>();
unsafe { BIGRAM_FREQS = Some(freqs) };
@ -186,8 +247,39 @@ fn bigram_effort(bigram: &str, l: &Layout) -> f64 {
let finger1 = MATRIX_FINGERS[l.key_indices[ch1 as usize]];
let finger2 = MATRIX_FINGERS[l.key_indices[ch2 as usize]];
let ch1xy = l.key_to_xy(ch1 as char);
let ch2xy = l.key_to_xy(ch2 as char);
let x_diff = ch1xy.0.abs_diff(ch2xy.0);
let y_diff = ch1xy.1.abs_diff(ch2xy.1);
if finger1 == finger2 {
eff *= 10.;
if y_diff > 1 {
eff *= 1000.;
} else {
eff *= 500.;
}
}
if finger1.hand != finger2.hand {
eff /= 2.;
} else if x_diff == 1 {
let mult = if (finger1.hand == Hand::Left && ch1xy.0 < ch2xy.0)
|| (finger1.hand == Hand::Right && ch1xy.0 > ch2xy.0)
{
10.
} else {
1.
};
if y_diff == 0 {
eff /= 1000. * mult;
} else {
eff /= 250. * mult;
}
} else {
eff *= (x_diff + 1) as f64;
eff *= (y_diff + 1) as f64;
}
eff
@ -210,24 +302,143 @@ fn all_bigrams() -> &'static [String] {
unsafe { ALL_BIGRAMS.as_ref().unwrap_unchecked() }
}
fn main() {
const N_WORKERS: usize = 6;
fn mutate_layout(layout: &mut Layout, rng: &mut ThreadRng, max_swaps: usize) {
let num_swaps = rng.gen_range(1..=max_swaps);
for _ in 0..num_swaps {
let ch1 = CHARACTERS
.chars()
.nth(rng.gen_range(0..CHARACTERS.len()))
.unwrap();
let ch2 = CHARACTERS
.chars()
.nth(rng.gen_range(0..CHARACTERS.len()))
.unwrap();
layout.key_indices.swap(ch1 as usize, ch2 as usize);
}
}
struct WorkerThreadInit {
initial_layout: Layout,
report: Sender<(f64, Layout)>,
/// The maximum amount of deviation from the min_effort value allowed before
/// reverting back to the currently-known best layout
max_dev: f64,
/// The maximum number of swaps to make between iterations
max_swaps: usize,
}
fn worker_thread_run(init: WorkerThreadInit) {
let mut rng = rand::thread_rng();
let mut layout = init.initial_layout;
let mut best_layout = layout;
let mut min_effort: f64 = f64::MAX;
loop {
let mut eff: f64 =
CHARACTERS.chars().map(|v| key_effort(v, &layout)).sum();
eff += all_bigrams()
.iter()
.map(|b| bigram_effort(b, &layout))
.sum::<f64>();
eff *= layout.balance_penalty();
if eff < min_effort {
min_effort = eff;
best_layout = layout;
init.report.send((eff, layout)).unwrap();
} else if (eff - min_effort) > init.max_dev {
layout = best_layout;
}
mutate_layout(&mut layout, &mut rng, init.max_swaps);
}
}
fn print_layout(layout: Layout) {
let km = layout.to_key_matrix();
for chunk in km.chunks(MATRIX_COUNT / 3) {
for ch in chunk {
print!("{ch}");
}
println!();
}
}
fn clear_screen() {
print!("\x1B[2J\x1B[H");
io::stdout().flush().unwrap();
}
fn ui_thread_run(rx: Receiver<(f64, Layout)>) {
for (eff, layout) in rx.iter() {
clear_screen();
print_layout(layout);
println!();
println!("{eff}");
}
}
fn aggregator_thread_run(
layout: Layout,
rx: Receiver<(f64, Layout)>,
tx: Sender<(f64, Layout)>,
) {
let mut best_layout = layout;
let mut min_effort = f64::MAX;
for (eff, layout) in rx.iter() {
if eff < min_effort {
best_layout = layout;
min_effort = eff;
tx.send((min_effort, best_layout)).unwrap();
}
}
}
fn main() -> Result<()> {
unsafe {
init_bigram_freqs();
init_all_bigrams();
init_bigram_freqs();
};
let eff: f64 = CHARACTERS
.chars()
.map(|v| {
let mut eff = key_effort(v, &INITIAL_LAYOUT);
eff += all_bigrams()
.iter()
.map(|b| bigram_effort(b, &INITIAL_LAYOUT))
.sum::<f64>();
let (tx_agg, rx_agg) = std::sync::mpsc::channel();
let (tx_ui, rx_ui) = std::sync::mpsc::channel();
eff
let ui_thread = std::thread::spawn(|| {
ui_thread_run(rx_ui);
});
let aggregator_thread = std::thread::spawn(|| {
aggregator_thread_run(INITIAL_LAYOUT, rx_agg, tx_ui);
});
let worker_threads = (0..N_WORKERS)
.map(|_| {
let tx = tx_agg.clone();
std::thread::spawn(move || {
worker_thread_run(WorkerThreadInit {
initial_layout: INITIAL_LAYOUT,
report: tx,
max_dev: 1000.,
max_swaps: 10,
})
})
})
.sum();
.collect::<Vec<_>>();
println!("{eff}");
ui_thread.join().unwrap();
aggregator_thread.join().unwrap();
for j in worker_threads {
j.join().unwrap();
}
Ok(())
}

View file

@ -1,36 +1,37 @@
{
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "nixpkgs";
};
outputs = inputs:
let lib = import ./lib.nix;
in
inputs.flake-utils.lib.eachDefaultSystem (system:
let
nixpkgs = inputs.nixpkgs.legacyPackages.${system};
versions = [
["2.0.3" "sha256-++wvqD6TunG47jp2SKW+clGOJ6Sy9CnEu2e6AgKP1X0="]
["2.0.0" "sha256-WQ4B0sT3qTVl4/Moj0FcFg5LDZIBPbnmcfUxwrmFyYY="]
["1.46.3" "sha256-vnDzegjO7XFqBj3dZ1T4TZfuFr3Ur2f4/2zlFUQUwSI="]
];
packages = builtins.listToAttrs (builtins.map (l:
let
version = builtins.elemAt l 0;
zipHash = builtins.elemAt l 1;
in {
name = "deno-${builtins.replaceStrings ["."] ["_"] version}";
value = lib.mkDeno { inherit version zipHash nixpkgs; };
}
) versions);
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "nixpkgs";
};
outputs = inputs:
let lib = import ./lib.nix;
in
inputs.flake-utils.lib.eachDefaultSystem (system:
let
nixpkgs = inputs.nixpkgs.legacyPackages.${system};
versions = [
["2.1.5" "sha256-xzQRtCwpksRA1XB2ILE3Gdc3r4ftT63M1WBmi6yXZzw="]
["2.0.3" "sha256-++wvqD6TunG47jp2SKW+clGOJ6Sy9CnEu2e6AgKP1X0="]
["2.0.0" "sha256-WQ4B0sT3qTVl4/Moj0FcFg5LDZIBPbnmcfUxwrmFyYY="]
["1.46.3" "sha256-vnDzegjO7XFqBj3dZ1T4TZfuFr3Ur2f4/2zlFUQUwSI="]
];
in { packages = packages // {
deno-latest =
let
v = (builtins.elemAt (builtins.elemAt versions 0) 0);
a = "deno-${builtins.replaceStrings ["."] ["_"] v}";
in packages.${a};
};
});
packages = builtins.listToAttrs (builtins.map (l:
let
version = builtins.elemAt l 0;
zipHash = builtins.elemAt l 1;
in {
name = "deno-${builtins.replaceStrings ["."] ["_"] version}";
value = lib.mkDeno { inherit version zipHash nixpkgs; };
}
) versions);
in { packages = packages // {
deno-latest =
let
v = (builtins.elemAt (builtins.elemAt versions 0) 0);
a = "deno-${builtins.replaceStrings ["."] ["_"] v}";
in packages.${a};
};
});
}

245
pritty/colors/flexoki.toml Normal file
View file

@ -0,0 +1,245 @@
name = "flexoki"
[palette]
black = "#100F0F"
paper = "#FFFCF0"
base-50 = "#F2F0E5"
base-100 = "#E6E4D9"
base-150 = "#DAD8CE"
base-200 = "#CECDC3"
base-300 = "#B7B5AC"
base-400 = "#9F9D96"
base-500 = "#878580"
base-600 = "#6F6E69"
base-700 = "#575653"
base-800 = "#403E3C"
base-850 = "#343331"
base-900 = "#282726"
base-950 = "#1C1B1A"
red-50 = "#FFE1D5"
red-100 = "#FFCABB"
red-150 = "#FDB2A2"
red-200 = "#F89A8A"
red-300 = "#E8705F"
red-400 = "#D14D41"
red-500 = "#C03E35"
red-600 = "#AF3029"
red-700 = "#942822"
red-800 = "#6C201C"
red-850 = "#551B18"
red-900 = "#3E1715"
red-950 = "#261312"
orange-50 = "#FFE7CE"
orange-100 = "#FED3AF"
orange-150 = "#FCC192"
orange-200 = "#F9AE77"
orange-300 = "#EC8B49"
orange-400 = "#DA702C"
orange-500 = "#CB6120"
orange-600 = "#BC5215"
orange-700 = "#9D4310"
orange-800 = "#71320D"
orange-850 = "#59290D"
orange-900 = "#40200D"
orange-950 = "#27180E"
yellow-50 = "#FAEEC6"
yellow-100 = "#F6E2A0"
yellow-150 = "#F1D67E"
yellow-200 = "#ECCB60"
yellow-300 = "#DFB431"
yellow-400 = "#D0A215"
yellow-500 = "#BE9207"
yellow-600 = "#AD8301"
yellow-700 = "#8E6B01"
yellow-800 = "#664D01"
yellow-850 = "#503D02"
yellow-900 = "#3A2D04"
yellow-950 = "#241E08"
green-50 = "#EDEECF"
green-100 = "#DDE2B2"
green-150 = "#CDD597"
green-200 = "#BEC97E"
green-300 = "#A0AF54"
green-400 = "#879A39"
green-500 = "#768D21"
green-600 = "#66800B"
green-700 = "#536907"
green-800 = "#3D4C07"
green-850 = "#313D07"
green-900 = "#252D09"
green-950 = "#1A1E0C"
cyan-50 = "#DDF1E4"
cyan-100 = "#BFE8D9"
cyan-150 = "#A2DECE"
cyan-200 = "#87D3C3"
cyan-300 = "#5ABDAC"
cyan-400 = "#3AA99F"
cyan-500 = "#2F968D"
cyan-600 = "#24837B"
cyan-700 = "#1C6C66"
cyan-800 = "#164F4A"
cyan-850 = "#143F3C"
cyan-900 = "#122F2C"
cyan-950 = "#101F1D"
blue-50 = "#E1ECEB"
blue-100 = "#C6DDE8"
blue-150 = "#ABCFE2"
blue-200 = "#92BFDB"
blue-300 = "#66A0C8"
blue-400 = "#4385BE"
blue-500 = "#3171B2"
blue-600 = "#205EA6"
blue-700 = "#1A4F8C"
blue-800 = "#163B66"
blue-850 = "#133051"
blue-900 = "#12253B"
blue-950 = "#101A24"
purple-50 = "#F0EAEC"
purple-100 = "#E2D9E9"
purple-150 = "#D3CAE6"
purple-200 = "#C4B9E0"
purple-300 = "#A699D0"
purple-400 = "#8B7EC8"
purple-500 = "#735EB5"
purple-600 = "#5E409D"
purple-700 = "#4F3685"
purple-800 = "#3C2A62"
purple-850 = "#31234E"
purple-900 = "#261C39"
purple-950 = "#1A1623"
magenta-50 = "#FEE4E5"
magenta-100 = "#FCCFDA"
magenta-150 = "#F9B9CF"
magenta-200 = "#F4A4C2"
magenta-300 = "#E47DA8"
magenta-400 = "#CE5D97"
magenta-500 = "#B74583"
magenta-600 = "#A02F6F"
magenta-700 = "#87285E"
magenta-800 = "#641F46"
magenta-850 = "#4F1B39"
magenta-900 = "#39172B"
magenta-950 = "#24131D"
[dark.backgrounds]
primary = "@black"
secondary = "@base-950"
selection = "@base-800"
status-bar-active = "@base-900"
status-bar-inactive = "@black"
cursor = "@base-200"
[dark.borders]
default = "@base-900"
hovered = "@base-850"
active = "@base-800"
[dark.text]
primary = "@base-200"
muted = "@base-500"
faint = "@base-700"
error = "@red-400"
warning = "@orange-400"
success = "@green-400"
link = "@cyan-400"
selection = "@base-200"
cursor = "@black"
[dark.syntax]
operator = "@base-500"
import = "@red-400"
function = "@orange-400"
constant = "@yellow-400"
keyword = "@green-400"
string = "@cyan-400"
identifier = "@blue-400"
number = "@purple-400"
comment = "@base-700"
macro = "@magenta-400"
other = "@magenta-400"
[dark.terminal-16]
bright-black = "@base-600"
bright-red = "@red-400"
bright-green = "@green-400"
bright-yellow = "@yellow-400"
bright-blue = "@blue-400"
bright-magenta = "@magenta-400"
bright-cyan = "@cyan-400"
bright-white = "@base-200"
black = "@black"
red = "@red-600"
green = "@green-600"
yellow = "@yellow-600"
blue = "@blue-600"
magenta = "@magenta-600"
cyan = "@cyan-600"
white = "@base-500"
[light.backgrounds]
primary = "@paper"
secondary = "@base-50"
selection = "@base-200"
status-bar-active = "@base-100"
status-bar-inactive = "@paper"
cursor = "@black"
[light.borders]
default = "@base-900"
hovered = "@base-850"
active = "@base-800"
[light.text]
primary = "@black"
muted = "@base-600"
faint = "@base-300"
error = "@red-600"
warning = "@orange-600"
success = "@green-600"
link = "@cyan-600"
selection = "@black"
cursor = "@black"
[light.syntax]
operator = "@base-500"
import = "@red-600"
function = "@orange-600"
constant = "@yellow-600"
keyword = "@green-600"
string = "@cyan-600"
identifier = "@blue-600"
number = "@purple-600"
comment = "@base-300"
macro = "@magenta-600"
other = "@magenta-600"
[light.terminal-16]
bright-black = "@base-600"
red = "@red-400"
green = "@green-400"
yellow = "@yellow-400"
blue = "@blue-400"
magenta = "@magenta-400"
cyan = "@cyan-400"
bright-white = "@base-200"
black = "@black"
bright-red = "@red-600"
bright-green = "@green-600"
bright-yellow = "@yellow-600"
bright-blue = "@blue-600"
bright-magenta = "@magenta-600"
bright-cyan = "@cyan-600"
white = "@base-500"

24
pritty/deno.jsonc Normal file
View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"checkJs": true,
"lib": [
"esnext",
"dom",
"dom.iterable",
"deno.window",
"deno.unstable"
]
},
"tasks": {
"check": "deno check src/main.ts"
},
"imports": {
"@std/toml": "jsr:@std/toml@^1.0.2"
},
"fmt": {
"semiColons": true,
"singleQuote": true,
"useTabs": true,
"lineWidth": 90
}
}

23
pritty/deno.lock Normal file
View file

@ -0,0 +1,23 @@
{
"version": "4",
"specifiers": {
"jsr:@std/collections@^1.0.9": "1.0.9",
"jsr:@std/toml@^1.0.2": "1.0.2"
},
"jsr": {
"@std/collections@1.0.9": {
"integrity": "4f58104ead08a04a2199374247f07befe50ba01d9cca8cbb23ab9a0419921e71"
},
"@std/toml@1.0.2": {
"integrity": "5892ba489c5b512265a384238a8fe8dddbbb9498b4b210ef1b9f0336a423a39b",
"dependencies": [
"jsr:@std/collections"
]
}
},
"workspace": {
"dependencies": [
"jsr:@std/toml@^1.0.2"
]
}
}

32
pritty/scripts/colortest-16.sh Executable file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Default 16 ANSI color names
color_names=(
"Black"
"Red"
"Green"
"Yellow"
"Blue"
"Magenta"
"Cyan"
"White"
"Bright Black"
"Bright Red"
"Bright Green"
"Bright Yellow"
"Bright Blue"
"Bright Magenta"
"Bright Cyan"
"Bright White"
)
echo "16-color palette with indices & names:"
for c in {0..15}; do
# Print the color block
printf "\033[38;5;%sm%2d: %-13s\033[0m " "$c" "$c" "${color_names[$c]}"
# New line every 4 colors
if (( (c+1) % 4 == 0 )); then
echo
fi
done
echo

326
pritty/src/main.ts Normal file
View file

@ -0,0 +1,326 @@
import * as toml from '@std/toml';
type OutputType = 'ghostty' | 'emacs';
type Output = {
outputType: OutputType;
dir: string;
};
const USAGE = `
pritty <input_path> <output_type:output_dir>...
`;
function printUsageAndDie() {
console.log(USAGE);
Deno.exit(1);
}
type RawPalette = {
[k: string]: string | RawPalette;
};
type Palette = Record<string, string>;
function flattenPalette(
input: RawPalette,
prefix: string = '',
output: Record<string, string> = {},
) {
for (const key in input) {
const value = input[key];
if (typeof value === 'object') {
flattenPalette(value, `${prefix}${key}.`, output);
} else {
output[`${prefix}${key}`] = value;
}
}
return output;
}
type TableEntryColor = `#${string}`;
type TableEntryRef = `@${string}`;
type TableEntry = TableEntryColor | TableEntryRef;
type Tables = Partial<{
backgrounds: Partial<{
primary: TableEntry;
secondary: TableEntry;
'status-bar-active': TableEntry;
'status-bar-inactive': TableEntry;
}>;
borders: Partial<{
default: TableEntry;
hovered: TableEntry;
active: TableEntry;
}>;
text: Partial<{
primary: TableEntry;
muted: TableEntry;
faint: TableEntry;
error: TableEntry;
warning: TableEntry;
success: TableEntry;
link: TableEntry;
}>;
syntax: Partial<{
operator: TableEntry;
import: TableEntry;
function: TableEntry;
constant: TableEntry;
keyword: TableEntry;
string: TableEntry;
identifier: TableEntry;
number: TableEntry;
macro: TableEntry;
other: TableEntry;
}>;
'terminal-16': Partial<{
black: TableEntry;
red: TableEntry;
green: TableEntry;
yellow: TableEntry;
blue: TableEntry;
magenta: TableEntry;
cyan: TableEntry;
white: TableEntry;
'bright-black': TableEntry;
'bright-red': TableEntry;
'bright-green': TableEntry;
'bright-yellow': TableEntry;
'bright-blue': TableEntry;
'bright-magenta': TableEntry;
'bright-cyan': TableEntry;
'bright-white': TableEntry;
}>;
}>;
function get(entry: TableEntry | undefined, palette: Palette) {
if (entry === undefined) {
return undefined;
}
if (entry.startsWith('@')) {
const c = palette[entry.substring(1)];
if (c === undefined) {
throw new Error(`Undefined reference ${entry}`);
}
return c;
}
return entry;
}
function bindGet(palette: Palette) {
return (entry: TableEntry | undefined) => get(entry, palette);
}
async function generateGhostty(
dir: string,
name: string,
variantName: string,
tables: Tables,
palette: Palette,
) {
let output = '';
const get = bindGet(palette);
const w = (prop: string, v: string | undefined) => {
if (v !== undefined) {
output += `${prop} = ${v}\n`;
}
};
const p = (index: number, v: string | undefined) => {
if (v !== undefined) {
output += `palette = ${index}=${v}\n`;
}
};
w('background', get(tables['backgrounds']?.['primary']));
w('foreground', get(tables['text']?.['primary']));
w('selection-background', get(tables['backgrounds']?.['selection']));
w('selection-foreground', get(tables['text']?.['selection']));
p(0, get(tables['terminal-16']?.['black']));
p(1, get(tables['terminal-16']?.['red']));
p(2, get(tables['terminal-16']?.['green']));
p(3, get(tables['terminal-16']?.['yellow']));
p(4, get(tables['terminal-16']?.['blue']));
p(5, get(tables['terminal-16']?.['magenta']));
p(6, get(tables['terminal-16']?.['cyan']));
p(7, get(tables['terminal-16']?.['white']));
p(8, get(tables['terminal-16']?.['bright-black']));
p(9, get(tables['terminal-16']?.['bright-red']));
p(10, get(tables['terminal-16']?.['bright-green']));
p(11, get(tables['terminal-16']?.['bright-yellow']));
p(12, get(tables['terminal-16']?.['bright-blue']));
p(13, get(tables['terminal-16']?.['bright-magenta']));
p(14, get(tables['terminal-16']?.['bright-cyan']));
p(15, get(tables['terminal-16']?.['bright-white']));
const path = `${dir}/${name}-${variantName}.ghostty`;
await Deno.writeTextFile(path, output);
}
async function generateEmacs(
dir: string,
name: string,
variantName: string,
tables: Tables,
palette: Palette,
) {
let output = `(deftheme ${name}-${variantName})\n`;
output += `(custom-theme-set-faces '${name}-${variantName}\n`;
const get = bindGet(palette);
const f = (faceName: string, props: Record<string, boolean | string | undefined>) => {
let out = '';
for (const [prop, value] of Object.entries(props)) {
if (value !== undefined) {
if (out === '') {
out += ` \`(${faceName} ((t (`;
}
if (typeof value === 'string') {
out += `:${prop} "${value}" `;
} else if (typeof value === 'boolean') {
const v = value ? 't' : 'nil';
out += `:${prop} ${v} `;
}
}
}
if (out !== '') {
out += '))))\n';
}
output += out;
};
f('default', {
foreground: get(tables['text']?.['primary']),
background: get(tables['backgrounds']?.['primary']),
});
f('mode-line-active', {
foreground: get(tables['text']?.['primary']),
background: get(tables['backgrounds']?.['status-bar-active']),
box: true,
});
f('mode-line-inactive', {
foreground: get(tables['text']?.['primary']),
background: get(tables['backgrounds']?.['status-bar-inactive']),
box: true,
});
f('cursor', {
foreground: get(tables['text']?.['cursor']),
background: get(tables['backgrounds']?.['cursor']),
});
f('region', {
foreground: get(tables['text']?.['selection']),
background: get(tables['backgrounds']?.['selection']),
});
f('vertico-current', {
foreground: get(tables['text']?.['selection']),
background: get(tables['backgrounds']?.['selection']),
});
f('whitespace-tab', { foreground: get(tables['text']?.['faint']) });
const s = (faceName: string, syntaxEntry: string) => {
const v = get(tables['syntax']?.[syntaxEntry]) ?? get(tables['text']?.[syntaxEntry]);
f(faceName, { foreground: v });
};
const syntaxMappings = [
['font-lock-type-face', 'identifier'],
['font-lock-constant-face', 'constant'],
['font-lock-builtin-face', 'other'],
['font-lock-preprocessor-face', 'macro'],
['font-lock-doc-face', 'comment'],
['font-lock-function-name-face', 'function'],
['font-lock-keyword-face', 'keyword'],
['font-lock-variable-name-face', 'identifier'],
['font-lock-string-face', 'string'],
['font-lock-variable-use-face', 'identifier'],
['font-lock-negation-char-face', 'operator'],
['font-lock-property-use-face', 'identifier'],
['font-lock-punctuation-face', 'operator'],
['font-lock-doc-markup-face', 'comment'],
['font-lock-comment-delimiter-face', 'comment'],
['font-lock-delimiter-face', 'operator'],
['font-lock-number-face', 'constant'],
['font-lock-function-call-face', 'function'],
['font-lock-comment-face', 'comment'],
['font-lock-operator-face', 'operator'],
['font-lock-property-name-face', 'identifier'],
['font-lock-misc-punctuation-face', 'operator'],
['font-lock-escape-face', 'other'],
['font-lock-warning-face', 'warning'],
['font-lock-error-face', 'error'],
] as const;
for (const [faceName, syntaxEntry] of syntaxMappings) {
s(faceName, syntaxEntry);
}
output += `)\n`;
const path = `${dir}/${name}-${variantName}-theme.el`;
await Deno.writeTextFile(path, output);
}
async function generateOutput(
output: Output,
name: string,
variants: Record<string, Tables>,
palette: Palette,
) {
const { outputType, dir } = output;
await Deno.mkdir(dir, { recursive: true });
const genFunctions = {
'ghostty': generateGhostty,
'emacs': generateEmacs,
};
const genFunction = genFunctions[outputType];
if (genFunction === undefined) {
throw new Error(`Unsupported output type "${outputType}"`);
}
for (const [variantName, tables] of Object.entries(variants)) {
await genFunction(dir, name, variantName, tables, palette);
}
}
async function main() {
const inputPath = Deno.args[1];
if (inputPath === undefined) {
printUsageAndDie();
}
const outputs_ = Deno.args.slice(2);
if (outputs_.length === 0) {
printUsageAndDie();
}
const outputs: Output[] = outputs_.map((o) => {
const [outputType, dir] = o.split(':');
return { outputType: outputType as OutputType, dir };
});
const input = await Deno.readTextFile(inputPath);
const parsed = toml.parse(input);
const { name, palette: palette_, ...variants } = parsed;
const palette = flattenPalette(palette_ as RawPalette);
for (const output of outputs) {
await generateOutput(
output,
name as string,
variants as Record<string, Tables>,
palette,
);
}
}
await main();