forked from NotAShelf/beer
config: load beer.toml and apply font, geometry, scrollback, word delimiters
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I5008a74307d856f9df472776cb66c8b06a6a6964
This commit is contained in:
parent
9df4e8fb8a
commit
ccc30d1bbd
7 changed files with 324 additions and 29 deletions
121
Cargo.lock
generated
121
Cargo.lock
generated
|
|
@ -41,8 +41,10 @@ dependencies = [
|
||||||
"lru",
|
"lru",
|
||||||
"pound",
|
"pound",
|
||||||
"rustix",
|
"rustix",
|
||||||
|
"serde",
|
||||||
"smithay-client-toolkit",
|
"smithay-client-toolkit",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
|
|
@ -83,6 +85,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7"
|
checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
"nix",
|
||||||
"polling",
|
"polling",
|
||||||
"rustix",
|
"rustix",
|
||||||
"slab",
|
"slab",
|
||||||
|
|
@ -117,6 +120,12 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
@ -229,6 +238,16 @@ version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -308,6 +327,18 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.31.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.50.3"
|
version = "0.50.3"
|
||||||
|
|
@ -426,6 +457,45 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
|
|
@ -523,6 +593,45 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.9.12+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.7.5+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||||
|
dependencies = [
|
||||||
|
"winnow 1.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.1.1+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.44"
|
version = "0.1.44"
|
||||||
|
|
@ -762,6 +871,18 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xcursor"
|
name = "xcursor"
|
||||||
version = "0.3.10"
|
version = "0.3.10"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ readme = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.102"
|
anyhow = "1.0.102"
|
||||||
calloop = "0.14.4"
|
calloop = { version = "0.14.4", features = ["signals"] }
|
||||||
calloop-wayland-source = "0.4.1"
|
calloop-wayland-source = "0.4.1"
|
||||||
fontconfig = "0.11.0"
|
fontconfig = "0.11.0"
|
||||||
freetype-rs = "0.38.0"
|
freetype-rs = "0.38.0"
|
||||||
|
|
@ -22,8 +22,10 @@ rustix = { version = "1.1.4", features = [
|
||||||
"stdio",
|
"stdio",
|
||||||
"fs",
|
"fs",
|
||||||
] }
|
] }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
smithay-client-toolkit = "0.20.0"
|
smithay-client-toolkit = "0.20.0"
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
|
toml = "0.9"
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||||
unicode-width = "0.2.2"
|
unicode-width = "0.2.2"
|
||||||
|
|
|
||||||
131
src/config.rs
Normal file
131
src/config.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
//! User configuration: a TOML file at `$XDG_CONFIG_HOME/beer/beer.toml`
|
||||||
|
//! deserialized into a typed [`Config`]. A missing file uses defaults; a
|
||||||
|
//! malformed one warns and falls back to defaults rather than failing to start.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// Top-level configuration. Unknown keys are ignored so a config written for a
|
||||||
|
/// newer beer still loads.
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
|
#[serde(default, rename_all = "kebab-case")]
|
||||||
|
pub struct Config {
|
||||||
|
pub main: Main,
|
||||||
|
pub scrollback: Scrollback,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `[main]`: fonts, window geometry, padding, and the terminal name.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(default, rename_all = "kebab-case")]
|
||||||
|
pub struct Main {
|
||||||
|
/// Primary font family, resolved via fontconfig.
|
||||||
|
pub font: String,
|
||||||
|
/// Font size in pixels.
|
||||||
|
pub font_size: u32,
|
||||||
|
/// `TERM` value exported to the child shell.
|
||||||
|
pub term: String,
|
||||||
|
/// Initial size in character cells.
|
||||||
|
pub initial_cols: u16,
|
||||||
|
pub initial_rows: u16,
|
||||||
|
/// Characters that break a word for double-click selection. Empty/unset
|
||||||
|
/// keeps the built-in default.
|
||||||
|
pub word_delimiters: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Main {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
font: "monospace".to_string(),
|
||||||
|
font_size: 16,
|
||||||
|
term: "beer".to_string(),
|
||||||
|
initial_cols: 80,
|
||||||
|
initial_rows: 24,
|
||||||
|
word_delimiters: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `[scrollback]`: history retention.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(default, rename_all = "kebab-case")]
|
||||||
|
pub struct Scrollback {
|
||||||
|
/// Lines of history retained for the main screen.
|
||||||
|
pub lines: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Scrollback {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { lines: 10_000 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Load configuration from `explicit` if given, else the default path.
|
||||||
|
/// Any read/parse failure logs a warning and returns defaults.
|
||||||
|
pub fn load(explicit: Option<&Path>) -> Self {
|
||||||
|
let Some(path) = explicit.map(Path::to_path_buf).or_else(default_path) else {
|
||||||
|
return Self::default();
|
||||||
|
};
|
||||||
|
if !path.exists() {
|
||||||
|
return Self::default();
|
||||||
|
}
|
||||||
|
match std::fs::read_to_string(&path) {
|
||||||
|
Ok(text) => match toml::from_str(&text) {
|
||||||
|
Ok(config) => {
|
||||||
|
tracing::info!("loaded config from {}", path.display());
|
||||||
|
config
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!("config {}: {err}; using defaults", path.display());
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!("read config {}: {err}; using defaults", path.display());
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `$XDG_CONFIG_HOME/beer/beer.toml`, or `~/.config/beer/beer.toml`.
|
||||||
|
fn default_path() -> Option<PathBuf> {
|
||||||
|
if let Some(dir) = std::env::var_os("XDG_CONFIG_HOME").filter(|s| !s.is_empty()) {
|
||||||
|
return Some(PathBuf::from(dir).join("beer/beer.toml"));
|
||||||
|
}
|
||||||
|
let home = std::env::var_os("HOME")?;
|
||||||
|
Some(PathBuf::from(home).join(".config/beer/beer.toml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn defaults_are_sane() {
|
||||||
|
let c = Config::default();
|
||||||
|
assert_eq!(c.main.font, "monospace");
|
||||||
|
assert_eq!(c.main.font_size, 16);
|
||||||
|
assert_eq!(c.scrollback.lines, 10_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_partial_config_and_ignores_unknown() {
|
||||||
|
let toml = r##"
|
||||||
|
[main]
|
||||||
|
font = "JetBrains Mono"
|
||||||
|
font-size = 14
|
||||||
|
unknown-key = "tolerated"
|
||||||
|
|
||||||
|
[colors]
|
||||||
|
background = "#000000"
|
||||||
|
"##;
|
||||||
|
let c: Config = toml::from_str(toml).unwrap();
|
||||||
|
assert_eq!(c.main.font, "JetBrains Mono");
|
||||||
|
assert_eq!(c.main.font_size, 14);
|
||||||
|
// Unset keys keep defaults; unknown tables/keys are ignored.
|
||||||
|
assert_eq!(c.main.term, "beer");
|
||||||
|
assert_eq!(c.scrollback.lines, 10_000);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/grid.rs
39
src/grid.rs
|
|
@ -226,6 +226,10 @@ pub struct Grid {
|
||||||
focus_events: bool,
|
focus_events: bool,
|
||||||
/// Active incremental scrollback search, if any.
|
/// Active incremental scrollback search, if any.
|
||||||
search: Option<SearchState>,
|
search: Option<SearchState>,
|
||||||
|
/// Characters that break a word for double-click selection.
|
||||||
|
word_delimiters: String,
|
||||||
|
/// History retention cap for the main screen.
|
||||||
|
scrollback_cap: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_tabs(cols: usize) -> Vec<bool> {
|
fn default_tabs(cols: usize) -> Vec<bool> {
|
||||||
|
|
@ -237,9 +241,9 @@ fn default_tabs(cols: usize) -> Vec<bool> {
|
||||||
/// flags select as one unit.
|
/// flags select as one unit.
|
||||||
const WORD_DELIMITERS: &str = " \t`!@#$%^&*()+=[]{}\\|;'\",<>?";
|
const WORD_DELIMITERS: &str = " \t`!@#$%^&*()+=[]{}\\|;'\",<>?";
|
||||||
|
|
||||||
/// Whether `c` is part of a word (not whitespace, not a delimiter).
|
/// Whether `c` is part of a word (not whitespace, not in `delims`).
|
||||||
fn is_word(c: char) -> bool {
|
fn is_word(c: char, delims: &str) -> bool {
|
||||||
!c.is_whitespace() && !WORD_DELIMITERS.contains(c)
|
!c.is_whitespace() && !delims.contains(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Grid {
|
impl Grid {
|
||||||
|
|
@ -276,9 +280,27 @@ impl Grid {
|
||||||
mouse_encoding: MouseEncoding::X10,
|
mouse_encoding: MouseEncoding::X10,
|
||||||
focus_events: false,
|
focus_events: false,
|
||||||
search: None,
|
search: None,
|
||||||
|
word_delimiters: WORD_DELIMITERS.to_string(),
|
||||||
|
scrollback_cap: SCROLLBACK_CAP,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Override the word-delimiter set; `None` keeps the built-in default.
|
||||||
|
pub fn set_word_delimiters(&mut self, delims: Option<String>) {
|
||||||
|
if let Some(d) = delims {
|
||||||
|
self.word_delimiters = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the scrollback retention cap, trimming history if it shrank.
|
||||||
|
pub fn set_scrollback_cap(&mut self, cap: usize) {
|
||||||
|
self.scrollback_cap = cap;
|
||||||
|
while self.scrollback.len() > cap {
|
||||||
|
self.scrollback.pop_front();
|
||||||
|
}
|
||||||
|
self.view_offset = self.view_offset.min(self.scrollback.len());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cols(&self) -> usize {
|
pub fn cols(&self) -> usize {
|
||||||
self.cols
|
self.cols
|
||||||
}
|
}
|
||||||
|
|
@ -403,7 +425,7 @@ impl Grid {
|
||||||
while live.len() < rows {
|
while live.len() < rows {
|
||||||
live.push(Line::blank(cols));
|
live.push(Line::blank(cols));
|
||||||
}
|
}
|
||||||
while scrollback.len() > SCROLLBACK_CAP {
|
while scrollback.len() > self.scrollback_cap {
|
||||||
scrollback.pop_front();
|
scrollback.pop_front();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -722,7 +744,7 @@ impl Grid {
|
||||||
self.scrollback.push_back(line);
|
self.scrollback.push_back(line);
|
||||||
}
|
}
|
||||||
let mut evicted = 0;
|
let mut evicted = 0;
|
||||||
while self.scrollback.len() > SCROLLBACK_CAP {
|
while self.scrollback.len() > self.scrollback_cap {
|
||||||
self.scrollback.pop_front();
|
self.scrollback.pop_front();
|
||||||
evicted += 1;
|
evicted += 1;
|
||||||
}
|
}
|
||||||
|
|
@ -995,17 +1017,18 @@ impl Grid {
|
||||||
/// Select the word at an absolute point, breaking on whitespace and the
|
/// Select the word at an absolute point, breaking on whitespace and the
|
||||||
/// default delimiter set.
|
/// default delimiter set.
|
||||||
pub fn select_word(&mut self, row: usize, col: usize) {
|
pub fn select_word(&mut self, row: usize, col: usize) {
|
||||||
|
let delims = &self.word_delimiters;
|
||||||
let line = self.abs_row(row);
|
let line = self.abs_row(row);
|
||||||
if col >= line.len() || !is_word(line[col].c) {
|
if col >= line.len() || !is_word(line[col].c, delims) {
|
||||||
self.start_selection(row, col);
|
self.start_selection(row, col);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut lo = col;
|
let mut lo = col;
|
||||||
while lo > 0 && is_word(line[lo - 1].c) {
|
while lo > 0 && is_word(line[lo - 1].c, delims) {
|
||||||
lo -= 1;
|
lo -= 1;
|
||||||
}
|
}
|
||||||
let mut hi = col;
|
let mut hi = col;
|
||||||
while hi + 1 < line.len() && is_word(line[hi + 1].c) {
|
while hi + 1 < line.len() && is_word(line[hi + 1].c, delims) {
|
||||||
hi += 1;
|
hi += 1;
|
||||||
}
|
}
|
||||||
self.selection = Some((Point { row, col: lo }, Point { row, col: hi }));
|
self.selection = Some((Point { row, col: lo }, Point { row, col: hi }));
|
||||||
|
|
|
||||||
10
src/main.rs
10
src/main.rs
|
|
@ -1,5 +1,6 @@
|
||||||
//! beer, a fast, software-rendered, Wayland-native terminal emulator.
|
//! beer, a fast, software-rendered, Wayland-native terminal emulator.
|
||||||
|
|
||||||
|
mod config;
|
||||||
mod font;
|
mod font;
|
||||||
mod grid;
|
mod grid;
|
||||||
mod input;
|
mod input;
|
||||||
|
|
@ -8,10 +9,13 @@ mod render;
|
||||||
mod vt;
|
mod vt;
|
||||||
mod wayland;
|
mod wayland;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use pound::Parse;
|
use pound::Parse;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
/// A fast, software-rendered, Wayland-native terminal emulator.
|
/// A fast, software-rendered, Wayland-native terminal emulator.
|
||||||
#[derive(Parse)]
|
#[derive(Parse)]
|
||||||
#[pound(name = "beer", version = "0.0.0")]
|
#[pound(name = "beer", version = "0.0.0")]
|
||||||
|
|
@ -19,6 +23,9 @@ struct Cli {
|
||||||
/// Run as a daemon hosting multiple windows.
|
/// Run as a daemon hosting multiple windows.
|
||||||
#[pound(long)]
|
#[pound(long)]
|
||||||
server: bool,
|
server: bool,
|
||||||
|
/// Path to a config file (default: $XDG_CONFIG_HOME/beer/beer.toml).
|
||||||
|
#[pound(long)]
|
||||||
|
config: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
|
|
@ -51,6 +58,7 @@ fn run(cli: Cli) -> anyhow::Result<ExitCode> {
|
||||||
anyhow::bail!("server mode is not implemented yet");
|
anyhow::bail!("server mode is not implemented yet");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let config = Config::load(cli.config.as_deref());
|
||||||
tracing::info!("starting beer");
|
tracing::info!("starting beer");
|
||||||
wayland::run()
|
wayland::run(config)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ pub struct Pty {
|
||||||
|
|
||||||
impl Pty {
|
impl Pty {
|
||||||
/// Open a PTY, size it to `cols`x`rows`, and exec the user's login shell on
|
/// Open a PTY, size it to `cols`x`rows`, and exec the user's login shell on
|
||||||
/// the slave end with `TERM=beer`.
|
/// the slave end with `TERM=term`.
|
||||||
pub fn spawn(cols: u16, rows: u16) -> anyhow::Result<Self> {
|
pub fn spawn(cols: u16, rows: u16, term: &str) -> anyhow::Result<Self> {
|
||||||
let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)
|
let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)
|
||||||
.context("open pty master")?;
|
.context("open pty master")?;
|
||||||
grantpt(&master).context("grantpt")?;
|
grantpt(&master).context("grantpt")?;
|
||||||
|
|
@ -45,7 +45,7 @@ impl Pty {
|
||||||
|
|
||||||
let mut cmd = Command::new(&shell);
|
let mut cmd = Command::new(&shell);
|
||||||
cmd.arg0(&argv0)
|
cmd.arg0(&argv0)
|
||||||
.env("TERM", "beer")
|
.env("TERM", term)
|
||||||
.env_remove("COLUMNS")
|
.env_remove("COLUMNS")
|
||||||
.env_remove("LINES")
|
.env_remove("LINES")
|
||||||
.env_remove("TERMCAP")
|
.env_remove("TERMCAP")
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ use calloop::timer::{TimeoutAction, Timer};
|
||||||
use calloop::{EventLoop, Interest, LoopHandle, Mode, PostAction, RegistrationToken};
|
use calloop::{EventLoop, Interest, LoopHandle, Mode, PostAction, RegistrationToken};
|
||||||
use calloop_wayland_source::WaylandSource;
|
use calloop_wayland_source::WaylandSource;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
use crate::font::Fonts;
|
use crate::font::Fonts;
|
||||||
use crate::grid::{Cell, CursorShape, Grid, MouseProtocol};
|
use crate::grid::{Cell, CursorShape, Grid, MouseProtocol};
|
||||||
use crate::pty::Pty;
|
use crate::pty::Pty;
|
||||||
|
|
@ -161,15 +162,12 @@ fn row_snap(grid: &Grid, y: usize, focused: bool, blink_on: bool) -> RowSnap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default window size in pixels before the compositor suggests one.
|
/// Fallback window size in pixels if the configured geometry yields nothing.
|
||||||
const DEFAULT_W: u32 = 800;
|
const DEFAULT_W: u32 = 800;
|
||||||
const DEFAULT_H: u32 = 600;
|
const DEFAULT_H: u32 = 600;
|
||||||
/// Primary font family and pixel size, resolved via fontconfig.
|
|
||||||
const FONT_FAMILY: &str = "monospace";
|
|
||||||
const FONT_SIZE_PX: u32 = 16;
|
|
||||||
|
|
||||||
/// Run a single window until it is closed, returning the shell's exit code.
|
/// Run a single window until it is closed, returning the shell's exit code.
|
||||||
pub fn run() -> anyhow::Result<ExitCode> {
|
pub fn run(config: Config) -> anyhow::Result<ExitCode> {
|
||||||
let conn = Connection::connect_to_env().context("connect to Wayland compositor")?;
|
let conn = Connection::connect_to_env().context("connect to Wayland compositor")?;
|
||||||
let (globals, event_queue) =
|
let (globals, event_queue) =
|
||||||
registry_queue_init(&conn).context("initialize Wayland registry")?;
|
registry_queue_init(&conn).context("initialize Wayland registry")?;
|
||||||
|
|
@ -197,12 +195,20 @@ pub fn run() -> anyhow::Result<ExitCode> {
|
||||||
// First commit with no buffer kicks off the initial configure.
|
// First commit with no buffer kicks off the initial configure.
|
||||||
window.commit();
|
window.commit();
|
||||||
|
|
||||||
let pool = SlotPool::new(DEFAULT_W as usize * DEFAULT_H as usize * 4, &shm)
|
let fonts = Fonts::new(&config.main.font, config.main.font_size).context("load font")?;
|
||||||
.context("create shm slot pool")?;
|
|
||||||
|
|
||||||
let fonts = Fonts::new(FONT_FAMILY, FONT_SIZE_PX).context("load font")?;
|
|
||||||
let renderer = Renderer::new(fonts);
|
let renderer = Renderer::new(fonts);
|
||||||
|
|
||||||
|
// Start at the configured cell geometry; the compositor may override it on
|
||||||
|
// the first configure.
|
||||||
|
let m = renderer.metrics();
|
||||||
|
let width = (u32::from(config.main.initial_cols) * m.width).max(1);
|
||||||
|
let height = (u32::from(config.main.initial_rows) * m.height).max(1);
|
||||||
|
let pool = SlotPool::new(
|
||||||
|
(width * height * 4).max(DEFAULT_W * DEFAULT_H) as usize,
|
||||||
|
&shm,
|
||||||
|
)
|
||||||
|
.context("create shm slot pool")?;
|
||||||
|
|
||||||
let mut app = App {
|
let mut app = App {
|
||||||
registry_state: RegistryState::new(&globals),
|
registry_state: RegistryState::new(&globals),
|
||||||
output_state: OutputState::new(&globals, &qh),
|
output_state: OutputState::new(&globals, &qh),
|
||||||
|
|
@ -239,8 +245,9 @@ pub fn run() -> anyhow::Result<ExitCode> {
|
||||||
// startup SIGWINCH storm that makes it reprint its prompt.
|
// startup SIGWINCH storm that makes it reprint its prompt.
|
||||||
session: None,
|
session: None,
|
||||||
title: None,
|
title: None,
|
||||||
width: DEFAULT_W,
|
config,
|
||||||
height: DEFAULT_H,
|
width,
|
||||||
|
height,
|
||||||
needs_draw: false,
|
needs_draw: false,
|
||||||
frame_pending: false,
|
frame_pending: false,
|
||||||
frames: Vec::new(),
|
frames: Vec::new(),
|
||||||
|
|
@ -352,6 +359,8 @@ struct App {
|
||||||
session: Option<Session>,
|
session: Option<Session>,
|
||||||
/// Last title applied to the toplevel, to avoid redundant requests.
|
/// Last title applied to the toplevel, to avoid redundant requests.
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
|
/// The active user configuration.
|
||||||
|
config: Config,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
/// The grid changed and the window wants repainting on the next frame.
|
/// The grid changed and the window wants repainting on the next frame.
|
||||||
|
|
@ -379,7 +388,7 @@ impl App {
|
||||||
/// Spawn the shell at the current window size and start reading its output.
|
/// Spawn the shell at the current window size and start reading its output.
|
||||||
fn spawn_session(&mut self) {
|
fn spawn_session(&mut self) {
|
||||||
let (cols, rows) = grid_size(self.renderer.metrics(), self.width, self.height);
|
let (cols, rows) = grid_size(self.renderer.metrics(), self.width, self.height);
|
||||||
let pty = match Pty::spawn(cols, rows) {
|
let pty = match Pty::spawn(cols, rows, &self.config.main.term) {
|
||||||
Ok(pty) => pty,
|
Ok(pty) => pty,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::error!("spawn shell: {err:#}");
|
tracing::error!("spawn shell: {err:#}");
|
||||||
|
|
@ -428,10 +437,11 @@ impl App {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.session = Some(Session {
|
let mut term = Term::new(cols as usize, rows as usize);
|
||||||
pty,
|
let grid = term.grid_mut();
|
||||||
term: Term::new(cols as usize, rows as usize),
|
grid.set_word_delimiters(self.config.main.word_delimiters.clone());
|
||||||
});
|
grid.set_scrollback_cap(self.config.scrollback.lines);
|
||||||
|
self.session = Some(Session { pty, term });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a key (initial press or repeat): Shift+PageUp/PageDown scroll the
|
/// Handle a key (initial press or repeat): Shift+PageUp/PageDown scroll the
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue