mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge branch 'ghostty-org:main' into feature-custom-split-size
This commit is contained in:
79
CONTRIBUTING.md
Normal file
79
CONTRIBUTING.md
Normal file
@ -0,0 +1,79 @@
|
||||
# Ghostty Development Process
|
||||
|
||||
This document describes the development process for Ghostty. It is intended for
|
||||
anyone considering opening an **issue** or **pull request**. If in doubt,
|
||||
please open a [discussion](https://github.com/ghostty-org/ghostty/discussions);
|
||||
we can always convert that to an issue later.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> I'm sorry for the wall of text. I'm not trying to be difficult and I do
|
||||
> appreciate your contributions. Ghostty is a personal project for me that
|
||||
> I maintain in my free time. If you're expecting me to dedicate my personal
|
||||
> time to fixing bugs, maintaining features, and reviewing code, I do kindly
|
||||
> ask you spend a few minutes reading this document. Thank you. ❤️
|
||||
|
||||
## Quick Guide
|
||||
|
||||
**I'd like to contribute!**
|
||||
|
||||
All issues are actionable. Pick one and start working on it. Thank you.
|
||||
If you need help or guidance, comment on the issue. Issues that are extra
|
||||
friendly to new contributors are tagged with "contributor friendly".
|
||||
|
||||
**I have a bug!**
|
||||
|
||||
1. Search the issue tracker and discussions for similar issues.
|
||||
2. If you don't have steps to reproduce, open a discussion.
|
||||
3. If you have steps to reproduce, open an issue.
|
||||
|
||||
**I have an idea for a feature!**
|
||||
|
||||
1. Open a discussion.
|
||||
|
||||
**I've implemented a feature!**
|
||||
|
||||
1. If there is an issue for the feature, open a pull request.
|
||||
2. If there is no issue, open a discussion and link to your branch.
|
||||
3. If you want to live dangerously, open a pull request and hope for the best.
|
||||
|
||||
**I have a question!**
|
||||
|
||||
1. Open a discussion or use Discord.
|
||||
|
||||
## General Patterns
|
||||
|
||||
### Issues are Actionable
|
||||
|
||||
The Ghostty [issue tracker](https://github.com/ghostty-org/ghostty/issues)
|
||||
is for _actionable items_.
|
||||
|
||||
Unlike some other projects, Ghostty **does not use the issue tracker for
|
||||
discussion or feature requests**. Instead, we use GitHub
|
||||
[discussions](https://github.com/ghostty-org/ghostty/discussions) for that.
|
||||
Once a discussion reaches a point where a well-understood, actionable
|
||||
item is identified, it is moved to the issue tracker. **This pattern
|
||||
makes it easier for maintainers or contributors to find issues to work on
|
||||
since _every issue_ is ready to be worked on.**
|
||||
|
||||
If you are experiencing a bug and have clear steps to reproduce it, please
|
||||
open an issue. If you are experiencing a bug but you are not sure how to
|
||||
reproduce it or aren't sure if it's a bug, please open a discussion.
|
||||
If you have an idea for a feature, please open a discussion.
|
||||
|
||||
### Pull Requests Implement an Issue
|
||||
|
||||
Pull requests should be associated with a previously accepted issue.
|
||||
**If you open a pull request for something that wasn't previously discussed,**
|
||||
it may be closed or remain stale for an indefinite period of time. I'm not
|
||||
saying it will never be accepted, but the odds are stacked against you.
|
||||
|
||||
Issues tagged with "feature" represent accepted, well-scoped feature requests.
|
||||
If you implement an issue tagged with feature as described in the issue, your
|
||||
pull request will be accepted with a high degree of certainty.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> **Pull requests are NOT a place to discuss feature design.** Please do
|
||||
> not open a WIP pull request to discuss a feature. Instead, use a discussion
|
||||
> and link to your branch.
|
@ -91,3 +91,8 @@ relevant to package maintainers:
|
||||
- `-Dcpu=baseline`: Build for the "baseline" CPU of the target architecture.
|
||||
This avoids building for newer CPU features that may not be available on
|
||||
all target machines.
|
||||
|
||||
- `-Dtarget=$arch-$os-$abi`: Build for a specific target triple. This is
|
||||
often necessary for system packages to specify a specific minimum Linux
|
||||
version, glibc, etc. Run `zig targets` to a get a full list of available
|
||||
targets.
|
||||
|
15
README.md
15
README.md
@ -63,7 +63,7 @@ placed at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to
|
||||
|
||||
The file format is documented below as an example:
|
||||
|
||||
```
|
||||
```ini
|
||||
# The syntax is "key = value". The whitespace around the equals doesn't matter.
|
||||
background = 282c34
|
||||
foreground= ffffff
|
||||
@ -375,9 +375,9 @@ test cases.
|
||||
|
||||
We believe Ghostty is one of the most compliant terminal emulators available.
|
||||
|
||||
Terminal behavior is partially a dejour standard
|
||||
Terminal behavior is partially a de jure standard
|
||||
(i.e. [ECMA-48](https://ecma-international.org/publications-and-standards/standards/ecma-48/))
|
||||
but mostly a defacto standard as defined by popular terminal emulators
|
||||
but mostly a de facto standard as defined by popular terminal emulators
|
||||
worldwide. Ghostty takes the approach that our behavior is defined by
|
||||
(1) standards, if available, (2) xterm, if the feature exists, (3)
|
||||
other popular terminals, in that order. This defines what the Ghostty project
|
||||
@ -789,7 +789,14 @@ Below is an example:
|
||||
#
|
||||
# Instead, either run `nix flake update` or `nixos-rebuild build`
|
||||
# as the current user, and then run `sudo nixos-rebuild switch`.
|
||||
ghostty.url = "git+ssh://git@github.com/ghostty-org/ghostty";
|
||||
ghostty = {
|
||||
url = "git+ssh://git@github.com/ghostty-org/ghostty";
|
||||
|
||||
# NOTE: The below 2 lines are only required on nixos-unstable,
|
||||
# if you're on stable, they may break your build
|
||||
inputs.nixpkgs-stable.follows = "nixpkgs";
|
||||
inputs.nixpkgs-unstable.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { nixpkgs, ghostty, ... }: {
|
||||
|
45
build.zig
45
build.zig
@ -10,6 +10,7 @@ const font = @import("src/font/main.zig");
|
||||
const renderer = @import("src/renderer.zig");
|
||||
const terminfo = @import("src/terminfo/main.zig");
|
||||
const config_vim = @import("src/config/vim.zig");
|
||||
const config_sublime_syntax = @import("src/config/sublime_syntax.zig");
|
||||
const fish_completions = @import("src/build/fish_completions.zig");
|
||||
const build_config = @import("src/build_config.zig");
|
||||
const BuildConfig = build_config.BuildConfig;
|
||||
@ -499,6 +500,38 @@ pub fn build(b: *std.Build) !void {
|
||||
});
|
||||
}
|
||||
|
||||
// Neovim plugin
|
||||
// This is just a copy-paste of the Vim plugin, but using a Neovim subdir.
|
||||
// By default, Neovim doesn't look inside share/vim/vimfiles. Some distros
|
||||
// configure it to do that however. Fedora, does not as a counterexample.
|
||||
{
|
||||
const wf = b.addWriteFiles();
|
||||
_ = wf.add("syntax/ghostty.vim", config_vim.syntax);
|
||||
_ = wf.add("ftdetect/ghostty.vim", config_vim.ftdetect);
|
||||
_ = wf.add("ftplugin/ghostty.vim", config_vim.ftplugin);
|
||||
b.installDirectory(.{
|
||||
.source_dir = wf.getDirectory(),
|
||||
.install_dir = .prefix,
|
||||
.install_subdir = "share/nvim/site",
|
||||
});
|
||||
}
|
||||
|
||||
// Sublime syntax highlighting for bat cli tool
|
||||
// NOTE: The current implementation requires symlinking the generated
|
||||
// 'ghostty.sublime-syntax' file from zig-out to the '~.config/bat/syntaxes'
|
||||
// directory. The syntax then needs to be mapped to the correct language in
|
||||
// the config file within the '~.config/bat' directory
|
||||
// (ex: --map-syntax "/Users/user/.config/ghostty/config:Ghostty Config").
|
||||
{
|
||||
const wf = b.addWriteFiles();
|
||||
_ = wf.add("ghostty.sublime-syntax", config_sublime_syntax.syntax);
|
||||
b.installDirectory(.{
|
||||
.source_dir = wf.getDirectory(),
|
||||
.install_dir = .prefix,
|
||||
.install_subdir = "share/bat/syntaxes",
|
||||
});
|
||||
}
|
||||
|
||||
// Documentation
|
||||
if (emit_docs) {
|
||||
try buildDocumentation(b, config);
|
||||
@ -975,7 +1008,7 @@ fn addDeps(
|
||||
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
step.linkSystemLibrary2("bzip2", dynamic_link_opts);
|
||||
step.linkSystemLibrary2("freetype", dynamic_link_opts);
|
||||
step.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
} else {
|
||||
step.linkLibrary(freetype_dep.artifact("freetype"));
|
||||
try static_libs.append(freetype_dep.artifact("freetype").getEmittedBin());
|
||||
@ -1068,6 +1101,7 @@ fn addDeps(
|
||||
step.root_module.addImport("glslang", glslang_dep.module("glslang"));
|
||||
if (b.systemIntegrationOption("glslang", .{})) {
|
||||
step.linkSystemLibrary2("glslang", dynamic_link_opts);
|
||||
step.linkSystemLibrary2("glslang-default-resource-limits", dynamic_link_opts);
|
||||
} else {
|
||||
step.linkLibrary(glslang_dep.artifact("glslang"));
|
||||
try static_libs.append(glslang_dep.artifact("glslang").getEmittedBin());
|
||||
@ -1226,14 +1260,7 @@ fn addDeps(
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// This is a bit of a hack that should probably be fixed upstream
|
||||
// in zig-objc, but we need to add the apple SDK paths to the
|
||||
// zig-objc module so that it can find the objc runtime headers.
|
||||
const module = objc_dep.module("objc");
|
||||
module.resolved_target = step.root_module.resolved_target;
|
||||
try @import("apple_sdk").addPaths(b, module);
|
||||
step.root_module.addImport("objc", module);
|
||||
|
||||
step.root_module.addImport("objc", objc_dep.module("objc"));
|
||||
step.root_module.addImport("macos", macos_dep.module("macos"));
|
||||
step.linkLibrary(macos_dep.artifact("macos"));
|
||||
try static_libs.append(macos_dep.artifact("macos").getEmittedBin());
|
||||
|
@ -14,8 +14,8 @@
|
||||
.lazy = true,
|
||||
},
|
||||
.zig_objc = .{
|
||||
.url = "https://github.com/mitchellh/zig-objc/archive/fe5ac419530cf800294369d996133fe9cd067aec.tar.gz",
|
||||
.hash = "122034b3e15d582d8d101a7713e5f13c872b8b8eb6d9cb47515b8e34ee75e122630d",
|
||||
.url = "https://github.com/mitchellh/zig-objc/archive/9b8ba849b0f58fe207ecd6ab7c147af55b17556e.tar.gz",
|
||||
.hash = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634",
|
||||
},
|
||||
.zig_js = .{
|
||||
.url = "https://github.com/mitchellh/zig-js/archive/d0b8b0a57c52fbc89f9d9fecba75ca29da7dd7d1.tar.gz",
|
||||
@ -49,8 +49,8 @@
|
||||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b4a9c4d.tar.gz",
|
||||
.hash = "122056fbb29863ec1678b7954fb76b1533ad8c581a34577c1b2efe419e29e05596df",
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/da56d590c4237c96d81cc5ed987ea098eebefdf6.tar.gz",
|
||||
.hash = "1220fac17a112b0dd11ec85e5b31a30f05bfaed897c03f31285276544db30d010c41",
|
||||
},
|
||||
.vaxis = .{
|
||||
.url = "git+https://github.com/rockorager/libvaxis?ref=main#a1b43d24653670d612b91f0855b165e6c987b809",
|
||||
|
@ -512,6 +512,21 @@ typedef struct {
|
||||
ghostty_input_trigger_s trigger;
|
||||
} ghostty_action_key_sequence_s;
|
||||
|
||||
// apprt.action.ColorKind
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_COLOR_KIND_FOREGROUND = -1,
|
||||
GHOSTTY_ACTION_COLOR_KIND_BACKGROUND = -2,
|
||||
GHOSTTY_ACTION_COLOR_KIND_CURSOR = -3,
|
||||
} ghostty_action_color_kind_e;
|
||||
|
||||
// apprt.action.ColorChange
|
||||
typedef struct {
|
||||
ghostty_action_color_kind_e kind;
|
||||
uint8_t r;
|
||||
uint8_t g;
|
||||
uint8_t b;
|
||||
} ghostty_action_color_change_s;
|
||||
|
||||
// apprt.Action.Key
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_NEW_WINDOW,
|
||||
@ -545,6 +560,7 @@ typedef enum {
|
||||
GHOSTTY_ACTION_QUIT_TIMER,
|
||||
GHOSTTY_ACTION_SECURE_INPUT,
|
||||
GHOSTTY_ACTION_KEY_SEQUENCE,
|
||||
GHOSTTY_ACTION_COLOR_CHANGE,
|
||||
} ghostty_action_tag_e;
|
||||
|
||||
typedef union {
|
||||
@ -567,6 +583,7 @@ typedef union {
|
||||
ghostty_action_quit_timer_e quit_timer;
|
||||
ghostty_action_secure_input_e secure_input;
|
||||
ghostty_action_key_sequence_s key_sequence;
|
||||
ghostty_action_color_change_s color_change;
|
||||
} ghostty_action_u;
|
||||
|
||||
typedef struct {
|
||||
|
@ -310,6 +310,19 @@ class QuickTerminalController: BaseTerminalController {
|
||||
return
|
||||
}
|
||||
|
||||
// Terminals typically operate in sRGB color space and macOS defaults
|
||||
// to "native" which is typically P3. There is a lot more resources
|
||||
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
|
||||
// Ghostty defaults to sRGB but this can be overridden.
|
||||
switch (ghostty.config.windowColorspace) {
|
||||
case "display-p3":
|
||||
window.colorSpace = .displayP3
|
||||
case "srgb":
|
||||
fallthrough
|
||||
default:
|
||||
window.colorSpace = .sRGB
|
||||
}
|
||||
|
||||
// If we have window transparency then set it transparent. Otherwise set it opaque.
|
||||
if (ghostty.config.backgroundOpacity < 1) {
|
||||
window.isOpaque = false
|
||||
|
@ -152,8 +152,12 @@ class BaseTerminalController: NSWindowController,
|
||||
// screen then we clamp it back to within the screen.
|
||||
guard let window else { return }
|
||||
guard window.isVisible else { return }
|
||||
guard let screen = window.screen else { return }
|
||||
|
||||
// We ignore fullscreen windows because macOS automatically resizes
|
||||
// those back to the fullscreen bounds.
|
||||
guard !window.styleMask.contains(.fullScreen) else { return }
|
||||
|
||||
guard let screen = window.screen else { return }
|
||||
let visibleFrame = screen.visibleFrame
|
||||
var newFrame = window.frame
|
||||
|
||||
|
@ -227,7 +227,19 @@ class TerminalManager {
|
||||
// are closing a tabbed window, we want to set the cascade point to be
|
||||
// the next cascade point from this window.
|
||||
if focusedWindow != controller.window {
|
||||
// The cascadeTopLeft call below should NOT move the window. Starting with
|
||||
// macOS 15, we found that specifically when used with the new window snapping
|
||||
// features of macOS 15, this WOULD move the frame. So we keep track of the
|
||||
// old frame and restore it if necessary. Issue:
|
||||
// https://github.com/ghostty-org/ghostty/issues/2565
|
||||
let oldFrame = focusedWindow.frame
|
||||
|
||||
Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: NSZeroPoint)
|
||||
|
||||
if focusedWindow.frame != oldFrame {
|
||||
focusedWindow.setFrame(oldFrame, display: true)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -521,6 +521,8 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
||||
keySequence(app, target: target, v: action.action.key_sequence)
|
||||
|
||||
case GHOSTTY_ACTION_COLOR_CHANGE:
|
||||
fallthrough
|
||||
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
||||
fallthrough
|
||||
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
||||
# more details.
|
||||
"sha256-5LBZAExb4PJefW+M0Eo+TcoszhBdIFTGBOv6lte5L0Q="
|
||||
"sha256-fTNqNTfElvZPxJiNQJ/RxrSMCiKZPU3705CY7fznKhY="
|
||||
|
@ -32,7 +32,7 @@ pub fn build(b: *std.Build) !void {
|
||||
};
|
||||
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
lib.linkSystemLibrary2("freetype", dynamic_link_opts);
|
||||
lib.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
} else {
|
||||
const freetype = b.dependency("freetype", .{
|
||||
.target = target,
|
||||
|
@ -76,7 +76,8 @@ pub fn build(b: *std.Build) !void {
|
||||
});
|
||||
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
lib.linkSystemLibrary2("freetype", dynamic_link_opts);
|
||||
lib.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
module.linkSystemLibrary("freetype2", dynamic_link_opts);
|
||||
} else {
|
||||
lib.linkLibrary(freetype.artifact("freetype"));
|
||||
module.addIncludePath(freetype.builder.dependency("freetype", .{}).path("include"));
|
||||
|
@ -66,6 +66,11 @@ font_grid_set: font.SharedGridSet,
|
||||
last_notification_time: ?std.time.Instant = null,
|
||||
last_notification_digest: u64 = 0,
|
||||
|
||||
/// Set to false once we've created at least one surface. This
|
||||
/// never goes true again. This can be used by surfaces to determine
|
||||
/// if they are the first surface.
|
||||
first: bool = true,
|
||||
|
||||
pub const CreateError = Allocator.Error || font.SharedGridSet.InitError;
|
||||
|
||||
/// Initialize the main app instance. This creates the main window, sets
|
||||
|
@ -130,6 +130,11 @@ config: DerivedConfig,
|
||||
/// This is used to determine if we need to confirm, hold open, etc.
|
||||
child_exited: bool = false,
|
||||
|
||||
/// We maintain our focus state and assume we're focused by default.
|
||||
/// If we're not initially focused then apprts can call focusCallback
|
||||
/// to let us know.
|
||||
focused: bool = true,
|
||||
|
||||
/// The effect of an input event. This can be used by callers to take
|
||||
/// the appropriate action after an input event. For example, key
|
||||
/// input can be forwarded to the OS for further processing if it
|
||||
@ -469,13 +474,19 @@ pub fn init(
|
||||
.config = derived_config,
|
||||
};
|
||||
|
||||
// The command we're going to execute
|
||||
const command: ?[]const u8 = if (app.first)
|
||||
config.@"initial-command" orelse config.command
|
||||
else
|
||||
config.command;
|
||||
|
||||
// Start our IO implementation
|
||||
// This separate block ({}) is important because our errdefers must
|
||||
// be scoped here to be valid.
|
||||
{
|
||||
// Initialize our IO backend
|
||||
var io_exec = try termio.Exec.init(alloc, .{
|
||||
.command = config.command,
|
||||
.command = command,
|
||||
.shell_integration = config.@"shell-integration",
|
||||
.shell_integration_features = config.@"shell-integration-features",
|
||||
.working_directory = config.@"working-directory",
|
||||
@ -613,9 +624,9 @@ pub fn init(
|
||||
// For xdg-terminal-exec execution we special-case and set the window
|
||||
// title to the command being executed. This allows window managers
|
||||
// to set custom styling based on the command being executed.
|
||||
const command = config.command orelse break :xdg;
|
||||
if (command.len > 0) {
|
||||
const title = alloc.dupeZ(u8, command) catch |err| {
|
||||
const v = command orelse break :xdg;
|
||||
if (v.len > 0) {
|
||||
const title = alloc.dupeZ(u8, v) catch |err| {
|
||||
log.warn(
|
||||
"error copying command for title, title will not be set err={}",
|
||||
.{err},
|
||||
@ -630,6 +641,9 @@ pub fn init(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// We are no longer the first surface
|
||||
app.first = false;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Surface) void {
|
||||
@ -799,6 +813,22 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.color_change => |change| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.color_change,
|
||||
.{
|
||||
.kind = switch (change.kind) {
|
||||
.background => .background,
|
||||
.foreground => .foreground,
|
||||
.cursor => .cursor,
|
||||
.palette => |v| @enumFromInt(v),
|
||||
},
|
||||
.r = change.color.r,
|
||||
.g = change.color.g,
|
||||
.b = change.color.b,
|
||||
},
|
||||
),
|
||||
|
||||
.set_mouse_shape => |shape| {
|
||||
log.debug("changing mouse shape: {}", .{shape});
|
||||
try self.rt_app.performAction(
|
||||
@ -1972,6 +2002,10 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
|
||||
crash.sentry.thread_state = self.crashThreadState();
|
||||
defer crash.sentry.thread_state = null;
|
||||
|
||||
// If our focus state is the same we do nothing.
|
||||
if (self.focused == focused) return;
|
||||
self.focused = focused;
|
||||
|
||||
// Notify our render thread of the new state
|
||||
_ = self.renderer_thread.mailbox.push(.{
|
||||
.focus = focused,
|
||||
@ -2028,6 +2062,12 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
|
||||
// Schedule render which also drains our mailbox
|
||||
try self.queueRender();
|
||||
|
||||
// Whenever our focus changes we unhide the mouse. The mouse will be
|
||||
// hidden again if the user starts typing. This helps alleviate some
|
||||
// buggy behavior upstream in macOS with the mouse never becoming visible
|
||||
// again when tabbing between programs (see #2525).
|
||||
self.showMouse();
|
||||
|
||||
// Update the focus state and notify the terminal
|
||||
{
|
||||
self.renderer_state.mutex.lock();
|
||||
@ -2713,12 +2753,20 @@ pub fn mouseButtonCallback(
|
||||
}
|
||||
|
||||
// For left button click release we check if we are moving our cursor.
|
||||
if (button == .left and action == .release and mods.alt) click_move: {
|
||||
if (button == .left and
|
||||
action == .release and
|
||||
mods.alt)
|
||||
click_move: {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
// If we have a selection then we do not do click to move because
|
||||
// it means that we moved our cursor while pressing the mouse button.
|
||||
if (self.io.terminal.screen.selection != null) break :click_move;
|
||||
|
||||
// Moving always resets the click count so that we don't highlight.
|
||||
self.mouse.left_click_count = 0;
|
||||
const pin = self.mouse.left_click_pin orelse break :click_move;
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
try self.clickMoveCursor(pin.*);
|
||||
return true;
|
||||
}
|
||||
@ -3437,7 +3485,7 @@ fn dragLeftClickSingle(
|
||||
try self.setSelection(if (selected) terminal.Selection.init(
|
||||
drag_pin,
|
||||
drag_pin,
|
||||
self.mouse.mods.ctrlOrSuper() and self.mouse.mods.alt,
|
||||
SurfaceMouse.isRectangleSelectState(self.mouse.mods),
|
||||
) else null);
|
||||
|
||||
return;
|
||||
@ -3472,7 +3520,7 @@ fn dragLeftClickSingle(
|
||||
try self.setSelection(terminal.Selection.init(
|
||||
start,
|
||||
drag_pin,
|
||||
self.mouse.mods.ctrlOrSuper() and self.mouse.mods.alt,
|
||||
SurfaceMouse.isRectangleSelectState(self.mouse.mods),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
@ -186,6 +186,10 @@ pub const Action = union(Key) {
|
||||
/// key mode because other input may be ignored.
|
||||
key_sequence: KeySequence,
|
||||
|
||||
/// A terminal color was changed programmatically through things
|
||||
/// such as OSC 10/11.
|
||||
color_change: ColorChange,
|
||||
|
||||
/// Sync with: ghostty_action_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
new_window,
|
||||
@ -219,6 +223,7 @@ pub const Action = union(Key) {
|
||||
quit_timer,
|
||||
secure_input,
|
||||
key_sequence,
|
||||
color_change,
|
||||
};
|
||||
|
||||
/// Sync with: ghostty_action_u
|
||||
@ -454,3 +459,20 @@ pub const KeySequence = union(enum) {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const ColorChange = extern struct {
|
||||
kind: ColorKind,
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
};
|
||||
|
||||
pub const ColorKind = enum(c_int) {
|
||||
// Negative numbers indicate some named kind
|
||||
foreground = -1,
|
||||
background = -2,
|
||||
cursor = -3,
|
||||
|
||||
// 0+ values indicate a palette index
|
||||
_,
|
||||
};
|
||||
|
@ -223,6 +223,7 @@ pub const App = struct {
|
||||
.mouse_over_link,
|
||||
.cell_size,
|
||||
.renderer_health,
|
||||
.color_change,
|
||||
=> log.info("unimplemented action={}", .{action}),
|
||||
}
|
||||
}
|
||||
|
@ -485,6 +485,7 @@ pub fn performAction(
|
||||
.key_sequence,
|
||||
.render_inspector,
|
||||
.renderer_health,
|
||||
.color_change,
|
||||
=> log.warn("unimplemented action={}", .{action}),
|
||||
}
|
||||
}
|
||||
@ -928,6 +929,13 @@ fn loadRuntimeCss(
|
||||
\\ --headerbar-bg-color: rgb({d},{d},{d});
|
||||
\\ --headerbar-backdrop-color: oklab(from var(--headerbar-bg-color) calc(l * 0.9) a b / alpha);
|
||||
\\}}
|
||||
\\windowhandle {{
|
||||
\\ background-color: var(--headerbar-bg-color);
|
||||
\\ color: var(--headerbar-fg-color);
|
||||
\\}}
|
||||
\\windowhandle:backdrop {{
|
||||
\\ background-color: var(--headerbar-backdrop-color);
|
||||
\\}}
|
||||
, .{
|
||||
headerbar_foreground.r,
|
||||
headerbar_foreground.g,
|
||||
|
@ -92,13 +92,9 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
break :window window;
|
||||
}
|
||||
};
|
||||
errdefer c.gtk_window_destroy(@ptrCast(window));
|
||||
|
||||
const gtk_window: *c.GtkWindow = @ptrCast(window);
|
||||
errdefer if (self.isAdwWindow()) {
|
||||
c.adw_application_window_destroy(window);
|
||||
} else {
|
||||
c.gtk_application_window_destroy(gtk_window);
|
||||
};
|
||||
self.window = gtk_window;
|
||||
c.gtk_window_set_title(gtk_window, "Ghostty");
|
||||
c.gtk_window_set_default_size(gtk_window, 1000, 600);
|
||||
|
@ -70,6 +70,12 @@ pub const Message = union(enum) {
|
||||
/// unless the surface exits.
|
||||
password_input: bool,
|
||||
|
||||
/// A terminal color was changed using OSC sequences.
|
||||
color_change: struct {
|
||||
kind: terminal.osc.Command.ColorKind,
|
||||
color: terminal.color.RGB,
|
||||
},
|
||||
|
||||
pub const ReportTitleStyle = enum {
|
||||
csi_21_t,
|
||||
|
||||
|
@ -71,6 +71,13 @@ pub const Action = enum {
|
||||
var pending_help: bool = false;
|
||||
var pending: ?Action = null;
|
||||
while (iter.next()) |arg| {
|
||||
// If we see a "-e" and we haven't seen a command yet, then
|
||||
// we are done looking for commands. This special case enables
|
||||
// `ghostty -e ghostty +command`. If we've seen a command we
|
||||
// still want to keep looking because
|
||||
// `ghostty +command -e +command` is invalid.
|
||||
if (std.mem.eql(u8, arg, "-e") and pending == null) return null;
|
||||
|
||||
// Special case, --version always outputs the version no
|
||||
// matter what, no matter what other args exist.
|
||||
if (std.mem.eql(u8, arg, "--version")) return .version;
|
||||
@ -240,3 +247,30 @@ test "parse action plus" {
|
||||
try testing.expect(action.? == .version);
|
||||
}
|
||||
}
|
||||
|
||||
test "parse action plus ignores -e" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--a=42 -e +version",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try Action.detectIter(&iter);
|
||||
try testing.expect(action == null);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"+list-fonts --a=42 -e +version",
|
||||
);
|
||||
defer iter.deinit();
|
||||
try testing.expectError(
|
||||
Action.Error.MultipleActions,
|
||||
Action.detectIter(&iter),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -513,7 +513,26 @@ palette: Palette = .{},
|
||||
/// arguments are provided, the command will be executed using `/bin/sh -c`.
|
||||
/// Ghostty does not do any shell command parsing.
|
||||
///
|
||||
/// If you're using the `ghostty` CLI there is also a shortcut to run a command
|
||||
/// This command will be used for all new terminal surfaces, i.e. new windows,
|
||||
/// tabs, etc. If you want to run a command only for the first terminal surface
|
||||
/// created when Ghostty starts, use the `initial-command` configuration.
|
||||
///
|
||||
/// Ghostty supports the common `-e` flag for executing a command with
|
||||
/// arguments. For example, `ghostty -e fish --with --custom --args`.
|
||||
/// This flag sets the `initial-command` configuration, see that for more
|
||||
/// information.
|
||||
command: ?[]const u8 = null,
|
||||
|
||||
/// This is the same as "command", but only applies to the first terminal
|
||||
/// surface created when Ghostty starts. Subsequent terminal surfaces will use
|
||||
/// the `command` configuration.
|
||||
///
|
||||
/// After the first terminal surface is created (or closed), there is no
|
||||
/// way to run this initial command again automatically. As such, setting
|
||||
/// this at runtime works but will only affect the next terminal surface
|
||||
/// if it is the first one ever created.
|
||||
///
|
||||
/// If you're using the `ghostty` CLI there is also a shortcut to set this
|
||||
/// with arguments directly: you can use the `-e` flag. For example: `ghostty -e
|
||||
/// fish --with --custom --args`. The `-e` flag automatically forces some
|
||||
/// other behaviors as well:
|
||||
@ -525,7 +544,7 @@ palette: Palette = .{},
|
||||
/// process will exit when the command exits. Additionally, the
|
||||
/// `quit-after-last-window-closed-delay` is unset.
|
||||
///
|
||||
command: ?[]const u8 = null,
|
||||
@"initial-command": ?[]const u8 = null,
|
||||
|
||||
/// If true, keep the terminal open after the command exits. Normally, the
|
||||
/// terminal window closes when the running command (such as a shell) exits.
|
||||
@ -2356,7 +2375,7 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
|
||||
}
|
||||
|
||||
self.@"_xdg-terminal-exec" = true;
|
||||
self.command = command.items[0 .. command.items.len - 1];
|
||||
self.@"initial-command" = command.items[0 .. command.items.len - 1];
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -2755,7 +2774,7 @@ pub fn parseManuallyHook(
|
||||
return false;
|
||||
}
|
||||
|
||||
self.command = command.items[0 .. command.items.len - 1];
|
||||
self.@"initial-command" = command.items[0 .. command.items.len - 1];
|
||||
|
||||
// See "command" docs for the implied configurations and why.
|
||||
self.@"gtk-single-instance" = .false;
|
||||
@ -2945,7 +2964,7 @@ test "parse e: command only" {
|
||||
|
||||
var it: TestIterator = .{ .data = &.{"foo"} };
|
||||
try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it));
|
||||
try testing.expectEqualStrings("foo", cfg.command.?);
|
||||
try testing.expectEqualStrings("foo", cfg.@"initial-command".?);
|
||||
}
|
||||
|
||||
test "parse e: command and args" {
|
||||
@ -2956,7 +2975,7 @@ test "parse e: command and args" {
|
||||
|
||||
var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } };
|
||||
try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it));
|
||||
try testing.expectEqualStrings("echo foo bar baz", cfg.command.?);
|
||||
try testing.expectEqualStrings("echo foo bar baz", cfg.@"initial-command".?);
|
||||
}
|
||||
|
||||
test "clone default" {
|
||||
|
51
src/config/sublime_syntax.zig
Normal file
51
src/config/sublime_syntax.zig
Normal file
@ -0,0 +1,51 @@
|
||||
const std = @import("std");
|
||||
const Config = @import("Config.zig");
|
||||
|
||||
const Template = struct {
|
||||
const header =
|
||||
\\%YAML 1.2
|
||||
\\---
|
||||
\\# See http://www.sublimetext.com/docs/syntax.html
|
||||
\\name: Ghostty Config
|
||||
\\file_extensions:
|
||||
\\ - ghostty
|
||||
\\scope: source.ghostty
|
||||
\\
|
||||
\\contexts:
|
||||
\\ main:
|
||||
\\ # Comments
|
||||
\\ - match: '#.*$'
|
||||
\\ scope: comment.line.number-sign.ghostty
|
||||
\\
|
||||
\\ # Keywords
|
||||
\\ - match: '\b(
|
||||
;
|
||||
const footer =
|
||||
\\)\b'
|
||||
\\ scope: keyword.other.ghostty
|
||||
\\
|
||||
;
|
||||
};
|
||||
|
||||
/// Check if a field is internal (starts with underscore)
|
||||
fn isInternal(name: []const u8) bool {
|
||||
return name.len > 0 and name[0] == '_';
|
||||
}
|
||||
|
||||
/// Generate keywords from Config fields
|
||||
fn generateKeywords() []const u8 {
|
||||
@setEvalBranchQuota(5000);
|
||||
var keywords: []const u8 = "";
|
||||
const config_fields = @typeInfo(Config).Struct.fields;
|
||||
|
||||
for (config_fields) |field| {
|
||||
if (isInternal(field.name)) continue;
|
||||
if (keywords.len > 0) keywords = keywords ++ "|";
|
||||
keywords = keywords ++ field.name;
|
||||
}
|
||||
|
||||
return keywords;
|
||||
}
|
||||
|
||||
/// Complete Sublime syntax file content
|
||||
pub const syntax = Template.header ++ generateKeywords() ++ Template.footer;
|
@ -57,6 +57,11 @@ pub const CoreText = struct {
|
||||
/// The initialized font
|
||||
font: *macos.text.Font,
|
||||
|
||||
/// Variations to apply to this font. We apply the variations to the
|
||||
/// search descriptor but sometimes when the font collection is
|
||||
/// made the variation axes are reset so we have to reapply them.
|
||||
variations: []const font.face.Variation,
|
||||
|
||||
pub fn deinit(self: *CoreText) void {
|
||||
self.font.release();
|
||||
self.* = undefined;
|
||||
@ -194,7 +199,10 @@ fn loadCoreText(
|
||||
) !Face {
|
||||
_ = lib;
|
||||
const ct = self.ct.?;
|
||||
return try Face.initFontCopy(ct.font, opts);
|
||||
var face = try Face.initFontCopy(ct.font, opts);
|
||||
errdefer face.deinit();
|
||||
try face.setVariations(ct.variations, opts);
|
||||
return face;
|
||||
}
|
||||
|
||||
fn loadCoreTextFreetype(
|
||||
@ -236,43 +244,7 @@ fn loadCoreTextFreetype(
|
||||
//std.log.warn("path={s}", .{path_slice});
|
||||
var face = try Face.initFile(lib, buf[0..path_slice.len :0], 0, opts);
|
||||
errdefer face.deinit();
|
||||
|
||||
// If our ct font has variations, apply them to the face.
|
||||
if (ct.font.copyAttribute(.variation)) |variations| vars: {
|
||||
defer variations.release();
|
||||
if (variations.getCount() == 0) break :vars;
|
||||
|
||||
// This configuration is just used for testing so we don't want to
|
||||
// have to pass a full allocator through so use the stack. We
|
||||
// shouldn't have a lot of variations and if we do we should use
|
||||
// another mechanism.
|
||||
//
|
||||
// On macOS the default stack size for a thread is 512KB and the main
|
||||
// thread gets megabytes so 16KB is a safe stack allocation.
|
||||
var data: [1024 * 16]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&data);
|
||||
const alloc = fba.allocator();
|
||||
|
||||
var face_vars = std.ArrayList(font.face.Variation).init(alloc);
|
||||
const kav = try variations.getKeysAndValues(alloc);
|
||||
for (kav.keys, kav.values) |key, value| {
|
||||
const num: *const macos.foundation.Number = @ptrCast(key.?);
|
||||
const val: *const macos.foundation.Number = @ptrCast(value.?);
|
||||
|
||||
var num_i32: i32 = undefined;
|
||||
if (!num.getValue(.sint32, &num_i32)) continue;
|
||||
|
||||
var val_f64: f64 = undefined;
|
||||
if (!val.getValue(.float64, &val_f64)) continue;
|
||||
|
||||
try face_vars.append(.{
|
||||
.id = @bitCast(num_i32),
|
||||
.value = val_f64,
|
||||
});
|
||||
}
|
||||
|
||||
try face.setVariations(face_vars.items, opts);
|
||||
}
|
||||
try face.setVariations(ct.variations, opts);
|
||||
|
||||
return face;
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ pub const Descriptor = struct {
|
||||
|
||||
// This is not correct, but we don't currently depend on the
|
||||
// hash value being different based on decimal values of variations.
|
||||
autoHash(hasher, @as(u64, @intFromFloat(variation.value)));
|
||||
autoHash(hasher, @as(i64, @intFromFloat(variation.value)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -235,21 +235,7 @@ pub const Descriptor = struct {
|
||||
);
|
||||
}
|
||||
|
||||
// Build our descriptor from attrs
|
||||
var desc = try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs));
|
||||
errdefer desc.release();
|
||||
|
||||
// Variations are built by copying the descriptor. I don't know a way
|
||||
// to set it on attrs directly.
|
||||
for (self.variations) |v| {
|
||||
const id = try macos.foundation.Number.create(.int, @ptrCast(&v.id));
|
||||
defer id.release();
|
||||
const next = try desc.createCopyWithVariation(id, v.value);
|
||||
desc.release();
|
||||
desc = next;
|
||||
}
|
||||
|
||||
return desc;
|
||||
return try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs));
|
||||
}
|
||||
};
|
||||
|
||||
@ -384,6 +370,7 @@ pub const CoreText = struct {
|
||||
return DiscoverIterator{
|
||||
.alloc = alloc,
|
||||
.list = zig_list,
|
||||
.variations = desc.variations,
|
||||
.i = 0,
|
||||
};
|
||||
}
|
||||
@ -420,6 +407,7 @@ pub const CoreText = struct {
|
||||
return DiscoverIterator{
|
||||
.alloc = alloc,
|
||||
.list = list,
|
||||
.variations = desc.variations,
|
||||
.i = 0,
|
||||
};
|
||||
}
|
||||
@ -443,6 +431,7 @@ pub const CoreText = struct {
|
||||
return DiscoverIterator{
|
||||
.alloc = alloc,
|
||||
.list = list,
|
||||
.variations = desc.variations,
|
||||
.i = 0,
|
||||
};
|
||||
}
|
||||
@ -682,30 +671,29 @@ pub const CoreText = struct {
|
||||
break :style .unmatched;
|
||||
defer style.release();
|
||||
|
||||
// If we have a specific desired style, attempt to search for that.
|
||||
if (desc.style) |desired_style| {
|
||||
// Get our style string
|
||||
var buf: [128]u8 = undefined;
|
||||
const style_str = style.cstring(&buf, .utf8) orelse break :style .unmatched;
|
||||
|
||||
// If we have a specific desired style, attempt to search for that.
|
||||
if (desc.style) |desired_style| {
|
||||
// Matching style string gets highest score
|
||||
if (std.mem.eql(u8, desired_style, style_str)) break :style .match;
|
||||
|
||||
// Otherwise the score is based on the length of the style string.
|
||||
// Shorter styles are scored higher.
|
||||
break :style @enumFromInt(100 -| style_str.len);
|
||||
}
|
||||
|
||||
} else if (!desc.bold and !desc.italic) {
|
||||
// If we do not, and we have no symbolic traits, then we try
|
||||
// to find "regular" (or no style). If we have symbolic traits
|
||||
// we do nothing but we can improve scoring by taking that into
|
||||
// account, too.
|
||||
if (!desc.bold and !desc.italic) {
|
||||
var buf: [128]u8 = undefined;
|
||||
const style_str = style.cstring(&buf, .utf8) orelse break :style .unmatched;
|
||||
if (std.mem.eql(u8, "Regular", style_str)) break :style .match;
|
||||
if (std.mem.eql(u8, "Regular", style_str)) {
|
||||
break :style .match;
|
||||
}
|
||||
}
|
||||
|
||||
break :style .unmatched;
|
||||
// Otherwise the score is based on the length of the style string.
|
||||
// Shorter styles are scored higher. This is a heuristic that
|
||||
// if we don't have a desired style then shorter tends to be
|
||||
// more often the "regular" style.
|
||||
break :style @enumFromInt(100 -| style_str.len);
|
||||
};
|
||||
|
||||
score_acc.traits = traits: {
|
||||
@ -721,6 +709,7 @@ pub const CoreText = struct {
|
||||
pub const DiscoverIterator = struct {
|
||||
alloc: Allocator,
|
||||
list: []const *macos.text.FontDescriptor,
|
||||
variations: []const Variation,
|
||||
i: usize,
|
||||
|
||||
pub fn deinit(self: *DiscoverIterator) void {
|
||||
@ -756,7 +745,10 @@ pub const CoreText = struct {
|
||||
defer self.i += 1;
|
||||
|
||||
return DeferredFace{
|
||||
.ct = .{ .font = font },
|
||||
.ct = .{
|
||||
.font = font,
|
||||
.variations = self.variations,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ pub const emoji = @embedFile("res/NotoColorEmoji.ttf");
|
||||
pub const emoji_text = @embedFile("res/NotoEmoji-Regular.ttf");
|
||||
|
||||
/// Fonts with general properties
|
||||
pub const arabic = @embedFile("res/KawkabMono-Regular.ttf");
|
||||
pub const variable = @embedFile("res/Lilex-VF.ttf");
|
||||
|
||||
/// Font with nerd fonts embedded.
|
||||
|
@ -229,6 +229,9 @@ pub const Face = struct {
|
||||
vs: []const font.face.Variation,
|
||||
opts: font.face.Options,
|
||||
) !void {
|
||||
// If we have no variations, we don't need to do anything.
|
||||
if (vs.len == 0) return;
|
||||
|
||||
// Create a new font descriptor with all the variations set.
|
||||
var desc = self.font.copyDescriptor();
|
||||
defer desc.release();
|
||||
|
BIN
src/font/res/KawkabMono-Regular.ttf
Normal file
BIN
src/font/res/KawkabMono-Regular.ttf
Normal file
Binary file not shown.
@ -190,6 +190,11 @@ pub const Shaper = struct {
|
||||
// Reset the buffer for our current run
|
||||
self.shaper.hb_buf.reset();
|
||||
self.shaper.hb_buf.setContentType(.unicode);
|
||||
|
||||
// We don't support RTL text because RTL in terminals is messy.
|
||||
// Its something we want to improve. For now, we force LTR because
|
||||
// our renderers assume a strictly increasing X value.
|
||||
self.shaper.hb_buf.setDirection(.ltr);
|
||||
}
|
||||
|
||||
pub fn addCodepoint(self: RunIteratorHook, cp: u32, cluster: u32) !void {
|
||||
@ -453,6 +458,46 @@ test "shape monaspace ligs" {
|
||||
}
|
||||
}
|
||||
|
||||
// Ghostty doesn't currently support RTL and our renderers assume
|
||||
// that cells are in strict LTR order. This means that we need to
|
||||
// force RTL text to be LTR for rendering. This test ensures that
|
||||
// we are correctly forcing RTL text to be LTR.
|
||||
test "shape arabic forced LTR" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var testdata = try testShaperWithFont(alloc, .arabic);
|
||||
defer testdata.deinit();
|
||||
|
||||
var screen = try terminal.Screen.init(alloc, 120, 30, 0);
|
||||
defer screen.deinit();
|
||||
try screen.testWriteString(@embedFile("testdata/arabic.txt"));
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
try testing.expectEqual(@as(usize, 25), run.cells);
|
||||
|
||||
const cells = try shaper.shape(run);
|
||||
try testing.expectEqual(@as(usize, 25), cells.len);
|
||||
|
||||
var x: u16 = cells[0].x;
|
||||
for (cells[1..]) |cell| {
|
||||
try testing.expectEqual(x + 1, cell.x);
|
||||
x = cell.x;
|
||||
}
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
|
||||
test "shape emoji width" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
@ -1146,6 +1191,7 @@ const TestShaper = struct {
|
||||
const TestFont = enum {
|
||||
inconsolata,
|
||||
monaspace_neon,
|
||||
arabic,
|
||||
};
|
||||
|
||||
/// Helper to return a fully initialized shaper.
|
||||
@ -1159,6 +1205,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
|
||||
const testFont = switch (font_req) {
|
||||
.inconsolata => font.embedded.inconsolata,
|
||||
.monaspace_neon => font.embedded.monaspace_neon,
|
||||
.arabic => font.embedded.arabic,
|
||||
};
|
||||
|
||||
var lib = try Library.init();
|
||||
|
3
src/font/shaper/testdata/arabic.txt
vendored
Normal file
3
src/font/shaper/testdata/arabic.txt
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
غريبه لاني عربي أبا عن جد
|
||||
واتكلم الانجليزية بطلاقة اكثر من ٢٥ سنه
|
||||
ومع هذا اجد العربيه افضل لان فيها الكثير من المفردات الاكثر دقه بالوصف
|
142
src/os/hostname.zig
Normal file
142
src/os/hostname.zig
Normal file
@ -0,0 +1,142 @@
|
||||
const std = @import("std");
|
||||
const posix = std.posix;
|
||||
|
||||
pub const HostnameParsingError = error{
|
||||
NoHostnameInUri,
|
||||
NoSpaceLeft,
|
||||
};
|
||||
|
||||
/// Print the hostname from a file URI into a buffer.
|
||||
pub fn bufPrintHostnameFromFileUri(
|
||||
buf: []u8,
|
||||
uri: std.Uri,
|
||||
) HostnameParsingError![]const u8 {
|
||||
// Get the raw string of the URI. Its unclear to me if the various
|
||||
// tags of this enum guarantee no percent-encoding so we just
|
||||
// check all of it. This isn't a performance critical path.
|
||||
const host_component = uri.host orelse return error.NoHostnameInUri;
|
||||
const host: []const u8 = switch (host_component) {
|
||||
.raw => |v| v,
|
||||
.percent_encoded => |v| v,
|
||||
};
|
||||
|
||||
// When the "Private Wi-Fi address" setting is toggled on macOS the hostname
|
||||
// is set to a random mac address, e.g. '12:34:56:78:90:ab'.
|
||||
// The URI will be parsed as if the last set of digits is a port number, so
|
||||
// we need to make sure that part is included when it's set.
|
||||
|
||||
// We're only interested in special port handling when the current hostname is a
|
||||
// partial MAC address that's potentially missing the last component.
|
||||
// If that's not the case we just return the plain URI hostname directly.
|
||||
// NOTE: This implementation is not sufficient to verify a valid mac address, but
|
||||
// it's probably sufficient for this specific purpose.
|
||||
if (host.len != 14 or std.mem.count(u8, host, ":") != 4) return host;
|
||||
|
||||
// If we don't have a port then we can return the hostname as-is because
|
||||
// it's not a partial MAC-address.
|
||||
const port = uri.port orelse return host;
|
||||
|
||||
// If the port is not a 2-digit number we're not looking at a partial
|
||||
// MAC-address, and instead just a regular port so we return the plain
|
||||
// URI hostname.
|
||||
if (port < 10 or port > 99) return host;
|
||||
|
||||
var fbs = std.io.fixedBufferStream(buf);
|
||||
try std.fmt.format(
|
||||
fbs.writer(),
|
||||
"{s}:{d}",
|
||||
.{ host, port },
|
||||
);
|
||||
|
||||
return fbs.getWritten();
|
||||
}
|
||||
|
||||
pub const LocalHostnameValidationError = error{
|
||||
PermissionDenied,
|
||||
Unexpected,
|
||||
};
|
||||
|
||||
/// Checks if a hostname is local to the current machine. This matches
|
||||
/// both "localhost" and the current hostname of the machine (as returned
|
||||
/// by `gethostname`).
|
||||
pub fn isLocalHostname(hostname: []const u8) LocalHostnameValidationError!bool {
|
||||
// A 'localhost' hostname is always considered local.
|
||||
if (std.mem.eql(u8, "localhost", hostname)) return true;
|
||||
|
||||
// If hostname is not "localhost" it must match our hostname.
|
||||
var buf: [posix.HOST_NAME_MAX]u8 = undefined;
|
||||
const ourHostname = try posix.gethostname(&buf);
|
||||
return std.mem.eql(u8, hostname, ourHostname);
|
||||
}
|
||||
|
||||
test "bufPrintHostnameFromFileUri succeeds with ascii hostname" {
|
||||
const uri = try std.Uri.parse("file://localhost/");
|
||||
|
||||
var buf: [posix.HOST_NAME_MAX]u8 = undefined;
|
||||
const actual = try bufPrintHostnameFromFileUri(&buf, uri);
|
||||
try std.testing.expectEqualStrings("localhost", actual);
|
||||
}
|
||||
|
||||
test "bufPrintHostnameFromFileUri succeeds with hostname as mac address" {
|
||||
const uri = try std.Uri.parse("file://12:34:56:78:90:12");
|
||||
|
||||
var buf: [posix.HOST_NAME_MAX]u8 = undefined;
|
||||
const actual = try bufPrintHostnameFromFileUri(&buf, uri);
|
||||
try std.testing.expectEqualStrings("12:34:56:78:90:12", actual);
|
||||
}
|
||||
|
||||
test "bufPrintHostnameFromFileUri returns only hostname when there is a port component in the URI" {
|
||||
// First: try with a non-2-digit port, to test general port handling.
|
||||
const four_port_uri = try std.Uri.parse("file://has-a-port:1234");
|
||||
|
||||
var four_port_buf: [posix.HOST_NAME_MAX]u8 = undefined;
|
||||
const four_port_actual = try bufPrintHostnameFromFileUri(&four_port_buf, four_port_uri);
|
||||
try std.testing.expectEqualStrings("has-a-port", four_port_actual);
|
||||
|
||||
// Second: try with a 2-digit port to test mac-address handling.
|
||||
const two_port_uri = try std.Uri.parse("file://has-a-port:12");
|
||||
|
||||
var two_port_buf: [posix.HOST_NAME_MAX]u8 = undefined;
|
||||
const two_port_actual = try bufPrintHostnameFromFileUri(&two_port_buf, two_port_uri);
|
||||
try std.testing.expectEqualStrings("has-a-port", two_port_actual);
|
||||
|
||||
// Third: try with a mac-address that has a port-component added to it to test mac-address handling.
|
||||
const mac_with_port_uri = try std.Uri.parse("file://12:34:56:78:90:12:1234");
|
||||
|
||||
var mac_with_port_buf: [posix.HOST_NAME_MAX]u8 = undefined;
|
||||
const mac_with_port_actual = try bufPrintHostnameFromFileUri(&mac_with_port_buf, mac_with_port_uri);
|
||||
try std.testing.expectEqualStrings("12:34:56:78:90:12", mac_with_port_actual);
|
||||
}
|
||||
|
||||
test "bufPrintHostnameFromFileUri returns NoHostnameInUri error when hostname is missing from uri" {
|
||||
const uri = try std.Uri.parse("file:///");
|
||||
|
||||
var buf: [posix.HOST_NAME_MAX]u8 = undefined;
|
||||
const actual = bufPrintHostnameFromFileUri(&buf, uri);
|
||||
try std.testing.expectError(HostnameParsingError.NoHostnameInUri, actual);
|
||||
}
|
||||
|
||||
test "bufPrintHostnameFromFileUri returns NoSpaceLeft error when provided buffer has insufficient size" {
|
||||
const uri = try std.Uri.parse("file://12:34:56:78:90:12/");
|
||||
|
||||
var buf: [5]u8 = undefined;
|
||||
const actual = bufPrintHostnameFromFileUri(&buf, uri);
|
||||
try std.testing.expectError(HostnameParsingError.NoSpaceLeft, actual);
|
||||
}
|
||||
|
||||
test "isLocalHostname returns true when provided hostname is localhost" {
|
||||
try std.testing.expect(try isLocalHostname("localhost"));
|
||||
}
|
||||
|
||||
test "isLocalHostname returns true when hostname is local" {
|
||||
var buf: [posix.HOST_NAME_MAX]u8 = undefined;
|
||||
const localHostname = try posix.gethostname(&buf);
|
||||
try std.testing.expect(try isLocalHostname(localHostname));
|
||||
}
|
||||
|
||||
test "isLocalHostname returns false when hostname is not local" {
|
||||
try std.testing.expectEqual(
|
||||
false,
|
||||
try isLocalHostname("not-the-local-hostname"),
|
||||
);
|
||||
}
|
@ -17,6 +17,7 @@ const resourcesdir = @import("resourcesdir.zig");
|
||||
// Namespaces
|
||||
pub const args = @import("args.zig");
|
||||
pub const cgroup = @import("cgroup.zig");
|
||||
pub const hostname = @import("hostname.zig");
|
||||
pub const passwd = @import("passwd.zig");
|
||||
pub const xdg = @import("xdg.zig");
|
||||
pub const windows = @import("windows.zig");
|
||||
|
@ -175,9 +175,9 @@ pub const GPUState = struct {
|
||||
instance: InstanceBuffer, // MTLBuffer
|
||||
|
||||
pub fn init() !GPUState {
|
||||
const device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice());
|
||||
const device = try chooseDevice();
|
||||
const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{});
|
||||
errdefer queue.msgSend(void, objc.sel("release"), .{});
|
||||
errdefer queue.release();
|
||||
|
||||
var instance = try InstanceBuffer.initFill(device, &.{
|
||||
0, 1, 3, // Top-left triangle
|
||||
@ -200,13 +200,33 @@ pub const GPUState = struct {
|
||||
return result;
|
||||
}
|
||||
|
||||
fn chooseDevice() error{NoMetalDevice}!objc.Object {
|
||||
const devices = objc.Object.fromId(mtl.MTLCopyAllDevices());
|
||||
defer devices.release();
|
||||
var chosen_device: ?objc.Object = null;
|
||||
var iter = devices.iterate();
|
||||
while (iter.next()) |device| {
|
||||
// We want a GPU that’s connected to a display.
|
||||
if (device.getProperty(bool, "isHeadless")) continue;
|
||||
chosen_device = device;
|
||||
// If the user has an eGPU plugged in, they probably want
|
||||
// to use it. Otherwise, integrated GPUs are better for
|
||||
// battery life and thermals.
|
||||
if (device.getProperty(bool, "isRemovable") or
|
||||
device.getProperty(bool, "isLowPower")) break;
|
||||
}
|
||||
const device = chosen_device orelse return error.NoMetalDevice;
|
||||
return device.retain();
|
||||
}
|
||||
|
||||
pub fn deinit(self: *GPUState) void {
|
||||
// Wait for all of our inflight draws to complete so that
|
||||
// we can cleanly deinit our GPU state.
|
||||
for (0..BufferCount) |_| self.frame_sema.wait();
|
||||
for (&self.frames) |*frame| frame.deinit();
|
||||
self.instance.deinit();
|
||||
self.queue.msgSend(void, objc.sel("release"), .{});
|
||||
self.queue.release();
|
||||
self.device.release();
|
||||
}
|
||||
|
||||
/// Get the next frame state to draw to. This will wait on the
|
||||
@ -269,13 +289,13 @@ pub const FrameState = struct {
|
||||
.size = 8,
|
||||
.format = .grayscale,
|
||||
});
|
||||
errdefer deinitMTLResource(grayscale);
|
||||
errdefer grayscale.release();
|
||||
const color = try initAtlasTexture(device, &.{
|
||||
.data = undefined,
|
||||
.size = 8,
|
||||
.format = .rgba,
|
||||
});
|
||||
errdefer deinitMTLResource(color);
|
||||
errdefer color.release();
|
||||
|
||||
return .{
|
||||
.uniforms = uniforms,
|
||||
@ -290,8 +310,8 @@ pub const FrameState = struct {
|
||||
self.uniforms.deinit();
|
||||
self.cells.deinit();
|
||||
self.cells_bg.deinit();
|
||||
deinitMTLResource(self.grayscale);
|
||||
deinitMTLResource(self.color);
|
||||
self.grayscale.release();
|
||||
self.color.release();
|
||||
}
|
||||
};
|
||||
|
||||
@ -319,8 +339,8 @@ pub const CustomShaderState = struct {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *CustomShaderState) void {
|
||||
deinitMTLResource(self.front_texture);
|
||||
deinitMTLResource(self.back_texture);
|
||||
self.front_texture.release();
|
||||
self.back_texture.release();
|
||||
self.sampler.deinit();
|
||||
}
|
||||
};
|
||||
@ -2057,8 +2077,8 @@ pub fn setScreenSize(
|
||||
// Only free our previous texture if this isn't our first
|
||||
// time setting the custom shader state.
|
||||
if (state.uniforms.resolution[0] > 0) {
|
||||
deinitMTLResource(state.front_texture);
|
||||
deinitMTLResource(state.back_texture);
|
||||
state.front_texture.release();
|
||||
state.back_texture.release();
|
||||
}
|
||||
|
||||
state.uniforms.resolution = .{
|
||||
@ -2982,7 +3002,7 @@ fn syncAtlasTexture(device: objc.Object, atlas: *const font.Atlas, texture: *obj
|
||||
const width = texture.getProperty(c_ulong, "width");
|
||||
if (atlas.size > width) {
|
||||
// Free our old texture
|
||||
deinitMTLResource(texture.*);
|
||||
texture.*.release();
|
||||
|
||||
// Reallocate
|
||||
texture.* = try initAtlasTexture(device, atlas);
|
||||
@ -3049,12 +3069,6 @@ fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object
|
||||
return objc.Object.fromId(id);
|
||||
}
|
||||
|
||||
/// Deinitialize a metal resource (buffer, texture, etc.) and free the
|
||||
/// memory associated with it.
|
||||
fn deinitMTLResource(obj: objc.Object) void {
|
||||
obj.msgSend(void, objc.sel("release"), .{});
|
||||
}
|
||||
|
||||
test {
|
||||
_ = mtl_cell;
|
||||
}
|
||||
|
@ -175,4 +175,4 @@ pub const MTLSize = extern struct {
|
||||
depth: c_ulong,
|
||||
};
|
||||
|
||||
pub extern "c" fn MTLCreateSystemDefaultDevice() ?*anyopaque;
|
||||
pub extern "c" fn MTLCopyAllDevices() ?*anyopaque;
|
||||
|
@ -113,14 +113,19 @@ fn eligibleMouseShapeKeyEvent(physical_key: input.Key) bool {
|
||||
physical_key.leftOrRightAlt();
|
||||
}
|
||||
|
||||
fn isRectangleSelectState(mods: input.Mods) bool {
|
||||
return mods.ctrlOrSuper() and mods.alt;
|
||||
}
|
||||
|
||||
fn isMouseModeOverrideState(mods: input.Mods) bool {
|
||||
return mods.shift;
|
||||
}
|
||||
|
||||
/// Returns true if our modifiers put us in a state where dragging
|
||||
/// should cause a rectangle select.
|
||||
pub fn isRectangleSelectState(mods: input.Mods) bool {
|
||||
return if (comptime builtin.target.isDarwin())
|
||||
mods.alt
|
||||
else
|
||||
mods.ctrlOrSuper() and mods.alt;
|
||||
}
|
||||
|
||||
test "keyToMouseShape" {
|
||||
const testing = std.testing;
|
||||
|
||||
|
@ -2621,12 +2621,59 @@ pub fn plainStringUnwrapped(self: *Terminal, alloc: Allocator) ![]const u8 {
|
||||
return try self.screen.dumpStringAllocUnwrapped(alloc, .{ .viewport = .{} });
|
||||
}
|
||||
|
||||
/// Full reset
|
||||
/// Full reset.
|
||||
///
|
||||
/// This will attempt to free the existing screen memory and allocate
|
||||
/// new screens but if that fails this will reuse the existing memory
|
||||
/// from the prior screens. In the latter case, memory may be wasted
|
||||
/// (since its unused) but it isn't leaked.
|
||||
pub fn fullReset(self: *Terminal) void {
|
||||
// Switch back to primary screen and clear it. We do not restore cursor
|
||||
// because see the next step...
|
||||
self.primaryScreen(.{ .clear_on_exit = true, .cursor_save = false });
|
||||
// Attempt to initialize new screens.
|
||||
var new_primary = Screen.init(
|
||||
self.screen.alloc,
|
||||
self.cols,
|
||||
self.rows,
|
||||
self.screen.pages.explicit_max_size,
|
||||
) catch |err| {
|
||||
log.warn("failed to allocate new primary screen, reusing old memory err={}", .{err});
|
||||
self.fallbackReset();
|
||||
return;
|
||||
};
|
||||
const new_secondary = Screen.init(
|
||||
self.secondary_screen.alloc,
|
||||
self.cols,
|
||||
self.rows,
|
||||
0,
|
||||
) catch |err| {
|
||||
log.warn("failed to allocate new secondary screen, reusing old memory err={}", .{err});
|
||||
new_primary.deinit();
|
||||
self.fallbackReset();
|
||||
return;
|
||||
};
|
||||
|
||||
// If we got here, both new screens were successfully allocated
|
||||
// and we can deinitialize the old screens.
|
||||
self.screen.deinit();
|
||||
self.secondary_screen.deinit();
|
||||
|
||||
// Replace with the newly allocated screens.
|
||||
self.screen = new_primary;
|
||||
self.secondary_screen = new_secondary;
|
||||
|
||||
self.resetCommonState();
|
||||
}
|
||||
|
||||
fn fallbackReset(self: *Terminal) void {
|
||||
// Clear existing screens without reallocation
|
||||
self.primaryScreen(.{ .clear_on_exit = true, .cursor_save = false });
|
||||
self.screen.clearSelection();
|
||||
self.eraseDisplay(.scrollback, false);
|
||||
self.eraseDisplay(.complete, false);
|
||||
self.screen.cursorAbsolute(0, 0);
|
||||
self.resetCommonState();
|
||||
}
|
||||
|
||||
fn resetCommonState(self: *Terminal) void {
|
||||
// We set the saved cursor to null and then restore. This will force
|
||||
// our cursor to go back to the default which will also move the cursor
|
||||
// to the top-left.
|
||||
@ -2640,7 +2687,6 @@ pub fn fullReset(self: *Terminal) void {
|
||||
self.modes = .{};
|
||||
self.flags = .{};
|
||||
self.tabstops.reset(TABSTOP_INTERVAL);
|
||||
self.screen.clearSelection();
|
||||
self.screen.kitty_keyboard = .{};
|
||||
self.secondary_screen.kitty_keyboard = .{};
|
||||
self.screen.protected_mode = .off;
|
||||
@ -2651,9 +2697,6 @@ pub fn fullReset(self: *Terminal) void {
|
||||
.right = self.cols - 1,
|
||||
};
|
||||
self.previous_char = null;
|
||||
self.eraseDisplay(.scrollback, false);
|
||||
self.eraseDisplay(.complete, false);
|
||||
self.screen.cursorAbsolute(0, 0);
|
||||
self.pwd.clearRetainingCapacity();
|
||||
self.status_display = .main;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ const xev = @import("xev");
|
||||
const apprt = @import("../apprt.zig");
|
||||
const build_config = @import("../build_config.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const termio = @import("../termio.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
@ -1048,31 +1049,38 @@ pub const StreamHandler = struct {
|
||||
return;
|
||||
}
|
||||
|
||||
// RFC 793 defines port numbers as 16-bit numbers. 5 digits is sufficient to represent
|
||||
// the maximum since 2^16 - 1 = 65_535.
|
||||
// See https://www.rfc-editor.org/rfc/rfc793#section-3.1.
|
||||
const PORT_NUMBER_MAX_DIGITS = 5;
|
||||
// Make sure there is space for a max length hostname + the max number of digits.
|
||||
var host_and_port_buf: [posix.HOST_NAME_MAX + PORT_NUMBER_MAX_DIGITS]u8 = undefined;
|
||||
const hostname_from_uri = internal_os.hostname.bufPrintHostnameFromFileUri(
|
||||
&host_and_port_buf,
|
||||
uri,
|
||||
) catch |err| switch (err) {
|
||||
error.NoHostnameInUri => {
|
||||
log.warn("OSC 7 uri must contain a hostname: {}", .{err});
|
||||
return;
|
||||
},
|
||||
error.NoSpaceLeft => |e| {
|
||||
log.warn("failed to get full hostname for OSC 7 validation: {}", .{e});
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
// OSC 7 is a little sketchy because anyone can send any value from
|
||||
// any host (such an SSH session). The best practice terminals follow
|
||||
// is to valid the hostname to be local.
|
||||
const host_valid = host_valid: {
|
||||
const host_component = uri.host orelse break :host_valid false;
|
||||
|
||||
// Get the raw string of the URI. Its unclear to me if the various
|
||||
// tags of this enum guarantee no percent-encoding so we just
|
||||
// check all of it. This isn't a performance critical path.
|
||||
const host = switch (host_component) {
|
||||
.raw => |v| v,
|
||||
.percent_encoded => |v| v,
|
||||
};
|
||||
if (host.len == 0 or std.mem.eql(u8, "localhost", host)) {
|
||||
break :host_valid true;
|
||||
}
|
||||
|
||||
// Otherwise, it must match our hostname.
|
||||
var buf: [posix.HOST_NAME_MAX]u8 = undefined;
|
||||
const hostname = posix.gethostname(&buf) catch |err| {
|
||||
const host_valid = internal_os.hostname.isLocalHostname(
|
||||
hostname_from_uri,
|
||||
) catch |err| switch (err) {
|
||||
error.PermissionDenied,
|
||||
error.Unexpected,
|
||||
=> {
|
||||
log.warn("failed to get hostname for OSC 7 validation: {}", .{err});
|
||||
break :host_valid false;
|
||||
};
|
||||
|
||||
break :host_valid std.mem.eql(u8, host, hostname);
|
||||
return;
|
||||
},
|
||||
};
|
||||
if (!host_valid) {
|
||||
log.warn("OSC 7 host must be local", .{});
|
||||
@ -1229,6 +1237,12 @@ pub const StreamHandler = struct {
|
||||
}, .{ .forever = {} });
|
||||
},
|
||||
}
|
||||
|
||||
// Notify the surface of the color change
|
||||
self.surfaceMessageWriter(.{ .color_change = .{
|
||||
.kind = kind,
|
||||
.color = color,
|
||||
} });
|
||||
}
|
||||
|
||||
pub fn resetColor(
|
||||
@ -1247,6 +1261,11 @@ pub const StreamHandler = struct {
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
|
||||
mask.unset(i);
|
||||
|
||||
self.surfaceMessageWriter(.{ .color_change = .{
|
||||
.kind = .{ .palette = @intCast(i) },
|
||||
.color = self.terminal.color_palette.colors[i],
|
||||
} });
|
||||
}
|
||||
} else {
|
||||
var it = std.mem.tokenizeScalar(u8, value, ';');
|
||||
@ -1257,6 +1276,11 @@ pub const StreamHandler = struct {
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
|
||||
mask.unset(i);
|
||||
|
||||
self.surfaceMessageWriter(.{ .color_change = .{
|
||||
.kind = .{ .palette = @intCast(i) },
|
||||
.color = self.terminal.color_palette.colors[i],
|
||||
} });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1266,18 +1290,35 @@ pub const StreamHandler = struct {
|
||||
_ = self.renderer_mailbox.push(.{
|
||||
.foreground_color = self.foreground_color,
|
||||
}, .{ .forever = {} });
|
||||
|
||||
self.surfaceMessageWriter(.{ .color_change = .{
|
||||
.kind = .foreground,
|
||||
.color = self.foreground_color,
|
||||
} });
|
||||
},
|
||||
.background => {
|
||||
self.background_color = self.default_background_color;
|
||||
_ = self.renderer_mailbox.push(.{
|
||||
.background_color = self.background_color,
|
||||
}, .{ .forever = {} });
|
||||
|
||||
self.surfaceMessageWriter(.{ .color_change = .{
|
||||
.kind = .background,
|
||||
.color = self.background_color,
|
||||
} });
|
||||
},
|
||||
.cursor => {
|
||||
self.cursor_color = self.default_cursor_color;
|
||||
_ = self.renderer_mailbox.push(.{
|
||||
.cursor_color = self.cursor_color,
|
||||
}, .{ .forever = {} });
|
||||
|
||||
if (self.cursor_color) |color| {
|
||||
self.surfaceMessageWriter(.{ .color_change = .{
|
||||
.kind = .cursor,
|
||||
.color = color,
|
||||
} });
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user