Merge branch 'ghostty-org:main' into feature-custom-split-size

This commit is contained in:
I Duran
2024-11-07 12:34:43 +03:00
committed by GitHub
37 changed files with 788 additions and 167 deletions

79
CONTRIBUTING.md Normal file
View 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.

View File

@ -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.

View File

@ -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,7 +375,7 @@ 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 de facto standard as defined by popular terminal emulators
worldwide. Ghostty takes the approach that our behavior is defined by
@ -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, ... }: {

View File

@ -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());

View File

@ -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",

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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:

View File

@ -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="

View File

@ -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,

View File

@ -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"));

View File

@ -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

View File

@ -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;
}

View File

@ -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
_,
};

View File

@ -223,6 +223,7 @@ pub const App = struct {
.mouse_over_link,
.cell_size,
.renderer_health,
.color_change,
=> log.info("unimplemented action={}", .{action}),
}
}

View File

@ -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,

View File

@ -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);

View File

@ -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,

View File

@ -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),
);
}
}

View File

@ -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" {

View 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;

View File

@ -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;
}

View File

@ -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,
},
};
}
};

View File

@ -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.

View File

@ -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();

Binary file not shown.

View File

@ -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
View File

@ -0,0 +1,3 @@
غريبه لاني عربي أبا عن جد
واتكلم الانجليزية بطلاقة اكثر من ٢٥ سنه
ومع هذا اجد العربيه افضل لان فيها الكثير من المفردات الاكثر دقه بالوصف

142
src/os/hostname.zig Normal file
View 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"),
);
}

View File

@ -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");

View File

@ -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 thats 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;
}

View File

@ -175,4 +175,4 @@ pub const MTLSize = extern struct {
depth: c_ulong,
};
pub extern "c" fn MTLCreateSystemDefaultDevice() ?*anyopaque;
pub extern "c" fn MTLCopyAllDevices() ?*anyopaque;

View File

@ -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;

View File

@ -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;
}

View File

@ -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,
} });
}
},
}
}