Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 74 additions & 65 deletions lisp/ghostel.el
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,15 @@ or backing scale factor."
:type '(choice (const :tag "Auto-detect from display DPI" auto)
(number :tag "Explicit ratio")))

(defcustom ghostel-glyph-scale-floor 0.0
"Minimum scale for glyphs whose font metrics don't fit the cell.
0.0 (default) preserves strict grid alignment. 1.0 disables
shrinking entirely so CJK and other fallback glyphs render at
natural size, potentially making rows slightly taller and cells slightly wider."
:type '(float 0.0 1.0)
:local t
:group 'ghostel)

(defcustom ghostel-kitty-graphics-storage-limit (* 320 1024 1024) ; 320 MiB
"Kitty graphics image storage cap, in bytes, per terminal.

Expand Down Expand Up @@ -4185,8 +4194,8 @@ mis-rendering is visible as an error instead of silent."
(list src-x src-y src-w src-h pixel-w pixel-h))))

(defun ghostel--kitty-apply-row-slice (row cw ch img
vp-col-clamped visible-cols
slice-x slice-w)
vp-col-clamped visible-cols
slice-x slice-w)
"Apply one row of the sliced image at point.
ROW is the slice index (0-based) into the image's grid of cells.
CW / CH are the cell pixel dimensions. IMG is the Emacs image object.
Expand Down Expand Up @@ -5755,69 +5764,69 @@ Returns nil on failure."
(tinfo (and (ghostel--ssh-install-enabled-p)
(ghostel--push-remote-terminfo remote-prefix)))
(base (pcase shell-type
;; Bash: --rcfile replaces normal rc loading, so we source
;; startup files explicitly before the integration.
('bash
(let* ((temp (make-temp-file
(concat remote-prefix "ghostel-") nil ".bash"))
(path (file-remote-p temp 'localname)))
(ghostel--write-remote-file temp
(concat
"# Source standard startup files\n"
"if shopt -q login_shell 2>/dev/null; then\n"
" [ -r /etc/profile ] && . /etc/profile\n"
" for __gf in ~/.bash_profile ~/.bash_login ~/.profile; do\n"
" [ -r \"$__gf\" ] && { . \"$__gf\"; break; }; done\n"
" unset __gf\n"
"else\n"
" for __gf in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do\n"
" [ -r \"$__gf\" ] && { . \"$__gf\"; break; }; done\n"
" unset __gf\n"
" [ -r ~/.bashrc ] && . ~/.bashrc\n"
"fi\n"
integration))
(list :env nil :args (list "--rcfile" path)
:stty ghostel--default-stty :temp-files (list temp))))
;; Zsh: ZDOTDIR replaces .zshenv search, so we restore it,
;; source the user's .zshenv, then load integration.
('zsh
(let* ((temp-dir (make-temp-file
(concat remote-prefix "ghostel-") t))
(temp-zshenv (concat (file-name-as-directory temp-dir)
".zshenv"))
(remote-dir (file-remote-p temp-dir 'localname)))
(ghostel--write-remote-file temp-zshenv
(concat
"if [[ -n \"${GHOSTEL_ZSH_ZDOTDIR+X}\" ]]; then\n"
" 'builtin' 'export' ZDOTDIR=\"$GHOSTEL_ZSH_ZDOTDIR\"\n"
" 'builtin' 'unset' 'GHOSTEL_ZSH_ZDOTDIR'\n"
"else\n"
" 'builtin' 'unset' 'ZDOTDIR'\n"
"fi\n"
"{\n"
" 'builtin' 'typeset' _ghostel_file="
"\"${ZDOTDIR-$HOME}/.zshenv\"\n"
" [[ ! -r \"$_ghostel_file\" ]] || "
"'builtin' 'source' '--' \"$_ghostel_file\"\n"
"} always {\n"
" if [[ -o 'interactive' ]]; then\n"
integration "\n"
" fi\n"
" 'builtin' 'unset' '_ghostel_file'\n"
"}\n"))
(list :env (list (format "ZDOTDIR=%s" remote-dir))
:args nil :stty ghostel--default-stty
:temp-dirs (list temp-dir))))
;; Fish: -C runs after config, so just source the script.
('fish
(let* ((temp (make-temp-file
(concat remote-prefix "ghostel-") nil ".fish"))
(path (file-remote-p temp 'localname)))
(ghostel--write-remote-file temp integration)
(list :env nil
:args (list "-C" (format "source %s"
(shell-quote-argument path)))
:stty ghostel--default-stty :temp-files (list temp)))))))
;; Bash: --rcfile replaces normal rc loading, so we source
;; startup files explicitly before the integration.
('bash
(let* ((temp (make-temp-file
(concat remote-prefix "ghostel-") nil ".bash"))
(path (file-remote-p temp 'localname)))
(ghostel--write-remote-file temp
(concat
"# Source standard startup files\n"
"if shopt -q login_shell 2>/dev/null; then\n"
" [ -r /etc/profile ] && . /etc/profile\n"
" for __gf in ~/.bash_profile ~/.bash_login ~/.profile; do\n"
" [ -r \"$__gf\" ] && { . \"$__gf\"; break; }; done\n"
" unset __gf\n"
"else\n"
" for __gf in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do\n"
" [ -r \"$__gf\" ] && { . \"$__gf\"; break; }; done\n"
" unset __gf\n"
" [ -r ~/.bashrc ] && . ~/.bashrc\n"
"fi\n"
integration))
(list :env nil :args (list "--rcfile" path)
:stty ghostel--default-stty :temp-files (list temp))))
;; Zsh: ZDOTDIR replaces .zshenv search, so we restore it,
;; source the user's .zshenv, then load integration.
('zsh
(let* ((temp-dir (make-temp-file
(concat remote-prefix "ghostel-") t))
(temp-zshenv (concat (file-name-as-directory temp-dir)
".zshenv"))
(remote-dir (file-remote-p temp-dir 'localname)))
(ghostel--write-remote-file temp-zshenv
(concat
"if [[ -n \"${GHOSTEL_ZSH_ZDOTDIR+X}\" ]]; then\n"
" 'builtin' 'export' ZDOTDIR=\"$GHOSTEL_ZSH_ZDOTDIR\"\n"
" 'builtin' 'unset' 'GHOSTEL_ZSH_ZDOTDIR'\n"
"else\n"
" 'builtin' 'unset' 'ZDOTDIR'\n"
"fi\n"
"{\n"
" 'builtin' 'typeset' _ghostel_file="
"\"${ZDOTDIR-$HOME}/.zshenv\"\n"
" [[ ! -r \"$_ghostel_file\" ]] || "
"'builtin' 'source' '--' \"$_ghostel_file\"\n"
"} always {\n"
" if [[ -o 'interactive' ]]; then\n"
integration "\n"
" fi\n"
" 'builtin' 'unset' '_ghostel_file'\n"
"}\n"))
(list :env (list (format "ZDOTDIR=%s" remote-dir))
:args nil :stty ghostel--default-stty
:temp-dirs (list temp-dir))))
;; Fish: -C runs after config, so just source the script.
('fish
(let* ((temp (make-temp-file
(concat remote-prefix "ghostel-") nil ".fish"))
(path (file-remote-p temp 'localname)))
(ghostel--write-remote-file temp integration)
(list :env nil
:args (list "-C" (format "source %s"
(shell-quote-argument path)))
:stty ghostel--default-stty :temp-files (list temp)))))))
(if tinfo
(ghostel--merge-integration-plists base tinfo)
base))
Expand Down
42 changes: 33 additions & 9 deletions src/Renderer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ pending_resize: ?ViewportSize = null,
/// Reusable instance of RowContent to reduce allocations
row: RowContent = .{},

/// Cached information about font metrics, used for glyph scaling
/// Cached font metrics and rendering parameters that affect glyph layout.
/// When any field changes between redraws the viewport is fully invalidated.
font_info: ?FontInfo = null,

/// Bold text coloring configuration.
Expand All @@ -40,6 +41,7 @@ const FontInfo = struct {
width: i64,
height: i64,
coverage: u32,
glyph_scale_floor: f64,
};

pub fn init(term: *gt.Terminal) !Self {
Expand Down Expand Up @@ -106,8 +108,9 @@ pub fn redraw(self: *Self, env: emacs.Env, term: *Terminal, force_full_arg: bool

const scrollbar = term.terminal.screens.active.pages.scrollbar();

// If the font changed, the font metrics are no longer valid, so we rebuild.
const font_changed = self.updateFontInfo(env);
// If the font metrics or related parameters changed, the cached metrics
// are no longer valid, so we rebuild.
const font_info_changed = self.updateFontInfo(env);

// We always reset scrollback if the number of columns changed
const cols_changed = if (self.pending_resize) |rz| rz.cols != self.size.cols else false;
Expand All @@ -123,14 +126,17 @@ pub fn redraw(self: *Self, env: emacs.Env, term: *Terminal, force_full_arg: bool
// cap and do not know how much we missed.
const scrollbar_hit_cap = had_scrollback and scrollbar.offset == 0;

var force_full = false;
if (force_full_arg or font_changed or cols_changed or scrollbar_reset or scrollbar_hit_cap) {
const force_full =
force_full_arg or
font_info_changed or
cols_changed or
scrollbar_reset or
scrollbar_hit_cap;
if (force_full) {
env.eraseBuffer();
// Commit any pending resize since we're doing a rebuild anyway.
try self.commitResize(&term.terminal);

self.rows_in_buffer = 0;
force_full = true;
}

// Unpark the viewport. When we have scrollback the viewport is sitting at
Expand Down Expand Up @@ -194,10 +200,26 @@ pub fn redraw(self: *Self, env: emacs.Env, term: *Terminal, force_full_arg: bool
term.terminal.scrollViewport(.{ .delta = -1 });
}

/// Read the default font and rendering parameters from Emacs, compare
/// against the cached values, and signal whether a full invalidation is
/// required.
fn updateFontInfo(self: *Self, env: emacs.Env) bool {
const new_font = getDefaultFont(env);
const current_font = env.symbolValue("ghostel--rendered-font");
if (env.eq(new_font, current_font)) return false;

const raw_floor = env.symbolValue("ghostel-glyph-scale-floor");
const floor = std.math.clamp(env.asFloat(raw_floor, 0.0), 0.0, 1.0);

// Fast path: nothing changed since last redraw.
if (env.eq(new_font, current_font)) {
if (self.font_info) |cached| {
const old_bits: u64 = @bitCast(cached.glyph_scale_floor);
const new_bits: u64 = @bitCast(floor);
if (old_bits == new_bits) return false;
} else {
return false; // no font before, no font now
}
}

_ = env.set("ghostel--rendered-font", new_font);

Expand All @@ -215,6 +237,7 @@ fn updateFontInfo(self: *Self, env: emacs.Env) bool {
.width = env.extractInteger(env.vecGet(default_font_info, 6)),
.height = cell_ascent + cell_descent,
.coverage = probeCoverage(env, new_font),
.glyph_scale_floor = floor,
};
}
return true;
Expand Down Expand Up @@ -697,7 +720,8 @@ fn adjustGlyph(
// We add a fudge factor of +1 to the denominator to ensure fit
const scale_width = @as(f64, @floatFromInt(slot_width)) / @as(f64, @floatFromInt(width + 1));
const scale_height = @as(f64, @floatFromInt(default_font_info.height)) / @as(f64, @floatFromInt(height + 1));
const scale = @min(scale_width, scale_height);
const computed_scale = @min(scale_width, scale_height);
const scale = @max(computed_scale, default_font_info.glyph_scale_floor);

const min_width_spec = env.list(.{ s.@"min-width", env.list(.{char_width}) });
const scale_spec = env.list(.{ s.height, scale });
Expand Down
11 changes: 11 additions & 0 deletions src/emacs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ pub const Env = struct {
return self.raw.extract_float.?(self.raw, val);
}

/// Extract val as f64, coercing Emacs integers to floats.
/// Returns default if val is not a number, avoiding a wrong-type-argument
/// signal that would corrupt the env's non-local exit state.
pub fn asFloat(self: Env, val: Value, default: f64) f64 {
if (self.isNil(self.f("numberp", .{val}))) return default;
return self.extractFloat(self.f("float", .{val}));
}

pub fn extractString(self: Env, val: Value, buf: []u8) ?[]const u8 {
var len: isize = @intCast(buf.len);
if (self.raw.copy_string_contents.?(self.raw, val, buf.ptr, &len)) {
Expand Down Expand Up @@ -414,6 +422,7 @@ const interned_symbols = [_][:0]const u8{
"error",
"face",
"face-attribute",
"float",
"font-at",
"font-get-glyphs",
"font-has-char-p",
Expand All @@ -439,6 +448,7 @@ const interned_symbols = [_][:0]const u8{
"ghostel--osc52-handle",
"ghostel--query-font-cached",
"ghostel--rendered-font",
"ghostel-glyph-scale-floor",
"ghostel--set-buffer-face",
"ghostel--set-cursor-style",
"ghostel--set-title",
Expand Down Expand Up @@ -466,6 +476,7 @@ const interned_symbols = [_][:0]const u8{
"mouse-face",
"move-to-column",
"nil",
"numberp",
"point",
"point-max",
"point-min",
Expand Down
35 changes: 35 additions & 0 deletions test/ghostel-glyph-kitty-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,41 @@ the bright variant just like in `bright' mode."
(should (equal min-w '(1)))))))))
(kill-buffer buf))))

(ert-deftest ghostel-test-glyph-scale-floor-clamps-scale ()
"A non-zero `ghostel-glyph-scale-floor' prevents shrinking below the floor.
Sets floor to 1.0 and feeds a glyph larger than the cell. With floor
0.0 the glyph would be scaled to ~0.8; with floor 1.0 it stays at 1.0."
:tags '(native)
(let ((buf (generate-new-buffer " *ghostel-test-glyph-floor*")))
(unwind-protect
(save-window-excursion
(with-selected-window (display-buffer buf)
(ghostel-mode)
(let* ((term (ghostel--new 5 80 1000))
(ghostel--term term)
(ghostel--term-rows 5)
(ghostel-glyph-scale-floor 1.0) ; clamp: never shrink
(inhibit-read-only t)
(df (ghostel-test--make-font ghostel-test--default-font-info))
;; Glyph: 12px wide x 25px tall (larger than 10x20 cell);
;; without floor this would scale to ~0.8.
(glyph-font (ghostel-test--make-font
["MockGlyph" "mock.ttf" 12 120 12 13 12 12 0]
[[0 1 ?\u0100 0 12 0 0 12 13 0]])))
(ghostel--write-input term "\u0100")
(ghostel-test--with-glyph-mocks
(:default-font df
:glyph-font glyph-font)
(ghostel--redraw term t)
(goto-char (point-min))
(let ((disp (get-text-property (point) 'display)))
(should disp)
(let ((scale (cadr (assq 'height disp))))
(should scale)
;; Floor 1.0 clamps the scale so the glyph is NOT shrunk.
(should (>= scale 1.0))))))))
(kill-buffer buf))))

(ert-deftest ghostel-test-glyph-adjust-covered-by-main-font ()
"A codepoint below the coverage threshold is not registered in adjust_cells."
:tags '(native)
Expand Down
Loading