mirror of
https://github.com/socatlolmeow/softty.git
synced 2026-11-03 11:20:21 +00:00
Initial commit
This commit is contained in:
commit
a536cfaadc
8 changed files with 3552 additions and 0 deletions
395
Cargo.lock
generated
Normal file
395
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,395 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ab_glyph"
|
||||||
|
version = "0.2.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2"
|
||||||
|
dependencies = [
|
||||||
|
"ab_glyph_rasterizer",
|
||||||
|
"owned_ttf_parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ab_glyph_rasterizer"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50a649af8a827553c29fb0cb4bd4a6f1a0dd695bd3232b9bc98bd9c8a3ffbb8b"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "downcast-rs"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.190"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memmap2"
|
||||||
|
version = "0.9.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "owned_ttf_parser"
|
||||||
|
version = "0.25.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b"
|
||||||
|
dependencies = [
|
||||||
|
"ttf-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.41.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.16.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "softty"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"ab_glyph",
|
||||||
|
"libc",
|
||||||
|
"memmap2",
|
||||||
|
"serde",
|
||||||
|
"toml",
|
||||||
|
"unicode-width",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-protocols",
|
||||||
|
"xkbcommon",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_write",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ttf-parser"
|
||||||
|
version = "0.25.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wayland-backend"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"downcast-rs",
|
||||||
|
"rustix",
|
||||||
|
"smallvec",
|
||||||
|
"wayland-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wayland-client"
|
||||||
|
version = "0.31.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"rustix",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-scanner",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wayland-protocols"
|
||||||
|
version = "0.32.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-scanner",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wayland-scanner"
|
||||||
|
version = "0.31.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quick-xml",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wayland-sys"
|
||||||
|
version = "0.31.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
|
||||||
|
dependencies = [
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xkbcommon"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"memmap2",
|
||||||
|
"xkeysym",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xkeysym"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||||
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "softty"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.70"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ab_glyph = "0.2"
|
||||||
|
libc = "0.2"
|
||||||
|
memmap2 = "0.9"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
toml = "0.8"
|
||||||
|
wayland-client = "0.31"
|
||||||
|
wayland-protocols = { version = "0.32", features = ["client", "staging", "unstable"] }
|
||||||
|
unicode-width = "0.2"
|
||||||
|
xkbcommon = "0.8"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
390
src/config.rs
Normal file
390
src/config.rs
Normal file
|
|
@ -0,0 +1,390 @@
|
||||||
|
use std::io::{ErrorKind, Write};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
|
use xkbcommon::xkb;
|
||||||
|
use xkbcommon::xkb::keysyms::{KEY_C, KEY_End, KEY_Home, KEY_Insert, KEY_N, KEY_Next, KEY_Prior, KEY_R, KEY_V, KEY_X, KEY_Z};
|
||||||
|
|
||||||
|
pub const SHIFT: u8 = 1;
|
||||||
|
pub const ALT: u8 = 2;
|
||||||
|
pub const CTRL: u8 = 4;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum CursorStyle {
|
||||||
|
Block,
|
||||||
|
Beam,
|
||||||
|
Underline,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Action {
|
||||||
|
Copy,
|
||||||
|
Paste,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
ZoomReset,
|
||||||
|
ScrollLineUp,
|
||||||
|
ScrollLineDown,
|
||||||
|
ScrollPageUp,
|
||||||
|
ScrollPageDown,
|
||||||
|
ScrollTop,
|
||||||
|
ScrollBottom,
|
||||||
|
Search,
|
||||||
|
PrevPrompt,
|
||||||
|
NextPrompt,
|
||||||
|
NewWindow,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
pub struct Config {
|
||||||
|
pub term: String,
|
||||||
|
pub window: Window,
|
||||||
|
pub font: Font,
|
||||||
|
pub mouse: Mouse,
|
||||||
|
pub scrollback: Scrollback,
|
||||||
|
pub colors: Colors,
|
||||||
|
pub cursor: Cursor,
|
||||||
|
pub shortcuts: Vec<Shortcut>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
pub struct Window {
|
||||||
|
pub title: String,
|
||||||
|
pub app_id: String,
|
||||||
|
pub cols: usize,
|
||||||
|
pub rows: usize,
|
||||||
|
pub border: usize,
|
||||||
|
pub alpha: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
pub struct Font {
|
||||||
|
pub family: String,
|
||||||
|
pub size: f32,
|
||||||
|
pub zoom_step: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
pub struct Mouse {
|
||||||
|
pub word_delims: String,
|
||||||
|
pub multi_click_ms: u64,
|
||||||
|
pub scroll_lines: usize,
|
||||||
|
pub url_launcher: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
pub struct Scrollback {
|
||||||
|
pub lines: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
pub struct Colors {
|
||||||
|
#[serde(deserialize_with = "color")]
|
||||||
|
pub foreground: u32,
|
||||||
|
#[serde(deserialize_with = "color")]
|
||||||
|
pub background: u32,
|
||||||
|
#[serde(deserialize_with = "palette")]
|
||||||
|
pub palette: [u32; 16],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
pub struct Cursor {
|
||||||
|
pub style: CursorStyle,
|
||||||
|
#[serde(deserialize_with = "color")]
|
||||||
|
pub color: u32,
|
||||||
|
#[serde(deserialize_with = "color")]
|
||||||
|
pub text: u32,
|
||||||
|
pub thickness: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(try_from = "RawShortcut")]
|
||||||
|
pub struct Shortcut {
|
||||||
|
pub mods: u8,
|
||||||
|
pub sym: u32,
|
||||||
|
pub action: Action,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct RawShortcut {
|
||||||
|
#[serde(default)]
|
||||||
|
mods: Vec<String>,
|
||||||
|
key: String,
|
||||||
|
action: Action,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<RawShortcut> for Shortcut {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(raw: RawShortcut) -> Result<Shortcut, String> {
|
||||||
|
let mut mods = 0;
|
||||||
|
for m in &raw.mods {
|
||||||
|
mods |= match m.to_ascii_lowercase().as_str() {
|
||||||
|
"shift" => SHIFT,
|
||||||
|
"alt" => ALT,
|
||||||
|
"ctrl" | "control" => CTRL,
|
||||||
|
_ => return Err(format!("unknown modifier {m:?}, expected \"shift\", \"alt\" or \"ctrl\"")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Letters arrive as upper case keysyms while shift is held, lower case otherwise.
|
||||||
|
let name = if raw.key.len() == 1 && raw.key.as_bytes()[0].is_ascii_alphabetic() {
|
||||||
|
if mods & SHIFT != 0 { raw.key.to_ascii_uppercase() } else { raw.key.to_ascii_lowercase() }
|
||||||
|
} else {
|
||||||
|
raw.key.clone()
|
||||||
|
};
|
||||||
|
let sym = xkb::keysym_from_name(&name, xkb::KEYSYM_NO_FLAGS).raw();
|
||||||
|
if sym == 0 {
|
||||||
|
return Err(format!("unknown key {:?}, use an xkb keysym name such as \"C\", \"Insert\" or \"Page_Up\"", raw.key));
|
||||||
|
}
|
||||||
|
Ok(Shortcut { mods, sym, action: raw.action })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_color(text: &str) -> Result<u32, String> {
|
||||||
|
let hex = text.trim().trim_start_matches('#').trim_start_matches("0x");
|
||||||
|
match u32::from_str_radix(hex, 16) {
|
||||||
|
Ok(value) if hex.len() == 6 => Ok(value),
|
||||||
|
_ => Err(format!("invalid color {text:?}, expected \"#rrggbb\"")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn color<'de, D: Deserializer<'de>>(d: D) -> Result<u32, D::Error> {
|
||||||
|
parse_color(&String::deserialize(d)?).map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn palette<'de, D: Deserializer<'de>>(d: D) -> Result<[u32; 16], D::Error> {
|
||||||
|
let list = Vec::<String>::deserialize(d)?;
|
||||||
|
if list.len() != 16 {
|
||||||
|
return Err(serde::de::Error::custom(format!("palette needs exactly 16 colors, got {}", list.len())));
|
||||||
|
}
|
||||||
|
let mut out = [0; 16];
|
||||||
|
for (slot, text) in out.iter_mut().zip(&list) {
|
||||||
|
*slot = parse_color(text).map_err(serde::de::Error::custom)?;
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Config {
|
||||||
|
Config {
|
||||||
|
term: "xterm-256color".into(),
|
||||||
|
window: Window::default(),
|
||||||
|
font: Font::default(),
|
||||||
|
mouse: Mouse::default(),
|
||||||
|
scrollback: Scrollback::default(),
|
||||||
|
colors: Colors::default(),
|
||||||
|
cursor: Cursor::default(),
|
||||||
|
shortcuts: vec![
|
||||||
|
Shortcut { mods: CTRL | SHIFT, sym: KEY_C, action: Action::Copy },
|
||||||
|
Shortcut { mods: CTRL | SHIFT, sym: KEY_V, action: Action::Paste },
|
||||||
|
Shortcut { mods: CTRL, sym: KEY_Insert, action: Action::Copy },
|
||||||
|
Shortcut { mods: SHIFT, sym: KEY_Insert, action: Action::Paste },
|
||||||
|
Shortcut { mods: CTRL | SHIFT, sym: KEY_Prior, action: Action::ZoomIn },
|
||||||
|
Shortcut { mods: CTRL | SHIFT, sym: KEY_Next, action: Action::ZoomOut },
|
||||||
|
Shortcut { mods: CTRL | SHIFT, sym: KEY_Home, action: Action::ZoomReset },
|
||||||
|
Shortcut { mods: SHIFT, sym: KEY_Prior, action: Action::ScrollPageUp },
|
||||||
|
Shortcut { mods: SHIFT, sym: KEY_Next, action: Action::ScrollPageDown },
|
||||||
|
Shortcut { mods: SHIFT, sym: KEY_Home, action: Action::ScrollTop },
|
||||||
|
Shortcut { mods: SHIFT, sym: KEY_End, action: Action::ScrollBottom },
|
||||||
|
Shortcut { mods: CTRL | SHIFT, sym: KEY_R, action: Action::Search },
|
||||||
|
Shortcut { mods: CTRL | SHIFT, sym: KEY_Z, action: Action::PrevPrompt },
|
||||||
|
Shortcut { mods: CTRL | SHIFT, sym: KEY_X, action: Action::NextPrompt },
|
||||||
|
Shortcut { mods: CTRL | SHIFT, sym: KEY_N, action: Action::NewWindow },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Window {
|
||||||
|
fn default() -> Window {
|
||||||
|
Window { title: "softty".into(), app_id: "softty".into(), cols: 80, rows: 24, border: 10, alpha: 0.5 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Font {
|
||||||
|
fn default() -> Font {
|
||||||
|
Font { family: "Iosevka Nerd Font".into(), size: 24.0, zoom_step: 2.0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Mouse {
|
||||||
|
fn default() -> Mouse {
|
||||||
|
Mouse { word_delims: " `'\"()[]{}<>|&;,".into(), multi_click_ms: 300, scroll_lines: 3, url_launcher: "xdg-open".into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Scrollback {
|
||||||
|
fn default() -> Scrollback {
|
||||||
|
Scrollback { lines: 10_000 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Colors {
|
||||||
|
fn default() -> Colors {
|
||||||
|
Colors {
|
||||||
|
foreground: 0xa9b1d6,
|
||||||
|
background: 0x1a1b26,
|
||||||
|
palette: [
|
||||||
|
0x15161e, 0xf7768e, 0x9ece6a, 0xe0af68, 0x7aa2f7, 0xbb9af7, 0x7dcfff, 0xa9b1d6,
|
||||||
|
0x414868, 0xf7768e, 0x9ece6a, 0xe0af68, 0x7aa2f7, 0xbb9af7, 0x7dcfff, 0xc0caf5,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Cursor {
|
||||||
|
fn default() -> Cursor {
|
||||||
|
Cursor { style: CursorStyle::Block, color: 0x7aa2f7, text: 0x1a1b26, thickness: 2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Returns the key that is wrong and what is wrong with it.
|
||||||
|
fn check(&self) -> Result<(), (&'static str, &'static str)> {
|
||||||
|
if self.window.cols == 0 {
|
||||||
|
return Err(("cols", "window.cols must be at least 1"));
|
||||||
|
}
|
||||||
|
if self.window.rows == 0 {
|
||||||
|
return Err(("rows", "window.rows must be at least 1"));
|
||||||
|
}
|
||||||
|
if !(self.font.size > 0.0) {
|
||||||
|
return Err(("size", "font.size must be greater than 0"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Written to the config path on first run. Every value in it is a default.
|
||||||
|
const TEMPLATE: &str = include_str!("../config.example.toml");
|
||||||
|
|
||||||
|
static CONFIG: OnceLock<Config> = OnceLock::new();
|
||||||
|
|
||||||
|
/// The active configuration. Falls back to the defaults if `load` has not run.
|
||||||
|
pub fn get() -> &'static Config {
|
||||||
|
CONFIG.get_or_init(Config::default)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `$XDG_CONFIG_HOME/softty/config.toml`, which is `~/.config/softty/config.toml` by default.
|
||||||
|
fn path() -> Option<PathBuf> {
|
||||||
|
let base = match std::env::var_os("XDG_CONFIG_HOME") {
|
||||||
|
Some(dir) if !dir.is_empty() => PathBuf::from(dir),
|
||||||
|
_ => PathBuf::from(std::env::var_os("HOME")?).join(".config"),
|
||||||
|
};
|
||||||
|
Some(base.join("softty").join("config.toml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the config file once at startup. A missing file is created from the template.
|
||||||
|
/// If the file is unreadable or invalid the defaults are used too, and the
|
||||||
|
/// returned message says what is wrong and where, ready to show to the user.
|
||||||
|
pub fn load() -> Option<String> {
|
||||||
|
let (config, error) = match path().map(|p| read(&p)) {
|
||||||
|
Some(Err(message)) => (Config::default(), Some(message)),
|
||||||
|
Some(Ok(config)) => (config, None),
|
||||||
|
None => (Config::default(), None),
|
||||||
|
};
|
||||||
|
let _ = CONFIG.set(config);
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(path: &Path) -> Result<Config, String> {
|
||||||
|
let text = match std::fs::read_to_string(path) {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(e) if e.kind() == ErrorKind::NotFound => {
|
||||||
|
create_default(path);
|
||||||
|
return Ok(Config::default());
|
||||||
|
}
|
||||||
|
Err(e) => return Err(format!("{}: cannot read file: {e}", path.display())),
|
||||||
|
};
|
||||||
|
let config: Config = toml::from_str(&text)
|
||||||
|
.map_err(|e| report(path, &text, e.span().map(|s| s.start), e.message()))?;
|
||||||
|
config
|
||||||
|
.check()
|
||||||
|
.map_err(|(key, message)| report(path, &text, find_key(&text, key), message))?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_default(path: &Path) {
|
||||||
|
if let Some(dir) = path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(dir);
|
||||||
|
}
|
||||||
|
let created = std::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(path)
|
||||||
|
.and_then(|mut file| file.write_all(TEMPLATE.as_bytes()));
|
||||||
|
match created {
|
||||||
|
Err(e) if e.kind() != ErrorKind::AlreadyExists => eprintln!("softty: cannot create {}: {e}", path.display()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Byte offset of the first line that assigns `key`.
|
||||||
|
fn find_key(text: &str, key: &str) -> Option<usize> {
|
||||||
|
let mut start = 0;
|
||||||
|
for line in text.split_inclusive('\n') {
|
||||||
|
let indent = line.len() - line.trim_start().len();
|
||||||
|
if let Some(after) = line[indent..].strip_prefix(key) {
|
||||||
|
if after.trim_start().starts_with('=') {
|
||||||
|
return Some(start + indent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start += line.len();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats `path:line:col: message`, followed by the offending line and a caret under the spot.
|
||||||
|
fn report(path: &Path, text: &str, offset: Option<usize>, message: &str) -> String {
|
||||||
|
let Some(offset) = offset else { return format!("{}: {message}", path.display()) };
|
||||||
|
let offset = offset.min(text.len());
|
||||||
|
let line_no = text[..offset].matches('\n').count() + 1;
|
||||||
|
let line_start = text[..offset].rfind('\n').map_or(0, |i| i + 1);
|
||||||
|
let col = text[line_start..offset].chars().count() + 1;
|
||||||
|
let source = text[line_start..].lines().next().unwrap_or("");
|
||||||
|
let gutter = line_no.to_string();
|
||||||
|
let pad = " ".repeat(gutter.len());
|
||||||
|
format!(
|
||||||
|
"{}:{line_no}:{col}: {message}\n {gutter} | {source}\n {pad} | {}^",
|
||||||
|
path.display(),
|
||||||
|
" ".repeat(col - 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn report_points_at_the_offending_spot() {
|
||||||
|
let text = "a = 1\nbad = 2\n";
|
||||||
|
let message = report(Path::new("c.toml"), text, Some(6), "oops");
|
||||||
|
assert_eq!(message, "c.toml:2:1: oops\n 2 | bad = 2\n | ^");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn template_is_valid_and_matches_defaults() {
|
||||||
|
let config: Config = toml::from_str(TEMPLATE).unwrap();
|
||||||
|
let default = Config::default();
|
||||||
|
assert!(config.check().is_ok());
|
||||||
|
assert_eq!(config.colors.palette, default.colors.palette);
|
||||||
|
assert_eq!(config.mouse.word_delims, default.mouse.word_delims);
|
||||||
|
assert_eq!(config.shortcuts.len(), default.shortcuts.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_key_skips_other_keys() {
|
||||||
|
let text = "[window]\ncolumns = 1\n cols = 0\n";
|
||||||
|
assert_eq!(find_key(text, "cols"), Some(text.find("cols").unwrap()));
|
||||||
|
}
|
||||||
|
}
|
||||||
140
src/font.rs
Normal file
140
src/font.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
use ab_glyph::{point, Font, FontRef, PxScale, ScaleFont};
|
||||||
|
use memmap2::Mmap;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Bitmap {
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub width: usize,
|
||||||
|
pub height: usize,
|
||||||
|
pub alpha: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Fonts {
|
||||||
|
faces: Vec<FontRef<'static>>,
|
||||||
|
paths: HashMap<String, usize>,
|
||||||
|
bold: Option<usize>,
|
||||||
|
chosen: HashMap<(char, bool), usize>,
|
||||||
|
cache: HashMap<(char, bool), Bitmap>,
|
||||||
|
size: f32,
|
||||||
|
pub cell_w: usize,
|
||||||
|
pub cell_h: usize,
|
||||||
|
pub ascent: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_file(pattern: &str) -> Option<String> {
|
||||||
|
let output = Command::new("fc-match").args(["-f", "%{file}", pattern]).output().ok()?;
|
||||||
|
let path = String::from_utf8(output.stdout).ok()?;
|
||||||
|
(!path.is_empty()).then_some(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(path: &str) -> Option<FontRef<'static>> {
|
||||||
|
let file = File::open(path).ok()?;
|
||||||
|
let map: &'static Mmap = Box::leak(Box::new(unsafe { Mmap::map(&file) }.ok()?));
|
||||||
|
FontRef::try_from_slice(map).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn px_scale(font: &FontRef, size: f32) -> PxScale {
|
||||||
|
font.pt_to_px_scale(size * 0.75).expect("font has no units per em")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rasterize(font: &FontRef, ch: char, size: f32, baseline: f32) -> Bitmap {
|
||||||
|
let glyph = font.glyph_id(ch).with_scale_and_position(px_scale(font, size), point(0.0, baseline));
|
||||||
|
let Some(outline) = font.outline_glyph(glyph) else { return Bitmap::default() };
|
||||||
|
let bounds = outline.px_bounds();
|
||||||
|
let (width, height) = (bounds.width() as usize, bounds.height() as usize);
|
||||||
|
let mut alpha = vec![0u8; width * height];
|
||||||
|
outline.draw(|x, y, coverage| alpha[y as usize * width + x as usize] = (coverage.min(1.0) * 255.0) as u8);
|
||||||
|
Bitmap { x: bounds.min.x as i32, y: bounds.min.y as i32, width, height, alpha }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fonts {
|
||||||
|
pub fn new() -> Fonts {
|
||||||
|
let path = match_file(&config::get().font.family).expect("fc-match found no font");
|
||||||
|
let regular = load(&path).unwrap_or_else(|| panic!("cannot load font {path:?}"));
|
||||||
|
let mut fonts = Fonts {
|
||||||
|
faces: vec![regular],
|
||||||
|
paths: HashMap::from([(path, 0)]),
|
||||||
|
bold: None,
|
||||||
|
chosen: HashMap::new(),
|
||||||
|
cache: HashMap::new(),
|
||||||
|
size: config::get().font.size,
|
||||||
|
cell_w: 0,
|
||||||
|
cell_h: 0,
|
||||||
|
ascent: 0,
|
||||||
|
};
|
||||||
|
fonts.measure();
|
||||||
|
fonts
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> f32 {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_size(&mut self, size: f32) {
|
||||||
|
self.size = size.clamp(6.0, 96.0);
|
||||||
|
self.measure();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(&mut self) {
|
||||||
|
self.cache.clear();
|
||||||
|
let regular = &self.faces[0];
|
||||||
|
let scaled = regular.as_scaled(px_scale(regular, self.size));
|
||||||
|
self.ascent = scaled.ascent().round() as usize;
|
||||||
|
self.cell_h = (scaled.ascent() - scaled.descent() + scaled.line_gap()).ceil() as usize;
|
||||||
|
self.cell_w = (scaled.h_advance(regular.glyph_id('M')).round() as usize).max(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn face(&mut self, pattern: &str) -> Option<usize> {
|
||||||
|
let path = match_file(pattern)?;
|
||||||
|
if let Some(&index) = self.paths.get(&path) {
|
||||||
|
return Some(index);
|
||||||
|
}
|
||||||
|
let font = load(&path)?;
|
||||||
|
self.faces.push(font);
|
||||||
|
self.paths.insert(path, self.faces.len() - 1);
|
||||||
|
Some(self.faces.len() - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pick(&mut self, ch: char, bold: bool) -> usize {
|
||||||
|
let primary = if !bold {
|
||||||
|
0
|
||||||
|
} else if let Some(index) = self.bold {
|
||||||
|
index
|
||||||
|
} else {
|
||||||
|
let index = self.face(&format!("{}:bold", config::get().font.family)).unwrap_or(0);
|
||||||
|
self.bold = Some(index);
|
||||||
|
index
|
||||||
|
};
|
||||||
|
if self.faces[primary].glyph_id(ch).0 != 0 {
|
||||||
|
return primary;
|
||||||
|
}
|
||||||
|
let style = if bold { ":bold" } else { "" };
|
||||||
|
let pattern = format!("{}{style}:charset={:x}", config::get().font.family, ch as u32);
|
||||||
|
match self.face(&pattern) {
|
||||||
|
Some(index) if self.faces[index].glyph_id(ch).0 != 0 => index,
|
||||||
|
_ => primary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn glyph(&mut self, ch: char, bold: bool) -> &Bitmap {
|
||||||
|
if !self.cache.contains_key(&(ch, bold)) {
|
||||||
|
let face = match self.chosen.get(&(ch, bold)) {
|
||||||
|
Some(&face) => face,
|
||||||
|
None => {
|
||||||
|
let face = self.pick(ch, bold);
|
||||||
|
self.chosen.insert((ch, bold), face);
|
||||||
|
face
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let bitmap = rasterize(&self.faces[face], ch, self.size, self.ascent as f32);
|
||||||
|
self.cache.insert((ch, bold), bitmap);
|
||||||
|
}
|
||||||
|
&self.cache[&(ch, bold)]
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/keys.rs
Normal file
53
src/keys.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
#![allow(non_upper_case_globals)]
|
||||||
|
|
||||||
|
use xkbcommon::xkb::keysyms::*;
|
||||||
|
|
||||||
|
use crate::config::{ALT, CTRL};
|
||||||
|
|
||||||
|
pub fn encode(sym: u32, text: &str, mods: u8, app_cursor: bool) -> Vec<u8> {
|
||||||
|
let with_mods = |prefix: &str, code: &str, suffix: char| -> Vec<u8> {
|
||||||
|
if mods == 0 {
|
||||||
|
format!("{prefix}{code}{suffix}").into_bytes()
|
||||||
|
} else {
|
||||||
|
format!("\x1b[{};{}{}", if code.is_empty() { "1" } else { code }, mods + 1, suffix).into_bytes()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let cursor = |letter: char| {
|
||||||
|
if app_cursor { with_mods("\x1bO", "", letter) } else { with_mods("\x1b[", "", letter) }
|
||||||
|
};
|
||||||
|
let tilde = |code: &str| with_mods("\x1b[", code, '~');
|
||||||
|
let ss3 = |letter: char| with_mods("\x1bO", "", letter);
|
||||||
|
|
||||||
|
match sym {
|
||||||
|
KEY_Up => cursor('A'),
|
||||||
|
KEY_Down => cursor('B'),
|
||||||
|
KEY_Right => cursor('C'),
|
||||||
|
KEY_Left => cursor('D'),
|
||||||
|
KEY_Home => cursor('H'),
|
||||||
|
KEY_End => cursor('F'),
|
||||||
|
KEY_Insert => tilde("2"),
|
||||||
|
KEY_Delete => tilde("3"),
|
||||||
|
KEY_Prior => tilde("5"),
|
||||||
|
KEY_Next => tilde("6"),
|
||||||
|
KEY_F1 => ss3('P'),
|
||||||
|
KEY_F2 => ss3('Q'),
|
||||||
|
KEY_F3 => ss3('R'),
|
||||||
|
KEY_F4 => ss3('S'),
|
||||||
|
KEY_F5 => tilde("15"),
|
||||||
|
KEY_F6 => tilde("17"),
|
||||||
|
KEY_F7 => tilde("18"),
|
||||||
|
KEY_F8 => tilde("19"),
|
||||||
|
KEY_F9 => tilde("20"),
|
||||||
|
KEY_F10 => tilde("21"),
|
||||||
|
KEY_F11 => tilde("23"),
|
||||||
|
KEY_F12 => tilde("24"),
|
||||||
|
KEY_ISO_Left_Tab => b"\x1b[Z".to_vec(),
|
||||||
|
KEY_BackSpace => {
|
||||||
|
let byte = if mods & CTRL != 0 { 0x08 } else { 0x7f };
|
||||||
|
if mods & ALT != 0 { vec![0x1b, byte] } else { vec![byte] }
|
||||||
|
}
|
||||||
|
_ if text.is_empty() => Vec::new(),
|
||||||
|
_ if mods & ALT != 0 => [b"\x1b".as_slice(), text.as_bytes()].concat(),
|
||||||
|
_ => text.as_bytes().to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
1230
src/main.rs
Normal file
1230
src/main.rs
Normal file
File diff suppressed because it is too large
Load diff
73
src/pty.rs
Normal file
73
src/pty.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::os::fd::RawFd;
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
|
|
||||||
|
pub struct Pty {
|
||||||
|
pub fd: RawFd,
|
||||||
|
pub out: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn winsize(cols: usize, rows: usize) -> libc::winsize {
|
||||||
|
libc::winsize { ws_row: rows as u16, ws_col: cols as u16, ws_xpixel: 0, ws_ypixel: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pty {
|
||||||
|
pub fn spawn(cmd: &[String], cols: usize, rows: usize) -> Pty {
|
||||||
|
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into());
|
||||||
|
let args = if cmd.is_empty() { vec![shell] } else { cmd.to_vec() };
|
||||||
|
let argv: Vec<CString> = args.into_iter().map(|a| CString::new(a).unwrap()).collect();
|
||||||
|
let mut ptrs: Vec<*const libc::c_char> = argv.iter().map(|a| a.as_ptr()).collect();
|
||||||
|
ptrs.push(std::ptr::null());
|
||||||
|
let term = CString::new(config::get().term.as_str()).unwrap();
|
||||||
|
let ws = winsize(cols, rows);
|
||||||
|
let mut fd = 0;
|
||||||
|
|
||||||
|
let pid = unsafe { libc::forkpty(&mut fd, std::ptr::null_mut(), std::ptr::null(), &ws) };
|
||||||
|
if pid < 0 {
|
||||||
|
panic!("forkpty: {}", std::io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
if pid == 0 {
|
||||||
|
unsafe {
|
||||||
|
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
||||||
|
libc::setenv(b"TERM\0".as_ptr().cast(), term.as_ptr(), 1);
|
||||||
|
libc::setenv(b"COLORTERM\0".as_ptr().cast(), b"truecolor\0".as_ptr().cast(), 1);
|
||||||
|
libc::execvp(ptrs[0], ptrs.as_ptr());
|
||||||
|
libc::_exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
let mut attrs: libc::termios = std::mem::zeroed();
|
||||||
|
if libc::tcgetattr(fd, &mut attrs) == 0 {
|
||||||
|
attrs.c_iflag |= libc::IUTF8;
|
||||||
|
libc::tcsetattr(fd, libc::TCSANOW, &attrs);
|
||||||
|
}
|
||||||
|
libc::fcntl(fd, libc::F_SETFL, libc::O_NONBLOCK);
|
||||||
|
}
|
||||||
|
Pty { fd, out: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize(&self, cols: usize, rows: usize) {
|
||||||
|
let ws = winsize(cols, rows);
|
||||||
|
unsafe { libc::ioctl(self.fd, libc::TIOCSWINSZ, &ws) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(&self, buf: &mut [u8]) -> isize {
|
||||||
|
unsafe { libc::read(self.fd, buf.as_mut_ptr().cast(), buf.len()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&mut self, data: &[u8]) {
|
||||||
|
self.out.extend_from_slice(data);
|
||||||
|
self.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flush(&mut self) {
|
||||||
|
while !self.out.is_empty() {
|
||||||
|
let n = unsafe { libc::write(self.fd, self.out.as_ptr().cast(), self.out.len()) };
|
||||||
|
if n <= 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self.out.drain(..n as usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1250
src/term.rs
Normal file
1250
src/term.rs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue