Merge branch 'ghostty-org:main' into deb-package

This commit is contained in:
Ronit Gandhi
2024-12-31 11:07:42 +05:30
committed by GitHub
23 changed files with 384 additions and 179 deletions

View File

@ -45,6 +45,11 @@ class BaseTerminalController: NSWindowController,
didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) }
}
/// Whether the terminal surface should focus when the mouse is over it.
var focusFollowsMouse: Bool {
self.derivedConfig.focusFollowsMouse
}
/// Non-nil when an alert is active so we don't overlap multiple.
private var alert: NSAlert? = nil
@ -106,8 +111,8 @@ class BaseTerminalController: NSWindowController,
// Listen for local events that we need to know of outside of
// single surface handlers.
self.eventMonitor = NSEvent.addLocalMonitorForEvents(
matching: [.flagsChanged],
handler: localEventHandler)
matching: [.flagsChanged]
) { [weak self] event in self?.localEventHandler(event) }
}
deinit {
@ -155,7 +160,7 @@ class BaseTerminalController: NSWindowController,
}
// MARK: Notifications
@objc private func didChangeScreenParametersNotification(_ notification: Notification) {
// If we have a window that is visible and it is outside the bounds of the
// screen then we clamp it back to within the screen.
@ -262,7 +267,6 @@ class BaseTerminalController: NSWindowController,
// Set the main window title
window.title = to
}
func pwdDidChange(to: URL?) {
@ -604,15 +608,18 @@ class BaseTerminalController: NSWindowController,
private struct DerivedConfig {
let macosTitlebarProxyIcon: Ghostty.MacOSTitlebarProxyIcon
let windowStepResize: Bool
let focusFollowsMouse: Bool
init() {
self.macosTitlebarProxyIcon = .visible
self.windowStepResize = false
self.focusFollowsMouse = false
}
init(_ config: Ghostty.Config) {
self.macosTitlebarProxyIcon = config.macosTitlebarProxyIcon
self.windowStepResize = config.windowStepResize
self.focusFollowsMouse = config.focusFollowsMouse
}
}
}

View File

@ -117,9 +117,6 @@ class TerminalController: BaseTerminalController {
// Update our derived config
self.derivedConfig = DerivedConfig(config)
guard let window = window as? TerminalWindow else { return }
window.focusFollowsMouse = config.focusFollowsMouse
// If we have no surfaces in our window (is that possible?) then we update
// our window appearance based on the root config. If we have surfaces, we
// don't call this because the TODO
@ -247,7 +244,7 @@ class TerminalController: BaseTerminalController {
let backgroundColor: OSColor
if let surfaceTree {
if let focusedSurface, surfaceTree.doesBorderTop(view: focusedSurface) {
backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor)
backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor).withAlphaComponent(0.0)
} else {
// We don't have a focused surface or our surface doesn't border the
// top. We choose to match the color of the top-left most surface.
@ -422,8 +419,6 @@ class TerminalController: BaseTerminalController {
}
}
window.focusFollowsMouse = config.focusFollowsMouse
// Apply any additional appearance-related properties to the new window. We
// apply this based on the root config but change it later based on surface
// config (see focused surface change callback).

View File

@ -414,8 +414,6 @@ class TerminalWindow: NSWindow {
}
}
var focusFollowsMouse: Bool = false
// Find the NSTextField responsible for displaying the titlebar's title.
private var titlebarTextField: NSTextField? {
guard let titlebarView = titlebarContainer?.subviews

View File

@ -617,11 +617,12 @@ extension Ghostty {
let mods = Ghostty.ghosttyMods(event.modifierFlags)
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods)
// If focus follows mouse is enabled then move focus to this surface.
if let window = self.window as? TerminalWindow,
window.isKeyWindow &&
window.focusFollowsMouse &&
!self.focused
// Handle focus-follows-mouse
if let window,
let controller = window.windowController as? BaseTerminalController,
(window.isKeyWindow &&
!self.focused &&
controller.focusFollowsMouse)
{
Ghostty.moveFocus(to: self)
}

View File

@ -853,11 +853,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
},
.color_change => |change| {
// On any color change, we have to report for mode 2031
// if it is enabled.
self.reportColorScheme(false);
// Notify our apprt
// Notify our apprt, but don't send a mode 2031 DSR report
// because VT sequences were used to change the color.
try self.rt_app.performAction(
.{ .surface = self },
.color_change,
@ -4293,11 +4290,16 @@ fn writeScreenFile(
tmp_dir.deinit();
return;
};
// Use topLeft and bottomRight to ensure correct coordinate ordering
const tl = sel.topLeft(&self.io.terminal.screen);
const br = sel.bottomRight(&self.io.terminal.screen);
try self.io.terminal.screen.dumpString(
buf_writer.writer(),
.{
.tl = sel.start(),
.br = sel.end(),
.tl = tl,
.br = br,
.unwrap = true,
},
);

View File

@ -1400,7 +1400,15 @@ pub fn getColorScheme(self: *App) apprt.ColorScheme {
null,
&err,
) orelse {
if (err) |e| log.err("unable to get current color scheme: {s}", .{e.message});
if (err) |e| {
// If ReadOne is not yet implemented, fall back to deprecated "Read" method
// Error code: GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod: No such method ReadOne
if (e.code == 19) {
return self.getColorSchemeDeprecated();
}
// Otherwise, log the error and return .light
log.err("unable to get current color scheme: {s}", .{e.message});
}
return .light;
};
defer c.g_variant_unref(value);
@ -1417,6 +1425,49 @@ pub fn getColorScheme(self: *App) apprt.ColorScheme {
return .light;
}
/// Call the deprecated D-Bus "Read" method to determine the current color scheme. If
/// there is any error at any point we'll log the error and return "light"
fn getColorSchemeDeprecated(self: *App) apprt.ColorScheme {
const dbus_connection = c.g_application_get_dbus_connection(@ptrCast(self.app));
var err: ?*c.GError = null;
defer if (err) |e| c.g_error_free(e);
const value = c.g_dbus_connection_call_sync(
dbus_connection,
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.Settings",
"Read",
c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
c.G_VARIANT_TYPE("(v)"),
c.G_DBUS_CALL_FLAGS_NONE,
-1,
null,
&err,
) orelse {
if (err) |e| log.err("Read method failed: {s}", .{e.message});
return .light;
};
defer c.g_variant_unref(value);
if (c.g_variant_is_of_type(value, c.G_VARIANT_TYPE("(v)")) == 1) {
var inner: ?*c.GVariant = null;
c.g_variant_get(value, "(v)", &inner);
defer if (inner) |i| c.g_variant_unref(i);
if (inner) |i| {
const child = c.g_variant_get_child_value(i, 0) orelse {
return .light;
};
defer c.g_variant_unref(child);
const val = c.g_variant_get_uint32(child);
return if (val == 1) .dark else .light;
}
}
return .light;
}
/// This will be called by D-Bus when the style changes between light & dark.
fn gtkNotifyColorScheme(
_: ?*c.GDBusConnection,

View File

@ -238,7 +238,7 @@ fn promptText(req: apprt.ClipboardRequest) [:0]const u8 {
\\Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.
,
.osc_52_read =>
\\An appliclication is attempting to read from the clipboard.
\\An application is attempting to read from the clipboard.
\\The current clipboard contents are shown below.
,
.osc_52_write =>

View File

@ -156,6 +156,9 @@ pub fn init(self: *Window, app: *App) !void {
if (app.config.@"gtk-titlebar") {
const header = HeaderBar.init(self);
// If we are not decorated then we hide the titlebar.
header.setVisible(app.config.@"window-decoration");
{
const btn = c.gtk_menu_button_new();
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
@ -216,6 +219,14 @@ pub fn init(self: *Window, app: *App) !void {
}
}
// If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we
// need to stick the headerbar into the content box.
if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
if (self.header) |h| {
c.gtk_box_append(@ptrCast(box), h.asWidget());
}
}
// In debug we show a warning and apply the 'devel' class to the window.
// This is a really common issue where people build from source in debug and performance is really bad.
if (comptime std.debug.runtime_safety) {
@ -290,11 +301,6 @@ pub fn init(self: *Window, app: *App) !void {
if (self.header) |header| {
const header_widget = header.asWidget();
c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget);
// If we are not decorated then we hide the titlebar.
if (!app.config.@"window-decoration") {
c.gtk_widget_set_visible(header_widget, 0);
}
}
if (self.app.config.@"gtk-tabs-location" != .hidden) {
@ -363,8 +369,17 @@ pub fn init(self: *Window, app: *App) !void {
}
// The box is our main child
c.gtk_window_set_child(gtk_window, box);
if (self.header) |h| c.gtk_window_set_titlebar(gtk_window, h.asWidget());
if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
c.adw_application_window_set_content(
@ptrCast(gtk_window),
box,
);
} else {
c.gtk_window_set_child(gtk_window, box);
if (self.header) |h| {
c.gtk_window_set_titlebar(gtk_window, h.asWidget());
}
}
}
// Show the window
@ -499,13 +514,19 @@ pub fn toggleWindowDecorations(self: *Window) void {
const new_decorated = !old_decorated;
c.gtk_window_set_decorated(self.window, @intFromBool(new_decorated));
// Fix any artifacting that may occur in window corners.
if (new_decorated) {
c.gtk_widget_add_css_class(@ptrCast(self.window), "without-window-decoration-and-with-titlebar");
} else {
c.gtk_widget_remove_css_class(@ptrCast(self.window), "without-window-decoration-and-with-titlebar");
}
// If we have a titlebar, then we also show/hide it depending on the
// decorated state. GTK tends to consider the titlebar part of the frame
// and hides it with decorations, but libadwaita doesn't. This makes it
// explicit.
if (self.header) |v| {
const widget = v.asWidget();
c.gtk_widget_set_visible(widget, @intFromBool(new_decorated));
if (self.header) |headerbar| {
headerbar.setVisible(new_decorated);
}
}

View File

@ -30,6 +30,10 @@ pub const HeaderBar = union(enum) {
return .{ .gtk = @ptrCast(headerbar) };
}
pub fn setVisible(self: HeaderBar, visible: bool) void {
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
}
pub fn asWidget(self: HeaderBar) *c.GtkWidget {
return switch (self) {
.adw => |headerbar| @ptrCast(@alignCast(headerbar)),

View File

@ -2668,18 +2668,40 @@ pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void {
try self.expandPaths(std.fs.path.dirname(path).?);
}
pub const OptionalFileAction = enum { loaded, not_found, @"error" };
/// Load optional configuration file from `path`. All errors are ignored.
pub fn loadOptionalFile(self: *Config, alloc: Allocator, path: []const u8) void {
self.loadFile(alloc, path) catch |err| switch (err) {
error.FileNotFound => std.log.info(
"optional config file not found, not loading path={s}",
.{path},
),
else => std.log.warn(
"error reading optional config file, not loading err={} path={s}",
.{ err, path },
),
};
///
/// Returns the action that was taken.
pub fn loadOptionalFile(
self: *Config,
alloc: Allocator,
path: []const u8,
) OptionalFileAction {
if (self.loadFile(alloc, path)) {
return .loaded;
} else |err| switch (err) {
error.FileNotFound => return .not_found,
else => {
std.log.warn(
"error reading optional config file, not loading err={} path={s}",
.{ err, path },
);
return .@"error";
},
}
}
fn writeConfigTemplate(path: []const u8) !void {
log.info("creating template config file: path={s}", .{path});
const file = try std.fs.createFileAbsolute(path, .{});
defer file.close();
try std.fmt.format(
file.writer(),
@embedFile("./config-template"),
.{ .path = path },
);
}
/// Load configurations from the default configuration files. The default
@ -2688,14 +2710,30 @@ pub fn loadOptionalFile(self: *Config, alloc: Allocator, path: []const u8) void
/// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config`
/// is also loaded.
pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
// Load XDG first
const xdg_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" });
defer alloc.free(xdg_path);
self.loadOptionalFile(alloc, xdg_path);
const xdg_action = self.loadOptionalFile(alloc, xdg_path);
// On macOS load the app support directory as well
if (comptime builtin.os.tag == .macos) {
const app_support_path = try internal_os.macos.appSupportDir(alloc, "config");
defer alloc.free(app_support_path);
self.loadOptionalFile(alloc, app_support_path);
const app_support_action = self.loadOptionalFile(alloc, app_support_path);
// If both files are not found, then we create a template file.
// For macOS, we only create the template file in the app support
if (app_support_action == .not_found and xdg_action == .not_found) {
writeConfigTemplate(app_support_path) catch |err| {
log.warn("error creating template config file err={}", .{err});
};
}
} else {
if (xdg_action == .not_found) {
writeConfigTemplate(xdg_path) catch |err| {
log.warn("error creating template config file err={}", .{err});
};
}
}
}
@ -2805,17 +2843,21 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
// replace the entire list with the new list.
inline for (fields, 0..) |field, i| {
const v = &@field(self, field);
const len = v.list.items.len - counter[i];
if (len > 0) {
// Note: we don't have to worry about freeing the memory
// that we overwrite or cut off here because its all in
// an arena.
v.list.replaceRangeAssumeCapacity(
0,
len,
v.list.items[counter[i]..],
);
v.list.items.len = len;
// The list can be empty if it was reset, i.e. --font-family=""
if (v.list.items.len > 0) {
const len = v.list.items.len - counter[i];
if (len > 0) {
// Note: we don't have to worry about freeing the memory
// that we overwrite or cut off here because its all in
// an arena.
v.list.replaceRangeAssumeCapacity(
0,
len,
v.list.items[counter[i]..],
);
v.list.items.len = len;
}
}
}
}
@ -3797,17 +3839,22 @@ pub const Color = struct {
pub fn fromHex(input: []const u8) !Color {
// Trim the beginning '#' if it exists
const trimmed = if (input.len != 0 and input[0] == '#') input[1..] else input;
if (trimmed.len != 6 and trimmed.len != 3) return error.InvalidValue;
// We expect exactly 6 for RRGGBB
if (trimmed.len != 6) return error.InvalidValue;
// Expand short hex values to full hex values
const rgb: []const u8 = if (trimmed.len == 3) &.{
trimmed[0], trimmed[0],
trimmed[1], trimmed[1],
trimmed[2], trimmed[2],
} else trimmed;
// Parse the colors two at a time.
var result: Color = undefined;
comptime var i: usize = 0;
inline while (i < 6) : (i += 2) {
const v: u8 =
((try std.fmt.charToDigit(trimmed[i], 16)) * 16) +
try std.fmt.charToDigit(trimmed[i + 1], 16);
((try std.fmt.charToDigit(rgb[i], 16)) * 16) +
try std.fmt.charToDigit(rgb[i + 1], 16);
@field(result, switch (i) {
0 => "r",
@ -3827,6 +3874,8 @@ pub const Color = struct {
try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("#0A0B0C"));
try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("0A0B0C"));
try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFFFFF"));
try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFF"));
try testing.expectEqual(Color{ .r = 51, .g = 68, .b = 85 }, try Color.fromHex("#345"));
}
test "parseCLI from name" {
@ -4701,9 +4750,11 @@ pub const Keybinds = struct {
try list.parseCLI(alloc, "ctrl+z>2=goto_tab:2");
try list.formatEntry(formatterpkg.entryFormatter("keybind", buf.writer()));
// Note they turn into translated keys because they match
// their ASCII mapping.
const want =
\\keybind = ctrl+z>1=goto_tab:1
\\keybind = ctrl+z>2=goto_tab:2
\\keybind = ctrl+z>two=goto_tab:2
\\keybind = ctrl+z>one=goto_tab:1
\\
;
try std.testing.expectEqualStrings(want, buf.items);

View File

@ -0,0 +1,43 @@
# This is the configuration file for Ghostty.
#
# This template file has been automatically created at the following
# path since Ghostty couldn't find any existing config files on your system:
#
# {[path]s}
#
# The template does not set any default options, since Ghostty ships
# with sensible defaults for all options. Users should only need to set
# options that they want to change from the default.
#
# Run `ghostty +show-config --default --docs` to view a list of
# all available config options and their default values.
#
# Additionally, each config option is also explained in detail
# on Ghostty's website, at https://ghostty.org/docs/config.
# Config syntax crash course
# ==========================
# # The config file consists of simple key-value pairs,
# # separated by equals signs.
# font-family = Iosevka
# window-padding-x = 2
#
# # Spacing around the equals sign does not matter.
# # All of these are identical:
# key=value
# key= value
# key =value
# key = value
#
# # Any line beginning with a # is a comment. It's not possible to put
# # a comment after a config option, since it would be interpreted as a
# # part of the value. For example, this will have a value of "#123abc":
# background = #123abc
#
# # Empty values are used to reset config keys to default.
# key =
#
# # Some config options have unique syntaxes for their value,
# # which is explained in the docs for that config option.
# # Just for example:
# resize-overlay-duration = 4s 200ms

View File

@ -48,13 +48,7 @@ pub fn open(alloc_gpa: Allocator) !void {
///
/// The allocator must be an arena allocator. No memory is freed by this
/// function and the resulting path is not all the memory that is allocated.
///
/// NOTE: WHY IS THIS INLINE? This is inline because when this is not
/// inline then Zig 0.13 crashes [most of the time] when trying to compile
/// this file. This is a workaround for that issue. This function is only
/// called from one place that is not performance critical so it is fine
/// to be inline.
inline fn configPath(alloc_arena: Allocator) ![]const u8 {
fn configPath(alloc_arena: Allocator) ![]const u8 {
const paths: []const []const u8 = try configPathCandidates(alloc_arena);
assert(paths.len > 0);

View File

@ -362,16 +362,9 @@ pub const CoreText = struct {
const list = set.createMatchingFontDescriptors();
defer list.release();
// Bring the list of descriptors in to zig land
var zig_list = try copyMatchingDescriptors(alloc, list);
errdefer alloc.free(zig_list);
// Filter them. We don't use `CTFontCollectionSetExclusionDescriptors`
// to do this because that requires a mutable collection. This way is
// much more straight forward.
zig_list = try alloc.realloc(zig_list, filterDescriptors(zig_list));
// Sort our descriptors
const zig_list = try copyMatchingDescriptors(alloc, list);
errdefer alloc.free(zig_list);
sortMatchingDescriptors(&desc, zig_list);
return DiscoverIterator{
@ -558,47 +551,13 @@ pub const CoreText = struct {
for (0..result.len) |i| {
result[i] = list.getValueAtIndex(macos.text.FontDescriptor, i);
// We need to retain because once the list
// is freed it will release all its members.
// We need to retain becauseonce the list is freed it will
// release all its members.
result[i].retain();
}
return result;
}
/// Filter any descriptors out of the list that aren't acceptable for
/// some reason or another (e.g. the font isn't in a format we can handle).
///
/// Invalid descriptors are filled in from the end of
/// the list and the new length for the list is returned.
fn filterDescriptors(list: []*macos.text.FontDescriptor) usize {
var end = list.len;
var i: usize = 0;
while (i < end) {
if (validDescriptor(list[i])) {
i += 1;
} else {
list[i].release();
end -= 1;
list[i] = list[end];
}
}
return end;
}
/// Used by `filterDescriptors` to decide whether a descriptor is valid.
fn validDescriptor(desc: *macos.text.FontDescriptor) bool {
if (desc.copyAttribute(macos.text.FontAttribute.format)) |format| {
defer format.release();
var value: c_int = undefined;
assert(format.getValue(.int, &value));
// Bitmap fonts are not currently supported.
if (value == macos.text.c.kCTFontFormatBitmap) return false;
}
return true;
}
fn sortMatchingDescriptors(
desc: *const Descriptor,
list: []*macos.text.FontDescriptor,

View File

@ -34,3 +34,6 @@ pub const cozette = @embedFile("res/CozetteVector.ttf");
/// Monaspace has weird ligature behaviors we want to test in our shapers
/// so we embed it here.
pub const monaspace_neon = @embedFile("res/MonaspaceNeon-Regular.otf");
/// Terminus TTF is a scalable font with bitmap glyphs at various sizes.
pub const terminus_ttf = @embedFile("res/TerminusTTF-Regular.ttf");

View File

@ -515,8 +515,17 @@ pub const Face = struct {
fn calcMetrics(ct_font: *macos.text.Font) CalcMetricsError!font.face.Metrics {
// Read the 'head' table out of the font data.
const head: opentype.Head = head: {
const tag = macos.text.FontTableTag.init("head");
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
// macOS bitmap-only fonts use a 'bhed' tag rather than 'head', but
// the table format is byte-identical to the 'head' table, so if we
// can't find 'head' we try 'bhed' instead before failing.
//
// ref: https://fontforge.org/docs/techref/bitmaponlysfnt.html
const head_tag = macos.text.FontTableTag.init("head");
const bhed_tag = macos.text.FontTableTag.init("bhed");
const data =
ct_font.copyTable(head_tag) orelse
ct_font.copyTable(bhed_tag) orelse
return error.CopyTableError;
defer data.release();
const ptr = data.getPointer();
const len = data.getLength();

View File

@ -288,7 +288,6 @@ pub const Face = struct {
self.face.loadGlyph(glyph_id, .{
.render = true,
.color = self.face.hasColor(),
.no_bitmap = !self.face.hasColor(),
}) catch return false;
// If the glyph is SVG we assume colorized
@ -323,14 +322,6 @@ pub const Face = struct {
// glyph properties before render so we don't render here.
.render = !self.synthetic.bold,
// Disable bitmap strikes for now since it causes issues with
// our cell metrics and rasterization. In the future, this is
// all fixable so we can enable it.
//
// This must be enabled for color faces though because those are
// often colored bitmaps, which we support.
.no_bitmap = !self.face.hasColor(),
// use options from config
.no_hinting = !self.load_flags.hinting,
.force_autohint = !self.load_flags.@"force-autohint",
@ -385,7 +376,7 @@ pub const Face = struct {
return error.UnsupportedPixelMode;
};
log.warn("converting from pixel_mode={} to atlas_format={}", .{
log.debug("converting from pixel_mode={} to atlas_format={}", .{
bitmap_ft.pixel_mode,
atlas.format,
});
@ -1005,3 +996,59 @@ test "svg font table" {
try testing.expectEqual(430, table.len);
}
const terminus_i =
\\........
\\........
\\...#....
\\...#....
\\........
\\..##....
\\...#....
\\...#....
\\...#....
\\...#....
\\...#....
\\..###...
\\........
\\........
\\........
\\........
;
// Including the newline
const terminus_i_pitch = 9;
test "bitmap glyph" {
const alloc = testing.allocator;
const testFont = font.embedded.terminus_ttf;
var lib = try Library.init();
defer lib.deinit();
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
defer atlas.deinit(alloc);
// Any glyph at 12pt @ 96 DPI is a bitmap
var ft_font = try Face.init(lib, testFont, .{ .size = .{
.points = 12,
.xdpi = 96,
.ydpi = 96,
} });
defer ft_font.deinit();
// glyph 77 = 'i'
const glyph = try ft_font.renderGlyph(alloc, &atlas, 77, .{});
// should render crisp
try testing.expectEqual(8, glyph.width);
try testing.expectEqual(16, glyph.height);
for (0..glyph.height) |y| {
for (0..glyph.width) |x| {
const pixel = terminus_i[y * terminus_i_pitch + x];
try testing.expectEqual(
@as(u8, if (pixel == '#') 255 else 0),
atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)],
);
}
}
}

View File

@ -43,26 +43,14 @@ pub fn monoToGrayscale(alloc: Allocator, bm: Bitmap) Allocator.Error!Bitmap {
var buf = try alloc.alloc(u8, bm.width * bm.rows);
errdefer alloc.free(buf);
// width divided by 8 because each byte has 8 pixels. This is therefore
// the number of bytes in each row.
const bytes_per_row = bm.width >> 3;
var source_i: usize = 0;
var target_i: usize = 0;
var i: usize = bm.rows;
while (i > 0) : (i -= 1) {
var j: usize = bytes_per_row;
while (j > 0) : (j -= 1) {
var bit: u4 = 8;
while (bit > 0) : (bit -= 1) {
const mask = @as(u8, 1) << @as(u3, @intCast(bit - 1));
const bitval: u8 = if (bm.buffer[source_i + (j - 1)] & mask > 0) 0xFF else 0;
buf[target_i] = bitval;
target_i += 1;
}
for (0..bm.rows) |y| {
const row_offset = y * @as(usize, @intCast(bm.pitch));
for (0..bm.width) |x| {
const byte_offset = row_offset + @divTrunc(x, 8);
const mask = @as(u8, 1) << @intCast(7 - (x % 8));
const bit: u8 = @intFromBool((bm.buffer[byte_offset] & mask) != 0);
buf[y * bm.width + x] = bit * 255;
}
source_i += @intCast(bm.pitch);
}
var copy = bm;

View File

@ -25,6 +25,9 @@ This project uses several fonts which fall under the SIL Open Font License (OFL-
- [Copyright 2013 Google LLC](https://github.com/googlefonts/noto-emoji/blob/main/LICENSE)
- Cozette (MIT)
- [Copyright (c) 2020, Slavfox](https://github.com/slavfox/Cozette/blob/main/LICENSE)
- Terminus TTF (OFL-1.1)
- [Copyright (c) 2010-2020 Dimitar Toshkov Zhekov with Reserved Font Name "Terminus Font"](https://sourceforge.net/projects/terminus-font/)
- [Copyright (c) 2011-2023 Tilman Blumenbach with Reserved Font Name "Terminus (TTF)"](https://files.ax86.net/terminus-ttf/)
A full copy of the OFL license can be found at [OFL.txt](./OFL.txt).
An accompanying FAQ is also available at <https://openfontlicense.org/>.

Binary file not shown.

View File

@ -1019,6 +1019,14 @@ pub const Trigger = struct {
const cp = it.nextCodepoint() orelse break :unicode;
if (it.nextCodepoint() != null) break :unicode;
// If this is ASCII and we have a translated key, set that.
if (std.math.cast(u8, cp)) |ascii| {
if (key.Key.fromASCII(ascii)) |k| {
result.key = .{ .translated = k };
continue :loop;
}
}
result.key = .{ .unicode = cp };
continue :loop;
}
@ -1554,6 +1562,19 @@ test "parse: triggers" {
try parseSingle("a=ignore"),
);
// unicode keys that map to translated
try testing.expectEqual(Binding{
.trigger = .{ .key = .{ .translated = .one } },
.action = .{ .ignore = {} },
}, try parseSingle("1=ignore"));
try testing.expectEqual(Binding{
.trigger = .{
.mods = .{ .super = true },
.key = .{ .translated = .period },
},
.action = .{ .ignore = {} },
}, try parseSingle("cmd+.=ignore"));
// single modifier
try testing.expectEqual(Binding{
.trigger = .{

View File

@ -729,7 +729,9 @@ pub const Key = enum(c_int) {
.{ '\t', .tab },
// Keypad entries. We just assume keypad with the kp_ prefix
// so that has some special meaning. These must also always be last.
// so that has some special meaning. These must also always be last,
// so that our `fromASCII` function doesn't accidentally map them
// over normal numerics and other keys.
.{ '0', .kp_0 },
.{ '1', .kp_1 },
.{ '2', .kp_2 },

View File

@ -18,7 +18,31 @@ pub fn open(
typ: Type,
url: []const u8,
) !void {
const cmd = try openCommand(alloc, typ, url);
const cmd: OpenCommand = switch (builtin.os.tag) {
.linux => .{ .child = std.process.Child.init(
&.{ "xdg-open", url },
alloc,
) },
.windows => .{ .child = std.process.Child.init(
&.{ "rundll32", "url.dll,FileProtocolHandler", url },
alloc,
) },
.macos => .{
.child = std.process.Child.init(
switch (typ) {
.text => &.{ "open", "-t", url },
.unknown => &.{ "open", url },
},
alloc,
),
.wait = true,
},
.ios => return error.Unimplemented,
else => @compileError("unsupported OS"),
};
var exe = cmd.child;
if (cmd.wait) {
@ -53,31 +77,3 @@ const OpenCommand = struct {
child: std.process.Child,
wait: bool = false,
};
fn openCommand(alloc: Allocator, typ: Type, url: []const u8) !OpenCommand {
return switch (builtin.os.tag) {
.linux => .{ .child = std.process.Child.init(
&.{ "xdg-open", url },
alloc,
) },
.windows => .{ .child = std.process.Child.init(
&.{ "rundll32", "url.dll,FileProtocolHandler", url },
alloc,
) },
.macos => .{
.child = std.process.Child.init(
switch (typ) {
.text => &.{ "open", "-t", url },
.unknown => &.{ "open", url },
},
alloc,
),
.wait = true,
},
.ios => return error.Unimplemented,
else => @compileError("unsupported OS"),
};
}

View File

@ -3413,6 +3413,16 @@ pub const Pin = struct {
direction: Direction,
limit: ?Pin,
) PageIterator {
if (build_config.slow_runtime_safety) {
if (limit) |l| {
// Check the order according to the iteration direction.
switch (direction) {
.right_down => assert(self.eql(l) or self.before(l)),
.left_up => assert(self.eql(l) or l.before(self)),
}
}
}
return .{
.row = self,
.limit = if (limit) |p| .{ .row = p } else .{ .none = {} },