mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge branch 'main' into feat/quickterm-with-tab
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,3 +17,4 @@ test/cases/**/*.actual.png
|
||||
glad.zip
|
||||
/Box_test.ppm
|
||||
/Box_test_diff.ppm
|
||||
/ghostty.qcow2
|
||||
|
@ -77,3 +77,100 @@ pull request will be accepted with a high degree of certainty.
|
||||
> **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.
|
||||
|
||||
## Nix Virtual Machines
|
||||
|
||||
Several Nix virtual machine definitions are provided by the project for testing
|
||||
and developing Ghostty against multiple different Linux desktop environments.
|
||||
|
||||
Running these requires a working Nix installation, either Nix on your
|
||||
favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further
|
||||
requirements for macOS are detailed below.
|
||||
|
||||
VMs should only be run on your local desktop and then powered off when not in
|
||||
use, which will discard any changes to the VM.
|
||||
|
||||
The VM definitions provide minimal software "out of the box" but additional
|
||||
software can be installed by using standard Nix mechanisms like `nix run nixpkgs#<package>`.
|
||||
|
||||
### Linux
|
||||
|
||||
1. Check out the Ghostty source and change to the directory.
|
||||
2. Run `nix run .#<vmtype>`. `<vmtype>` can be any of the VMs defined in the
|
||||
`nix/vm` directory (without the `.nix` suffix) excluding any file prefixed
|
||||
with `common` or `create`.
|
||||
3. The VM will build and then launch. Depending on the speed of your system, this
|
||||
can take a while, but eventually you should get a new VM window.
|
||||
4. The Ghostty source directory should be mounted to `/tmp/shared` in the VM. Depending
|
||||
on what UID and GID of the user that you launched the VM as, `/tmp/shared` _may_ be
|
||||
writable by the VM user, so be careful!
|
||||
|
||||
### macOS
|
||||
|
||||
1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin`
|
||||
config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your
|
||||
configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/)
|
||||
blog post for more information about the Linux builder and how to tune the performance.
|
||||
2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions
|
||||
above to launch a VM.
|
||||
|
||||
### Custom VMs
|
||||
|
||||
To easily create a custom VM without modifying the Ghostty source, create a new
|
||||
directory, then create a file called `flake.nix` with the following text in the
|
||||
new directory.
|
||||
|
||||
```
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "nixpkgs/nixpkgs-unstable";
|
||||
ghostty.url = "github:ghostty-org/ghostty";
|
||||
};
|
||||
outputs = {
|
||||
nixpkgs,
|
||||
ghostty,
|
||||
...
|
||||
}: {
|
||||
nixosConfigurations.custom-vm = ghostty.create-gnome-vm {
|
||||
nixpkgs = nixpkgs;
|
||||
system = "x86_64-linux";
|
||||
overlay = ghostty.overlays.releasefast;
|
||||
# module = ./configuration.nix # also works
|
||||
module = {pkgs, ...}: {
|
||||
environment.systemPackages = [
|
||||
pkgs.btop
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The custom VM can then be run with a command like this:
|
||||
|
||||
```
|
||||
nix run .#nixosConfigurations.custom-vm.config.system.build.vm
|
||||
```
|
||||
|
||||
A file named `ghostty.qcow2` will be created that is used to persist any changes
|
||||
made in the VM. To "reset" the VM to default delete the file and it will be
|
||||
recreated the next time you run the VM.
|
||||
|
||||
### Contributing new VM definitions
|
||||
|
||||
#### VM Acceptance Criteria
|
||||
|
||||
We welcome the contribution of new VM definitions, as long as they meet the following criteria:
|
||||
|
||||
1. The should be different enough from existing VM definitions that they represent a distinct
|
||||
user (and developer) experience.
|
||||
2. There's a significant Ghostty user population that uses a similar environment.
|
||||
3. The VMs can be built using only packages from the current stable NixOS release.
|
||||
|
||||
#### VM Definition Criteria
|
||||
|
||||
1. VMs should be as minimal as possible so that they build and launch quickly.
|
||||
Additional software can be added at runtime with a command like `nix run nixpkgs#<package name>`.
|
||||
2. VMs should not expose any services to the network, or run any remote access
|
||||
software like SSH daemons, VNC or RDP.
|
||||
3. VMs should auto-login using the "ghostty" user.
|
||||
|
@ -5,8 +5,8 @@
|
||||
.dependencies = .{
|
||||
// Zig libs
|
||||
.libxev = .{
|
||||
.url = "https://github.com/mitchellh/libxev/archive/db6a52bafadf00360e675fefa7926e8e6c0e9931.tar.gz",
|
||||
.hash = "12206029de146b685739f69b10a6f08baee86b3d0a5f9a659fa2b2b66c9602078bbf",
|
||||
.url = "https://github.com/mitchellh/libxev/archive/31eed4e337fed7b0149319e5cdbb62b848c24fbd.tar.gz",
|
||||
.hash = "1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c",
|
||||
},
|
||||
.mach_glfw = .{
|
||||
.url = "https://github.com/mitchellh/mach-glfw/archive/37c2995f31abcf7e8378fba68ddcf4a3faa02de0.tar.gz",
|
||||
@ -79,8 +79,8 @@
|
||||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4762ad5bd6d3906e28babdc2bda8a967d63a63be.tar.gz",
|
||||
.hash = "1220a263b22113273d01bd33e3c06b8119cb2f63b4e5d414a85d88e3aa95bb68a2de",
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/0e23daf59234fc892cba949562d7bf69204594bb.tar.gz",
|
||||
.hash = "12204fc99743d8232e691ac22e058519bfc6ea92de4a11c6dba59b117531c847cd6a",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
0
dist/linux/ghostty_dolphin.desktop
vendored
Normal file → Executable file
0
dist/linux/ghostty_dolphin.desktop
vendored
Normal file → Executable file
55
flake.nix
55
flake.nix
@ -31,7 +31,9 @@
|
||||
zig,
|
||||
...
|
||||
}:
|
||||
builtins.foldl' nixpkgs-stable.lib.recursiveUpdate {} (builtins.map (system: let
|
||||
builtins.foldl' nixpkgs-stable.lib.recursiveUpdate {} (
|
||||
builtins.map (
|
||||
system: let
|
||||
pkgs-stable = nixpkgs-stable.legacyPackages.${system};
|
||||
pkgs-unstable = nixpkgs-unstable.legacyPackages.${system};
|
||||
in {
|
||||
@ -57,12 +59,53 @@
|
||||
|
||||
formatter.${system} = pkgs-stable.alejandra;
|
||||
|
||||
# Our supported systems are the same supported systems as the Zig binaries.
|
||||
}) (builtins.attrNames zig.packages))
|
||||
// {
|
||||
overlays.default = final: prev: {
|
||||
ghostty = self.packages.${prev.system}.default;
|
||||
apps.${system} = let
|
||||
runVM = (
|
||||
module: let
|
||||
vm = import ./nix/vm/create.nix {
|
||||
inherit system module;
|
||||
nixpkgs = nixpkgs-stable;
|
||||
overlay = self.overlays.debug;
|
||||
};
|
||||
program = pkgs-stable.writeShellScript "run-ghostty-vm" ''
|
||||
SHARED_DIR=$(pwd)
|
||||
export SHARED_DIR
|
||||
|
||||
${pkgs-stable.lib.getExe vm.config.system.build.vm} "$@"
|
||||
'';
|
||||
in {
|
||||
type = "app";
|
||||
program = "${program}";
|
||||
}
|
||||
);
|
||||
in {
|
||||
wayland-cinnamon = runVM ./nix/vm/wayland-cinnamon.nix;
|
||||
wayland-gnome = runVM ./nix/vm/wayland-gnome.nix;
|
||||
wayland-plasma6 = runVM ./nix/vm/wayland-plasma6.nix;
|
||||
x11-cinnamon = runVM ./nix/vm/x11-cinnamon.nix;
|
||||
x11-gnome = runVM ./nix/vm/x11-gnome.nix;
|
||||
x11-plasma6 = runVM ./nix/vm/x11-plasma6.nix;
|
||||
x11-xfce = runVM ./nix/vm/x11-xfce.nix;
|
||||
};
|
||||
}
|
||||
# Our supported systems are the same supported systems as the Zig binaries.
|
||||
) (builtins.attrNames zig.packages)
|
||||
)
|
||||
// {
|
||||
overlays = {
|
||||
default = self.overlays.releasefast;
|
||||
releasefast = final: prev: {
|
||||
ghostty = self.packages.${prev.system}.ghostty-releasefast;
|
||||
};
|
||||
debug = final: prev: {
|
||||
ghostty = self.packages.${prev.system}.ghostty-debug;
|
||||
};
|
||||
};
|
||||
create-vm = import ./nix/vm/create.nix;
|
||||
create-cinnamon-vm = import ./nix/vm/create-cinnamon.nix;
|
||||
create-gnome-vm = import ./nix/vm/create-gnome.nix;
|
||||
create-plasma6-vm = import ./nix/vm/create-plasma6.nix;
|
||||
create-xfce-vm = import ./nix/vm/create-xfce.nix;
|
||||
};
|
||||
|
||||
nixConfig = {
|
||||
|
@ -159,7 +159,7 @@ typedef enum {
|
||||
GHOSTTY_KEY_EQUAL,
|
||||
GHOSTTY_KEY_LEFT_BRACKET, // [
|
||||
GHOSTTY_KEY_RIGHT_BRACKET, // ]
|
||||
GHOSTTY_KEY_BACKSLASH, // /
|
||||
GHOSTTY_KEY_BACKSLASH, // \
|
||||
|
||||
// control
|
||||
GHOSTTY_KEY_UP,
|
||||
@ -565,6 +565,7 @@ typedef enum {
|
||||
GHOSTTY_ACTION_CLOSE_TAB,
|
||||
GHOSTTY_ACTION_NEW_SPLIT,
|
||||
GHOSTTY_ACTION_CLOSE_ALL_WINDOWS,
|
||||
GHOSTTY_ACTION_TOGGLE_MAXIMIZE,
|
||||
GHOSTTY_ACTION_TOGGLE_FULLSCREEN,
|
||||
GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW,
|
||||
GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS,
|
||||
|
@ -69,6 +69,8 @@
|
||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; };
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
|
||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
||||
A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3C92D4445E20033CF96 /* Dock.swift */; };
|
||||
A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */; };
|
||||
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; };
|
||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
|
||||
@ -167,6 +169,8 @@
|
||||
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = "<group>"; };
|
||||
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = "<group>"; };
|
||||
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = "<group>"; };
|
||||
A5A2A3C92D4445E20033CF96 /* Dock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dock.swift; sourceTree = "<group>"; };
|
||||
A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+Extension.swift"; sourceTree = "<group>"; };
|
||||
A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = "<group>"; };
|
||||
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
@ -279,6 +283,7 @@
|
||||
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
|
||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */,
|
||||
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
|
||||
A5A2A3C92D4445E20033CF96 /* Dock.swift */,
|
||||
A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */,
|
||||
A59630962AEE163600D64628 /* HostingWindow.swift */,
|
||||
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */,
|
||||
@ -286,6 +291,7 @@
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
|
||||
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
|
||||
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
|
||||
A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */,
|
||||
A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */,
|
||||
A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */,
|
||||
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
|
||||
@ -656,6 +662,7 @@
|
||||
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
|
||||
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
|
||||
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */,
|
||||
A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */,
|
||||
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
|
||||
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
|
||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
|
||||
@ -681,6 +688,7 @@
|
||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
||||
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
|
||||
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
|
||||
A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */,
|
||||
A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */,
|
||||
A5FEB3002ABB69450068369E /* main.swift in Sources */,
|
||||
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
|
||||
|
@ -27,6 +27,9 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// The active space when the quick terminal was last shown.
|
||||
private var previousActiveSpace: size_t = 0
|
||||
|
||||
/// Non-nil if we have hidden dock state.
|
||||
private var hiddenDock: HiddenDock? = nil
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private var derivedConfig: DerivedConfig
|
||||
|
||||
@ -48,6 +51,11 @@ class QuickTerminalController: BaseTerminalController {
|
||||
|
||||
// Setup our notifications for behaviors
|
||||
let center = NotificationCenter.default
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(applicationWillTerminate(_:)),
|
||||
name: NSApplication.willTerminateNotification,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onToggleFullscreen),
|
||||
@ -83,6 +91,9 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// Remove all of our notificationcenter subscriptions
|
||||
let center = NotificationCenter.default
|
||||
center.removeObserver(self)
|
||||
|
||||
// Make sure we restore our hidden dock
|
||||
hiddenDock = nil
|
||||
}
|
||||
|
||||
// MARK: NSWindowController
|
||||
@ -137,6 +148,17 @@ class QuickTerminalController: BaseTerminalController {
|
||||
|
||||
// MARK: NSWindowDelegate
|
||||
|
||||
override func windowDidBecomeKey(_ notification: Notification) {
|
||||
super.windowDidBecomeKey(notification)
|
||||
|
||||
// If we're not visible we don't care to run the logic below. It only
|
||||
// applies if we can be seen.
|
||||
guard visible else { return }
|
||||
|
||||
// Re-hide the dock if we were hiding it before.
|
||||
hiddenDock?.hide()
|
||||
}
|
||||
|
||||
override func windowDidResignKey(_ notification: Notification) {
|
||||
super.windowDidResignKey(notification)
|
||||
|
||||
@ -157,6 +179,10 @@ class QuickTerminalController: BaseTerminalController {
|
||||
self.previousApp = nil
|
||||
}
|
||||
|
||||
// Regardless of autohide, we always want to bring the dock back
|
||||
// when we lose focus.
|
||||
hiddenDock?.restore()
|
||||
|
||||
if derivedConfig.quickTerminalAutoHide {
|
||||
switch derivedConfig.quickTerminalSpaceBehavior {
|
||||
case .remain:
|
||||
@ -277,25 +303,50 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// Move our window off screen to the top
|
||||
position.setInitial(in: window, on: screen)
|
||||
|
||||
// We need to set our window level to a high value. In testing, only
|
||||
// popUpMenu and above do what we want. This gets it above the menu bar
|
||||
// and lets us render off screen.
|
||||
window.level = .popUpMenu
|
||||
|
||||
// Move it to the visible position since animation requires this
|
||||
DispatchQueue.main.async {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
// If our dock position would conflict with our target location then
|
||||
// we autohide the dock.
|
||||
if position.conflictsWithDock(on: screen) {
|
||||
if (hiddenDock == nil) {
|
||||
hiddenDock = .init()
|
||||
}
|
||||
|
||||
hiddenDock?.hide()
|
||||
} else {
|
||||
// Ensure we don't have any hidden dock if we don't conflict.
|
||||
// The deinit will restore.
|
||||
hiddenDock = nil
|
||||
}
|
||||
|
||||
// Run the animation that moves our window into the proper place and makes
|
||||
// it visible.
|
||||
NSAnimationContext.runAnimationGroup(
|
||||
{ context in
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = derivedConfig.quickTerminalAnimationDuration
|
||||
context.timingFunction = .init(name: .easeIn)
|
||||
position.setFinal(in: window.animator(), on: screen)
|
||||
},
|
||||
completionHandler: {
|
||||
}, completionHandler: {
|
||||
// There is a very minor delay here so waiting at least an event loop tick
|
||||
// keeps us safe from the view not being on the window.
|
||||
DispatchQueue.main.async {
|
||||
// If we canceled our animation in we do nothing
|
||||
guard self.visible else { return }
|
||||
// If we canceled our animation clean up some state.
|
||||
guard self.visible else {
|
||||
self.hiddenDock = nil
|
||||
return
|
||||
}
|
||||
|
||||
// After animating in, we reset the window level to a value that
|
||||
// is above other windows but not as high as popUpMenu. This allows
|
||||
// things like IME dropdowns to appear properly.
|
||||
window.level = .floating
|
||||
|
||||
// Now that the window is visible, sync our appearance. This function
|
||||
// requires the window is visible.
|
||||
@ -359,6 +410,9 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) {
|
||||
// If we hid the dock then we unhide it.
|
||||
hiddenDock = nil
|
||||
|
||||
// If the window isn't on our active space then we don't animate, we just
|
||||
// hide it.
|
||||
if !window.isOnActiveSpace {
|
||||
@ -388,13 +442,16 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
}
|
||||
|
||||
NSAnimationContext.runAnimationGroup(
|
||||
{ context in
|
||||
// We need to set our window level to a high value. In testing, only
|
||||
// popUpMenu and above do what we want. This gets it above the menu bar
|
||||
// and lets us render off screen.
|
||||
window.level = .popUpMenu
|
||||
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = derivedConfig.quickTerminalAnimationDuration
|
||||
context.timingFunction = .init(name: .easeIn)
|
||||
position.setInitial(in: window.animator(), on: screen)
|
||||
},
|
||||
completionHandler: {
|
||||
}, completionHandler: {
|
||||
// This causes the window to be removed from the screen list and macOS
|
||||
// handles what should be focused next.
|
||||
window.orderOut(self)
|
||||
@ -411,19 +468,6 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// Some APIs such as window blur have no effect unless the window is visible.
|
||||
guard window.isVisible else { 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 self.derivedConfig.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 self.derivedConfig.backgroundOpacity < 1 {
|
||||
window.isOpaque = false
|
||||
@ -462,6 +506,13 @@ class QuickTerminalController: BaseTerminalController {
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@objc private func applicationWillTerminate(_ notification: Notification) {
|
||||
// If the application is going to terminate we want to make sure we
|
||||
// restore any global dock state. I think deinit should be called which
|
||||
// would call this anyways but I can't be sure so I will do this too.
|
||||
hiddenDock = nil
|
||||
}
|
||||
|
||||
@objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard target == self.focusedSurface else { return }
|
||||
@ -497,7 +548,6 @@ class QuickTerminalController: BaseTerminalController {
|
||||
let quickTerminalAnimationDuration: Double
|
||||
let quickTerminalAutoHide: Bool
|
||||
let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior
|
||||
let windowColorspace: String
|
||||
let backgroundOpacity: Double
|
||||
|
||||
init() {
|
||||
@ -505,7 +555,6 @@ class QuickTerminalController: BaseTerminalController {
|
||||
self.quickTerminalAnimationDuration = 0.2
|
||||
self.quickTerminalAutoHide = true
|
||||
self.quickTerminalSpaceBehavior = .move
|
||||
self.windowColorspace = ""
|
||||
self.backgroundOpacity = 1.0
|
||||
}
|
||||
|
||||
@ -514,10 +563,38 @@ class QuickTerminalController: BaseTerminalController {
|
||||
self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration
|
||||
self.quickTerminalAutoHide = config.quickTerminalAutoHide
|
||||
self.quickTerminalSpaceBehavior = config.quickTerminalSpaceBehavior
|
||||
self.windowColorspace = config.windowColorspace
|
||||
self.backgroundOpacity = config.backgroundOpacity
|
||||
}
|
||||
}
|
||||
|
||||
/// Hides the dock globally (not just NSApp). This is only used if the quick terminal is
|
||||
/// in a conflicting position with the dock.
|
||||
private class HiddenDock {
|
||||
let previousAutoHide: Bool
|
||||
private var hidden: Bool = false
|
||||
|
||||
init() {
|
||||
previousAutoHide = Dock.autoHideEnabled
|
||||
}
|
||||
|
||||
deinit {
|
||||
restore()
|
||||
}
|
||||
|
||||
func hide() {
|
||||
guard !hidden else { return }
|
||||
NSApp.acquirePresentationOption(.autoHideDock)
|
||||
Dock.autoHideEnabled = true
|
||||
hidden = true
|
||||
}
|
||||
|
||||
func restore() {
|
||||
guard hidden else { return }
|
||||
NSApp.releasePresentationOption(.autoHideDock)
|
||||
Dock.autoHideEnabled = previousAutoHide
|
||||
hidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Notification.Name {
|
||||
|
@ -69,7 +69,7 @@ enum QuickTerminalPosition : String {
|
||||
finalSize.width = screen.frame.width
|
||||
|
||||
case .left, .right:
|
||||
finalSize.height = screen.frame.height
|
||||
finalSize.height = screen.visibleFrame.height
|
||||
|
||||
case .center:
|
||||
finalSize.width = screen.frame.width / 2
|
||||
@ -118,4 +118,22 @@ enum QuickTerminalPosition : String {
|
||||
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
func conflictsWithDock(on screen: NSScreen) -> Bool {
|
||||
// Screen must have a dock for it to conflict
|
||||
guard screen.hasDock else { return false }
|
||||
|
||||
// Get the dock orientation for this screen
|
||||
guard let orientation = Dock.orientation else { return false }
|
||||
|
||||
// Depending on the orientation of the dock, we conflict if our quick terminal
|
||||
// would potentially "hit" the dock. In the future we should probably consider
|
||||
// the frame of the quick terminal.
|
||||
return switch (orientation) {
|
||||
case .top: self == .top || self == .left || self == .right
|
||||
case .bottom: self == .bottom || self == .left || self == .right
|
||||
case .left: self == .top || self == .bottom
|
||||
case .right: self == .top || self == .bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,23 +28,5 @@ class QuickTerminalWindow: NSPanel {
|
||||
|
||||
// We don't want to activate the owning app when quick terminal is triggered.
|
||||
self.styleMask.insert(.nonactivatingPanel)
|
||||
|
||||
// We need to set our window level to a high value. In testing, only
|
||||
// popUpMenu and above do what we want. This gets it above the menu bar
|
||||
// and lets us render off screen.
|
||||
self.level = .popUpMenu
|
||||
|
||||
// This plus the level above was what was needed for the animation to work,
|
||||
// because it gets the window off screen properly. Plus we add some fields
|
||||
// we just want the behavior of.
|
||||
self.collectionBehavior = [
|
||||
// We want this to be part of every space because it is a singleton.
|
||||
.canJoinAllSpaces,
|
||||
|
||||
// We don't want to be part of command-tilde
|
||||
.ignoresCycle,
|
||||
|
||||
// We want to show the window on another space if it is visible
|
||||
.fullScreenAuxiliary]
|
||||
}
|
||||
}
|
||||
|
@ -452,6 +452,7 @@ class BaseTerminalController: NSWindowController,
|
||||
self.alert = nil
|
||||
switch (response) {
|
||||
case .alertFirstButtonReturn:
|
||||
alert.window.orderOut(nil)
|
||||
window.close()
|
||||
|
||||
default:
|
||||
|
@ -22,7 +22,7 @@ class TerminalController: BaseTerminalController {
|
||||
private var restorable: Bool = true
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private var derivedConfig: DerivedConfig
|
||||
private(set) var derivedConfig: DerivedConfig
|
||||
|
||||
/// The notification cancellable for focused surface property changes.
|
||||
private var surfaceAppearanceCancellables: Set<AnyCancellable> = []
|
||||
@ -366,19 +366,6 @@ class TerminalController: BaseTerminalController {
|
||||
// If window decorations are disabled, remove our title
|
||||
if (!config.windowDecorations) { window.styleMask.remove(.titled) }
|
||||
|
||||
// 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 (config.windowColorspace) {
|
||||
case "display-p3":
|
||||
window.colorSpace = .displayP3
|
||||
case "srgb":
|
||||
fallthrough
|
||||
default:
|
||||
window.colorSpace = .sRGB
|
||||
}
|
||||
|
||||
// If we have only a single surface (no splits) and that surface requested
|
||||
// an initial size then we set it here now.
|
||||
if case let .leaf(leaf) = surfaceTree {
|
||||
@ -789,7 +776,7 @@ class TerminalController: BaseTerminalController {
|
||||
toggleFullscreen(mode: fullscreenMode)
|
||||
}
|
||||
|
||||
private struct DerivedConfig {
|
||||
struct DerivedConfig {
|
||||
let backgroundColor: Color
|
||||
let macosTitlebarStyle: String
|
||||
|
||||
|
@ -115,6 +115,21 @@ class TerminalWindow: NSWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// We override this so that with the hidden titlebar style the titlebar
|
||||
// area is not draggable.
|
||||
override var contentLayoutRect: CGRect {
|
||||
var rect = super.contentLayoutRect
|
||||
|
||||
// If we are using a hidden titlebar style, the content layout is the
|
||||
// full frame making it so that it is not draggable.
|
||||
if let controller = windowController as? TerminalController,
|
||||
controller.derivedConfig.macosTitlebarStyle == "hidden" {
|
||||
rect.origin.y = 0
|
||||
rect.size.height = self.frame.height
|
||||
}
|
||||
return rect
|
||||
}
|
||||
|
||||
// The window theme configuration from Ghostty. This is used to control some
|
||||
// behaviors that don't look quite right in certain situations.
|
||||
var windowTheme: TerminalWindowTheme?
|
||||
|
@ -132,15 +132,6 @@ extension Ghostty {
|
||||
return v
|
||||
}
|
||||
|
||||
var windowColorspace: String {
|
||||
guard let config = self.config else { return "" }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "window-colorspace"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
|
||||
guard let ptr = v else { return "" }
|
||||
return String(cString: ptr)
|
||||
}
|
||||
|
||||
var windowSaveState: String {
|
||||
guard let config = self.config else { return "" }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
@ -174,11 +165,14 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
var windowDecorations: Bool {
|
||||
guard let config = self.config else { return true }
|
||||
var v = false;
|
||||
let defaultValue = true
|
||||
guard let config = self.config else { return defaultValue }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "window-decoration"
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||
return v;
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
|
||||
guard let ptr = v else { return defaultValue }
|
||||
let str = String(cString: ptr)
|
||||
return WindowDecoration(rawValue: str)?.enabled() ?? defaultValue
|
||||
}
|
||||
|
||||
var windowTheme: String? {
|
||||
@ -345,7 +339,7 @@ extension Ghostty {
|
||||
var backgroundBlurRadius: Int {
|
||||
guard let config = self.config else { return 1 }
|
||||
var v: Int = 0
|
||||
let key = "background-blur-radius"
|
||||
let key = "background-blur"
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||
return v;
|
||||
}
|
||||
@ -563,4 +557,18 @@ extension Ghostty.Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum WindowDecoration: String {
|
||||
case none
|
||||
case client
|
||||
case server
|
||||
case auto
|
||||
|
||||
func enabled() -> Bool {
|
||||
switch self {
|
||||
case .client, .server, .auto: return true
|
||||
case .none: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -205,6 +205,7 @@ extension Ghostty {
|
||||
alert.beginSheetModal(for: window, completionHandler: { response in
|
||||
switch (response) {
|
||||
case .alertFirstButtonReturn:
|
||||
alert.window.orderOut(nil)
|
||||
node = nil
|
||||
|
||||
default:
|
||||
|
@ -92,22 +92,6 @@ extension Ghostty {
|
||||
windowFocus = false
|
||||
}
|
||||
}
|
||||
.onDrop(of: [.fileURL], isTargeted: nil) { providers in
|
||||
providers.forEach { provider in
|
||||
_ = provider.loadObject(ofClass: URL.self) { url, _ in
|
||||
guard let url = url else { return }
|
||||
let path = Shell.escape(url.path)
|
||||
DispatchQueue.main.async {
|
||||
surfaceView.insertText(
|
||||
path,
|
||||
replacementRange: NSMakeRange(0, 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
#endif
|
||||
|
||||
// If our geo size changed then we show the resize overlay as configured.
|
||||
|
@ -1,3 +1,4 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
import CoreText
|
||||
import UserNotifications
|
||||
@ -230,6 +231,9 @@ extension Ghostty {
|
||||
|
||||
ghostty_surface_set_color_scheme(surface, scheme)
|
||||
}
|
||||
|
||||
// The UTTypes that can be dragged onto this view.
|
||||
registerForDraggedTypes(Array(Self.dropTypes))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -1509,3 +1513,62 @@ extension Ghostty.SurfaceView: NSMenuItemValidation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NSDraggingDestination
|
||||
|
||||
extension Ghostty.SurfaceView {
|
||||
static let dropTypes: Set<NSPasteboard.PasteboardType> = [
|
||||
.string,
|
||||
.fileURL,
|
||||
.URL
|
||||
]
|
||||
|
||||
override func draggingEntered(_ sender: any NSDraggingInfo) -> NSDragOperation {
|
||||
guard let types = sender.draggingPasteboard.types else { return [] }
|
||||
|
||||
// If the dragging object contains none of our types then we return none.
|
||||
// This shouldn't happen because AppKit should guarantee that we only
|
||||
// receive types we registered for but its good to check.
|
||||
if Set(types).isDisjoint(with: Self.dropTypes) {
|
||||
return []
|
||||
}
|
||||
|
||||
// We use copy to get the proper icon
|
||||
return .copy
|
||||
}
|
||||
|
||||
override func performDragOperation(_ sender: any NSDraggingInfo) -> Bool {
|
||||
let pb = sender.draggingPasteboard
|
||||
|
||||
let content: String?
|
||||
if let url = pb.string(forType: .URL) {
|
||||
// URLs first, they get escaped as-is.
|
||||
content = Ghostty.Shell.escape(url)
|
||||
} else if let urls = pb.readObjects(forClasses: [NSURL.self]) as? [URL],
|
||||
urls.count > 0 {
|
||||
// File URLs next. They get escaped individually and then joined by a
|
||||
// space if there are multiple.
|
||||
content = urls
|
||||
.map { Ghostty.Shell.escape($0.path) }
|
||||
.joined(separator: " ")
|
||||
} else if let str = pb.string(forType: .string) {
|
||||
// Strings are not escaped because they may be copy/pasting a
|
||||
// command they want to execute.
|
||||
content = str
|
||||
} else {
|
||||
content = nil
|
||||
}
|
||||
|
||||
if let content {
|
||||
DispatchQueue.main.async {
|
||||
self.insertText(
|
||||
content,
|
||||
replacementRange: NSMakeRange(0, 0)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
38
macos/Sources/Helpers/Dock.swift
Normal file
38
macos/Sources/Helpers/Dock.swift
Normal file
@ -0,0 +1,38 @@
|
||||
import Cocoa
|
||||
|
||||
// Private API to get Dock location
|
||||
@_silgen_name("CoreDockGetOrientationAndPinning")
|
||||
func CoreDockGetOrientationAndPinning(
|
||||
_ outOrientation: UnsafeMutablePointer<Int32>,
|
||||
_ outPinning: UnsafeMutablePointer<Int32>)
|
||||
|
||||
// Private API to get the current Dock auto-hide state
|
||||
@_silgen_name("CoreDockGetAutoHideEnabled")
|
||||
func CoreDockGetAutoHideEnabled() -> Bool
|
||||
|
||||
// Toggles the Dock's auto-hide state
|
||||
@_silgen_name("CoreDockSetAutoHideEnabled")
|
||||
func CoreDockSetAutoHideEnabled(_ flag: Bool)
|
||||
|
||||
enum DockOrientation: Int {
|
||||
case top = 1
|
||||
case bottom = 2
|
||||
case left = 3
|
||||
case right = 4
|
||||
}
|
||||
|
||||
class Dock {
|
||||
/// Returns the orientation of the dock or nil if it can't be determined.
|
||||
static var orientation: DockOrientation? {
|
||||
var orientation: Int32 = 0
|
||||
var pinning: Int32 = 0
|
||||
CoreDockGetOrientationAndPinning(&orientation, &pinning)
|
||||
return .init(rawValue: Int(orientation)) ?? nil
|
||||
}
|
||||
|
||||
/// Set the dock autohide.
|
||||
static var autoHideEnabled: Bool {
|
||||
get { return CoreDockGetAutoHideEnabled() }
|
||||
set { CoreDockSetAutoHideEnabled(newValue) }
|
||||
}
|
||||
}
|
@ -307,21 +307,21 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
||||
// MARK: Dock
|
||||
|
||||
private func hideDock() {
|
||||
NSApp.presentationOptions.insert(.autoHideDock)
|
||||
NSApp.acquirePresentationOption(.autoHideDock)
|
||||
}
|
||||
|
||||
private func unhideDock() {
|
||||
NSApp.presentationOptions.remove(.autoHideDock)
|
||||
NSApp.releasePresentationOption(.autoHideDock)
|
||||
}
|
||||
|
||||
// MARK: Menu
|
||||
|
||||
func hideMenu() {
|
||||
NSApp.presentationOptions.insert(.autoHideMenuBar)
|
||||
NSApp.acquirePresentationOption(.autoHideMenuBar)
|
||||
}
|
||||
|
||||
func unhideMenu() {
|
||||
NSApp.presentationOptions.remove(.autoHideMenuBar)
|
||||
NSApp.releasePresentationOption(.autoHideMenuBar)
|
||||
}
|
||||
|
||||
/// The state that must be saved for non-native fullscreen to exit fullscreen.
|
||||
|
31
macos/Sources/Helpers/NSApplication+Extension.swift
Normal file
31
macos/Sources/Helpers/NSApplication+Extension.swift
Normal file
@ -0,0 +1,31 @@
|
||||
import Cocoa
|
||||
|
||||
extension NSApplication {
|
||||
private static var presentationOptionCounts: [NSApplication.PresentationOptions.Element: UInt] = [:]
|
||||
|
||||
/// Add a presentation option to the application and main a reference count so that and equal
|
||||
/// number of pops is required to disable it. This is useful so that multiple classes can affect global
|
||||
/// app state without overriding others.
|
||||
func acquirePresentationOption(_ option: NSApplication.PresentationOptions.Element) {
|
||||
Self.presentationOptionCounts[option, default: 0] += 1
|
||||
presentationOptions.insert(option)
|
||||
}
|
||||
|
||||
/// See acquirePresentationOption
|
||||
func releasePresentationOption(_ option: NSApplication.PresentationOptions.Element) {
|
||||
guard let value = Self.presentationOptionCounts[option] else { return }
|
||||
guard value > 0 else { return }
|
||||
if (value == 1) {
|
||||
presentationOptions.remove(option)
|
||||
Self.presentationOptionCounts.removeValue(forKey: option)
|
||||
} else {
|
||||
Self.presentationOptionCounts[option] = value - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSApplication.PresentationOptions.Element: @retroactive Hashable {
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(rawValue)
|
||||
}
|
||||
}
|
@ -9,13 +9,15 @@ extension NSPasteboard {
|
||||
|
||||
/// Gets the contents of the pasteboard as a string following a specific set of semantics.
|
||||
/// Does these things in order:
|
||||
/// - Tries to get the absolute filesystem path of the file in the pasteboard if there is one.
|
||||
/// - Tries to get the absolute filesystem path of the file in the pasteboard if there is one and ensures the file path is properly escaped.
|
||||
/// - Tries to get any string from the pasteboard.
|
||||
/// If all of the above fail, returns None.
|
||||
func getOpinionatedStringContents() -> String? {
|
||||
if let urls = readObjects(forClasses: [NSURL.self]) as? [URL],
|
||||
urls.count > 0 {
|
||||
return urls.map { $0.path }.joined(separator: " ")
|
||||
return urls
|
||||
.map { $0.isFileURL ? Ghostty.Shell.escape($0.path) : $0.absoluteString }
|
||||
.joined(separator: " ")
|
||||
}
|
||||
|
||||
return self.string(forType: .string)
|
||||
|
18
nix/vm/common-cinnamon.nix
Normal file
18
nix/vm/common-cinnamon.nix
Normal file
@ -0,0 +1,18 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common.nix
|
||||
];
|
||||
|
||||
services.xserver = {
|
||||
displayManager = {
|
||||
lightdm = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
desktopManager = {
|
||||
cinnamon = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
136
nix/vm/common-gnome.nix
Normal file
136
nix/vm/common-gnome.nix
Normal file
@ -0,0 +1,136 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
imports = [
|
||||
./common.nix
|
||||
];
|
||||
|
||||
services.xserver = {
|
||||
displayManager = {
|
||||
gdm = {
|
||||
enable = true;
|
||||
autoSuspend = false;
|
||||
};
|
||||
};
|
||||
desktopManager = {
|
||||
gnome = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [
|
||||
pkgs.gnomeExtensions.no-overview
|
||||
];
|
||||
|
||||
environment.gnome.excludePackages = with pkgs; [
|
||||
atomix
|
||||
baobab
|
||||
cheese
|
||||
epiphany
|
||||
evince
|
||||
file-roller
|
||||
geary
|
||||
gnome-backgrounds
|
||||
gnome-calculator
|
||||
gnome-calendar
|
||||
gnome-clocks
|
||||
gnome-connections
|
||||
gnome-contacts
|
||||
gnome-disk-utility
|
||||
gnome-extension-manager
|
||||
gnome-logs
|
||||
gnome-maps
|
||||
gnome-music
|
||||
gnome-photos
|
||||
gnome-software
|
||||
gnome-system-monitor
|
||||
gnome-text-editor
|
||||
gnome-themes-extra
|
||||
gnome-tour
|
||||
gnome-user-docs
|
||||
gnome-weather
|
||||
hitori
|
||||
iagno
|
||||
loupe
|
||||
nautilus
|
||||
orca
|
||||
seahorse
|
||||
simple-scan
|
||||
snapshot
|
||||
sushi
|
||||
tali
|
||||
totem
|
||||
yelp
|
||||
];
|
||||
|
||||
programs.dconf = {
|
||||
enable = true;
|
||||
profiles.user.databases = [
|
||||
{
|
||||
settings = with lib.gvariant; {
|
||||
"org/gnome/desktop/background" = {
|
||||
picture-uri = "file://${pkgs.ghostty}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png";
|
||||
picture-uri-dark = "file://${pkgs.ghostty}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png";
|
||||
picture-options = "centered";
|
||||
primary-color = "#000000000000";
|
||||
secondary-color = "#000000000000";
|
||||
};
|
||||
"org/gnome/desktop/interface" = {
|
||||
color-scheme = "prefer-dark";
|
||||
};
|
||||
"org/gnome/desktop/notifications" = {
|
||||
show-in-lock-screen = false;
|
||||
};
|
||||
"org/gnome/desktop/screensaver" = {
|
||||
lock-enabled = false;
|
||||
picture-uri = "file://${pkgs.ghostty}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png";
|
||||
picture-options = "centered";
|
||||
primary-color = "#000000000000";
|
||||
secondary-color = "#000000000000";
|
||||
};
|
||||
"org/gnome/desktop/session" = {
|
||||
idle-delay = mkUint32 0;
|
||||
};
|
||||
"org/gnome/shell" = {
|
||||
disable-user-extensions = false;
|
||||
enabled-extensions = builtins.map (x: x.extensionUuid) (
|
||||
lib.filter (p: p ? extensionUuid) config.environment.systemPackages
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
programs.geary.enable = false;
|
||||
|
||||
services.gnome = {
|
||||
gnome-browser-connector.enable = false;
|
||||
gnome-initial-setup.enable = false;
|
||||
gnome-online-accounts.enable = false;
|
||||
gnome-remote-desktop.enable = false;
|
||||
rygel.enable = false;
|
||||
};
|
||||
|
||||
system.activationScripts = {
|
||||
face = {
|
||||
text = ''
|
||||
mkdir -p /var/lib/AccountsService/{icons,users}
|
||||
|
||||
cp ${pkgs.ghostty}/share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png /var/lib/AccountsService/icons/ghostty
|
||||
|
||||
echo -e "[User]\nIcon=/var/lib/AccountsService/icons/ghostty\n" > /var/lib/AccountsService/users/ghostty
|
||||
|
||||
chown root:root /var/lib/AccountsService/users/ghostty
|
||||
chmod 0600 /var/lib/AccountsService/users/ghostty
|
||||
|
||||
chown root:root /var/lib/AccountsService/icons/ghostty
|
||||
chmod 0444 /var/lib/AccountsService/icons/ghostty
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
21
nix/vm/common-plasma6.nix
Normal file
21
nix/vm/common-plasma6.nix
Normal file
@ -0,0 +1,21 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common.nix
|
||||
];
|
||||
|
||||
services = {
|
||||
displayManager = {
|
||||
sddm = {
|
||||
enable = true;
|
||||
wayland = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
desktopManager = {
|
||||
plasma6 = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
18
nix/vm/common-xfce.nix
Normal file
18
nix/vm/common-xfce.nix
Normal file
@ -0,0 +1,18 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common.nix
|
||||
];
|
||||
|
||||
services.xserver = {
|
||||
displayManager = {
|
||||
lightdm = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
desktopManager = {
|
||||
xfce = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
83
nix/vm/common.nix
Normal file
83
nix/vm/common.nix
Normal file
@ -0,0 +1,83 @@
|
||||
{pkgs, ...}: {
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
documentation.nixos.enable = false;
|
||||
|
||||
networking.hostName = "ghostty";
|
||||
networking.domain = "mitchellh.com";
|
||||
|
||||
virtualisation.vmVariant = {
|
||||
virtualisation.memorySize = 2048;
|
||||
};
|
||||
|
||||
nix = {
|
||||
settings = {
|
||||
trusted-users = [
|
||||
"root"
|
||||
"ghostty"
|
||||
];
|
||||
};
|
||||
extraOptions = ''
|
||||
experimental-features = nix-command flakes
|
||||
'';
|
||||
};
|
||||
|
||||
users.mutableUsers = false;
|
||||
|
||||
users.groups.ghostty = {};
|
||||
|
||||
users.users.ghostty = {
|
||||
description = "Ghostty";
|
||||
group = "ghostty";
|
||||
extraGroups = ["wheel"];
|
||||
isNormalUser = true;
|
||||
initialPassword = "ghostty";
|
||||
};
|
||||
|
||||
environment.etc = {
|
||||
"xdg/autostart/com.mitchellh.ghostty.desktop" = {
|
||||
source = "${pkgs.ghostty}/share/applications/com.mitchellh.ghostty.desktop";
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [
|
||||
pkgs.kitty
|
||||
pkgs.fish
|
||||
pkgs.ghostty
|
||||
pkgs.helix
|
||||
pkgs.neovim
|
||||
pkgs.xterm
|
||||
pkgs.zsh
|
||||
];
|
||||
|
||||
security.polkit = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.dbus = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.displayManager = {
|
||||
autoLogin = {
|
||||
user = "ghostty";
|
||||
};
|
||||
};
|
||||
|
||||
services.libinput = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.qemuGuest = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.spice-vdagentd = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.xserver = {
|
||||
enable = true;
|
||||
};
|
||||
}
|
12
nix/vm/create-cinnamon.nix
Normal file
12
nix/vm/create-cinnamon.nix
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}:
|
||||
import ./create.nix {
|
||||
inherit system nixpkgs overlay module uid gid;
|
||||
common = ./common-cinnamon.nix;
|
||||
}
|
12
nix/vm/create-gnome.nix
Normal file
12
nix/vm/create-gnome.nix
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}:
|
||||
import ./create.nix {
|
||||
inherit system nixpkgs overlay module uid gid;
|
||||
common = ./common-gnome.nix;
|
||||
}
|
12
nix/vm/create-plasma6.nix
Normal file
12
nix/vm/create-plasma6.nix
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}:
|
||||
import ./create.nix {
|
||||
inherit system nixpkgs overlay module uid gid;
|
||||
common = ./common-plasma6.nix;
|
||||
}
|
12
nix/vm/create-xfce.nix
Normal file
12
nix/vm/create-xfce.nix
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}:
|
||||
import ./create.nix {
|
||||
inherit system nixpkgs overlay module uid gid;
|
||||
common = ./common-xfce.nix;
|
||||
}
|
42
nix/vm/create.nix
Normal file
42
nix/vm/create.nix
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
common ? ./common.nix,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [
|
||||
overlay
|
||||
];
|
||||
};
|
||||
in
|
||||
nixpkgs.lib.nixosSystem {
|
||||
system = builtins.replaceStrings ["darwin"] ["linux"] system;
|
||||
modules = [
|
||||
{
|
||||
virtualisation.vmVariant = {
|
||||
virtualisation.host.pkgs = pkgs;
|
||||
};
|
||||
|
||||
nixpkgs.overlays = [
|
||||
overlay
|
||||
];
|
||||
|
||||
users.groups.ghostty = {
|
||||
gid = gid;
|
||||
};
|
||||
|
||||
users.users.ghostty = {
|
||||
uid = uid;
|
||||
};
|
||||
|
||||
system.stateVersion = nixpkgs.lib.trivial.release;
|
||||
}
|
||||
common
|
||||
module
|
||||
];
|
||||
}
|
7
nix/vm/wayland-cinnamon.nix
Normal file
7
nix/vm/wayland-cinnamon.nix
Normal file
@ -0,0 +1,7 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-cinnamon.nix
|
||||
];
|
||||
|
||||
services.displayManager.defaultSession = "cinnamon-wayland";
|
||||
}
|
9
nix/vm/wayland-gnome.nix
Normal file
9
nix/vm/wayland-gnome.nix
Normal file
@ -0,0 +1,9 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-gnome.nix
|
||||
];
|
||||
|
||||
services.displayManager = {
|
||||
defaultSession = "gnome";
|
||||
};
|
||||
}
|
6
nix/vm/wayland-plasma6.nix
Normal file
6
nix/vm/wayland-plasma6.nix
Normal file
@ -0,0 +1,6 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-plasma6.nix
|
||||
];
|
||||
services.displayManager.defaultSession = "plasma";
|
||||
}
|
7
nix/vm/x11-cinnamon.nix
Normal file
7
nix/vm/x11-cinnamon.nix
Normal file
@ -0,0 +1,7 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-cinnamon.nix
|
||||
];
|
||||
|
||||
services.displayManager.defaultSession = "cinnamon";
|
||||
}
|
9
nix/vm/x11-gnome.nix
Normal file
9
nix/vm/x11-gnome.nix
Normal file
@ -0,0 +1,9 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-gnome.nix
|
||||
];
|
||||
|
||||
services.displayManager = {
|
||||
defaultSession = "gnome-xorg";
|
||||
};
|
||||
}
|
6
nix/vm/x11-plasma6.nix
Normal file
6
nix/vm/x11-plasma6.nix
Normal file
@ -0,0 +1,6 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-plasma6.nix
|
||||
];
|
||||
services.displayManager.defaultSession = "plasmax11";
|
||||
}
|
7
nix/vm/x11-xfce.nix
Normal file
7
nix/vm/x11-xfce.nix
Normal file
@ -0,0 +1,7 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-xfce.nix
|
||||
];
|
||||
|
||||
services.displayManager.defaultSession = "xfce";
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
||||
# more details.
|
||||
"sha256-Nx1tOhDnEZ7LVi/pKxYS3sg/Sf8TAUXDmST6EtBgDoQ="
|
||||
"sha256-Bjy31evaKgpRX1mGwAFkai44eiiorTV1gW3VdP9Ins8="
|
||||
|
@ -18,9 +18,72 @@ pub const ColorSpace = opaque {
|
||||
) orelse Allocator.Error.OutOfMemory;
|
||||
}
|
||||
|
||||
pub fn createNamed(name: Name) Allocator.Error!*ColorSpace {
|
||||
return @as(
|
||||
?*ColorSpace,
|
||||
@ptrFromInt(@intFromPtr(c.CGColorSpaceCreateWithName(name.cfstring()))),
|
||||
) orelse Allocator.Error.OutOfMemory;
|
||||
}
|
||||
|
||||
pub fn release(self: *ColorSpace) void {
|
||||
c.CGColorSpaceRelease(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub const Name = enum {
|
||||
/// This color space uses the DCI P3 primaries, a D65 white point, and
|
||||
/// the sRGB transfer function.
|
||||
displayP3,
|
||||
/// The Display P3 color space with a linear transfer function and
|
||||
/// extended-range values.
|
||||
extendedLinearDisplayP3,
|
||||
/// The sRGB colorimetry and non-linear transfer function are specified
|
||||
/// in IEC 61966-2-1.
|
||||
sRGB,
|
||||
/// This color space has the same colorimetry as `sRGB`, but uses a
|
||||
/// linear transfer function.
|
||||
linearSRGB,
|
||||
/// This color space has the same colorimetry as `sRGB`, but you can
|
||||
/// encode component values below `0.0` and above `1.0`. Negative values
|
||||
/// are encoded as the signed reflection of the original encoding
|
||||
/// function, as shown in the formula below:
|
||||
/// ```
|
||||
/// extendedTransferFunction(x) = sign(x) * sRGBTransferFunction(abs(x))
|
||||
/// ```
|
||||
extendedSRGB,
|
||||
/// This color space has the same colorimetry as `sRGB`; in addition,
|
||||
/// you may encode component values below `0.0` and above `1.0`.
|
||||
extendedLinearSRGB,
|
||||
/// ...
|
||||
genericGrayGamma2_2,
|
||||
/// ...
|
||||
linearGray,
|
||||
/// This color space has the same colorimetry as `genericGrayGamma2_2`,
|
||||
/// but you can encode component values below `0.0` and above `1.0`.
|
||||
/// Negative values are encoded as the signed reflection of the
|
||||
/// original encoding function, as shown in the formula below:
|
||||
/// ```
|
||||
/// extendedGrayTransferFunction(x) = sign(x) * gamma22Function(abs(x))
|
||||
/// ```
|
||||
extendedGray,
|
||||
/// This color space has the same colorimetry as `linearGray`; in
|
||||
/// addition, you may encode component values below `0.0` and above `1.0`.
|
||||
extendedLinearGray,
|
||||
|
||||
fn cfstring(self: Name) c.CFStringRef {
|
||||
return switch (self) {
|
||||
.displayP3 => c.kCGColorSpaceDisplayP3,
|
||||
.extendedLinearDisplayP3 => c.kCGColorSpaceExtendedLinearDisplayP3,
|
||||
.sRGB => c.kCGColorSpaceSRGB,
|
||||
.extendedSRGB => c.kCGColorSpaceExtendedSRGB,
|
||||
.linearSRGB => c.kCGColorSpaceLinearSRGB,
|
||||
.extendedLinearSRGB => c.kCGColorSpaceExtendedLinearSRGB,
|
||||
.genericGrayGamma2_2 => c.kCGColorSpaceGenericGrayGamma2_2,
|
||||
.extendedGray => c.kCGColorSpaceExtendedGray,
|
||||
.linearGray => c.kCGColorSpaceLinearGray,
|
||||
.extendedLinearGray => c.kCGColorSpaceExtendedLinearGray,
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
test {
|
||||
|
@ -162,4 +162,26 @@ pub const Binding = struct {
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn copySubImage2D(
|
||||
b: Binding,
|
||||
level: c.GLint,
|
||||
xoffset: c.GLint,
|
||||
yoffset: c.GLint,
|
||||
x: c.GLint,
|
||||
y: c.GLint,
|
||||
width: c.GLsizei,
|
||||
height: c.GLsizei,
|
||||
) !void {
|
||||
glad.context.CopyTexSubImage2D.?(
|
||||
@intFromEnum(b.target),
|
||||
level,
|
||||
xoffset,
|
||||
yoffset,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -55,7 +55,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
|
||||
|
||||
c.wuffs_base__pixel_config__set(
|
||||
&image_config.pixcfg,
|
||||
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
|
||||
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL,
|
||||
c.WUFFS_BASE__PIXEL_SUBSAMPLING__NONE,
|
||||
width,
|
||||
height,
|
||||
@ -95,16 +95,6 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
var frame_config: c.wuffs_base__frame_config = undefined;
|
||||
{
|
||||
const status = c.wuffs_jpeg__decoder__decode_frame_config(
|
||||
decoder,
|
||||
&frame_config,
|
||||
&source_buffer,
|
||||
);
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
{
|
||||
const status = c.wuffs_jpeg__decoder__decode_frame(
|
||||
decoder,
|
||||
|
@ -55,7 +55,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
|
||||
|
||||
c.wuffs_base__pixel_config__set(
|
||||
&image_config.pixcfg,
|
||||
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
|
||||
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL,
|
||||
c.WUFFS_BASE__PIXEL_SUBSAMPLING__NONE,
|
||||
width,
|
||||
height,
|
||||
@ -95,16 +95,6 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
var frame_config: c.wuffs_base__frame_config = undefined;
|
||||
{
|
||||
const status = c.wuffs_png__decoder__decode_frame_config(
|
||||
decoder,
|
||||
&frame_config,
|
||||
&source_buffer,
|
||||
);
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
{
|
||||
const status = c.wuffs_png__decoder__decode_frame(
|
||||
decoder,
|
||||
|
@ -1041,6 +1041,9 @@ fn mouseRefreshLinks(
|
||||
pos_vp: terminal.point.Coordinate,
|
||||
over_link: bool,
|
||||
) !void {
|
||||
// If the position is outside our viewport, do nothing
|
||||
if (pos.x < 0 or pos.y < 0) return;
|
||||
|
||||
self.mouse.link_point = pos_vp;
|
||||
|
||||
if (try self.linkAtPos(pos)) |link| {
|
||||
@ -3563,22 +3566,21 @@ fn dragLeftClickTriple(
|
||||
const screen = &self.io.terminal.screen;
|
||||
const click_pin = self.mouse.left_click_pin.?.*;
|
||||
|
||||
// Get the word under our current point. If there isn't a word, do nothing.
|
||||
const word = screen.selectLine(.{ .pin = drag_pin }) orelse return;
|
||||
// Get the line selection under our current drag point. If there isn't a
|
||||
// line, do nothing.
|
||||
const line = screen.selectLine(.{ .pin = drag_pin }) orelse return;
|
||||
|
||||
// Get our selection to grow it. If we don't have a selection, start it now.
|
||||
// We may not have a selection if we started our dbl-click in an area
|
||||
// that had no data, then we dragged our mouse into an area with data.
|
||||
var sel = screen.selectLine(.{ .pin = click_pin }) orelse {
|
||||
try self.setSelection(word);
|
||||
return;
|
||||
};
|
||||
// Get the selection under our click point. We first try to trim
|
||||
// whitespace if we've selected a word. But if no word exists then
|
||||
// we select the blank line.
|
||||
const sel_ = screen.selectLine(.{ .pin = click_pin }) orelse
|
||||
screen.selectLine(.{ .pin = click_pin, .whitespace = null });
|
||||
|
||||
// Grow our selection
|
||||
var sel = sel_ orelse return;
|
||||
if (drag_pin.before(click_pin)) {
|
||||
sel.startPtr().* = word.start();
|
||||
sel.startPtr().* = line.start();
|
||||
} else {
|
||||
sel.endPtr().* = word.end();
|
||||
sel.endPtr().* = line.end();
|
||||
}
|
||||
try self.setSelection(sel);
|
||||
}
|
||||
@ -4177,6 +4179,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
{},
|
||||
),
|
||||
|
||||
.toggle_maximize => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_maximize,
|
||||
{},
|
||||
),
|
||||
|
||||
.toggle_fullscreen => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_fullscreen,
|
||||
|
@ -92,6 +92,9 @@ pub const Action = union(Key) {
|
||||
/// Close all open windows.
|
||||
close_all_windows,
|
||||
|
||||
/// Toggle maximized window state.
|
||||
toggle_maximize,
|
||||
|
||||
/// Toggle fullscreen mode.
|
||||
toggle_fullscreen: Fullscreen,
|
||||
|
||||
@ -231,6 +234,7 @@ pub const Action = union(Key) {
|
||||
close_tab,
|
||||
new_split,
|
||||
close_all_windows,
|
||||
toggle_maximize,
|
||||
toggle_fullscreen,
|
||||
toggle_tab_overview,
|
||||
toggle_window_decorations,
|
||||
|
@ -638,7 +638,7 @@ pub const Surface = struct {
|
||||
.y = @floatCast(opts.scale_factor),
|
||||
},
|
||||
.size = .{ .width = 800, .height = 600 },
|
||||
.cursor_pos = .{ .x = 0, .y = 0 },
|
||||
.cursor_pos = .{ .x = -1, .y = -1 },
|
||||
.keymap_state = .{},
|
||||
};
|
||||
|
||||
@ -1958,7 +1958,7 @@ pub const CAPI = struct {
|
||||
_ = CGSSetWindowBackgroundBlurRadius(
|
||||
CGSDefaultConnectionForThread(),
|
||||
nswindow.msgSend(usize, objc.sel("windowNumber"), .{}),
|
||||
@intCast(config.@"background-blur-radius".cval()),
|
||||
@intCast(config.@"background-blur".cval()),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -237,6 +237,7 @@ pub const App = struct {
|
||||
.color_change,
|
||||
.pwd,
|
||||
.config_change,
|
||||
.toggle_maximize,
|
||||
=> log.info("unimplemented action={}", .{action}),
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,11 @@ clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
|
||||
/// This is set to false when the main loop should exit.
|
||||
running: bool = true,
|
||||
|
||||
/// If we should retry querying D-Bus for the color scheme with the deprecated
|
||||
/// Read method, instead of the recommended ReadOne method. This is kind of
|
||||
/// nasty to have as struct state but its just a byte...
|
||||
dbus_color_scheme_retry: bool = true,
|
||||
|
||||
/// The base path of the transient cgroup used to put all surfaces
|
||||
/// into their own cgroup. This is only set if cgroups are enabled
|
||||
/// and initialization was successful.
|
||||
@ -507,6 +512,7 @@ pub fn performAction(
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
}),
|
||||
.toggle_maximize => self.toggleMaximize(target),
|
||||
.toggle_fullscreen => self.toggleFullscreen(target, value),
|
||||
|
||||
.new_tab => try self.newTab(target),
|
||||
@ -578,7 +584,7 @@ fn closeTab(_: *App, target: apprt.Target) !void {
|
||||
return;
|
||||
};
|
||||
|
||||
tab.window.closeTab(tab);
|
||||
tab.closeWithConfirmation();
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -709,6 +715,22 @@ fn controlInspector(
|
||||
surface.controlInspector(mode);
|
||||
}
|
||||
|
||||
fn toggleMaximize(_: *App, target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"toggleMaximize invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
window.toggleMaximize();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn toggleFullscreen(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
@ -1254,7 +1276,8 @@ pub fn run(self: *App) !void {
|
||||
self.transient_cgroup_base = path;
|
||||
} else log.debug("cgroup isolation disabled config={}", .{self.config.@"linux-cgroup"});
|
||||
|
||||
// Setup our D-Bus connection for listening to settings changes.
|
||||
// Setup our D-Bus connection for listening to settings changes,
|
||||
// and asynchronously request the initial color scheme
|
||||
self.initDbus();
|
||||
|
||||
// Setup our menu items
|
||||
@ -1262,9 +1285,6 @@ pub fn run(self: *App) !void {
|
||||
self.initMenu();
|
||||
self.initContextMenu();
|
||||
|
||||
// Setup our initial color scheme
|
||||
self.colorSchemeEvent(self.getColorScheme());
|
||||
|
||||
// On startup, we want to check for configuration errors right away
|
||||
// so we can show our error window. We also need to setup other initial
|
||||
// state.
|
||||
@ -1312,6 +1332,22 @@ fn initDbus(self: *App) void {
|
||||
self,
|
||||
null,
|
||||
);
|
||||
|
||||
// Request the initial color scheme asynchronously.
|
||||
c.g_dbus_connection_call(
|
||||
dbus,
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings",
|
||||
"ReadOne",
|
||||
c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
|
||||
c.G_VARIANT_TYPE("(v)"),
|
||||
c.G_DBUS_CALL_FLAGS_NONE,
|
||||
-1,
|
||||
null,
|
||||
dbusColorSchemeCallback,
|
||||
self,
|
||||
);
|
||||
}
|
||||
|
||||
// This timeout function is started when no surfaces are open. It can be
|
||||
@ -1549,61 +1585,37 @@ fn gtkWindowIsActive(
|
||||
core_app.focusEvent(false);
|
||||
}
|
||||
|
||||
/// Call a D-Bus method to determine the current color scheme. If there
|
||||
/// is any error at any point we'll log the error and return "light"
|
||||
pub fn getColorScheme(self: *App) apprt.ColorScheme {
|
||||
const dbus_connection = c.g_application_get_dbus_connection(@ptrCast(self.app));
|
||||
fn dbusColorSchemeCallback(
|
||||
source_object: [*c]c.GObject,
|
||||
res: ?*c.GAsyncResult,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *App = @ptrCast(@alignCast(ud.?));
|
||||
const dbus: *c.GDBusConnection = @ptrCast(source_object);
|
||||
|
||||
var err: ?*c.GError = null;
|
||||
defer if (err) |e| c.g_error_free(e);
|
||||
|
||||
const value = c.g_dbus_connection_call_sync(
|
||||
dbus_connection,
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings",
|
||||
"ReadOne",
|
||||
c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
|
||||
c.G_VARIANT_TYPE("(v)"),
|
||||
c.G_DBUS_CALL_FLAGS_NONE,
|
||||
-1,
|
||||
null,
|
||||
&err,
|
||||
) orelse {
|
||||
if (err) |e| {
|
||||
// If ReadOne is not yet implemented, fall back to deprecated "Read" method
|
||||
// Error code: GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod: No such method “ReadOne”
|
||||
if (e.code == 19) {
|
||||
return self.getColorSchemeDeprecated();
|
||||
}
|
||||
// Otherwise, log the error and return .light
|
||||
log.err("unable to get current color scheme: {s}", .{e.message});
|
||||
}
|
||||
return .light;
|
||||
};
|
||||
defer c.g_variant_unref(value);
|
||||
|
||||
if (c.g_dbus_connection_call_finish(dbus, res, &err)) |value| {
|
||||
if (c.g_variant_is_of_type(value, c.G_VARIANT_TYPE("(v)")) == 1) {
|
||||
var inner: ?*c.GVariant = null;
|
||||
c.g_variant_get(value, "(v)", &inner);
|
||||
defer c.g_variant_unref(inner);
|
||||
if (c.g_variant_is_of_type(inner, c.G_VARIANT_TYPE("u")) == 1) {
|
||||
return if (c.g_variant_get_uint32(inner) == 1) .dark else .light;
|
||||
self.colorSchemeEvent(if (c.g_variant_get_uint32(inner) == 1)
|
||||
.dark
|
||||
else
|
||||
.light);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return .light;
|
||||
}
|
||||
|
||||
/// Call the deprecated D-Bus "Read" method to determine the current color scheme. If
|
||||
/// there is any error at any point we'll log the error and return "light"
|
||||
fn getColorSchemeDeprecated(self: *App) apprt.ColorScheme {
|
||||
const dbus_connection = c.g_application_get_dbus_connection(@ptrCast(self.app));
|
||||
var err: ?*c.GError = null;
|
||||
defer if (err) |e| c.g_error_free(e);
|
||||
|
||||
const value = c.g_dbus_connection_call_sync(
|
||||
dbus_connection,
|
||||
} else if (err) |e| {
|
||||
// If ReadOne is not yet implemented, fall back to deprecated "Read" method
|
||||
// Error code: GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod: No such method “ReadOne”
|
||||
if (self.dbus_color_scheme_retry and e.code == 19) {
|
||||
self.dbus_color_scheme_retry = false;
|
||||
c.g_dbus_connection_call(
|
||||
dbus,
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings",
|
||||
@ -1613,29 +1625,18 @@ fn getColorSchemeDeprecated(self: *App) apprt.ColorScheme {
|
||||
c.G_DBUS_CALL_FLAGS_NONE,
|
||||
-1,
|
||||
null,
|
||||
&err,
|
||||
) orelse {
|
||||
if (err) |e| log.err("Read method failed: {s}", .{e.message});
|
||||
return .light;
|
||||
};
|
||||
defer c.g_variant_unref(value);
|
||||
|
||||
if (c.g_variant_is_of_type(value, c.G_VARIANT_TYPE("(v)")) == 1) {
|
||||
var inner: ?*c.GVariant = null;
|
||||
c.g_variant_get(value, "(v)", &inner);
|
||||
defer if (inner) |i| c.g_variant_unref(i);
|
||||
|
||||
if (inner) |i| {
|
||||
const child = c.g_variant_get_child_value(i, 0) orelse {
|
||||
return .light;
|
||||
};
|
||||
defer c.g_variant_unref(child);
|
||||
|
||||
const val = c.g_variant_get_uint32(child);
|
||||
return if (val == 1) .dark else .light;
|
||||
dbusColorSchemeCallback,
|
||||
self,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, log the error and return .light
|
||||
log.warn("unable to get current color scheme: {s}", .{e.message});
|
||||
}
|
||||
return .light;
|
||||
|
||||
// Fall back
|
||||
self.colorSchemeEvent(.light);
|
||||
}
|
||||
|
||||
/// This will be called by D-Bus when the style changes between light & dark.
|
||||
@ -1864,7 +1865,6 @@ fn initContextMenu(self: *App) void {
|
||||
c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
|
||||
}
|
||||
|
||||
if (!self.config.@"window-decoration") {
|
||||
const section = c.g_menu_new();
|
||||
defer c.g_object_unref(section);
|
||||
const submenu = c.g_menu_new();
|
||||
@ -1873,7 +1873,6 @@ fn initContextMenu(self: *App) void {
|
||||
initMenuContent(@ptrCast(submenu));
|
||||
c.g_menu_append_submenu(section, "Menu", @ptrCast(@alignCast(submenu)));
|
||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
||||
}
|
||||
|
||||
self.context_menu = menu;
|
||||
}
|
||||
|
@ -368,10 +368,9 @@ cursor_pos: apprt.CursorPos,
|
||||
inspector: ?*inspector.Inspector = null,
|
||||
|
||||
/// Key input states. See gtkKeyPressed for detailed descriptions.
|
||||
in_keypress: bool = false,
|
||||
in_keyevent: bool = false,
|
||||
im_context: *c.GtkIMContext,
|
||||
im_composing: bool = false,
|
||||
im_commit_buffered: bool = false,
|
||||
im_buf: [128]u8 = undefined,
|
||||
im_len: u7 = 0,
|
||||
|
||||
@ -560,7 +559,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
.font_size = font_size,
|
||||
.init_config = init_config,
|
||||
.size = .{ .width = 800, .height = 600 },
|
||||
.cursor_pos = .{ .x = 0, .y = 0 },
|
||||
.cursor_pos = .{ .x = -1, .y = -1 },
|
||||
.im_context = im_context,
|
||||
.cgroup_path = cgroup_path,
|
||||
};
|
||||
@ -634,9 +633,6 @@ fn realize(self: *Surface) !void {
|
||||
try self.core_surface.setFontSize(size);
|
||||
}
|
||||
|
||||
// Set the initial color scheme
|
||||
try self.core_surface.colorSchemeCallback(self.app.getColorScheme());
|
||||
|
||||
// Note we're realized
|
||||
self.realized = true;
|
||||
}
|
||||
@ -1137,7 +1133,7 @@ pub fn setClipboardString(
|
||||
c.gdk_clipboard_set_text(clipboard, val.ptr);
|
||||
// We only toast if we are copying to the standard clipboard.
|
||||
if (clipboard_type == .standard and
|
||||
self.app.config.@"adw-toast".@"clipboard-copy")
|
||||
self.app.config.@"app-notifications".@"clipboard-copy")
|
||||
{
|
||||
if (self.container.window()) |window|
|
||||
window.sendToast("Copied to clipboard");
|
||||
@ -1384,12 +1380,10 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque)
|
||||
};
|
||||
|
||||
if (self.container.window()) |window| {
|
||||
if (window.winproto) |*winproto| {
|
||||
winproto.resizeEvent() catch |err| {
|
||||
window.winproto.resizeEvent() catch |err| {
|
||||
log.warn("failed to notify window protocol of resize={}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
self.resize_overlay.maybeShow();
|
||||
}
|
||||
@ -1496,17 +1490,22 @@ fn gtkMouseMotion(
|
||||
.y = @floatCast(scaled.y),
|
||||
};
|
||||
|
||||
// When the GLArea is resized under the mouse, GTK issues a mouse motion
|
||||
// event. This has the unfortunate side effect of causing focus to potentially
|
||||
// change when `focus-follows-mouse` is enabled. To prevent this, we check
|
||||
// if the cursor is still in the same place as the last event and only grab
|
||||
// focus if it has moved.
|
||||
// There seem to be at least two cases where GTK issues a mouse motion
|
||||
// event without the cursor actually moving:
|
||||
// 1. GLArea is resized under the mouse. This has the unfortunate
|
||||
// side effect of causing focus to potentially change when
|
||||
// `focus-follows-mouse` is enabled.
|
||||
// 2. The window title is updated. This can cause the mouse to unhide
|
||||
// incorrectly when hide-mouse-when-typing is enabled.
|
||||
// To prevent incorrect behavior, we'll only grab focus and
|
||||
// continue with callback logic if the cursor has actually moved.
|
||||
const is_cursor_still = @abs(self.cursor_pos.x - pos.x) < 1 and
|
||||
@abs(self.cursor_pos.y - pos.y) < 1;
|
||||
|
||||
if (!is_cursor_still) {
|
||||
// If we don't have focus, and we want it, grab it.
|
||||
const gl_widget = @as(*c.GtkWidget, @ptrCast(self.gl_area));
|
||||
if (!is_cursor_still and c.gtk_widget_has_focus(gl_widget) == 0 and self.app.config.@"focus-follows-mouse") {
|
||||
if (c.gtk_widget_has_focus(gl_widget) == 0 and self.app.config.@"focus-follows-mouse") {
|
||||
self.grabFocus();
|
||||
}
|
||||
|
||||
@ -1522,6 +1521,7 @@ fn gtkMouseMotion(
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkMouseLeave(
|
||||
ec: *c.GtkEventControllerMotion,
|
||||
@ -1600,30 +1600,36 @@ fn gtkKeyReleased(
|
||||
)) 1 else 0;
|
||||
}
|
||||
|
||||
/// Key press event. This is where we do ALL of our key handling,
|
||||
/// translation to keyboard layouts, dead key handling, etc. Key handling
|
||||
/// is complicated so this comment will explain what's going on.
|
||||
/// Key press event (press or release).
|
||||
///
|
||||
/// At a high level, we want to construct an `input.KeyEvent` and
|
||||
/// pass that to `keyCallback`. At a low level, this is more complicated
|
||||
/// than it appears because we need to construct all of this information
|
||||
/// and its not given to us.
|
||||
///
|
||||
/// For press events, we run the keypress through the input method context
|
||||
/// in order to determine if we're in a dead key state, completed unicode
|
||||
/// char, etc. This all happens through various callbacks: preedit, commit,
|
||||
/// etc. These inspect "in_keypress" if they have to and set some instance
|
||||
/// state.
|
||||
/// For all events, we run the GdkEvent through the input method context.
|
||||
/// This allows the input method to capture the event and trigger
|
||||
/// callbacks such as preedit, commit, etc.
|
||||
///
|
||||
/// We then take all of the information in order to determine if we have
|
||||
/// There are a couple important aspects to the prior paragraph: we must
|
||||
/// send ALL events through the input method context. This is because
|
||||
/// input methods use both key press and key release events to determine
|
||||
/// the state of the input method. For example, fcitx uses key release
|
||||
/// events on modifiers (i.e. ctrl+shift) to switch the input method.
|
||||
///
|
||||
/// We set some state to note we're in a key event (self.in_keyevent)
|
||||
/// because some of the input method callbacks change behavior based on
|
||||
/// this state. For example, we don't want to send character events
|
||||
/// like "a" via the input "commit" event if we're actively processing
|
||||
/// a keypress because we'd lose access to the keycode information.
|
||||
/// However, a "commit" event may still happen outside of a keypress
|
||||
/// event from e.g. a tablet or on-screen keyboard.
|
||||
///
|
||||
/// Finally, we take all of the information in order to determine if we have
|
||||
/// a unicode character or if we have to map the keyval to a code to
|
||||
/// get the underlying logical key, etc.
|
||||
///
|
||||
/// Finally, we can emit the keyCallback.
|
||||
///
|
||||
/// Note we ALSO have an IMContext attached directly to the widget
|
||||
/// which can emit preedit and commit callbacks. But, if we're not
|
||||
/// in a keypress, we let those automatically work.
|
||||
/// Then we can emit the keyCallback.
|
||||
pub fn keyEvent(
|
||||
self: *Surface,
|
||||
action: input.Action,
|
||||
@ -1632,26 +1638,15 @@ pub fn keyEvent(
|
||||
keycode: c.guint,
|
||||
gtk_mods: c.GdkModifierType,
|
||||
) bool {
|
||||
// log.warn("GTKIM: keyEvent action={}", .{action});
|
||||
const event = c.gtk_event_controller_get_current_event(
|
||||
@ptrCast(ec_key),
|
||||
) orelse return false;
|
||||
|
||||
const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
|
||||
|
||||
// Get the unshifted unicode value of the keyval. This is used
|
||||
// by the Kitty keyboard protocol.
|
||||
const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted(
|
||||
@ptrCast(self.gl_area),
|
||||
event,
|
||||
keycode,
|
||||
);
|
||||
|
||||
// We always reset our committed text when ending a keypress so that
|
||||
// future keypresses don't think we have a commit event.
|
||||
defer self.im_len = 0;
|
||||
|
||||
// We only want to send the event through the IM context if we're a press
|
||||
if (action == .press or action == .repeat) {
|
||||
// The block below is all related to input method handling. See the function
|
||||
// comment for some high level details and then the comments within
|
||||
// the block for more specifics.
|
||||
{
|
||||
// This can trigger an input method so we need to notify the im context
|
||||
// where the cursor is so it can render the dropdowns in the correct
|
||||
// place.
|
||||
@ -1663,41 +1658,94 @@ pub fn keyEvent(
|
||||
.height = 1,
|
||||
});
|
||||
|
||||
// We mark that we're in a keypress event. We use this in our
|
||||
// IM commit callback to determine if we need to send a char callback
|
||||
// to the core surface or not.
|
||||
self.in_keypress = true;
|
||||
defer self.in_keypress = false;
|
||||
|
||||
// Pass the event through the IM controller to handle dead key states.
|
||||
// Filter is true if the event was handled by the IM controller.
|
||||
const im_handled = c.gtk_im_context_filter_keypress(self.im_context, event) != 0;
|
||||
// log.warn("im_handled={} im_len={} im_composing={}", .{ im_handled, self.im_len, self.im_composing });
|
||||
|
||||
// If this is a dead key, then we're composing a character and
|
||||
// we need to set our proper preedit state.
|
||||
if (self.im_composing) preedit: {
|
||||
const text = self.im_buf[0..self.im_len];
|
||||
self.core_surface.preeditCallback(text) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
break :preedit;
|
||||
// Pass the event through the IM controller. This will return true
|
||||
// if the input method handled the event.
|
||||
//
|
||||
// Confusingly, not all events handled by the input method result
|
||||
// in this returning true so we have to maintain some local state to
|
||||
// find those and in one case we simply lose information.
|
||||
//
|
||||
// - If we change the input method via keypress while we have preedit
|
||||
// text, the input method will commit the pending text but will not
|
||||
// mark it as handled. We use the `was_composing` variable to detect
|
||||
// this case.
|
||||
//
|
||||
// - If we switch input methods (i.e. via ctrl+shift with fcitx),
|
||||
// the input method will handle the key release event but will not
|
||||
// mark it as handled. I don't know any way to detect this case so
|
||||
// it will result in a key event being sent to the key callback.
|
||||
// For Kitty text encoding, this will result in modifiers being
|
||||
// triggered despite being technically consumed. At the time of
|
||||
// writing, both Kitty and Alacritty have the same behavior. I
|
||||
// know of no way to fix this.
|
||||
const was_composing = self.im_composing;
|
||||
const im_handled = filter: {
|
||||
// We note that we're in a keypress because we want some logic to
|
||||
// depend on this. For example, we don't want to send character events
|
||||
// like "a" via the input "commit" event if we're actively processing
|
||||
// a keypress because we'd lose access to the keycode information.
|
||||
self.in_keyevent = true;
|
||||
defer self.in_keyevent = false;
|
||||
break :filter c.gtk_im_context_filter_keypress(
|
||||
self.im_context,
|
||||
event,
|
||||
) != 0;
|
||||
};
|
||||
// log.warn("GTKIM: im_handled={} im_len={} im_composing={}", .{
|
||||
// im_handled,
|
||||
// self.im_len,
|
||||
// self.im_composing,
|
||||
// });
|
||||
|
||||
// If we're composing then we don't want to send the key
|
||||
// event to the core surface so we always return immediately.
|
||||
if (im_handled) return true;
|
||||
} else {
|
||||
// If we aren't composing, then we set our preedit to
|
||||
// empty no matter what.
|
||||
self.core_surface.preeditCallback(null) catch {};
|
||||
// If the input method handled the event, you would think we would
|
||||
// never proceed with key encoding for Ghostty but that is not the
|
||||
// case. Input methods will handle basic character encoding like
|
||||
// typing "a" and we want to associate that with the key event.
|
||||
// So we have to check additional state to determine if we exit.
|
||||
if (im_handled) {
|
||||
// If we are composing then we're in a preedit state and do
|
||||
// not want to encode any keys. For example: type a deadkey
|
||||
// such as single quote on a US international keyboard layout.
|
||||
if (self.im_composing) return true;
|
||||
|
||||
// If the IM handled this and we have no text, then we just
|
||||
// return because this probably just changed the input method
|
||||
// or something.
|
||||
if (im_handled and self.im_len == 0) return true;
|
||||
// If we were composing and now we're not it means that we committed
|
||||
// the text. We also don't want to encode a key event for this.
|
||||
// Example: enable Japanese input method, press "konn" and then
|
||||
// press enter. The final enter should not be encoded and "konn"
|
||||
// (in hiragana) should be written as "こん".
|
||||
if (was_composing) return true;
|
||||
|
||||
// Not composing and our input method buffer is empty. This could
|
||||
// mean that the input method reacted to this event by activating
|
||||
// an onscreen keyboard or something equivalent. We don't know.
|
||||
// But the input method handled it and didn't give us text so
|
||||
// we will just assume we should not encode this. This handles a
|
||||
// real scenario when ibus starts the emoji input method
|
||||
// (super+.).
|
||||
if (self.im_len == 0) return true;
|
||||
}
|
||||
|
||||
// At this point, for the sake of explanation of internal state:
|
||||
// it is possible that im_len > 0 and im_composing == false. This
|
||||
// means that we received a commit event from the input method that
|
||||
// we want associated with the key event. This is common: its how
|
||||
// basic character translation for simple inputs like "a" work.
|
||||
}
|
||||
|
||||
// We always reset the length of the im buffer. There's only one scenario
|
||||
// we reach this point with im_len > 0 and that's if we received a commit
|
||||
// event from the input method. We don't want to keep that state around
|
||||
// since we've handled it here.
|
||||
defer self.im_len = 0;
|
||||
|
||||
// Get the keyvals for this event.
|
||||
const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
|
||||
const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted(
|
||||
@ptrCast(self.gl_area),
|
||||
event,
|
||||
keycode,
|
||||
);
|
||||
|
||||
// We want to get the physical unmapped key to process physical keybinds.
|
||||
// (These are keybinds explicitly marked as requesting physical mapping).
|
||||
const physical_key = keycode: for (input.keycodes.entries) |entry| {
|
||||
@ -1830,12 +1878,11 @@ fn gtkInputPreeditStart(
|
||||
_: *c.GtkIMContext,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
//log.debug("preedit start", .{});
|
||||
// log.warn("GTKIM: preedit start", .{});
|
||||
const self = userdataSelf(ud.?);
|
||||
if (!self.in_keypress) return;
|
||||
|
||||
// Mark that we are now composing a string with a dead key state.
|
||||
// We'll record the string in the preedit-changed callback.
|
||||
// Start our composing state for the input method and reset our
|
||||
// input buffer to empty.
|
||||
self.im_composing = true;
|
||||
self.im_len = 0;
|
||||
}
|
||||
@ -1844,54 +1891,35 @@ fn gtkInputPreeditChanged(
|
||||
ctx: *c.GtkIMContext,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
// log.warn("GTKIM: preedit change", .{});
|
||||
const self = userdataSelf(ud.?);
|
||||
|
||||
// If there's buffered character, send the characters directly to the surface.
|
||||
if (self.im_composing and self.im_commit_buffered) {
|
||||
defer self.im_commit_buffered = false;
|
||||
defer self.im_len = 0;
|
||||
_ = self.core_surface.keyCallback(.{
|
||||
.action = .press,
|
||||
.key = .invalid,
|
||||
.physical_key = .invalid,
|
||||
.mods = .{},
|
||||
.consumed_mods = .{},
|
||||
.composing = false,
|
||||
.utf8 = self.im_buf[0..self.im_len],
|
||||
}) catch |err| {
|
||||
log.err("error in key callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
if (!self.in_keypress) return;
|
||||
|
||||
// Get our pre-edit string that we'll use to show the user.
|
||||
var buf: [*c]u8 = undefined;
|
||||
_ = c.gtk_im_context_get_preedit_string(ctx, &buf, null, null);
|
||||
defer c.g_free(buf);
|
||||
const str = std.mem.sliceTo(buf, 0);
|
||||
|
||||
// If our string becomes empty we ignore this. This can happen after
|
||||
// a commit event when the preedit is being cleared and we don't want
|
||||
// to set im_len to zero. This is safe because preeditstart always sets
|
||||
// im_len to zero.
|
||||
if (str.len == 0) return;
|
||||
|
||||
// Copy the preedit string into the im_buf. This is safe because
|
||||
// commit will always overwrite this.
|
||||
self.im_len = @intCast(@min(self.im_buf.len, str.len));
|
||||
@memcpy(self.im_buf[0..self.im_len], str);
|
||||
// Update our preedit state in Ghostty core
|
||||
self.core_surface.preeditCallback(str) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkInputPreeditEnd(
|
||||
_: *c.GtkIMContext,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
//log.debug("preedit end", .{});
|
||||
// log.warn("GTKIM: preedit end", .{});
|
||||
const self = userdataSelf(ud.?);
|
||||
if (!self.in_keypress) return;
|
||||
|
||||
// End our composing state for GTK, allowing us to commit the text.
|
||||
self.im_composing = false;
|
||||
|
||||
// End our preedit state in Ghostty core
|
||||
self.core_surface.preeditCallback(null) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkInputCommit(
|
||||
@ -1899,38 +1927,45 @@ fn gtkInputCommit(
|
||||
bytes: [*:0]u8,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
// log.warn("GTKIM: input commit", .{});
|
||||
const self = userdataSelf(ud.?);
|
||||
const str = std.mem.sliceTo(bytes, 0);
|
||||
|
||||
// If we're in a key event, then we want to buffer the commit so
|
||||
// that we can send the proper keycallback followed by the char
|
||||
// callback.
|
||||
if (self.in_keypress) {
|
||||
if (str.len <= self.im_buf.len) {
|
||||
@memcpy(self.im_buf[0..str.len], str);
|
||||
self.im_len = @intCast(str.len);
|
||||
|
||||
// If composing is done and character should be committed,
|
||||
// It should be committed in preedit callback.
|
||||
if (self.im_composing) {
|
||||
self.im_commit_buffered = true;
|
||||
}
|
||||
|
||||
// log.debug("input commit len={}", .{self.im_len});
|
||||
} else {
|
||||
// If we're in a keyEvent (i.e. a keyboard event) and we're not composing,
|
||||
// then this is just a normal key press resulting in UTF-8 text. We
|
||||
// want the keyEvent to handle this so that the UTF-8 text can be associated
|
||||
// with a keyboard event.
|
||||
if (!self.im_composing and self.in_keyevent) {
|
||||
if (str.len > self.im_buf.len) {
|
||||
log.warn("not enough buffer space for input method commit", .{});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// This prevents staying in composing state after commit even though
|
||||
// input method has changed.
|
||||
// Copy our committed text to the buffer
|
||||
@memcpy(self.im_buf[0..str.len], str);
|
||||
self.im_len = @intCast(str.len);
|
||||
|
||||
// log.debug("input commit len={}", .{self.im_len});
|
||||
return;
|
||||
}
|
||||
|
||||
// If we reach this point from above it means we're composing OR
|
||||
// not in a keypress. In either case, we want to commit the text
|
||||
// given to us because that's what GTK is asking us to do. If we're
|
||||
// not in a keypress it means that this commit came via a non-keyboard
|
||||
// event (i.e. on-screen keyboard, tablet of some kind, etc.).
|
||||
|
||||
// Committing ends composing state
|
||||
self.im_composing = false;
|
||||
|
||||
// We're not in a keypress, so this was sent from an on-screen emoji
|
||||
// keyboard or something like that. Send the characters directly to
|
||||
// the surface.
|
||||
// End our preedit state. Well-behaved input methods do this for us
|
||||
// by triggering a preedit-end event but some do not (ibus 1.5.29).
|
||||
self.core_surface.preeditCallback(null) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
};
|
||||
|
||||
// Send the text to the core surface, associated with no key (an
|
||||
// invalid key, which should produce no PTY encoding).
|
||||
_ = self.core_surface.keyCallback(.{
|
||||
.action = .press,
|
||||
.key = .invalid,
|
||||
@ -1940,7 +1975,7 @@ fn gtkInputCommit(
|
||||
.composing = false,
|
||||
.utf8 = str,
|
||||
}) catch |err| {
|
||||
log.err("error in key callback err={}", .{err});
|
||||
log.warn("error in key callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ toast_overlay: ?*c.GtkWidget,
|
||||
adw_tab_overview_focus_timer: ?c.guint = null,
|
||||
|
||||
/// State and logic for windowing protocol for a window.
|
||||
winproto: ?winproto.Window,
|
||||
winproto: winproto.Window,
|
||||
|
||||
pub fn create(alloc: Allocator, app: *App) !*Window {
|
||||
// Allocate a fixed pointer for our window. We try to minimize
|
||||
@ -83,7 +83,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
.notebook = undefined,
|
||||
.context_menu = undefined,
|
||||
.toast_overlay = undefined,
|
||||
.winproto = null,
|
||||
.winproto = .none,
|
||||
};
|
||||
|
||||
// Create the window
|
||||
@ -204,11 +204,8 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
}
|
||||
|
||||
_ = c.g_signal_connect_data(gtk_window, "notify::decorated", c.G_CALLBACK(>kWindowNotifyDecorated), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// If we are disabling decorations then disable them right away.
|
||||
if (!app.config.@"window-decoration") {
|
||||
c.gtk_window_set_decorated(gtk_window, 0);
|
||||
}
|
||||
_ = c.g_signal_connect_data(gtk_window, "notify::maximized", c.G_CALLBACK(>kWindowNotifyMaximized), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gtk_window, "notify::fullscreened", c.G_CALLBACK(>kWindowNotifyFullscreened), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we
|
||||
// need to stick the headerbar into the content box.
|
||||
@ -265,6 +262,9 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
c.gtk_popover_set_has_arrow(@ptrCast(@alignCast(self.context_menu)), 0);
|
||||
c.gtk_widget_set_halign(self.context_menu, c.GTK_ALIGN_START);
|
||||
|
||||
// If we want the window to be maximized, we do that here.
|
||||
if (app.config.maximize) c.gtk_window_maximize(self.window);
|
||||
|
||||
// If we are in fullscreen mode, new windows start fullscreen.
|
||||
if (app.config.fullscreen) c.gtk_window_fullscreen(self.window);
|
||||
|
||||
@ -374,7 +374,11 @@ pub fn updateConfig(
|
||||
self: *Window,
|
||||
config: *const configpkg.Config,
|
||||
) !void {
|
||||
if (self.winproto) |*v| try v.updateConfigEvent(config);
|
||||
self.winproto.updateConfigEvent(config) catch |err| {
|
||||
// We want to continue attempting to make the other config
|
||||
// changes necessary so we just log the error and continue.
|
||||
log.warn("failed to update window protocol config error={}", .{err});
|
||||
};
|
||||
|
||||
// We always resync our appearance whenever the config changes.
|
||||
try self.syncAppearance(config);
|
||||
@ -386,16 +390,52 @@ pub fn updateConfig(
|
||||
/// TODO: Many of the initial style settings in `create` could possibly be made
|
||||
/// reactive by moving them here.
|
||||
pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void {
|
||||
if (config.@"background-opacity" < 1) {
|
||||
c.gtk_widget_remove_css_class(@ptrCast(self.window), "background");
|
||||
} else {
|
||||
c.gtk_widget_add_css_class(@ptrCast(self.window), "background");
|
||||
self.winproto.syncAppearance() catch |err| {
|
||||
log.warn("failed to sync winproto appearance error={}", .{err});
|
||||
};
|
||||
|
||||
toggleCssClass(
|
||||
@ptrCast(self.window),
|
||||
"background",
|
||||
config.@"background-opacity" >= 1,
|
||||
);
|
||||
|
||||
// If we are disabling CSDs then disable them right away.
|
||||
const csd_enabled = self.winproto.clientSideDecorationEnabled();
|
||||
c.gtk_window_set_decorated(self.window, @intFromBool(csd_enabled));
|
||||
|
||||
// If we are not decorated then we hide the titlebar.
|
||||
self.headerbar.setVisible(config.@"gtk-titlebar" and csd_enabled);
|
||||
|
||||
// Disable the title buttons (close, maximize, minimize, ...)
|
||||
// *inside* the tab overview if CSDs are disabled.
|
||||
// We do spare the search button, though.
|
||||
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and
|
||||
adwaita.enabled(&self.app.config))
|
||||
{
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
c.adw_tab_overview_set_show_start_title_buttons(
|
||||
@ptrCast(tab_overview),
|
||||
@intFromBool(csd_enabled),
|
||||
);
|
||||
c.adw_tab_overview_set_show_end_title_buttons(
|
||||
@ptrCast(tab_overview),
|
||||
@intFromBool(csd_enabled),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Window protocol specific appearance updates
|
||||
if (self.winproto) |*v| v.syncAppearance() catch |err| {
|
||||
log.warn("failed to sync window protocol appearance error={}", .{err});
|
||||
};
|
||||
fn toggleCssClass(
|
||||
widget: *c.GtkWidget,
|
||||
class: [:0]const u8,
|
||||
v: bool,
|
||||
) void {
|
||||
if (v) {
|
||||
c.gtk_widget_add_css_class(widget, class);
|
||||
} else {
|
||||
c.gtk_widget_remove_css_class(widget, class);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up the GTK actions for the window scope. Actions are how GTK handles
|
||||
@ -435,7 +475,7 @@ fn initActions(self: *Window) void {
|
||||
pub fn deinit(self: *Window) void {
|
||||
c.gtk_widget_unparent(@ptrCast(self.context_menu));
|
||||
|
||||
if (self.winproto) |*v| v.deinit(self.app.core_app.alloc);
|
||||
self.winproto.deinit(self.app.core_app.alloc);
|
||||
|
||||
if (self.adw_tab_overview_focus_timer) |timer| {
|
||||
_ = c.g_source_remove(timer);
|
||||
@ -497,9 +537,9 @@ pub fn moveTab(self: *Window, surface: *Surface, position: c_int) void {
|
||||
self.notebook.moveTab(tab, position);
|
||||
}
|
||||
|
||||
/// Go to the next tab for a surface.
|
||||
/// Go to the last tab for a surface.
|
||||
pub fn gotoLastTab(self: *Window) void {
|
||||
const max = self.notebook.nPages() -| 1;
|
||||
const max = self.notebook.nPages();
|
||||
self.gotoTab(@intCast(max));
|
||||
}
|
||||
|
||||
@ -522,6 +562,15 @@ pub fn toggleTabOverview(self: *Window) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle the maximized state for this window.
|
||||
pub fn toggleMaximize(self: *Window) void {
|
||||
if (c.gtk_window_is_maximized(self.window) == 0) {
|
||||
c.gtk_window_maximize(self.window);
|
||||
} else {
|
||||
c.gtk_window_unmaximize(self.window);
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle fullscreen for this window.
|
||||
pub fn toggleFullscreen(self: *Window) void {
|
||||
const is_fullscreen = c.gtk_window_is_fullscreen(self.window);
|
||||
@ -534,15 +583,11 @@ pub fn toggleFullscreen(self: *Window) void {
|
||||
|
||||
/// Toggle the window decorations for this window.
|
||||
pub fn toggleWindowDecorations(self: *Window) void {
|
||||
const old_decorated = c.gtk_window_get_decorated(self.window) == 1;
|
||||
const new_decorated = !old_decorated;
|
||||
c.gtk_window_set_decorated(self.window, @intFromBool(new_decorated));
|
||||
|
||||
// If we have a titlebar, then we also show/hide it depending on the
|
||||
// decorated state. GTK tends to consider the titlebar part of the frame
|
||||
// and hides it with decorations, but libadwaita doesn't. This makes it
|
||||
// explicit.
|
||||
self.headerbar.setVisible(new_decorated);
|
||||
self.app.config.@"window-decoration" = switch (self.app.config.@"window-decoration") {
|
||||
.auto, .client, .server => .none,
|
||||
.none => .client,
|
||||
};
|
||||
self.updateConfig(&self.app.config) catch {};
|
||||
}
|
||||
|
||||
/// Grabs focus on the currently selected tab.
|
||||
@ -588,22 +633,60 @@ fn gtkRealize(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
fn gtkWindowNotifyMaximized(
|
||||
_: *c.GObject,
|
||||
_: *c.GParamSpec,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self = userdataSelf(ud orelse return);
|
||||
|
||||
// Only toggle visibility of the header bar when we're using CSDs,
|
||||
// and actually intend on displaying the header bar
|
||||
if (!self.winproto.clientSideDecorationEnabled()) return;
|
||||
|
||||
// If we aren't maximized, we should show the headerbar again
|
||||
// if it was originally visible.
|
||||
const maximized = c.gtk_window_is_maximized(self.window) != 0;
|
||||
if (!maximized) {
|
||||
self.headerbar.setVisible(self.app.config.@"gtk-titlebar");
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are maximized, we should hide the headerbar if requested.
|
||||
if (self.app.config.@"gtk-titlebar-hide-when-maximized") {
|
||||
self.headerbar.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkWindowNotifyDecorated(
|
||||
object: *c.GObject,
|
||||
_: *c.GParamSpec,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
if (c.gtk_window_get_decorated(@ptrCast(object)) == 1) {
|
||||
c.gtk_widget_remove_css_class(@ptrCast(object), "ssd");
|
||||
c.gtk_widget_remove_css_class(@ptrCast(object), "no-border-radius");
|
||||
} else {
|
||||
const is_decorated = c.gtk_window_get_decorated(@ptrCast(object)) == 1;
|
||||
|
||||
// Fix any artifacting that may occur in window corners. The .ssd CSS
|
||||
// class is defined in the GtkWindow documentation:
|
||||
// https://docs.gtk.org/gtk4/class.Window.html#css-nodes. A definition
|
||||
// for .ssd is provided by GTK and Adwaita.
|
||||
c.gtk_widget_add_css_class(@ptrCast(object), "ssd");
|
||||
c.gtk_widget_add_css_class(@ptrCast(object), "no-border-radius");
|
||||
toggleCssClass(@ptrCast(object), "ssd", !is_decorated);
|
||||
toggleCssClass(@ptrCast(object), "no-border-radius", !is_decorated);
|
||||
}
|
||||
|
||||
fn gtkWindowNotifyFullscreened(
|
||||
object: *c.GObject,
|
||||
_: *c.GParamSpec,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self = userdataSelf(ud orelse return);
|
||||
const fullscreened = c.gtk_window_is_fullscreen(@ptrCast(object)) != 0;
|
||||
if (!fullscreened) {
|
||||
const csd_enabled = self.winproto.clientSideDecorationEnabled();
|
||||
self.headerbar.setVisible(self.app.config.@"gtk-titlebar" and csd_enabled);
|
||||
return;
|
||||
}
|
||||
|
||||
self.headerbar.setVisible(false);
|
||||
}
|
||||
|
||||
// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
|
||||
|
@ -18,9 +18,6 @@ pub const HeaderBar = union(enum) {
|
||||
} else {
|
||||
HeaderBarGtk.init(self);
|
||||
}
|
||||
|
||||
if (!window.app.config.@"gtk-titlebar" or !window.app.config.@"window-decoration")
|
||||
self.setVisible(false);
|
||||
}
|
||||
|
||||
pub fn setVisible(self: HeaderBar, visible: bool) void {
|
||||
|
@ -65,6 +65,7 @@ pub fn packStart(self: HeaderBarAdw, widget: *c.GtkWidget) void {
|
||||
}
|
||||
|
||||
pub fn setTitle(self: HeaderBarAdw, title: [:0]const u8) void {
|
||||
c.gtk_window_set_title(self.window.window, title);
|
||||
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
c.adw_window_title_set_title(self.title, title);
|
||||
}
|
||||
|
@ -137,6 +137,8 @@ pub const NotebookAdw = struct {
|
||||
|
||||
// If we have no more tabs we close the window
|
||||
if (self.nPages() == 0) {
|
||||
const window = tab.window.window;
|
||||
|
||||
// libadw versions <= 1.3.x leak the final page view
|
||||
// which causes our surface to not properly cleanup. We
|
||||
// unref to force the cleanup. This will trigger a critical
|
||||
@ -150,7 +152,7 @@ pub const NotebookAdw = struct {
|
||||
|
||||
// `self` will become invalid after this call because it will have
|
||||
// been freed up as part of the process of closing the window.
|
||||
c.gtk_window_destroy(tab.window.window);
|
||||
c.gtk_window_destroy(window);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -62,7 +62,7 @@ pub const App = union(Protocol) {
|
||||
|
||||
/// Per-Window state for the underlying windowing protocol.
|
||||
///
|
||||
/// In both X and Wayland, the terminology used is "Surface" and this is
|
||||
/// In Wayland, the terminology used is "Surface" and for it, this is
|
||||
/// really "Surface"-specific state. But Ghostty uses the term "Surface"
|
||||
/// heavily to mean something completely different, so we use "Window" here
|
||||
/// to better match what it generally maps to in the Ghostty codebase.
|
||||
@ -125,4 +125,10 @@ pub const Window = union(Protocol) {
|
||||
inline else => |*v| try v.syncAppearance(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
return switch (self) {
|
||||
inline else => |v| v.clientSideDecorationEnabled(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -53,4 +53,12 @@ pub const Window = struct {
|
||||
pub fn resizeEvent(_: *Window) !void {}
|
||||
|
||||
pub fn syncAppearance(_: *Window) !void {}
|
||||
|
||||
/// This returns true if CSD is enabled for this window. This
|
||||
/// should be the actual present state of the window, not the
|
||||
/// desired state.
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
_ = self;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
@ -18,6 +18,12 @@ pub const App = struct {
|
||||
|
||||
const Context = struct {
|
||||
kde_blur_manager: ?*org.KdeKwinBlurManager = null,
|
||||
|
||||
// FIXME: replace with `zxdg_decoration_v1` once GTK merges
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
|
||||
kde_decoration_manager: ?*org.KdeKwinServerDecorationManager = null,
|
||||
|
||||
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
@ -53,6 +59,12 @@ pub const App = struct {
|
||||
registry.setListener(*Context, registryListener, context);
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
|
||||
if (context.kde_decoration_manager != null) {
|
||||
// FIXME: Roundtrip again because we have to wait for the decoration
|
||||
// manager to respond with the preferred default mode. Ew.
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
}
|
||||
|
||||
return .{
|
||||
.display = display,
|
||||
.context = context,
|
||||
@ -78,17 +90,22 @@ pub const App = struct {
|
||||
) void {
|
||||
switch (event) {
|
||||
// https://wayland.app/protocols/wayland#wl_registry:event:global
|
||||
.global => |global| global: {
|
||||
.global => |global| {
|
||||
log.debug("wl_registry.global: interface={s}", .{global.interface});
|
||||
|
||||
if (registryBind(
|
||||
org.KdeKwinBlurManager,
|
||||
registry,
|
||||
global,
|
||||
1,
|
||||
)) |blur_manager| {
|
||||
context.kde_blur_manager = blur_manager;
|
||||
break :global;
|
||||
} else if (registryBind(
|
||||
org.KdeKwinServerDecorationManager,
|
||||
registry,
|
||||
global,
|
||||
)) |deco_manager| {
|
||||
context.kde_decoration_manager = deco_manager;
|
||||
deco_manager.setListener(*Context, decoManagerListener, context);
|
||||
}
|
||||
},
|
||||
|
||||
@ -97,11 +114,16 @@ pub const App = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind a Wayland interface to a global object. Returns non-null
|
||||
/// if the binding was successful, otherwise null.
|
||||
///
|
||||
/// The type T is the Wayland interface type that we're requesting.
|
||||
/// This function will verify that the global object is the correct
|
||||
/// interface and version before binding.
|
||||
fn registryBind(
|
||||
comptime T: type,
|
||||
registry: *wl.Registry,
|
||||
global: anytype,
|
||||
version: u32,
|
||||
) ?*T {
|
||||
if (std.mem.orderZ(
|
||||
u8,
|
||||
@ -109,7 +131,7 @@ pub const App = struct {
|
||||
T.interface.name,
|
||||
) != .eq) return null;
|
||||
|
||||
return registry.bind(global.name, T, version) catch |err| {
|
||||
return registry.bind(global.name, T, T.generated_version) catch |err| {
|
||||
log.warn("error binding interface {s} error={}", .{
|
||||
global.interface,
|
||||
err,
|
||||
@ -117,6 +139,18 @@ pub const App = struct {
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
fn decoManagerListener(
|
||||
_: *org.KdeKwinServerDecorationManager,
|
||||
event: org.KdeKwinServerDecorationManager.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
switch (event) {
|
||||
.default_mode => |mode| {
|
||||
context.default_deco_mode = @enumFromInt(mode.mode);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Per-window (wl_surface) state for the Wayland protocol.
|
||||
@ -130,14 +164,20 @@ pub const Window = struct {
|
||||
app_context: *App.Context,
|
||||
|
||||
/// A token that, when present, indicates that the window is blurred.
|
||||
blur_token: ?*org.KdeKwinBlur = null,
|
||||
blur_token: ?*org.KdeKwinBlur,
|
||||
|
||||
/// Object that controls the decoration mode (client/server/auto)
|
||||
/// of the window.
|
||||
decoration: ?*org.KdeKwinServerDecoration,
|
||||
|
||||
const DerivedConfig = struct {
|
||||
blur: bool,
|
||||
window_decoration: Config.WindowDecoration,
|
||||
|
||||
pub fn init(config: *const Config) DerivedConfig {
|
||||
return .{
|
||||
.blur = config.@"background-blur-radius".enabled(),
|
||||
.blur = config.@"background-blur".enabled(),
|
||||
.window_decoration = config.@"window-decoration",
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -165,19 +205,41 @@ pub const Window = struct {
|
||||
gdk_surface,
|
||||
) orelse return error.NoWaylandSurface);
|
||||
|
||||
// Get our decoration object so we can control the
|
||||
// CSD vs SSD status of this surface.
|
||||
const deco: ?*org.KdeKwinServerDecoration = deco: {
|
||||
const mgr = app.context.kde_decoration_manager orelse
|
||||
break :deco null;
|
||||
|
||||
const deco: *org.KdeKwinServerDecoration = mgr.create(
|
||||
wl_surface,
|
||||
) catch |err| {
|
||||
log.warn("could not create decoration object={}", .{err});
|
||||
break :deco null;
|
||||
};
|
||||
|
||||
break :deco deco;
|
||||
};
|
||||
|
||||
return .{
|
||||
.config = DerivedConfig.init(config),
|
||||
.surface = wl_surface,
|
||||
.app_context = app.context,
|
||||
.blur_token = null,
|
||||
.decoration = deco,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = alloc;
|
||||
if (self.blur_token) |blur| blur.release();
|
||||
if (self.decoration) |deco| deco.release();
|
||||
}
|
||||
|
||||
pub fn updateConfigEvent(self: *Window, config: *const Config) !void {
|
||||
pub fn updateConfigEvent(
|
||||
self: *Window,
|
||||
config: *const Config,
|
||||
) !void {
|
||||
self.config = DerivedConfig.init(config);
|
||||
}
|
||||
|
||||
@ -185,6 +247,18 @@ pub const Window = struct {
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
try self.syncBlur();
|
||||
try self.syncDecoration();
|
||||
}
|
||||
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
// Compositor doesn't support the SSD protocol
|
||||
if (self.decoration == null) return true;
|
||||
|
||||
return switch (self.getDecorationMode()) {
|
||||
.Client => true,
|
||||
.Server, .None => false,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
/// Update the blur state of the window.
|
||||
@ -208,4 +282,21 @@ pub const Window = struct {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn syncDecoration(self: *Window) !void {
|
||||
const deco = self.decoration orelse return;
|
||||
|
||||
// The protocol requests uint instead of enum so we have
|
||||
// to convert it.
|
||||
deco.requestMode(@intCast(@intFromEnum(self.getDecorationMode())));
|
||||
}
|
||||
|
||||
fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode {
|
||||
return switch (self.config.window_decoration) {
|
||||
.auto => self.app_context.default_deco_mode orelse .Client,
|
||||
.client => .Client,
|
||||
.server => .Server,
|
||||
.none => .None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -161,10 +161,15 @@ pub const Window = struct {
|
||||
|
||||
const DerivedConfig = struct {
|
||||
blur: bool,
|
||||
has_decoration: bool,
|
||||
|
||||
pub fn init(config: *const Config) DerivedConfig {
|
||||
return .{
|
||||
.blur = config.@"background-blur-radius".enabled(),
|
||||
.blur = config.@"background-blur".enabled(),
|
||||
.has_decoration = switch (config.@"window-decoration") {
|
||||
.none => false,
|
||||
.auto, .client, .server => true,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -239,6 +244,10 @@ pub const Window = struct {
|
||||
try self.syncBlur();
|
||||
}
|
||||
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
return self.config.has_decoration;
|
||||
}
|
||||
|
||||
fn syncBlur(self: *Window) !void {
|
||||
// FIXME: This doesn't currently factor in rounded corners on Adwaita,
|
||||
// which means that the blur region will grow slightly outside of the
|
||||
|
@ -55,6 +55,8 @@ emit_helpgen: bool = false,
|
||||
emit_docs: bool = false,
|
||||
emit_webdata: bool = false,
|
||||
emit_xcframework: bool = false,
|
||||
emit_terminfo: bool = false,
|
||||
emit_termcap: bool = false,
|
||||
|
||||
/// Environmental properties
|
||||
env: std.process.EnvMap,
|
||||
@ -306,6 +308,27 @@ pub fn init(b: *std.Build) !Config {
|
||||
break :emit_docs path != null;
|
||||
};
|
||||
|
||||
config.emit_terminfo = b.option(
|
||||
bool,
|
||||
"emit-terminfo",
|
||||
"Install Ghostty terminfo source file",
|
||||
) orelse switch (target.result.os.tag) {
|
||||
.windows => true,
|
||||
else => switch (optimize) {
|
||||
.Debug => true,
|
||||
.ReleaseSafe, .ReleaseFast, .ReleaseSmall => false,
|
||||
},
|
||||
};
|
||||
|
||||
config.emit_termcap = b.option(
|
||||
bool,
|
||||
"emit-termcap",
|
||||
"Install Ghostty termcap file",
|
||||
) orelse switch (optimize) {
|
||||
.Debug => true,
|
||||
.ReleaseSafe, .ReleaseFast, .ReleaseSmall => false,
|
||||
};
|
||||
|
||||
config.emit_webdata = b.option(
|
||||
bool,
|
||||
"emit-webdata",
|
||||
@ -486,6 +509,7 @@ pub const ExeEntrypoint = enum {
|
||||
mdgen_ghostty_5,
|
||||
webgen_config,
|
||||
webgen_actions,
|
||||
webgen_commands,
|
||||
bench_parser,
|
||||
bench_stream,
|
||||
bench_codepoint_width,
|
||||
|
@ -23,9 +23,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
||||
|
||||
// Write it
|
||||
var wf = b.addWriteFiles();
|
||||
const src_source = wf.add("share/terminfo/ghostty.terminfo", str.items);
|
||||
const src_install = b.addInstallFile(src_source, "share/terminfo/ghostty.terminfo");
|
||||
try steps.append(&src_install.step);
|
||||
const source = wf.add("ghostty.terminfo", str.items);
|
||||
|
||||
if (cfg.emit_terminfo) {
|
||||
const source_install = b.addInstallFile(source, "share/terminfo/ghostty.terminfo");
|
||||
try steps.append(&source_install.step);
|
||||
}
|
||||
|
||||
// Windows doesn't have the binaries below.
|
||||
if (cfg.target.result.os.tag == .windows) break :terminfo;
|
||||
@ -33,10 +36,10 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
||||
// Convert to termcap source format if thats helpful to people and
|
||||
// install it. The resulting value here is the termcap source in case
|
||||
// that is used for other commands.
|
||||
{
|
||||
if (cfg.emit_termcap) {
|
||||
const run_step = RunStep.create(b, "infotocap");
|
||||
run_step.addArg("infotocap");
|
||||
run_step.addFileArg(src_source);
|
||||
run_step.addFileArg(source);
|
||||
const out_source = run_step.captureStdOut();
|
||||
_ = run_step.captureStdErr(); // so we don't see stderr
|
||||
|
||||
@ -49,14 +52,20 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
||||
const run_step = RunStep.create(b, "tic");
|
||||
run_step.addArgs(&.{ "tic", "-x", "-o" });
|
||||
const path = run_step.addOutputFileArg("terminfo");
|
||||
run_step.addFileArg(src_source);
|
||||
run_step.addFileArg(source);
|
||||
_ = run_step.captureStdErr(); // so we don't see stderr
|
||||
|
||||
// Depend on the terminfo source install step so that Zig build
|
||||
// creates the "share" directory for us.
|
||||
run_step.step.dependOn(&src_install.step);
|
||||
// Ensure that `share/terminfo` is a directory, otherwise the `cp
|
||||
// -R` will create a file named `share/terminfo`
|
||||
const mkdir_step = RunStep.create(b, "make share/terminfo directory");
|
||||
switch (cfg.target.result.os.tag) {
|
||||
// windows mkdir shouldn't need "-p"
|
||||
.windows => mkdir_step.addArgs(&.{"mkdir"}),
|
||||
else => mkdir_step.addArgs(&.{ "mkdir", "-p" }),
|
||||
}
|
||||
mkdir_step.addArg(b.fmt("{s}/share/terminfo", .{b.install_path}));
|
||||
try steps.append(&mkdir_step.step);
|
||||
|
||||
{
|
||||
// Use cp -R instead of Step.InstallDir because we need to preserve
|
||||
// symlinks in the terminfo database. Zig's InstallDir step doesn't
|
||||
// handle symlinks correctly yet.
|
||||
@ -64,10 +73,10 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
||||
copy_step.addArgs(&.{ "cp", "-R" });
|
||||
copy_step.addFileArg(path);
|
||||
copy_step.addArg(b.fmt("{s}/share", .{b.install_path}));
|
||||
copy_step.step.dependOn(&mkdir_step.step);
|
||||
try steps.append(©_step.step);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shell-integration
|
||||
{
|
||||
|
@ -73,6 +73,35 @@ pub fn init(
|
||||
).step);
|
||||
}
|
||||
|
||||
{
|
||||
const webgen_commands = b.addExecutable(.{
|
||||
.name = "webgen_commands",
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = b.host,
|
||||
});
|
||||
deps.help_strings.addImport(webgen_commands);
|
||||
|
||||
{
|
||||
const buildconfig = config: {
|
||||
var copy = deps.config.*;
|
||||
copy.exe_entrypoint = .webgen_commands;
|
||||
break :config copy;
|
||||
};
|
||||
|
||||
const options = b.addOptions();
|
||||
try buildconfig.addOptions(options);
|
||||
webgen_commands.root_module.addOptions("build_options", options);
|
||||
}
|
||||
|
||||
const webgen_commands_step = b.addRunArtifact(webgen_commands);
|
||||
const webgen_commands_out = webgen_commands_step.captureStdOut();
|
||||
|
||||
try steps.append(&b.addInstallFile(
|
||||
webgen_commands_out,
|
||||
"share/ghostty/webdata/commands.mdx",
|
||||
).step);
|
||||
}
|
||||
|
||||
return .{ .steps = steps.items };
|
||||
}
|
||||
|
||||
|
@ -450,10 +450,14 @@ pub fn add(
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
|
||||
scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/blur.xml"));
|
||||
scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/server-decoration.xml"));
|
||||
|
||||
scanner.generate("wl_compositor", 1);
|
||||
scanner.generate("org_kde_kwin_blur_manager", 1);
|
||||
scanner.generate("org_kde_kwin_server_decoration_manager", 1);
|
||||
|
||||
step.root_module.addImport("wayland", wayland);
|
||||
step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
|
||||
|
@ -15,7 +15,6 @@ pub fn init(b: *std.Build) !UnicodeTables {
|
||||
.root_source_file = b.path("src/unicode/props.zig"),
|
||||
.target = b.host,
|
||||
});
|
||||
exe.linkLibC();
|
||||
|
||||
const ziglyph_dep = b.dependency("ziglyph", .{
|
||||
.target = b.host,
|
||||
|
51
src/build/webgen/main_commands.zig
Normal file
51
src/build/webgen/main_commands.zig
Normal file
@ -0,0 +1,51 @@
|
||||
const std = @import("std");
|
||||
const Action = @import("../../cli/action.zig").Action;
|
||||
const help_strings = @import("help_strings");
|
||||
|
||||
pub fn main() !void {
|
||||
const output = std.io.getStdOut().writer();
|
||||
try genActions(output);
|
||||
}
|
||||
|
||||
// Note: as a shortcut for defining inline editOnGithubLinks per cli action the user
|
||||
// is directed to the folder view on Github. This includes a README pointing them to
|
||||
// the files to edit.
|
||||
pub fn genActions(writer: anytype) !void {
|
||||
// Write the header
|
||||
try writer.writeAll(
|
||||
\\---
|
||||
\\title: Reference
|
||||
\\description: Reference of all Ghostty action subcommands.
|
||||
\\editOnGithubLink: https://github.com/ghostty-org/ghostty/tree/main/src/cli
|
||||
\\---
|
||||
\\Ghostty includes a number of utility actions that can be accessed as subcommands.
|
||||
\\Actions provide utilities to work with config, list keybinds, list fonts, demo themes,
|
||||
\\and debug.
|
||||
\\
|
||||
);
|
||||
|
||||
inline for (@typeInfo(Action).Enum.fields) |field| {
|
||||
const action = std.meta.stringToEnum(Action, field.name).?;
|
||||
|
||||
switch (action) {
|
||||
.help, .version => try writer.writeAll("## " ++ field.name ++ "\n"),
|
||||
else => try writer.writeAll("## " ++ field.name ++ "\n"),
|
||||
}
|
||||
|
||||
if (@hasDecl(help_strings.Action, field.name)) {
|
||||
var iter = std.mem.splitScalar(u8, @field(help_strings.Action, field.name), '\n');
|
||||
var first = true;
|
||||
while (iter.next()) |s| {
|
||||
try writer.writeAll(s);
|
||||
try writer.writeAll("\n");
|
||||
first = false;
|
||||
}
|
||||
try writer.writeAll("\n```\n");
|
||||
switch (action) {
|
||||
.help, .version => try writer.writeAll("ghostty --" ++ field.name ++ "\n"),
|
||||
else => try writer.writeAll("ghostty +" ++ field.name ++ "\n"),
|
||||
}
|
||||
try writer.writeAll("```\n\n");
|
||||
}
|
||||
}
|
||||
}
|
13
src/cli/README.md
Normal file
13
src/cli/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Subcommand Actions
|
||||
|
||||
This is the cli specific code. It contains cli actions and tui definitions and
|
||||
argument parsing.
|
||||
|
||||
This README is meant as developer documentation and not as user documentation.
|
||||
For user documentation, see the main README or [ghostty.org](https://ghostty.org/docs).
|
||||
|
||||
## Updating documentation
|
||||
|
||||
Each cli action is defined in it's own file. Documentation for each action is defined
|
||||
in the doc comment associated with the `run` function. For example the `run` function
|
||||
in `list_keybinds.zig` contains the help text for `ghostty +list-keybinds`.
|
@ -45,12 +45,12 @@ pub const Action = enum {
|
||||
// Validate passed config file
|
||||
@"validate-config",
|
||||
|
||||
// List, (eventually) view, and (eventually) send crash reports.
|
||||
@"crash-report",
|
||||
|
||||
// Show which font face Ghostty loads a codepoint from.
|
||||
@"show-face",
|
||||
|
||||
// List, (eventually) view, and (eventually) send crash reports.
|
||||
@"crash-report",
|
||||
|
||||
pub const Error = error{
|
||||
/// Multiple actions were detected. You can specify at most one
|
||||
/// action on the CLI otherwise the behavior desired is ambiguous.
|
||||
|
@ -38,6 +38,12 @@ pub const Error = error{
|
||||
/// "DiagnosticList" and any diagnostic messages will be added to that list.
|
||||
/// When diagnostics are present, only allocation errors will be returned.
|
||||
///
|
||||
/// If the destination type has a decl "renamed", it must be of type
|
||||
/// std.StaticStringMap([]const u8) and contains a mapping from the old
|
||||
/// field name to the new field name. This is used to allow renaming fields
|
||||
/// while still supporting the old name. If a renamed field is set, parsing
|
||||
/// will automatically set the new field name.
|
||||
///
|
||||
/// Note: If the arena is already non-null, then it will be used. In this
|
||||
/// case, in the case of an error some memory might be leaked into the arena.
|
||||
pub fn parse(
|
||||
@ -49,6 +55,24 @@ pub fn parse(
|
||||
const info = @typeInfo(T);
|
||||
assert(info == .Struct);
|
||||
|
||||
comptime {
|
||||
// Verify all renamed fields are valid (source does not exist,
|
||||
// destination does exist).
|
||||
if (@hasDecl(T, "renamed")) {
|
||||
for (T.renamed.keys(), T.renamed.values()) |key, value| {
|
||||
if (@hasField(T, key)) {
|
||||
@compileLog(key);
|
||||
@compileError("renamed field source exists");
|
||||
}
|
||||
|
||||
if (!@hasField(T, value)) {
|
||||
@compileLog(value);
|
||||
@compileError("renamed field destination does not exist");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make an arena for all our allocations if we support it. Otherwise,
|
||||
// use an allocator that always fails. If the arena is already set on
|
||||
// the config, then we reuse that. See memory note in parse docs.
|
||||
@ -367,6 +391,16 @@ pub fn parseIntoField(
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown field, is the field renamed?
|
||||
if (@hasDecl(T, "renamed")) {
|
||||
for (T.renamed.keys(), T.renamed.values()) |old, new| {
|
||||
if (mem.eql(u8, old, key)) {
|
||||
try parseIntoField(T, alloc, dst, new, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return error.InvalidField;
|
||||
}
|
||||
|
||||
@ -1104,6 +1138,24 @@ test "parseIntoField: tagged union missing tag" {
|
||||
);
|
||||
}
|
||||
|
||||
test "parseIntoField: renamed field" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var data: struct {
|
||||
a: []const u8,
|
||||
|
||||
const renamed = std.StaticStringMap([]const u8).initComptime(&.{
|
||||
.{ "old", "a" },
|
||||
});
|
||||
} = undefined;
|
||||
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "old", "42");
|
||||
try testing.expectEqualStrings("42", data.a);
|
||||
}
|
||||
|
||||
/// An iterator that considers its location to be CLI args. It
|
||||
/// iterates through an underlying iterator and increments a counter
|
||||
/// to track the current CLI arg index.
|
||||
|
@ -15,9 +15,11 @@ pub const Options = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// The `help` command shows general help about Ghostty. You can also specify
|
||||
/// `--help` or `-h` along with any action such as `+list-themes` to see help
|
||||
/// for a specific action.
|
||||
/// The `help` command shows general help about Ghostty. Recognized as either
|
||||
/// `-h, `--help`, or like other actions `+help`.
|
||||
///
|
||||
/// You can also specify `--help` or `-h` along with any action such as
|
||||
/// `+list-themes` to see help for a specific action.
|
||||
pub fn run(alloc: Allocator) !u8 {
|
||||
var opts: Options = .{};
|
||||
defer opts.deinit();
|
||||
|
@ -24,7 +24,9 @@ pub const Options = struct {
|
||||
/// actions for Ghostty. These are distinct from the CLI Actions which can
|
||||
/// be listed via `+help`
|
||||
///
|
||||
/// The `--docs` argument will print out the documentation for each action.
|
||||
/// Flags:
|
||||
///
|
||||
/// * `--docs`: will print out the documentation for each action.
|
||||
pub fn run(alloc: Allocator) !u8 {
|
||||
var opts: Options = .{};
|
||||
defer opts.deinit();
|
||||
|
@ -44,14 +44,21 @@ pub const Options = struct {
|
||||
/// the sorting will be disabled and the results instead will be shown in the
|
||||
/// same priority order Ghostty would use to pick a font.
|
||||
///
|
||||
/// The `--family` argument can be used to filter results to a specific family.
|
||||
/// The family handling is identical to the `font-family` set of Ghostty
|
||||
/// configuration values, so this can be used to debug why your desired font may
|
||||
/// not be loading.
|
||||
/// Flags:
|
||||
///
|
||||
/// The `--bold` and `--italic` arguments can be used to filter results to
|
||||
/// specific styles. It is not guaranteed that only those styles are returned,
|
||||
/// it will just prioritize fonts that match those styles.
|
||||
/// * `--bold`: Filter results to specific bold styles. It is not guaranteed
|
||||
/// that only those styles are returned. They are only prioritized.
|
||||
///
|
||||
/// * `--italic`: Filter results to specific italic styles. It is not guaranteed
|
||||
/// that only those styles are returned. They are only prioritized.
|
||||
///
|
||||
/// * `--style`: Filter results based on the style string advertised by a font.
|
||||
/// It is not guaranteed that only those styles are returned. They are only
|
||||
/// prioritized.
|
||||
///
|
||||
/// * `--family`: Filter results to a specific font family. The family handling
|
||||
/// is identical to the `font-family` set of Ghostty configuration values, so
|
||||
/// this can be used to debug why your desired font may not be loading.
|
||||
pub fn run(alloc: Allocator) !u8 {
|
||||
var iter = try args.argsIterator(alloc);
|
||||
defer iter.deinit();
|
||||
|
@ -42,10 +42,14 @@ pub const Options = struct {
|
||||
/// changes to the keybinds it will print out the default ones configured for
|
||||
/// Ghostty
|
||||
///
|
||||
/// The `--default` argument will print out all the default keybinds configured
|
||||
/// for Ghostty
|
||||
/// Flags:
|
||||
///
|
||||
/// The `--plain` flag will disable formatting and make the output more
|
||||
/// * `--default`: will print out all the default keybinds
|
||||
///
|
||||
/// * `--docs`: currently does nothing, intended to print out documentation
|
||||
/// about the action associated with the keybinds
|
||||
///
|
||||
/// * `--plain`: will disable formatting and make the output more
|
||||
/// friendly for Unix tooling. This is default when not printing to a tty.
|
||||
pub fn run(alloc: Allocator) !u8 {
|
||||
var opts: Options = .{};
|
||||
@ -64,7 +68,9 @@ pub fn run(alloc: Allocator) !u8 {
|
||||
|
||||
// Despite being under the posix namespace, this also works on Windows as of zig 0.13.0
|
||||
if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) {
|
||||
return prettyPrint(alloc, config.keybind);
|
||||
var arena = std.heap.ArenaAllocator.init(alloc);
|
||||
defer arena.deinit();
|
||||
return prettyPrint(arena.allocator(), config.keybind);
|
||||
} else {
|
||||
try config.keybind.formatEntryDocs(
|
||||
configpkg.entryFormatter("keybind", stdout.writer()),
|
||||
@ -75,6 +81,111 @@ pub fn run(alloc: Allocator) !u8 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const TriggerList = std.SinglyLinkedList(Binding.Trigger);
|
||||
|
||||
const ChordBinding = struct {
|
||||
triggers: TriggerList,
|
||||
action: Binding.Action,
|
||||
|
||||
// Order keybinds based on various properties
|
||||
// 1. Longest chord sequence
|
||||
// 2. Most active modifiers
|
||||
// 3. Alphabetically by active modifiers
|
||||
// 4. Trigger key order
|
||||
// These properties propagate through chorded keypresses
|
||||
//
|
||||
// Adapted from Binding.lessThan
|
||||
pub fn lessThan(_: void, lhs: ChordBinding, rhs: ChordBinding) bool {
|
||||
const lhs_len = lhs.triggers.len();
|
||||
const rhs_len = rhs.triggers.len();
|
||||
|
||||
std.debug.assert(lhs_len != 0);
|
||||
std.debug.assert(rhs_len != 0);
|
||||
|
||||
if (lhs_len != rhs_len) {
|
||||
return lhs_len > rhs_len;
|
||||
}
|
||||
|
||||
const lhs_count: usize = blk: {
|
||||
var count: usize = 0;
|
||||
var maybe_trigger = lhs.triggers.first;
|
||||
while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) {
|
||||
if (trigger.data.mods.super) count += 1;
|
||||
if (trigger.data.mods.ctrl) count += 1;
|
||||
if (trigger.data.mods.shift) count += 1;
|
||||
if (trigger.data.mods.alt) count += 1;
|
||||
}
|
||||
break :blk count;
|
||||
};
|
||||
const rhs_count: usize = blk: {
|
||||
var count: usize = 0;
|
||||
var maybe_trigger = rhs.triggers.first;
|
||||
while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) {
|
||||
if (trigger.data.mods.super) count += 1;
|
||||
if (trigger.data.mods.ctrl) count += 1;
|
||||
if (trigger.data.mods.shift) count += 1;
|
||||
if (trigger.data.mods.alt) count += 1;
|
||||
}
|
||||
|
||||
break :blk count;
|
||||
};
|
||||
|
||||
if (lhs_count != rhs_count)
|
||||
return lhs_count > rhs_count;
|
||||
|
||||
{
|
||||
var l_trigger = lhs.triggers.first;
|
||||
var r_trigger = rhs.triggers.first;
|
||||
while (l_trigger != null and r_trigger != null) {
|
||||
const l_int = l_trigger.?.data.mods.int();
|
||||
const r_int = r_trigger.?.data.mods.int();
|
||||
|
||||
if (l_int != r_int) {
|
||||
return l_int > r_int;
|
||||
}
|
||||
|
||||
l_trigger = l_trigger.?.next;
|
||||
r_trigger = r_trigger.?.next;
|
||||
}
|
||||
}
|
||||
|
||||
var l_trigger = lhs.triggers.first;
|
||||
var r_trigger = rhs.triggers.first;
|
||||
|
||||
while (l_trigger != null and r_trigger != null) {
|
||||
const lhs_key: c_int = blk: {
|
||||
switch (l_trigger.?.data.key) {
|
||||
.translated => |key| break :blk @intFromEnum(key),
|
||||
.physical => |key| break :blk @intFromEnum(key),
|
||||
.unicode => |key| break :blk @intCast(key),
|
||||
}
|
||||
};
|
||||
const rhs_key: c_int = blk: {
|
||||
switch (r_trigger.?.data.key) {
|
||||
.translated => |key| break :blk @intFromEnum(key),
|
||||
.physical => |key| break :blk @intFromEnum(key),
|
||||
.unicode => |key| break :blk @intCast(key),
|
||||
}
|
||||
};
|
||||
|
||||
l_trigger = l_trigger.?.next;
|
||||
r_trigger = r_trigger.?.next;
|
||||
|
||||
if (l_trigger == null or r_trigger == null) {
|
||||
return lhs_key < rhs_key;
|
||||
}
|
||||
|
||||
if (lhs_key != rhs_key) {
|
||||
return lhs_key < rhs_key;
|
||||
}
|
||||
}
|
||||
|
||||
// The previous loop will always return something on its final iteration so we cannot
|
||||
// reach this point
|
||||
unreachable;
|
||||
}
|
||||
};
|
||||
|
||||
fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
||||
// Set up vaxis
|
||||
var tty = try vaxis.Tty.init();
|
||||
@ -107,26 +218,11 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
||||
|
||||
const win = vx.window();
|
||||
|
||||
// Get all of our keybinds into a list. We also search for the longest printed keyname so we can
|
||||
// align things nicely
|
||||
// Generate a list of bindings, recursively traversing chorded keybindings
|
||||
var iter = keybinds.set.bindings.iterator();
|
||||
var bindings = std.ArrayList(Binding).init(alloc);
|
||||
var widest_key: u16 = 0;
|
||||
var buf: [64]u8 = undefined;
|
||||
while (iter.next()) |bind| {
|
||||
const action = switch (bind.value_ptr.*) {
|
||||
.leader => continue, // TODO: support this
|
||||
.leaf => |leaf| leaf.action,
|
||||
};
|
||||
const key = switch (bind.key_ptr.key) {
|
||||
.translated => |k| try std.fmt.bufPrint(&buf, "{s}", .{@tagName(k)}),
|
||||
.physical => |k| try std.fmt.bufPrint(&buf, "physical:{s}", .{@tagName(k)}),
|
||||
.unicode => |c| try std.fmt.bufPrint(&buf, "{u}", .{c}),
|
||||
};
|
||||
widest_key = @max(widest_key, win.gwidth(key));
|
||||
try bindings.append(.{ .trigger = bind.key_ptr.*, .action = action });
|
||||
}
|
||||
std.mem.sort(Binding, bindings.items, {}, Binding.lessThan);
|
||||
const bindings, const widest_chord = try iterateBindings(alloc, &iter, &win);
|
||||
|
||||
std.mem.sort(ChordBinding, bindings, {}, ChordBinding.lessThan);
|
||||
|
||||
// Set up styles for each modifier
|
||||
const super_style: vaxis.Style = .{ .fg = .{ .index = 1 } };
|
||||
@ -134,41 +230,41 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
||||
const alt_style: vaxis.Style = .{ .fg = .{ .index = 3 } };
|
||||
const shift_style: vaxis.Style = .{ .fg = .{ .index = 4 } };
|
||||
|
||||
var longest_col: u16 = 0;
|
||||
|
||||
// Print the list
|
||||
for (bindings.items) |bind| {
|
||||
for (bindings) |bind| {
|
||||
win.clear();
|
||||
|
||||
var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false };
|
||||
const trigger = bind.trigger;
|
||||
if (trigger.mods.super) {
|
||||
var maybe_trigger = bind.triggers.first;
|
||||
while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) {
|
||||
if (trigger.data.mods.super) {
|
||||
result = win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col });
|
||||
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||
}
|
||||
if (trigger.mods.ctrl) {
|
||||
if (trigger.data.mods.ctrl) {
|
||||
result = win.printSegment(.{ .text = "ctrl ", .style = ctrl_style }, .{ .col_offset = result.col });
|
||||
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||
}
|
||||
if (trigger.mods.alt) {
|
||||
if (trigger.data.mods.alt) {
|
||||
result = win.printSegment(.{ .text = "alt ", .style = alt_style }, .{ .col_offset = result.col });
|
||||
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||
}
|
||||
if (trigger.mods.shift) {
|
||||
if (trigger.data.mods.shift) {
|
||||
result = win.printSegment(.{ .text = "shift", .style = shift_style }, .{ .col_offset = result.col });
|
||||
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||
}
|
||||
|
||||
const key = switch (trigger.key) {
|
||||
const key = switch (trigger.data.key) {
|
||||
.translated => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}),
|
||||
.physical => |k| try std.fmt.allocPrint(alloc, "physical:{s}", .{@tagName(k)}),
|
||||
.unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}),
|
||||
};
|
||||
// We don't track the key print because we index the action off the *widest* key so we get
|
||||
// nice alignment no matter what was printed for mods
|
||||
_ = win.printSegment(.{ .text = key }, .{ .col_offset = result.col });
|
||||
result = win.printSegment(.{ .text = key }, .{ .col_offset = result.col });
|
||||
|
||||
if (longest_col < result.col) longest_col = result.col;
|
||||
// Print a separator between chorded keys
|
||||
if (trigger.next != null) {
|
||||
result = win.printSegment(.{ .text = " > ", .style = .{ .bold = true, .fg = .{ .index = 6 } } }, .{ .col_offset = result.col });
|
||||
}
|
||||
}
|
||||
|
||||
const action = try std.fmt.allocPrint(alloc, "{}", .{bind.action});
|
||||
// If our action has an argument, we print the argument in a different color
|
||||
@ -177,12 +273,69 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
||||
.{ .text = action[0..idx] },
|
||||
.{ .text = action[idx .. idx + 1], .style = .{ .dim = true } },
|
||||
.{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } },
|
||||
}, .{ .col_offset = longest_col + widest_key + 2 });
|
||||
}, .{ .col_offset = widest_chord + 3 });
|
||||
} else {
|
||||
_ = win.printSegment(.{ .text = action }, .{ .col_offset = longest_col + widest_key + 2 });
|
||||
_ = win.printSegment(.{ .text = action }, .{ .col_offset = widest_chord + 3 });
|
||||
}
|
||||
try vx.prettyPrint(writer);
|
||||
}
|
||||
try buf_writer.flush();
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn iterateBindings(alloc: Allocator, iter: anytype, win: *const vaxis.Window) !struct { []ChordBinding, u16 } {
|
||||
var widest_chord: u16 = 0;
|
||||
var bindings = std.ArrayList(ChordBinding).init(alloc);
|
||||
while (iter.next()) |bind| {
|
||||
const width = blk: {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
const t = bind.key_ptr.*;
|
||||
|
||||
if (t.mods.super) try std.fmt.format(buf.writer(), "super + ", .{});
|
||||
if (t.mods.ctrl) try std.fmt.format(buf.writer(), "ctrl + ", .{});
|
||||
if (t.mods.alt) try std.fmt.format(buf.writer(), "alt + ", .{});
|
||||
if (t.mods.shift) try std.fmt.format(buf.writer(), "shift + ", .{});
|
||||
|
||||
switch (t.key) {
|
||||
.translated => |k| try std.fmt.format(buf.writer(), "{s}", .{@tagName(k)}),
|
||||
.physical => |k| try std.fmt.format(buf.writer(), "physical:{s}", .{@tagName(k)}),
|
||||
.unicode => |c| try std.fmt.format(buf.writer(), "{u}", .{c}),
|
||||
}
|
||||
|
||||
break :blk win.gwidth(buf.items);
|
||||
};
|
||||
|
||||
switch (bind.value_ptr.*) {
|
||||
.leader => |leader| {
|
||||
|
||||
// Recursively iterate on the set of bindings for this leader key
|
||||
var n_iter = leader.bindings.iterator();
|
||||
const sub_bindings, const max_width = try iterateBindings(alloc, &n_iter, win);
|
||||
|
||||
// Prepend the current keybind onto the list of sub-binds
|
||||
for (sub_bindings) |*nb| {
|
||||
const prepend_node = try alloc.create(TriggerList.Node);
|
||||
prepend_node.* = TriggerList.Node{ .data = bind.key_ptr.* };
|
||||
nb.triggers.prepend(prepend_node);
|
||||
}
|
||||
|
||||
// Add the longest sub-bind width to the current bind width along with a padding
|
||||
// of 5 for the ' > ' spacer
|
||||
widest_chord = @max(widest_chord, width + max_width + 5);
|
||||
try bindings.appendSlice(sub_bindings);
|
||||
},
|
||||
.leaf => |leaf| {
|
||||
const node = try alloc.create(TriggerList.Node);
|
||||
node.* = TriggerList.Node{ .data = bind.key_ptr.* };
|
||||
const triggers = TriggerList{
|
||||
.first = node,
|
||||
};
|
||||
|
||||
widest_chord = @max(widest_chord, width);
|
||||
try bindings.append(.{ .triggers = triggers, .action = leaf.action });
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return .{ try bindings.toOwnedSlice(), widest_chord };
|
||||
}
|
||||
|
@ -91,6 +91,7 @@ const ThemeListElement = struct {
|
||||
/// Flags:
|
||||
///
|
||||
/// * `--path`: Show the full path to the theme.
|
||||
///
|
||||
/// * `--plain`: Force a plain listing of themes.
|
||||
pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
|
||||
var opts: Options = .{};
|
||||
|
@ -23,10 +23,13 @@ pub const Options = struct {
|
||||
|
||||
/// The `validate-config` command is used to validate a Ghostty config file.
|
||||
///
|
||||
/// When executed without any arguments, this will load the config from the default location.
|
||||
/// When executed without any arguments, this will load the config from the default
|
||||
/// location.
|
||||
///
|
||||
/// The `--config-file` argument can be passed to validate a specific target config
|
||||
/// file in a non-default location.
|
||||
/// Flags:
|
||||
///
|
||||
/// * `--config-file`: can be passed to validate a specific target config file in
|
||||
/// a non-default location
|
||||
pub fn run(alloc: std.mem.Allocator) !u8 {
|
||||
var opts: Options = .{};
|
||||
defer opts.deinit();
|
||||
|
@ -10,7 +10,8 @@ const gtk = if (build_config.app_runtime == .gtk) @import("../apprt/gtk/c.zig").
|
||||
|
||||
pub const Options = struct {};
|
||||
|
||||
/// The `version` command is used to display information about Ghostty.
|
||||
/// The `version` command is used to display information about Ghostty. Recognized as
|
||||
/// either `+version` or `--version`.
|
||||
pub fn run(alloc: Allocator) !u8 {
|
||||
_ = alloc;
|
||||
|
||||
|
@ -42,6 +42,15 @@ const c = @cImport({
|
||||
@cInclude("unistd.h");
|
||||
});
|
||||
|
||||
/// Renamed fields, used by cli.parse
|
||||
pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
|
||||
// Ghostty 1.1 introduced background-blur support for Linux which
|
||||
// doesn't support a specific radius value. The renaming is to let
|
||||
// one field be used for both platforms (macOS retained the ability
|
||||
// to set a radius).
|
||||
.{ "background-blur-radius", "background-blur" },
|
||||
});
|
||||
|
||||
/// The font families to use.
|
||||
///
|
||||
/// You can generate the list of valid values using the CLI:
|
||||
@ -248,6 +257,32 @@ const c = @cImport({
|
||||
/// This is currently only supported on macOS.
|
||||
@"font-thicken-strength": u8 = 255,
|
||||
|
||||
/// What color space to use when performing alpha blending.
|
||||
///
|
||||
/// This affects how text looks for different background/foreground color pairs.
|
||||
///
|
||||
/// Valid values:
|
||||
///
|
||||
/// * `native` - Perform alpha blending in the native color space for the OS.
|
||||
/// On macOS this corresponds to Display P3, and on Linux it's sRGB.
|
||||
///
|
||||
/// * `linear` - Perform alpha blending in linear space. This will eliminate
|
||||
/// the darkening artifacts around the edges of text that are very visible
|
||||
/// when certain color combinations are used (e.g. red / green), but makes
|
||||
/// dark text look much thinner than normal and light text much thicker.
|
||||
/// This is also sometimes known as "gamma correction".
|
||||
/// (Currently only supported on macOS. Has no effect on Linux.)
|
||||
///
|
||||
/// * `linear-corrected` - Corrects the thinning/thickening effect of linear
|
||||
/// by applying a correction curve to the text alpha depending on its
|
||||
/// brightness. This compensates for the thinning and makes the weight of
|
||||
/// most text appear very similar to when it's blended non-linearly.
|
||||
///
|
||||
/// Note: This setting affects more than just text, images will also be blended
|
||||
/// in the selected color space, and custom shaders will receive colors in that
|
||||
/// color space as well.
|
||||
@"text-blending": TextBlending = .native,
|
||||
|
||||
/// All of the configurations behavior adjust various metrics determined by the
|
||||
/// font. The values can be integers (1, -1, etc.) or a percentage (20%, -15%,
|
||||
/// etc.). In each case, the values represent the amount to change the original
|
||||
@ -256,7 +291,7 @@ const c = @cImport({
|
||||
/// For example, a value of `1` increases the value by 1; it does not set it to
|
||||
/// literally 1. A value of `20%` increases the value by 20%. And so on.
|
||||
///
|
||||
/// There is little to no validation on these values so the wrong values (i.e.
|
||||
/// There is little to no validation on these values so the wrong values (e.g.
|
||||
/// `-100%`) can cause the terminal to be unusable. Use with caution and reason.
|
||||
///
|
||||
/// Some values are clamped to minimum or maximum values. This can make it
|
||||
@ -441,7 +476,7 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
|
||||
|
||||
/// The minimum contrast ratio between the foreground and background colors.
|
||||
/// The contrast ratio is a value between 1 and 21. A value of 1 allows for no
|
||||
/// contrast (i.e. black on black). This value is the contrast ratio as defined
|
||||
/// contrast (e.g. black on black). This value is the contrast ratio as defined
|
||||
/// by the [WCAG 2.0 specification](https://www.w3.org/TR/WCAG20/).
|
||||
///
|
||||
/// If you want to avoid invisible text (same color as background), a value of
|
||||
@ -623,7 +658,7 @@ palette: Palette = .{},
|
||||
/// need to set environment-specific settings and/or install third-party plugins
|
||||
/// in order to support background blur, as there isn't a unified interface for
|
||||
/// doing so.
|
||||
@"background-blur-radius": BackgroundBlur = .false,
|
||||
@"background-blur": BackgroundBlur = .false,
|
||||
|
||||
/// The opacity level (opposite of transparency) of an unfocused split.
|
||||
/// Unfocused splits by default are slightly faded out to make it easier to see
|
||||
@ -696,7 +731,7 @@ command: ?[]const u8 = null,
|
||||
/// injecting any configured shell integration into the command's
|
||||
/// environment. With `-e` its highly unlikely that you're executing a
|
||||
/// shell and forced shell integration is likely to cause problems
|
||||
/// (i.e. by wrapping your command in a shell, setting env vars, etc.).
|
||||
/// (e.g. by wrapping your command in a shell, setting env vars, etc.).
|
||||
/// This is a safety measure to prevent unexpected behavior. If you want
|
||||
/// shell integration with a `-e`-executed command, you must either
|
||||
/// name your binary appropriately or source the shell integration script
|
||||
@ -744,7 +779,7 @@ command: ?[]const u8 = null,
|
||||
|
||||
/// Match a regular expression against the terminal text and associate clicking
|
||||
/// it with an action. This can be used to match URLs, file paths, etc. Actions
|
||||
/// can be opening using the system opener (i.e. `open` or `xdg-open`) or
|
||||
/// can be opening using the system opener (e.g. `open` or `xdg-open`) or
|
||||
/// executing any arbitrary binding action.
|
||||
///
|
||||
/// Links that are configured earlier take precedence over links that are
|
||||
@ -764,6 +799,11 @@ link: RepeatableLink = .{},
|
||||
/// `link`). If you want to customize URL matching, use `link` and disable this.
|
||||
@"link-url": bool = true,
|
||||
|
||||
/// Whether to start the window in a maximized state. This setting applies
|
||||
/// to new windows and does not apply to tabs, splits, etc. However, this setting
|
||||
/// will apply to all new windows, not just the first one.
|
||||
maximize: bool = false,
|
||||
|
||||
/// Start new windows in fullscreen. This setting applies to new windows and
|
||||
/// does not apply to tabs, splits, etc. However, this setting will apply to all
|
||||
/// new windows, not just the first one.
|
||||
@ -845,7 +885,7 @@ class: ?[:0]const u8 = null,
|
||||
/// Valid keys are currently only listed in the
|
||||
/// [Ghostty source code](https://github.com/ghostty-org/ghostty/blob/d6e76858164d52cff460fedc61ddf2e560912d71/src/input/key.zig#L255).
|
||||
/// This is a documentation limitation and we will improve this in the future.
|
||||
/// A common gotcha is that numeric keys are written as words: i.e. `one`,
|
||||
/// A common gotcha is that numeric keys are written as words: e.g. `one`,
|
||||
/// `two`, `three`, etc. and not `1`, `2`, `3`. This will also be improved in
|
||||
/// the future.
|
||||
///
|
||||
@ -888,7 +928,7 @@ class: ?[:0]const u8 = null,
|
||||
/// * Ghostty will wait an indefinite amount of time for the next key in
|
||||
/// the sequence. There is no way to specify a timeout. The only way to
|
||||
/// force the output of a prefix key is to assign another keybind to
|
||||
/// specifically output that key (i.e. `ctrl+a>ctrl+a=text:foo`) or
|
||||
/// specifically output that key (e.g. `ctrl+a>ctrl+a=text:foo`) or
|
||||
/// press an unbound key which will send both keys to the program.
|
||||
///
|
||||
/// * If a prefix in a sequence is previously bound, the sequence will
|
||||
@ -918,13 +958,13 @@ class: ?[:0]const u8 = null,
|
||||
/// including `physical:`-prefixed triggers without specifying the
|
||||
/// prefix.
|
||||
///
|
||||
/// * `csi:text` - Send a CSI sequence. i.e. `csi:A` sends "cursor up".
|
||||
/// * `csi:text` - Send a CSI sequence. e.g. `csi:A` sends "cursor up".
|
||||
///
|
||||
/// * `esc:text` - Send an escape sequence. i.e. `esc:d` deletes to the
|
||||
/// * `esc:text` - Send an escape sequence. e.g. `esc:d` deletes to the
|
||||
/// end of the word to the right.
|
||||
///
|
||||
/// * `text:text` - Send a string. Uses Zig string literal syntax.
|
||||
/// i.e. `text:\x15` sends Ctrl-U.
|
||||
/// e.g. `text:\x15` sends Ctrl-U.
|
||||
///
|
||||
/// * All other actions can be found in the documentation or by using the
|
||||
/// `ghostty +list-actions` command.
|
||||
@ -950,12 +990,12 @@ class: ?[:0]const u8 = null,
|
||||
/// keybinds only apply to the focused terminal surface. If this is true,
|
||||
/// then the keybind will be sent to all terminal surfaces. This only
|
||||
/// applies to actions that are surface-specific. For actions that
|
||||
/// are already global (i.e. `quit`), this prefix has no effect.
|
||||
/// are already global (e.g. `quit`), this prefix has no effect.
|
||||
///
|
||||
/// * `global:` - Make the keybind global. By default, keybinds only work
|
||||
/// within Ghostty and under the right conditions (application focused,
|
||||
/// sometimes terminal focused, etc.). If you want a keybind to work
|
||||
/// globally across your system (i.e. even when Ghostty is not focused),
|
||||
/// globally across your system (e.g. even when Ghostty is not focused),
|
||||
/// specify this prefix. This prefix implies `all:`. Note: this does not
|
||||
/// work in all environments; see the additional notes below for more
|
||||
/// information.
|
||||
@ -1056,7 +1096,7 @@ keybind: Keybinds = .{},
|
||||
/// any of the heuristics that disable extending noted below.
|
||||
///
|
||||
/// The "extend" value will be disabled in certain scenarios. On primary
|
||||
/// screen applications (i.e. not something like Neovim), the color will not
|
||||
/// screen applications (e.g. not something like Neovim), the color will not
|
||||
/// be extended vertically if any of the following are true:
|
||||
///
|
||||
/// * The nearest row has any cells that have the default background color.
|
||||
@ -1096,21 +1136,52 @@ keybind: Keybinds = .{},
|
||||
/// configuration `font-size` will be used.
|
||||
@"window-inherit-font-size": bool = true,
|
||||
|
||||
/// Configure a preference for window decorations. This setting specifies
|
||||
/// a _preference_; the actual OS, desktop environment, window manager, etc.
|
||||
/// may override this preference. Ghostty will do its best to respect this
|
||||
/// preference but it may not always be possible.
|
||||
///
|
||||
/// Valid values:
|
||||
///
|
||||
/// * `true`
|
||||
/// * `false` - windows won't have native decorations, i.e. titlebar and
|
||||
/// borders. On macOS this also disables tabs and tab overview.
|
||||
/// * `none` - All window decorations will be disabled. Titlebar,
|
||||
/// borders, etc. will not be shown. On macOS, this will also disable
|
||||
/// tabs (enforced by the system).
|
||||
///
|
||||
/// * `auto` - Automatically decide to use either client-side or server-side
|
||||
/// decorations based on the detected preferences of the current OS and
|
||||
/// desktop environment. This option usually makes Ghostty look the most
|
||||
/// "native" for your desktop.
|
||||
///
|
||||
/// * `client` - Prefer client-side decorations.
|
||||
///
|
||||
/// * `server` - Prefer server-side decorations. This is only relevant
|
||||
/// on Linux with GTK. This currently only works on Linux with Wayland
|
||||
/// and the `org_kde_kwin_server_decoration` protocol available (e.g.
|
||||
/// KDE Plasma, but almost any non-GNOME desktop supports this protocol).
|
||||
///
|
||||
/// If `server` is set but the environment doesn't support server-side
|
||||
/// decorations, client-side decorations will be used instead.
|
||||
///
|
||||
/// The default value is `auto`.
|
||||
///
|
||||
/// For the sake of backwards compatibility and convenience, this setting also
|
||||
/// accepts boolean true and false values. If set to `true`, this is equivalent
|
||||
/// to `auto`. If set to `false`, this is equivalent to `none`.
|
||||
/// This is convenient for users who live primarily on systems that don't
|
||||
/// differentiate between client and server-side decorations (e.g. macOS and
|
||||
/// Windows).
|
||||
///
|
||||
/// The "toggle_window_decorations" keybind action can be used to create
|
||||
/// a keybinding to toggle this setting at runtime.
|
||||
/// a keybinding to toggle this setting at runtime. This will always toggle
|
||||
/// back to "auto" if the current value is "none" (this is an issue
|
||||
/// that will be fixed in the future).
|
||||
///
|
||||
/// Changing this configuration in your configuration and reloading will
|
||||
/// only affect new windows. Existing windows will not be affected.
|
||||
///
|
||||
/// macOS: To hide the titlebar without removing the native window borders
|
||||
/// or rounded corners, use `macos-titlebar-style = hidden` instead.
|
||||
@"window-decoration": bool = true,
|
||||
@"window-decoration": WindowDecoration = .auto,
|
||||
|
||||
/// The font that will be used for the application's window and tab titles.
|
||||
///
|
||||
@ -1333,7 +1404,7 @@ keybind: Keybinds = .{},
|
||||
@"resize-overlay-duration": Duration = .{ .duration = 750 * std.time.ns_per_ms },
|
||||
|
||||
/// If true, when there are multiple split panes, the mouse selects the pane
|
||||
/// that is focused. This only applies to the currently focused window; i.e.
|
||||
/// that is focused. This only applies to the currently focused window; e.g.
|
||||
/// mousing over a split in an unfocused window will not focus that split
|
||||
/// and bring the window to front.
|
||||
///
|
||||
@ -1377,7 +1448,7 @@ keybind: Keybinds = .{},
|
||||
/// and a minor amount of user interaction).
|
||||
@"title-report": bool = false,
|
||||
|
||||
/// The total amount of bytes that can be used for image data (i.e. the Kitty
|
||||
/// The total amount of bytes that can be used for image data (e.g. the Kitty
|
||||
/// image protocol) per terminal screen. The maximum value is 4,294,967,295
|
||||
/// (4GiB). The default is 320MB. If this is set to zero, then all image
|
||||
/// protocols will be disabled.
|
||||
@ -1608,7 +1679,9 @@ keybind: Keybinds = .{},
|
||||
/// The default value is `detect`.
|
||||
@"shell-integration": ShellIntegration = .detect,
|
||||
|
||||
/// Shell integration features to enable if shell integration itself is enabled.
|
||||
/// Shell integration features to enable. These require our shell integration
|
||||
/// to be loaded, either automatically via shell-integration or manually.
|
||||
///
|
||||
/// The format of this is a list of features to enable separated by commas. If
|
||||
/// you prefix a feature with `no-` then it is disabled. If you omit a feature,
|
||||
/// its default value is used, so you must explicitly disable features you don't
|
||||
@ -1637,7 +1710,7 @@ keybind: Keybinds = .{},
|
||||
///
|
||||
/// * `none` - OSC 4/10/11 queries receive no reply
|
||||
///
|
||||
/// * `8-bit` - Color components are return unscaled, i.e. `rr/gg/bb`
|
||||
/// * `8-bit` - Color components are return unscaled, e.g. `rr/gg/bb`
|
||||
///
|
||||
/// * `16-bit` - Color components are returned scaled, e.g. `rrrr/gggg/bbbb`
|
||||
///
|
||||
@ -1698,6 +1771,31 @@ keybind: Keybinds = .{},
|
||||
/// open terminals.
|
||||
@"custom-shader-animation": CustomShaderAnimation = .true,
|
||||
|
||||
/// Control the in-app notifications that Ghostty shows.
|
||||
///
|
||||
/// On Linux (GTK) with Adwaita, in-app notifications show up as toasts. Toasts
|
||||
/// appear overlaid on top of the terminal window. They are used to show
|
||||
/// information that is not critical but may be important.
|
||||
///
|
||||
/// Possible notifications are:
|
||||
///
|
||||
/// - `clipboard-copy` (default: true) - Show a notification when text is copied
|
||||
/// to the clipboard.
|
||||
///
|
||||
/// To specify a notification to enable, specify the name of the notification.
|
||||
/// To specify a notification to disable, prefix the name with `no-`. For
|
||||
/// example, to disable `clipboard-copy`, set this configuration to
|
||||
/// `no-clipboard-copy`. To enable it, set this configuration to `clipboard-copy`.
|
||||
///
|
||||
/// Multiple notifications can be enabled or disabled by separating them
|
||||
/// with a comma.
|
||||
///
|
||||
/// A value of "false" will disable all notifications. A value of "true" will
|
||||
/// enable all notifications.
|
||||
///
|
||||
/// This configuration only applies to GTK with Adwaita enabled.
|
||||
@"app-notifications": AppNotifications = .{},
|
||||
|
||||
/// If anything other than false, fullscreen mode on macOS will not use the
|
||||
/// native fullscreen, but make the window fullscreen without animations and
|
||||
/// using a new space. It's faster than the native fullscreen mode since it
|
||||
@ -1736,7 +1834,7 @@ keybind: Keybinds = .{},
|
||||
/// typical for a macOS application and may not work well with all themes.
|
||||
///
|
||||
/// The "transparent" style will also update in real-time to dynamic
|
||||
/// changes to the window background color, i.e. via OSC 11. To make this
|
||||
/// changes to the window background color, e.g. via OSC 11. To make this
|
||||
/// more aesthetically pleasing, this only happens if the terminal is
|
||||
/// a window, tab, or split that borders the top of the window. This
|
||||
/// avoids a disjointed appearance where the titlebar color changes
|
||||
@ -1752,9 +1850,12 @@ keybind: Keybinds = .{},
|
||||
/// The "hidden" style hides the titlebar. Unlike `window-decoration = false`,
|
||||
/// however, it does not remove the frame from the window or cause it to have
|
||||
/// squared corners. Changing to or from this option at run-time may affect
|
||||
/// existing windows in buggy ways. The top titlebar area of the window will
|
||||
/// continue to drag the window around and you will not be able to use
|
||||
/// the mouse for terminal events in this space.
|
||||
/// existing windows in buggy ways.
|
||||
///
|
||||
/// When "hidden", the top titlebar area can no longer be used for dragging
|
||||
/// the window. To drag the window, you can use option+click on the resizable
|
||||
/// areas of the frame to drag the window. This is a standard macOS behavior
|
||||
/// and not something Ghostty enables.
|
||||
///
|
||||
/// The default value is "transparent". This is an opinionated choice
|
||||
/// but its one I think is the most aesthetically pleasing and works in
|
||||
@ -1803,7 +1904,7 @@ keybind: Keybinds = .{},
|
||||
/// - U.S. International
|
||||
///
|
||||
/// Note that if an *Option*-sequence doesn't produce a printable character, it
|
||||
/// will be treated as *Alt* regardless of this setting. (i.e. `alt+ctrl+a`).
|
||||
/// will be treated as *Alt* regardless of this setting. (e.g. `alt+ctrl+a`).
|
||||
///
|
||||
/// Explicit values that can be set:
|
||||
///
|
||||
@ -2027,6 +2128,10 @@ keybind: Keybinds = .{},
|
||||
/// title bar, or you can switch tabs with keybinds.
|
||||
@"gtk-tabs-location": GtkTabsLocation = .top,
|
||||
|
||||
/// If this is `true`, the titlebar will be hidden when the window is maximized,
|
||||
/// and shown when the titlebar is unmaximized. GTK only.
|
||||
@"gtk-titlebar-hide-when-maximized": bool = false,
|
||||
|
||||
/// Determines the appearance of the top and bottom bars when using the
|
||||
/// Adwaita tab bar. This requires `gtk-adwaita` to be enabled (it is
|
||||
/// by default).
|
||||
@ -2041,29 +2146,6 @@ keybind: Keybinds = .{},
|
||||
/// Changing this value at runtime will only affect new windows.
|
||||
@"adw-toolbar-style": AdwToolbarStyle = .raised,
|
||||
|
||||
/// Control the toasts that Ghostty shows. Toasts are small notifications
|
||||
/// that appear overlaid on top of the terminal window. They are used to
|
||||
/// show information that is not critical but may be important.
|
||||
///
|
||||
/// Possible toasts are:
|
||||
///
|
||||
/// - `clipboard-copy` (default: true) - Show a toast when text is copied
|
||||
/// to the clipboard.
|
||||
///
|
||||
/// To specify a toast to enable, specify the name of the toast. To specify
|
||||
/// a toast to disable, prefix the name with `no-`. For example, to disable
|
||||
/// the clipboard-copy toast, set this configuration to `no-clipboard-copy`.
|
||||
/// To enable the clipboard-copy toast, set this configuration to
|
||||
/// `clipboard-copy`.
|
||||
///
|
||||
/// Multiple toasts can be enabled or disabled by separating them with a comma.
|
||||
///
|
||||
/// A value of "false" will disable all toasts. A value of "true" will
|
||||
/// enable all toasts.
|
||||
///
|
||||
/// This configuration only applies to GTK with Adwaita enabled.
|
||||
@"adw-toast": AdwToast = .{},
|
||||
|
||||
/// If `true` (default), then the Ghostty GTK tabs will be "wide." Wide tabs
|
||||
/// are the new typical Gnome style where tabs fill their available space.
|
||||
/// If you set this to `false` then tabs will only take up space they need,
|
||||
@ -2302,13 +2384,13 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .j }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
|
||||
.{ .write_scrollback_file = .paste },
|
||||
.{ .write_screen_file = .paste },
|
||||
);
|
||||
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .j }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true, .alt = true }) },
|
||||
.{ .write_scrollback_file = .open },
|
||||
.{ .write_screen_file = .open },
|
||||
);
|
||||
|
||||
// Expand Selection
|
||||
@ -5665,8 +5747,8 @@ pub const AdwToolbarStyle = enum {
|
||||
@"raised-border",
|
||||
};
|
||||
|
||||
/// See adw-toast
|
||||
pub const AdwToast = packed struct {
|
||||
/// See app-notifications
|
||||
pub const AppNotifications = packed struct {
|
||||
@"clipboard-copy": bool = true,
|
||||
};
|
||||
|
||||
@ -5744,6 +5826,20 @@ pub const GraphemeWidthMethod = enum {
|
||||
unicode,
|
||||
};
|
||||
|
||||
/// See text-blending
|
||||
pub const TextBlending = enum {
|
||||
native,
|
||||
linear,
|
||||
@"linear-corrected",
|
||||
|
||||
pub fn isLinear(self: TextBlending) bool {
|
||||
return switch (self) {
|
||||
.native => false,
|
||||
.linear, .@"linear-corrected" => true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// See freetype-load-flag
|
||||
pub const FreetypeLoadFlags = packed struct {
|
||||
// The defaults here at the time of writing this match the defaults
|
||||
@ -5769,7 +5865,7 @@ pub const AutoUpdate = enum {
|
||||
download,
|
||||
};
|
||||
|
||||
/// See background-blur-radius
|
||||
/// See background-blur
|
||||
pub const BackgroundBlur = union(enum) {
|
||||
false,
|
||||
true,
|
||||
@ -5841,6 +5937,62 @@ pub const BackgroundBlur = union(enum) {
|
||||
}
|
||||
};
|
||||
|
||||
/// See window-decoration
|
||||
pub const WindowDecoration = enum {
|
||||
auto,
|
||||
client,
|
||||
server,
|
||||
none,
|
||||
|
||||
pub fn parseCLI(input_: ?[]const u8) !WindowDecoration {
|
||||
const input = input_ orelse return .auto;
|
||||
|
||||
return if (cli.args.parseBool(input)) |b|
|
||||
if (b) .auto else .none
|
||||
else |_| if (std.meta.stringToEnum(WindowDecoration, input)) |v|
|
||||
v
|
||||
else
|
||||
error.InvalidValue;
|
||||
}
|
||||
|
||||
test "parse WindowDecoration" {
|
||||
const testing = std.testing;
|
||||
|
||||
{
|
||||
const v = try WindowDecoration.parseCLI(null);
|
||||
try testing.expectEqual(WindowDecoration.auto, v);
|
||||
}
|
||||
{
|
||||
const v = try WindowDecoration.parseCLI("true");
|
||||
try testing.expectEqual(WindowDecoration.auto, v);
|
||||
}
|
||||
{
|
||||
const v = try WindowDecoration.parseCLI("false");
|
||||
try testing.expectEqual(WindowDecoration.none, v);
|
||||
}
|
||||
{
|
||||
const v = try WindowDecoration.parseCLI("server");
|
||||
try testing.expectEqual(WindowDecoration.server, v);
|
||||
}
|
||||
{
|
||||
const v = try WindowDecoration.parseCLI("client");
|
||||
try testing.expectEqual(WindowDecoration.client, v);
|
||||
}
|
||||
{
|
||||
const v = try WindowDecoration.parseCLI("auto");
|
||||
try testing.expectEqual(WindowDecoration.auto, v);
|
||||
}
|
||||
{
|
||||
const v = try WindowDecoration.parseCLI("none");
|
||||
try testing.expectEqual(WindowDecoration.none, v);
|
||||
}
|
||||
{
|
||||
try testing.expectError(error.InvalidValue, WindowDecoration.parseCLI(""));
|
||||
try testing.expectError(error.InvalidValue, WindowDecoration.parseCLI("aaaa"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// See theme
|
||||
pub const Theme = struct {
|
||||
light: []const u8,
|
||||
|
@ -192,21 +192,21 @@ test "c_get: background-blur" {
|
||||
defer c.deinit();
|
||||
|
||||
{
|
||||
c.@"background-blur-radius" = .false;
|
||||
c.@"background-blur" = .false;
|
||||
var cval: u8 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
|
||||
try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
|
||||
try testing.expectEqual(0, cval);
|
||||
}
|
||||
{
|
||||
c.@"background-blur-radius" = .true;
|
||||
c.@"background-blur" = .true;
|
||||
var cval: u8 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
|
||||
try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
|
||||
try testing.expectEqual(20, cval);
|
||||
}
|
||||
{
|
||||
c.@"background-blur-radius" = .{ .radius = 42 };
|
||||
c.@"background-blur" = .{ .radius = 42 };
|
||||
var cval: u8 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
|
||||
try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
|
||||
try testing.expectEqual(42, cval);
|
||||
}
|
||||
}
|
||||
|
@ -343,13 +343,12 @@ pub const Face = struct {
|
||||
} = if (!self.isColorGlyph(glyph_index)) .{
|
||||
.color = false,
|
||||
.depth = 1,
|
||||
.space = try macos.graphics.ColorSpace.createDeviceGray(),
|
||||
.context_opts = @intFromEnum(macos.graphics.BitmapInfo.alpha_mask) &
|
||||
@intFromEnum(macos.graphics.ImageAlphaInfo.only),
|
||||
.space = try macos.graphics.ColorSpace.createNamed(.linearGray),
|
||||
.context_opts = @intFromEnum(macos.graphics.ImageAlphaInfo.only),
|
||||
} else .{
|
||||
.color = true,
|
||||
.depth = 4,
|
||||
.space = try macos.graphics.ColorSpace.createDeviceRGB(),
|
||||
.space = try macos.graphics.ColorSpace.createNamed(.displayP3),
|
||||
.context_opts = @intFromEnum(macos.graphics.BitmapInfo.byte_order_32_little) |
|
||||
@intFromEnum(macos.graphics.ImageAlphaInfo.premultiplied_first),
|
||||
};
|
||||
|
@ -111,6 +111,9 @@ pub const GlobalState = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Setup our signal handlers before logging
|
||||
initSignals();
|
||||
|
||||
// Output some debug information right away
|
||||
std.log.info("ghostty version={s}", .{build_config.version_string});
|
||||
std.log.info("ghostty build optimize={s}", .{build_config.mode_string});
|
||||
@ -175,6 +178,28 @@ pub const GlobalState = struct {
|
||||
_ = value.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
fn initSignals() void {
|
||||
// Only posix systems.
|
||||
if (comptime builtin.os.tag == .windows) return;
|
||||
|
||||
const p = std.posix;
|
||||
|
||||
var sa: p.Sigaction = .{
|
||||
.handler = .{ .handler = p.SIG.IGN },
|
||||
.mask = p.empty_sigset,
|
||||
.flags = 0,
|
||||
};
|
||||
|
||||
// We ignore SIGPIPE because it is a common signal we may get
|
||||
// due to how we implement termio. When a terminal is closed we
|
||||
// often write to a broken pipe to exit the read thread. This should
|
||||
// be fixed one day but for now this helps make this a bit more
|
||||
// robust.
|
||||
p.sigaction(p.SIG.PIPE, &sa, null) catch |err| {
|
||||
std.log.warn("failed to ignore SIGPIPE err={}", .{err});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Maintains the Unix resource limits that we set for our process. This
|
||||
|
@ -284,8 +284,15 @@ pub const Action = union(enum) {
|
||||
scroll_page_fractional: f32,
|
||||
scroll_page_lines: i16,
|
||||
|
||||
/// Adjust an existing selection in a given direction. This action
|
||||
/// does nothing if there is no active selection.
|
||||
/// Adjust the current selection in a given direction. Does nothing if no
|
||||
/// selection exists.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - left, right, up, down, page_up, page_down, home, end,
|
||||
/// beginning_of_line, end_of_line
|
||||
///
|
||||
/// Example: Extend selection to the right
|
||||
/// keybind = shift+right=adjust_selection:right
|
||||
adjust_selection: AdjustSelection,
|
||||
|
||||
/// Jump the viewport forward or back by prompt. Positive number is the
|
||||
@ -341,8 +348,13 @@ pub const Action = union(enum) {
|
||||
/// This only works with libadwaita enabled currently.
|
||||
toggle_tab_overview: void,
|
||||
|
||||
/// Create a new split in the given direction. The new split will appear in
|
||||
/// the direction given. For example `new_split:up`. Valid values are left, right, up, down and auto.
|
||||
/// Create a new split in the given direction.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - right, down, left, up, auto (splits along the larger direction)
|
||||
///
|
||||
/// Example: Create split on the right
|
||||
/// keybind = cmd+shift+d=new_split:right
|
||||
new_split: SplitDirection,
|
||||
|
||||
/// Focus on a split in a given direction. For example `goto_split:up`.
|
||||
@ -352,15 +364,26 @@ pub const Action = union(enum) {
|
||||
/// zoom/unzoom the current split.
|
||||
toggle_split_zoom: void,
|
||||
|
||||
/// Resize the current split by moving the split divider in the given
|
||||
/// direction. For example `resize_split:left,10`. The valid directions are up, down, left and right.
|
||||
/// Resize the current split in a given direction.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - up, down, left, right
|
||||
/// - the number of pixels to resize the split by
|
||||
///
|
||||
/// Example: Move divider up 10 pixels
|
||||
/// keybind = cmd+shift+up=resize_split:up,10
|
||||
resize_split: SplitResizeParameter,
|
||||
|
||||
/// Equalize all splits in the current window
|
||||
equalize_splits: void,
|
||||
|
||||
/// Show, hide, or toggle the terminal inspector for the currently focused
|
||||
/// terminal.
|
||||
/// Control the terminal inspector visibility.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - toggle, show, hide
|
||||
///
|
||||
/// Example: Toggle inspector visibility
|
||||
/// keybind = cmd+i=inspector:toggle
|
||||
inspector: InspectorMode,
|
||||
|
||||
/// Open the configuration file in the default OS editor. If your default OS
|
||||
@ -391,6 +414,9 @@ pub const Action = union(enum) {
|
||||
/// This only works for macOS currently.
|
||||
close_all_windows: void,
|
||||
|
||||
/// Toggle maximized window state. This only works on Linux.
|
||||
toggle_maximize: void,
|
||||
|
||||
/// Toggle fullscreen mode of window.
|
||||
toggle_fullscreen: void,
|
||||
|
||||
@ -416,7 +442,7 @@ pub const Action = union(enum) {
|
||||
/// is preserved between appearances, so you can always press the keybinding
|
||||
/// to bring it back up.
|
||||
///
|
||||
/// To enable the quick terminally globally so that Ghostty doesn't
|
||||
/// To enable the quick terminal globally so that Ghostty doesn't
|
||||
/// have to be focused, prefix your keybind with `global`. Example:
|
||||
///
|
||||
/// ```ini
|
||||
@ -513,7 +539,6 @@ pub const Action = union(enum) {
|
||||
pub const SplitFocusDirection = enum {
|
||||
previous,
|
||||
next,
|
||||
|
||||
up,
|
||||
left,
|
||||
down,
|
||||
@ -737,6 +762,7 @@ pub const Action = union(enum) {
|
||||
.close_surface,
|
||||
.close_tab,
|
||||
.close_window,
|
||||
.toggle_maximize,
|
||||
.toggle_fullscreen,
|
||||
.toggle_window_decorations,
|
||||
.toggle_secure_input,
|
||||
|
@ -9,6 +9,7 @@ const entrypoint = switch (build_config.exe_entrypoint) {
|
||||
.mdgen_ghostty_5 => @import("build/mdgen/main_ghostty_5.zig"),
|
||||
.webgen_config => @import("build/webgen/main_config.zig"),
|
||||
.webgen_actions => @import("build/webgen/main_actions.zig"),
|
||||
.webgen_commands => @import("build/webgen/main_commands.zig"),
|
||||
.bench_parser => @import("bench/parser.zig"),
|
||||
.bench_stream => @import("bench/stream.zig"),
|
||||
.bench_codepoint_width => @import("bench/codepoint-width.zig"),
|
||||
|
@ -77,7 +77,22 @@ pub fn cloneInto(cgroup: []const u8) !posix.pid_t {
|
||||
// Get a file descriptor that refers to the cgroup directory in the cgroup
|
||||
// sysfs to pass to the kernel in clone3.
|
||||
const fd: linux.fd_t = fd: {
|
||||
const rc = linux.open(path, linux.O{ .PATH = true, .DIRECTORY = true }, 0);
|
||||
const rc = linux.open(
|
||||
path,
|
||||
.{
|
||||
// Self-explanatory: we expect to open a directory, and
|
||||
// we only need the path-level permissions.
|
||||
.PATH = true,
|
||||
.DIRECTORY = true,
|
||||
|
||||
// We don't want to leak this fd to the child process
|
||||
// when we clone below since we're using this fd for
|
||||
// a cgroup clone.
|
||||
.CLOEXEC = true,
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
switch (posix.errno(rc)) {
|
||||
.SUCCESS => break :fd @as(linux.fd_t, @intCast(rc)),
|
||||
else => |errno| {
|
||||
|
@ -265,16 +265,12 @@ pub const FlatpakHostCommand = struct {
|
||||
}
|
||||
|
||||
// Build our args
|
||||
const args_ptr = c.g_ptr_array_new();
|
||||
{
|
||||
errdefer _ = c.g_ptr_array_free(args_ptr, 1);
|
||||
for (self.argv) |arg| {
|
||||
const args = try arena.alloc(?[*:0]u8, self.argv.len + 1);
|
||||
for (0.., self.argv) |i, arg| {
|
||||
const argZ = try arena.dupeZ(u8, arg);
|
||||
c.g_ptr_array_add(args_ptr, argZ.ptr);
|
||||
args[i] = argZ.ptr;
|
||||
}
|
||||
}
|
||||
const args = c.g_ptr_array_free(args_ptr, 0);
|
||||
defer c.g_free(@as(?*anyopaque, @ptrCast(args)));
|
||||
args[args.len - 1] = null;
|
||||
|
||||
// Get the cwd in case we don't have ours set. A small optimization
|
||||
// would be to do this only if we need it but this isn't a
|
||||
@ -286,7 +282,7 @@ pub const FlatpakHostCommand = struct {
|
||||
const params = c.g_variant_new(
|
||||
"(^ay^aay@a{uh}@a{ss}u)",
|
||||
@as(*const anyopaque, if (self.cwd) |*cwd| cwd.ptr else g_cwd),
|
||||
args,
|
||||
args.ptr,
|
||||
c.g_variant_builder_end(fd_builder),
|
||||
c.g_variant_builder_end(env_builder),
|
||||
@as(c_int, 0),
|
||||
|
@ -3,10 +3,11 @@ const builtin = @import("builtin");
|
||||
const windows = @import("windows.zig");
|
||||
const posix = std.posix;
|
||||
|
||||
/// pipe() that works on Windows and POSIX.
|
||||
/// pipe() that works on Windows and POSIX. For POSIX systems, this sets
|
||||
/// CLOEXEC on the file descriptors.
|
||||
pub fn pipe() ![2]posix.fd_t {
|
||||
switch (builtin.os.tag) {
|
||||
else => return try posix.pipe(),
|
||||
else => return try posix.pipe2(.{ .CLOEXEC = true }),
|
||||
.windows => {
|
||||
var read: windows.HANDLE = undefined;
|
||||
var write: windows.HANDLE = undefined;
|
||||
|
@ -21,7 +21,11 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
|
||||
|
||||
// This is the sentinel value we look for in the path to know
|
||||
// we've found the resources directory.
|
||||
const sentinel = "terminfo/ghostty.termcap";
|
||||
const sentinel = switch (comptime builtin.target.os.tag) {
|
||||
.windows => "terminfo/ghostty.terminfo",
|
||||
.macos => "terminfo/78/xterm-ghostty",
|
||||
else => "terminfo/x/xterm-ghostty",
|
||||
};
|
||||
|
||||
// Get the path to our running binary
|
||||
var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
|
27
src/pty.zig
27
src/pty.zig
@ -94,6 +94,9 @@ const PosixPty = struct {
|
||||
};
|
||||
|
||||
/// The file descriptors for the master and slave side of the pty.
|
||||
/// The slave side is never closed automatically by this struct
|
||||
/// so the caller is responsible for closing it if things
|
||||
/// go wrong.
|
||||
master: Fd,
|
||||
slave: Fd,
|
||||
|
||||
@ -117,6 +120,24 @@ const PosixPty = struct {
|
||||
_ = posix.system.close(slave_fd);
|
||||
}
|
||||
|
||||
// Set CLOEXEC on the master fd, only the slave fd should be inherited
|
||||
// by the child process (shell/command).
|
||||
cloexec: {
|
||||
const flags = std.posix.fcntl(master_fd, std.posix.F.GETFD, 0) catch |err| {
|
||||
log.warn("error getting flags for master fd err={}", .{err});
|
||||
break :cloexec;
|
||||
};
|
||||
|
||||
_ = std.posix.fcntl(
|
||||
master_fd,
|
||||
std.posix.F.SETFD,
|
||||
flags | std.posix.FD_CLOEXEC,
|
||||
) catch |err| {
|
||||
log.warn("error setting CLOEXEC on master fd err={}", .{err});
|
||||
break :cloexec;
|
||||
};
|
||||
}
|
||||
|
||||
// Enable UTF-8 mode. I think this is on by default on Linux but it
|
||||
// is NOT on by default on macOS so we ensure that it is always set.
|
||||
var attrs: c.termios = undefined;
|
||||
@ -126,7 +147,7 @@ const PosixPty = struct {
|
||||
if (c.tcsetattr(master_fd, c.TCSANOW, &attrs) != 0)
|
||||
return error.OpenptyFailed;
|
||||
|
||||
return Pty{
|
||||
return .{
|
||||
.master = master_fd,
|
||||
.slave = slave_fd,
|
||||
};
|
||||
@ -134,7 +155,6 @@ const PosixPty = struct {
|
||||
|
||||
pub fn deinit(self: *Pty) void {
|
||||
_ = posix.system.close(self.master);
|
||||
_ = posix.system.close(self.slave);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
@ -181,6 +201,7 @@ const PosixPty = struct {
|
||||
try posix.sigaction(posix.SIG.HUP, &sa, null);
|
||||
try posix.sigaction(posix.SIG.ILL, &sa, null);
|
||||
try posix.sigaction(posix.SIG.INT, &sa, null);
|
||||
try posix.sigaction(posix.SIG.PIPE, &sa, null);
|
||||
try posix.sigaction(posix.SIG.SEGV, &sa, null);
|
||||
try posix.sigaction(posix.SIG.TRAP, &sa, null);
|
||||
try posix.sigaction(posix.SIG.TERM, &sa, null);
|
||||
@ -201,8 +222,6 @@ const PosixPty = struct {
|
||||
// Can close master/slave pair now
|
||||
posix.close(self.slave);
|
||||
posix.close(self.master);
|
||||
|
||||
// TODO: reset signals
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -21,6 +21,7 @@ const renderer = @import("../renderer.zig");
|
||||
const math = @import("../math.zig");
|
||||
const Surface = @import("../Surface.zig");
|
||||
const link = @import("link.zig");
|
||||
const graphics = macos.graphics;
|
||||
const fgMode = @import("cell.zig").fgMode;
|
||||
const isCovering = @import("cell.zig").isCovering;
|
||||
const shadertoy = @import("shadertoy.zig");
|
||||
@ -105,10 +106,6 @@ default_cursor_color: ?terminal.color.RGB,
|
||||
/// foreground color as the cursor color.
|
||||
cursor_invert: bool,
|
||||
|
||||
/// The current frame background color. This is only updated during
|
||||
/// the updateFrame method.
|
||||
current_background_color: terminal.color.RGB,
|
||||
|
||||
/// The current set of cells to render. This is rebuilt on every frame
|
||||
/// but we keep this around so that we don't reallocate. Each set of
|
||||
/// cells goes into a separate shader.
|
||||
@ -151,6 +148,9 @@ layer: objc.Object, // CAMetalLayer
|
||||
/// a display link.
|
||||
display_link: ?DisplayLink = null,
|
||||
|
||||
/// The `CGColorSpace` that represents our current terminal color space
|
||||
terminal_colorspace: *graphics.ColorSpace,
|
||||
|
||||
/// Custom shader state. This is only set if we have custom shaders.
|
||||
custom_shader_state: ?CustomShaderState = null,
|
||||
|
||||
@ -390,6 +390,8 @@ pub const DerivedConfig = struct {
|
||||
custom_shaders: configpkg.RepeatablePath,
|
||||
links: link.Set,
|
||||
vsync: bool,
|
||||
colorspace: configpkg.Config.WindowColorspace,
|
||||
blending: configpkg.Config.TextBlending,
|
||||
|
||||
pub fn init(
|
||||
alloc_gpa: Allocator,
|
||||
@ -460,7 +462,8 @@ pub const DerivedConfig = struct {
|
||||
.custom_shaders = custom_shaders,
|
||||
.links = links,
|
||||
.vsync = config.@"window-vsync",
|
||||
|
||||
.colorspace = config.@"window-colorspace",
|
||||
.blending = config.@"text-blending",
|
||||
.arena = arena,
|
||||
};
|
||||
}
|
||||
@ -490,10 +493,6 @@ pub fn surfaceInit(surface: *apprt.Surface) !void {
|
||||
}
|
||||
|
||||
pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
var arena = ArenaAllocator.init(alloc);
|
||||
defer arena.deinit();
|
||||
const arena_alloc = arena.allocator();
|
||||
|
||||
const ViewInfo = struct {
|
||||
view: objc.Object,
|
||||
scaleFactor: f64,
|
||||
@ -512,7 +511,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
nswindow.getProperty(?*anyopaque, "contentView").?,
|
||||
);
|
||||
const scaleFactor = nswindow.getProperty(
|
||||
macos.graphics.c.CGFloat,
|
||||
graphics.c.CGFloat,
|
||||
"backingScaleFactor",
|
||||
);
|
||||
|
||||
@ -553,6 +552,40 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
layer.setProperty("opaque", options.config.background_opacity >= 1);
|
||||
layer.setProperty("displaySyncEnabled", options.config.vsync);
|
||||
|
||||
// Set our layer's pixel format appropriately.
|
||||
layer.setProperty(
|
||||
"pixelFormat",
|
||||
// Using an `*_srgb` pixel format makes Metal gamma encode
|
||||
// the pixels written to it *after* blending, which means
|
||||
// we get linear alpha blending rather than gamma-incorrect
|
||||
// blending.
|
||||
if (options.config.blending.isLinear())
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
|
||||
else
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
|
||||
);
|
||||
|
||||
// Set our layer's color space to Display P3.
|
||||
// This allows us to have "Apple-style" alpha blending,
|
||||
// since it seems to be the case that Apple apps like
|
||||
// Terminal and TextEdit render text in the display's
|
||||
// color space using converted colors, which reduces,
|
||||
// but does not fully eliminate blending artifacts.
|
||||
const colorspace = try graphics.ColorSpace.createNamed(.displayP3);
|
||||
defer colorspace.release();
|
||||
layer.setProperty("colorspace", colorspace);
|
||||
|
||||
// Create a colorspace the represents our terminal colors
|
||||
// this will allow us to create e.g. `CGColor`s for things
|
||||
// like the current background color.
|
||||
const terminal_colorspace = try graphics.ColorSpace.createNamed(
|
||||
switch (options.config.colorspace) {
|
||||
.@"display-p3" => .displayP3,
|
||||
.srgb => .sRGB,
|
||||
},
|
||||
);
|
||||
errdefer terminal_colorspace.release();
|
||||
|
||||
// Make our view layer-backed with our Metal layer. On iOS views are
|
||||
// always layer backed so we don't need to do this. But on iOS the
|
||||
// caller MUST be sure to set the layerClass to CAMetalLayer.
|
||||
@ -578,54 +611,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
});
|
||||
errdefer font_shaper.deinit();
|
||||
|
||||
// Load our custom shaders
|
||||
const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles(
|
||||
arena_alloc,
|
||||
options.config.custom_shaders,
|
||||
.msl,
|
||||
) catch |err| err: {
|
||||
log.warn("error loading custom shaders err={}", .{err});
|
||||
break :err &.{};
|
||||
};
|
||||
|
||||
// If we have custom shaders then setup our state
|
||||
var custom_shader_state: ?CustomShaderState = state: {
|
||||
if (custom_shaders.len == 0) break :state null;
|
||||
|
||||
// Build our sampler for our texture
|
||||
var sampler = try mtl_sampler.Sampler.init(gpu_state.device);
|
||||
errdefer sampler.deinit();
|
||||
|
||||
break :state .{
|
||||
// Resolution and screen textures will be fixed up by first
|
||||
// call to setScreenSize. Draw calls will bail out early if
|
||||
// the screen size hasn't been set yet, so it won't error.
|
||||
.front_texture = undefined,
|
||||
.back_texture = undefined,
|
||||
.sampler = sampler,
|
||||
.uniforms = .{
|
||||
.resolution = .{ 0, 0, 1 },
|
||||
.time = 1,
|
||||
.time_delta = 1,
|
||||
.frame_rate = 1,
|
||||
.frame = 1,
|
||||
.channel_time = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
|
||||
.channel_resolution = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
|
||||
.mouse = .{ 0, 0, 0, 0 },
|
||||
.date = .{ 0, 0, 0, 0 },
|
||||
.sample_rate = 1,
|
||||
},
|
||||
|
||||
.first_frame_time = try std.time.Instant.now(),
|
||||
.last_frame_time = try std.time.Instant.now(),
|
||||
};
|
||||
};
|
||||
errdefer if (custom_shader_state) |*state| state.deinit();
|
||||
|
||||
// Initialize our shaders
|
||||
var shaders = try Shaders.init(alloc, gpu_state.device, custom_shaders);
|
||||
errdefer shaders.deinit(alloc);
|
||||
|
||||
// Initialize all the data that requires a critical font section.
|
||||
const font_critical: struct {
|
||||
metrics: font.Metrics,
|
||||
@ -661,7 +646,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
.cursor_color = null,
|
||||
.default_cursor_color = options.config.cursor_color,
|
||||
.cursor_invert = options.config.cursor_invert,
|
||||
.current_background_color = options.config.background,
|
||||
|
||||
// Render state
|
||||
.cells = .{},
|
||||
@ -674,7 +658,16 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
.min_contrast = options.config.min_contrast,
|
||||
.cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) },
|
||||
.cursor_color = undefined,
|
||||
.bg_color = .{
|
||||
options.config.background.r,
|
||||
options.config.background.g,
|
||||
options.config.background.b,
|
||||
@intFromFloat(@round(options.config.background_opacity * 255.0)),
|
||||
},
|
||||
.cursor_wide = false,
|
||||
.use_display_p3 = options.config.colorspace == .@"display-p3",
|
||||
.use_linear_blending = options.config.blending.isLinear(),
|
||||
.use_experimental_linear_correction = options.config.blending == .@"linear-corrected",
|
||||
},
|
||||
|
||||
// Fonts
|
||||
@ -682,16 +675,19 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
.font_shaper = font_shaper,
|
||||
.font_shaper_cache = font.ShaperCache.init(),
|
||||
|
||||
// Shaders
|
||||
.shaders = shaders,
|
||||
// Shaders (initialized below)
|
||||
.shaders = undefined,
|
||||
|
||||
// Metal stuff
|
||||
.layer = layer,
|
||||
.display_link = display_link,
|
||||
.custom_shader_state = custom_shader_state,
|
||||
.terminal_colorspace = terminal_colorspace,
|
||||
.custom_shader_state = null,
|
||||
.gpu_state = gpu_state,
|
||||
};
|
||||
|
||||
try result.initShaders();
|
||||
|
||||
// Do an initialize screen size setup to ensure our undefined values
|
||||
// above are initialized.
|
||||
try result.setScreenSize(result.size);
|
||||
@ -709,6 +705,8 @@ pub fn deinit(self: *Metal) void {
|
||||
}
|
||||
}
|
||||
|
||||
self.terminal_colorspace.release();
|
||||
|
||||
self.cells.deinit(self.alloc);
|
||||
|
||||
self.font_shaper.deinit();
|
||||
@ -723,11 +721,82 @@ pub fn deinit(self: *Metal) void {
|
||||
}
|
||||
self.image_placements.deinit(self.alloc);
|
||||
|
||||
self.deinitShaders();
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
fn deinitShaders(self: *Metal) void {
|
||||
if (self.custom_shader_state) |*state| state.deinit();
|
||||
|
||||
self.shaders.deinit(self.alloc);
|
||||
}
|
||||
|
||||
self.* = undefined;
|
||||
fn initShaders(self: *Metal) !void {
|
||||
var arena = ArenaAllocator.init(self.alloc);
|
||||
defer arena.deinit();
|
||||
const arena_alloc = arena.allocator();
|
||||
|
||||
// Load our custom shaders
|
||||
const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles(
|
||||
arena_alloc,
|
||||
self.config.custom_shaders,
|
||||
.msl,
|
||||
) catch |err| err: {
|
||||
log.warn("error loading custom shaders err={}", .{err});
|
||||
break :err &.{};
|
||||
};
|
||||
|
||||
var custom_shader_state: ?CustomShaderState = state: {
|
||||
if (custom_shaders.len == 0) break :state null;
|
||||
|
||||
// Build our sampler for our texture
|
||||
var sampler = try mtl_sampler.Sampler.init(self.gpu_state.device);
|
||||
errdefer sampler.deinit();
|
||||
|
||||
break :state .{
|
||||
// Resolution and screen textures will be fixed up by first
|
||||
// call to setScreenSize. Draw calls will bail out early if
|
||||
// the screen size hasn't been set yet, so it won't error.
|
||||
.front_texture = undefined,
|
||||
.back_texture = undefined,
|
||||
.sampler = sampler,
|
||||
.uniforms = .{
|
||||
.resolution = .{ 0, 0, 1 },
|
||||
.time = 1,
|
||||
.time_delta = 1,
|
||||
.frame_rate = 1,
|
||||
.frame = 1,
|
||||
.channel_time = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
|
||||
.channel_resolution = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
|
||||
.mouse = .{ 0, 0, 0, 0 },
|
||||
.date = .{ 0, 0, 0, 0 },
|
||||
.sample_rate = 1,
|
||||
},
|
||||
|
||||
.first_frame_time = try std.time.Instant.now(),
|
||||
.last_frame_time = try std.time.Instant.now(),
|
||||
};
|
||||
};
|
||||
errdefer if (custom_shader_state) |*state| state.deinit();
|
||||
|
||||
var shaders = try Shaders.init(
|
||||
self.alloc,
|
||||
self.gpu_state.device,
|
||||
custom_shaders,
|
||||
// Using an `*_srgb` pixel format makes Metal gamma encode
|
||||
// the pixels written to it *after* blending, which means
|
||||
// we get linear alpha blending rather than gamma-incorrect
|
||||
// blending.
|
||||
if (self.config.blending.isLinear())
|
||||
mtl.MTLPixelFormat.bgra8unorm_srgb
|
||||
else
|
||||
mtl.MTLPixelFormat.bgra8unorm,
|
||||
);
|
||||
errdefer shaders.deinit(self.alloc);
|
||||
|
||||
self.shaders = shaders;
|
||||
self.custom_shader_state = custom_shader_state;
|
||||
}
|
||||
|
||||
/// This is called just prior to spinning up the renderer thread for
|
||||
@ -977,19 +1046,6 @@ pub fn updateFrame(
|
||||
}
|
||||
}
|
||||
|
||||
// If our terminal screen size doesn't match our expected renderer
|
||||
// size then we skip a frame. This can happen if the terminal state
|
||||
// is resized between when the renderer mailbox is drained and when
|
||||
// the state mutex is acquired inside this function.
|
||||
//
|
||||
// For some reason this doesn't seem to cause any significant issues
|
||||
// with flickering while resizing. '\_('-')_/'
|
||||
if (self.cells.size.rows != state.terminal.rows or
|
||||
self.cells.size.columns != state.terminal.cols)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the viewport pin so that we can compare it to the current.
|
||||
const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
|
||||
|
||||
@ -1111,7 +1167,38 @@ pub fn updateFrame(
|
||||
self.cells_viewport = critical.viewport_pin;
|
||||
|
||||
// Update our background color
|
||||
self.current_background_color = critical.bg;
|
||||
self.uniforms.bg_color = .{
|
||||
critical.bg.r,
|
||||
critical.bg.g,
|
||||
critical.bg.b,
|
||||
@intFromFloat(@round(self.config.background_opacity * 255.0)),
|
||||
};
|
||||
|
||||
// Update the background color on our layer
|
||||
//
|
||||
// TODO: Is this expensive? Should we be checking if our
|
||||
// bg color has changed first before doing this work?
|
||||
{
|
||||
const color = graphics.c.CGColorCreate(
|
||||
@ptrCast(self.terminal_colorspace),
|
||||
&[4]f64{
|
||||
@as(f64, @floatFromInt(critical.bg.r)) / 255.0,
|
||||
@as(f64, @floatFromInt(critical.bg.g)) / 255.0,
|
||||
@as(f64, @floatFromInt(critical.bg.b)) / 255.0,
|
||||
self.config.background_opacity,
|
||||
},
|
||||
);
|
||||
defer graphics.c.CGColorRelease(color);
|
||||
|
||||
// We use a CATransaction so that Core Animation knows that we
|
||||
// updated the background color property. Otherwise it behaves
|
||||
// weird, not updating the color until we resize.
|
||||
const CATransaction = objc.getClass("CATransaction").?;
|
||||
CATransaction.msgSend(void, "begin", .{});
|
||||
defer CATransaction.msgSend(void, "commit", .{});
|
||||
|
||||
self.layer.setProperty("backgroundColor", color);
|
||||
}
|
||||
|
||||
// Go through our images and see if we need to setup any textures.
|
||||
{
|
||||
@ -1233,10 +1320,10 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
|
||||
attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store));
|
||||
attachment.setProperty("texture", screen_texture.value);
|
||||
attachment.setProperty("clearColor", mtl.MTLClearColor{
|
||||
.red = @as(f32, @floatFromInt(self.current_background_color.r)) / 255 * self.config.background_opacity,
|
||||
.green = @as(f32, @floatFromInt(self.current_background_color.g)) / 255 * self.config.background_opacity,
|
||||
.blue = @as(f32, @floatFromInt(self.current_background_color.b)) / 255 * self.config.background_opacity,
|
||||
.alpha = self.config.background_opacity,
|
||||
.red = 0.0,
|
||||
.green = 0.0,
|
||||
.blue = 0.0,
|
||||
.alpha = 0.0,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1252,19 +1339,19 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
|
||||
defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
|
||||
|
||||
// Draw background images first
|
||||
try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]);
|
||||
try self.drawImagePlacements(encoder, frame, self.image_placements.items[0..self.image_bg_end]);
|
||||
|
||||
// Then draw background cells
|
||||
try self.drawCellBgs(encoder, frame);
|
||||
|
||||
// Then draw images under text
|
||||
try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]);
|
||||
try self.drawImagePlacements(encoder, frame, self.image_placements.items[self.image_bg_end..self.image_text_end]);
|
||||
|
||||
// Then draw fg cells
|
||||
try self.drawCellFgs(encoder, frame, fg_count);
|
||||
|
||||
// Then draw remaining images
|
||||
try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]);
|
||||
try self.drawImagePlacements(encoder, frame, self.image_placements.items[self.image_text_end..]);
|
||||
}
|
||||
|
||||
// If we have custom shaders, then we render them.
|
||||
@ -1457,6 +1544,7 @@ fn drawPostShader(
|
||||
fn drawImagePlacements(
|
||||
self: *Metal,
|
||||
encoder: objc.Object,
|
||||
frame: *const FrameState,
|
||||
placements: []const mtl_image.Placement,
|
||||
) !void {
|
||||
if (placements.len == 0) return;
|
||||
@ -1468,15 +1556,16 @@ fn drawImagePlacements(
|
||||
.{self.shaders.image_pipeline.value},
|
||||
);
|
||||
|
||||
// Set our uniform, which is the only shared buffer
|
||||
// Set our uniforms
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setVertexBytes:length:atIndex:"),
|
||||
.{
|
||||
@as(*const anyopaque, @ptrCast(&self.uniforms)),
|
||||
@as(c_ulong, @sizeOf(@TypeOf(self.uniforms))),
|
||||
@as(c_ulong, 1),
|
||||
},
|
||||
objc.sel("setVertexBuffer:offset:atIndex:"),
|
||||
.{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
|
||||
);
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setFragmentBuffer:offset:atIndex:"),
|
||||
.{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
|
||||
);
|
||||
|
||||
for (placements) |placement| {
|
||||
@ -1588,6 +1677,11 @@ fn drawCellBgs(
|
||||
);
|
||||
|
||||
// Set our buffers
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setVertexBuffer:offset:atIndex:"),
|
||||
.{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
|
||||
);
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setFragmentBuffer:offset:atIndex:"),
|
||||
@ -1647,18 +1741,17 @@ fn drawCellFgs(
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setFragmentTexture:atIndex:"),
|
||||
.{
|
||||
frame.grayscale.value,
|
||||
@as(c_ulong, 0),
|
||||
},
|
||||
.{ frame.grayscale.value, @as(c_ulong, 0) },
|
||||
);
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setFragmentTexture:atIndex:"),
|
||||
.{
|
||||
frame.color.value,
|
||||
@as(c_ulong, 1),
|
||||
},
|
||||
.{ frame.color.value, @as(c_ulong, 1) },
|
||||
);
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setFragmentBuffer:offset:atIndex:"),
|
||||
.{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 2) },
|
||||
);
|
||||
|
||||
encoder.msgSend(
|
||||
@ -2003,17 +2096,73 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
|
||||
// Set our new minimum contrast
|
||||
self.uniforms.min_contrast = config.min_contrast;
|
||||
|
||||
// Set our new color space and blending
|
||||
self.uniforms.use_display_p3 = config.colorspace == .@"display-p3";
|
||||
self.uniforms.use_linear_blending = config.blending.isLinear();
|
||||
self.uniforms.use_experimental_linear_correction = config.blending == .@"linear-corrected";
|
||||
|
||||
// Set our new colors
|
||||
self.default_background_color = config.background;
|
||||
self.default_foreground_color = config.foreground;
|
||||
self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
|
||||
self.cursor_invert = config.cursor_invert;
|
||||
|
||||
// Update our layer's opaqueness and display sync in case they changed.
|
||||
{
|
||||
// We use a CATransaction so that Core Animation knows that we
|
||||
// updated the opaque property. Otherwise it behaves weird, not
|
||||
// properly going from opaque to transparent unless we resize.
|
||||
const CATransaction = objc.getClass("CATransaction").?;
|
||||
CATransaction.msgSend(void, "begin", .{});
|
||||
defer CATransaction.msgSend(void, "commit", .{});
|
||||
|
||||
self.layer.setProperty("opaque", config.background_opacity >= 1);
|
||||
self.layer.setProperty("displaySyncEnabled", config.vsync);
|
||||
}
|
||||
|
||||
// Update our terminal colorspace if it changed
|
||||
if (self.config.colorspace != config.colorspace) {
|
||||
const terminal_colorspace = try graphics.ColorSpace.createNamed(
|
||||
switch (config.colorspace) {
|
||||
.@"display-p3" => .displayP3,
|
||||
.srgb => .sRGB,
|
||||
},
|
||||
);
|
||||
errdefer terminal_colorspace.release();
|
||||
self.terminal_colorspace.release();
|
||||
self.terminal_colorspace = terminal_colorspace;
|
||||
}
|
||||
|
||||
const old_blending = self.config.blending;
|
||||
const old_custom_shaders = self.config.custom_shaders;
|
||||
|
||||
self.config.deinit();
|
||||
self.config = config.*;
|
||||
|
||||
// Reset our viewport to force a rebuild, in case of a font change.
|
||||
self.cells_viewport = null;
|
||||
|
||||
// We reinitialize our shaders if our
|
||||
// blending or custom shaders changed.
|
||||
if (old_blending != config.blending or
|
||||
!old_custom_shaders.equal(config.custom_shaders))
|
||||
{
|
||||
self.deinitShaders();
|
||||
try self.initShaders();
|
||||
// We call setScreenSize to reinitialize
|
||||
// the textures used for custom shaders.
|
||||
if (self.custom_shader_state != null) {
|
||||
try self.setScreenSize(self.size);
|
||||
}
|
||||
// And we update our layer's pixel format appropriately.
|
||||
self.layer.setProperty(
|
||||
"pixelFormat",
|
||||
if (config.blending.isLinear())
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
|
||||
else
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize the screen.
|
||||
@ -2057,7 +2206,7 @@ pub fn setScreenSize(
|
||||
}
|
||||
|
||||
// Set the size of the drawable surface to the bounds
|
||||
self.layer.setProperty("drawableSize", macos.graphics.Size{
|
||||
self.layer.setProperty("drawableSize", graphics.Size{
|
||||
.width = @floatFromInt(size.screen.width),
|
||||
.height = @floatFromInt(size.screen.height),
|
||||
});
|
||||
@ -2089,7 +2238,11 @@ pub fn setScreenSize(
|
||||
.min_contrast = old.min_contrast,
|
||||
.cursor_pos = old.cursor_pos,
|
||||
.cursor_color = old.cursor_color,
|
||||
.bg_color = old.bg_color,
|
||||
.cursor_wide = old.cursor_wide,
|
||||
.use_display_p3 = old.use_display_p3,
|
||||
.use_linear_blending = old.use_linear_blending,
|
||||
.use_experimental_linear_correction = old.use_experimental_linear_correction,
|
||||
};
|
||||
|
||||
// Reset our cell contents if our grid size has changed.
|
||||
@ -2124,7 +2277,17 @@ pub fn setScreenSize(
|
||||
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
|
||||
break :init id_init;
|
||||
};
|
||||
desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm));
|
||||
desc.setProperty(
|
||||
"pixelFormat",
|
||||
// Using an `*_srgb` pixel format makes Metal gamma encode
|
||||
// the pixels written to it *after* blending, which means
|
||||
// we get linear alpha blending rather than gamma-incorrect
|
||||
// blending.
|
||||
if (self.config.blending.isLinear())
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
|
||||
else
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
|
||||
);
|
||||
desc.setProperty("width", @as(c_ulong, @intCast(size.screen.width)));
|
||||
desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height)));
|
||||
desc.setProperty(
|
||||
@ -2154,7 +2317,17 @@ pub fn setScreenSize(
|
||||
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
|
||||
break :init id_init;
|
||||
};
|
||||
desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm));
|
||||
desc.setProperty(
|
||||
"pixelFormat",
|
||||
// Using an `*_srgb` pixel format makes Metal gamma encode
|
||||
// the pixels written to it *after* blending, which means
|
||||
// we get linear alpha blending rather than gamma-incorrect
|
||||
// blending.
|
||||
if (self.config.blending.isLinear())
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
|
||||
else
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
|
||||
);
|
||||
desc.setProperty("width", @as(c_ulong, @intCast(size.screen.width)));
|
||||
desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height)));
|
||||
desc.setProperty(
|
||||
@ -2251,12 +2424,22 @@ fn rebuildCells(
|
||||
}
|
||||
}
|
||||
|
||||
// Go row-by-row to build the cells. We go row by row because we do
|
||||
// font shaping by row. In the future, we will also do dirty tracking
|
||||
// by row.
|
||||
// We rebuild the cells row-by-row because we
|
||||
// do font shaping and dirty tracking by row.
|
||||
var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
|
||||
var y: terminal.size.CellCountInt = screen.pages.rows;
|
||||
// If our cell contents buffer is shorter than the screen viewport,
|
||||
// we render the rows that fit, starting from the bottom. If instead
|
||||
// the viewport is shorter than the cell contents buffer, we align
|
||||
// the top of the viewport with the top of the contents buffer.
|
||||
var y: terminal.size.CellCountInt = @min(
|
||||
screen.pages.rows,
|
||||
self.cells.size.rows,
|
||||
);
|
||||
while (row_it.next()) |row| {
|
||||
// The viewport may have more rows than our cell contents,
|
||||
// so we need to break from the loop early if we hit y = 0.
|
||||
if (y == 0) break;
|
||||
|
||||
y -= 1;
|
||||
|
||||
if (!rebuild) {
|
||||
@ -2315,7 +2498,11 @@ fn rebuildCells(
|
||||
var shaper_cells: ?[]const font.shape.Cell = null;
|
||||
var shaper_cells_i: usize = 0;
|
||||
|
||||
const row_cells = row.cells(.all);
|
||||
const row_cells_all = row.cells(.all);
|
||||
|
||||
// If our viewport is wider than our cell contents buffer,
|
||||
// we still only process cells up to the width of the buffer.
|
||||
const row_cells = row_cells_all[0..@min(row_cells_all.len, self.cells.size.columns)];
|
||||
|
||||
for (row_cells, 0..) |*cell, x| {
|
||||
// If this cell falls within our preedit range then we
|
||||
@ -2466,8 +2653,10 @@ fn rebuildCells(
|
||||
// Foreground alpha for this cell.
|
||||
const alpha: u8 = if (style.flags.faint) 175 else 255;
|
||||
|
||||
// If the cell has a background color, set it.
|
||||
if (bg) |rgb| {
|
||||
// Set the cell's background color.
|
||||
{
|
||||
const rgb = bg orelse self.background_color orelse self.default_background_color;
|
||||
|
||||
// Determine our background alpha. If we have transparency configured
|
||||
// then this is dynamic depending on some situations. This is all
|
||||
// in an attempt to make transparency look the best for various
|
||||
@ -2477,23 +2666,19 @@ fn rebuildCells(
|
||||
|
||||
if (self.config.background_opacity >= 1) break :bg_alpha default;
|
||||
|
||||
// If we're selected, we do not apply background opacity
|
||||
// Cells that are selected should be fully opaque.
|
||||
if (selected) break :bg_alpha default;
|
||||
|
||||
// If we're reversed, do not apply background opacity
|
||||
// Cells that are reversed should be fully opaque.
|
||||
if (style.flags.inverse) break :bg_alpha default;
|
||||
|
||||
// If we have a background and its not the default background
|
||||
// then we apply background opacity
|
||||
if (style.bg(cell, color_palette) != null and !rgb.eql(self.background_color orelse self.default_background_color)) {
|
||||
// Cells that have an explicit bg color should be fully opaque.
|
||||
if (bg_style != null) {
|
||||
break :bg_alpha default;
|
||||
}
|
||||
|
||||
// We apply background opacity.
|
||||
var bg_alpha: f64 = @floatFromInt(default);
|
||||
bg_alpha *= self.config.background_opacity;
|
||||
bg_alpha = @ceil(bg_alpha);
|
||||
break :bg_alpha @intFromFloat(bg_alpha);
|
||||
// Otherwise, we use the configured background opacity.
|
||||
break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0));
|
||||
};
|
||||
|
||||
self.cells.bgCell(y, x).* = .{
|
||||
|
@ -706,8 +706,6 @@ pub fn updateFrame(
|
||||
|
||||
// Update all our data as tightly as possible within the mutex.
|
||||
var critical: Critical = critical: {
|
||||
const grid_size = self.size.grid();
|
||||
|
||||
state.mutex.lock();
|
||||
defer state.mutex.unlock();
|
||||
|
||||
@ -748,19 +746,6 @@ pub fn updateFrame(
|
||||
}
|
||||
}
|
||||
|
||||
// If our terminal screen size doesn't match our expected renderer
|
||||
// size then we skip a frame. This can happen if the terminal state
|
||||
// is resized between when the renderer mailbox is drained and when
|
||||
// the state mutex is acquired inside this function.
|
||||
//
|
||||
// For some reason this doesn't seem to cause any significant issues
|
||||
// with flickering while resizing. '\_('-')_/'
|
||||
if (grid_size.rows != state.terminal.rows or
|
||||
grid_size.columns != state.terminal.cols)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the viewport pin so that we can compare it to the current.
|
||||
const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
|
||||
|
||||
@ -1276,10 +1261,23 @@ pub fn rebuildCells(
|
||||
}
|
||||
}
|
||||
|
||||
// Build each cell
|
||||
const grid_size = self.size.grid();
|
||||
|
||||
// We rebuild the cells row-by-row because we do font shaping by row.
|
||||
var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
|
||||
var y: terminal.size.CellCountInt = screen.pages.rows;
|
||||
// If our cell contents buffer is shorter than the screen viewport,
|
||||
// we render the rows that fit, starting from the bottom. If instead
|
||||
// the viewport is shorter than the cell contents buffer, we align
|
||||
// the top of the viewport with the top of the contents buffer.
|
||||
var y: terminal.size.CellCountInt = @min(
|
||||
screen.pages.rows,
|
||||
grid_size.rows,
|
||||
);
|
||||
while (row_it.next()) |row| {
|
||||
// The viewport may have more rows than our cell contents,
|
||||
// so we need to break from the loop early if we hit y = 0.
|
||||
if (y == 0) break;
|
||||
|
||||
y -= 1;
|
||||
|
||||
// True if we want to do font shaping around the cursor. We want to
|
||||
@ -1356,7 +1354,11 @@ pub fn rebuildCells(
|
||||
var shaper_cells: ?[]const font.shape.Cell = null;
|
||||
var shaper_cells_i: usize = 0;
|
||||
|
||||
const row_cells = row.cells(.all);
|
||||
const row_cells_all = row.cells(.all);
|
||||
|
||||
// If our viewport is wider than our cell contents buffer,
|
||||
// we still only process cells up to the width of the buffer.
|
||||
const row_cells = row_cells_all[0..@min(row_cells_all.len, grid_size.columns)];
|
||||
|
||||
for (row_cells, 0..) |*cell, x| {
|
||||
// If this cell falls within our preedit range then we
|
||||
@ -2350,11 +2352,9 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void {
|
||||
}
|
||||
|
||||
/// Draw the custom shaders.
|
||||
fn drawCustomPrograms(
|
||||
self: *OpenGL,
|
||||
custom_state: *custom.State,
|
||||
) !void {
|
||||
fn drawCustomPrograms(self: *OpenGL, custom_state: *custom.State) !void {
|
||||
_ = self;
|
||||
assert(custom_state.programs.len > 0);
|
||||
|
||||
// Bind our state that is global to all custom shaders
|
||||
const custom_bind = try custom_state.bind();
|
||||
@ -2365,10 +2365,10 @@ fn drawCustomPrograms(
|
||||
|
||||
// Go through each custom shader and draw it.
|
||||
for (custom_state.programs) |program| {
|
||||
// Bind our cell program state, buffers
|
||||
const bind = try program.bind();
|
||||
defer bind.unbind();
|
||||
try bind.draw();
|
||||
try custom_state.copyFramebuffer();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,7 @@ pub const MTLPixelFormat = enum(c_ulong) {
|
||||
rgba8unorm = 70,
|
||||
rgba8uint = 73,
|
||||
bgra8unorm = 80,
|
||||
bgra8unorm_srgb = 81,
|
||||
};
|
||||
|
||||
/// https://developer.apple.com/documentation/metal/mtlpurgeablestate?language=objc
|
||||
|
@ -13,9 +13,7 @@ const log = std.log.scoped(.metal);
|
||||
pub const Shaders = struct {
|
||||
library: objc.Object,
|
||||
|
||||
/// The cell shader is the shader used to render the terminal cells.
|
||||
/// It is a single shader that is used for both the background and
|
||||
/// foreground.
|
||||
/// Renders cell foreground elements (text, decorations).
|
||||
cell_text_pipeline: objc.Object,
|
||||
|
||||
/// The cell background shader is the shader used to render the
|
||||
@ -40,17 +38,18 @@ pub const Shaders = struct {
|
||||
alloc: Allocator,
|
||||
device: objc.Object,
|
||||
post_shaders: []const [:0]const u8,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) !Shaders {
|
||||
const library = try initLibrary(device);
|
||||
errdefer library.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
const cell_text_pipeline = try initCellTextPipeline(device, library);
|
||||
const cell_text_pipeline = try initCellTextPipeline(device, library, pixel_format);
|
||||
errdefer cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
const cell_bg_pipeline = try initCellBgPipeline(device, library);
|
||||
const cell_bg_pipeline = try initCellBgPipeline(device, library, pixel_format);
|
||||
errdefer cell_bg_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
const image_pipeline = try initImagePipeline(device, library);
|
||||
const image_pipeline = try initImagePipeline(device, library, pixel_format);
|
||||
errdefer image_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
const post_pipelines: []const objc.Object = initPostPipelines(
|
||||
@ -58,6 +57,7 @@ pub const Shaders = struct {
|
||||
device,
|
||||
library,
|
||||
post_shaders,
|
||||
pixel_format,
|
||||
) catch |err| err: {
|
||||
// If an error happens while building postprocess shaders we
|
||||
// want to just not use any postprocess shaders since we don't
|
||||
@ -137,9 +137,29 @@ pub const Uniforms = extern struct {
|
||||
cursor_pos: [2]u16 align(4),
|
||||
cursor_color: [4]u8 align(4),
|
||||
|
||||
// Whether the cursor is 2 cells wide.
|
||||
/// The background color for the whole surface.
|
||||
bg_color: [4]u8 align(4),
|
||||
|
||||
/// Whether the cursor is 2 cells wide.
|
||||
cursor_wide: bool align(1),
|
||||
|
||||
/// Indicates that colors provided to the shader are already in
|
||||
/// the P3 color space, so they don't need to be converted from
|
||||
/// sRGB.
|
||||
use_display_p3: bool align(1),
|
||||
|
||||
/// Indicates that the color attachments for the shaders have
|
||||
/// an `*_srgb` pixel format, which means the shaders need to
|
||||
/// output linear RGB colors rather than gamma encoded colors,
|
||||
/// since blending will be performed in linear space and then
|
||||
/// Metal itself will re-encode the colors for storage.
|
||||
use_linear_blending: bool align(1),
|
||||
|
||||
/// Enables a weight correction step that makes text rendered
|
||||
/// with linear alpha blending have a similar apparent weight
|
||||
/// (thickness) to gamma-incorrect blending.
|
||||
use_experimental_linear_correction: bool align(1) = false,
|
||||
|
||||
const PaddingExtend = packed struct(u8) {
|
||||
left: bool = false,
|
||||
right: bool = false,
|
||||
@ -201,6 +221,7 @@ fn initPostPipelines(
|
||||
device: objc.Object,
|
||||
library: objc.Object,
|
||||
shaders: []const [:0]const u8,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) ![]const objc.Object {
|
||||
// If we have no shaders, do nothing.
|
||||
if (shaders.len == 0) return &.{};
|
||||
@ -220,7 +241,12 @@ fn initPostPipelines(
|
||||
// Build each shader. Note we don't use "0.." to build our index
|
||||
// because we need to keep track of our length to clean up above.
|
||||
for (shaders) |source| {
|
||||
pipelines[i] = try initPostPipeline(device, library, source);
|
||||
pipelines[i] = try initPostPipeline(
|
||||
device,
|
||||
library,
|
||||
source,
|
||||
pixel_format,
|
||||
);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
@ -232,6 +258,7 @@ fn initPostPipeline(
|
||||
device: objc.Object,
|
||||
library: objc.Object,
|
||||
data: [:0]const u8,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) !objc.Object {
|
||||
// Create our library which has the shader source
|
||||
const post_library = library: {
|
||||
@ -301,8 +328,7 @@ fn initPostPipeline(
|
||||
.{@as(c_ulong, 0)},
|
||||
);
|
||||
|
||||
// Value is MTLPixelFormatBGRA8Unorm
|
||||
attachment.setProperty("pixelFormat", @as(c_ulong, 80));
|
||||
attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
|
||||
}
|
||||
|
||||
// Make our state
|
||||
@ -343,7 +369,11 @@ pub const CellText = extern struct {
|
||||
};
|
||||
|
||||
/// Initialize the cell render pipeline for our shader library.
|
||||
fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
fn initCellTextPipeline(
|
||||
device: objc.Object,
|
||||
library: objc.Object,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) !objc.Object {
|
||||
// Get our vertex and fragment functions
|
||||
const func_vert = func_vert: {
|
||||
const str = try macos.foundation.String.createWithBytes(
|
||||
@ -427,8 +457,7 @@ fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object
|
||||
.{@as(c_ulong, 0)},
|
||||
);
|
||||
|
||||
// Value is MTLPixelFormatBGRA8Unorm
|
||||
attachment.setProperty("pixelFormat", @as(c_ulong, 80));
|
||||
attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
|
||||
|
||||
// Blending. This is required so that our text we render on top
|
||||
// of our drawable properly blends into the bg.
|
||||
@ -458,11 +487,15 @@ fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object
|
||||
pub const CellBg = [4]u8;
|
||||
|
||||
/// Initialize the cell background render pipeline for our shader library.
|
||||
fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
fn initCellBgPipeline(
|
||||
device: objc.Object,
|
||||
library: objc.Object,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) !objc.Object {
|
||||
// Get our vertex and fragment functions
|
||||
const func_vert = func_vert: {
|
||||
const str = try macos.foundation.String.createWithBytes(
|
||||
"full_screen_vertex",
|
||||
"cell_bg_vertex",
|
||||
.utf8,
|
||||
false,
|
||||
);
|
||||
@ -507,8 +540,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
.{@as(c_ulong, 0)},
|
||||
);
|
||||
|
||||
// Value is MTLPixelFormatBGRA8Unorm
|
||||
attachment.setProperty("pixelFormat", @as(c_ulong, 80));
|
||||
attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
|
||||
|
||||
// Blending. This is required so that our text we render on top
|
||||
// of our drawable properly blends into the bg.
|
||||
@ -535,7 +567,11 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
}
|
||||
|
||||
/// Initialize the image render pipeline for our shader library.
|
||||
fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
fn initImagePipeline(
|
||||
device: objc.Object,
|
||||
library: objc.Object,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) !objc.Object {
|
||||
// Get our vertex and fragment functions
|
||||
const func_vert = func_vert: {
|
||||
const str = try macos.foundation.String.createWithBytes(
|
||||
@ -619,8 +655,7 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
.{@as(c_ulong, 0)},
|
||||
);
|
||||
|
||||
// Value is MTLPixelFormatBGRA8Unorm
|
||||
attachment.setProperty("pixelFormat", @as(c_ulong, 80));
|
||||
attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
|
||||
|
||||
// Blending. This is required so that our text we render on top
|
||||
// of our drawable properly blends into the bg.
|
||||
|
@ -230,6 +230,21 @@ pub const State = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Copy the fbo's attached texture to the backbuffer.
|
||||
pub fn copyFramebuffer(self: *State) !void {
|
||||
const texbind = try self.fb_texture.bind(.@"2D");
|
||||
errdefer texbind.unbind();
|
||||
try texbind.copySubImage2D(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@intFromFloat(self.uniforms.resolution[0]),
|
||||
@intFromFloat(self.uniforms.resolution[1]),
|
||||
);
|
||||
}
|
||||
|
||||
pub const Binding = struct {
|
||||
vao: gl.VertexArray.Binding,
|
||||
ebo: gl.Buffer.Binding,
|
||||
@ -251,7 +266,6 @@ pub const Program = struct {
|
||||
const program = try gl.Program.createVF(
|
||||
@embedFile("../shaders/custom.v.glsl"),
|
||||
src,
|
||||
//@embedFile("../shaders/temp.f.glsl"),
|
||||
);
|
||||
errdefer program.destroy();
|
||||
|
||||
|
@ -18,7 +18,11 @@ struct Uniforms {
|
||||
float min_contrast;
|
||||
ushort2 cursor_pos;
|
||||
uchar4 cursor_color;
|
||||
uchar4 bg_color;
|
||||
bool cursor_wide;
|
||||
bool use_display_p3;
|
||||
bool use_linear_blending;
|
||||
bool use_experimental_linear_correction;
|
||||
};
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@ -26,40 +30,82 @@ struct Uniforms {
|
||||
//-------------------------------------------------------------------
|
||||
#pragma mark - Colors
|
||||
|
||||
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
|
||||
float luminance_component(float c) {
|
||||
if (c <= 0.03928f) {
|
||||
return c / 12.92f;
|
||||
} else {
|
||||
return pow((c + 0.055f) / 1.055f, 2.4f);
|
||||
}
|
||||
// D50-adapted sRGB to XYZ conversion matrix.
|
||||
// http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html
|
||||
constant float3x3 sRGB_XYZ = transpose(float3x3(
|
||||
0.4360747, 0.3850649, 0.1430804,
|
||||
0.2225045, 0.7168786, 0.0606169,
|
||||
0.0139322, 0.0971045, 0.7141733
|
||||
));
|
||||
// XYZ to Display P3 conversion matrix.
|
||||
// http://endavid.com/index.php?entry=79
|
||||
constant float3x3 XYZ_DP3 = transpose(float3x3(
|
||||
2.40414768,-0.99010704,-0.39759019,
|
||||
-0.84239098, 1.79905954, 0.01597023,
|
||||
0.04838763,-0.09752546, 1.27393636
|
||||
));
|
||||
// By composing the two above matrices we get
|
||||
// our sRGB to Display P3 conversion matrix.
|
||||
constant float3x3 sRGB_DP3 = XYZ_DP3 * sRGB_XYZ;
|
||||
|
||||
// Converts a color in linear sRGB to linear Display P3
|
||||
//
|
||||
// TODO: The color matrix should probably be computed
|
||||
// dynamically and passed as a uniform, rather
|
||||
// than being hard coded above.
|
||||
float3 srgb_to_display_p3(float3 srgb) {
|
||||
return sRGB_DP3 * srgb;
|
||||
}
|
||||
|
||||
float relative_luminance(float3 color) {
|
||||
color.r = luminance_component(color.r);
|
||||
color.g = luminance_component(color.g);
|
||||
color.b = luminance_component(color.b);
|
||||
float3 weights = float3(0.2126f, 0.7152f, 0.0722f);
|
||||
return dot(color, weights);
|
||||
// Converts a color from sRGB gamma encoding to linear.
|
||||
float4 linearize(float4 srgb) {
|
||||
bool3 cutoff = srgb.rgb <= 0.04045;
|
||||
float3 lower = srgb.rgb / 12.92;
|
||||
float3 higher = pow((srgb.rgb + 0.055) / 1.055, 2.4);
|
||||
srgb.rgb = mix(higher, lower, float3(cutoff));
|
||||
|
||||
return srgb;
|
||||
}
|
||||
|
||||
// Converts a color from linear to sRGB gamma encoding.
|
||||
float4 unlinearize(float4 linear) {
|
||||
bool3 cutoff = linear.rgb <= 0.0031308;
|
||||
float3 lower = linear.rgb * 12.92;
|
||||
float3 higher = pow(linear.rgb, 1.0 / 2.4) * 1.055 - 0.055;
|
||||
linear.rgb = mix(higher, lower, float3(cutoff));
|
||||
|
||||
return linear;
|
||||
}
|
||||
|
||||
// Compute the luminance of the provided color.
|
||||
//
|
||||
// Takes colors in linear RGB space. If your colors are gamma
|
||||
// encoded, linearize them before using them with this function.
|
||||
float luminance(float3 color) {
|
||||
return dot(color, float3(0.2126f, 0.7152f, 0.0722f));
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
|
||||
//
|
||||
// Takes colors in linear RGB space. If your colors are gamma
|
||||
// encoded, linearize them before using them with this function.
|
||||
float contrast_ratio(float3 color1, float3 color2) {
|
||||
float l1 = relative_luminance(color1);
|
||||
float l2 = relative_luminance(color2);
|
||||
float l1 = luminance(color1);
|
||||
float l2 = luminance(color2);
|
||||
return (max(l1, l2) + 0.05f) / (min(l1, l2) + 0.05f);
|
||||
}
|
||||
|
||||
// Return the fg if the contrast ratio is greater than min, otherwise
|
||||
// return a color that satisfies the contrast ratio. Currently, the color
|
||||
// is always white or black, whichever has the highest contrast ratio.
|
||||
//
|
||||
// Takes colors in linear RGB space. If your colors are gamma
|
||||
// encoded, linearize them before using them with this function.
|
||||
float4 contrasted_color(float min, float4 fg, float4 bg) {
|
||||
float3 fg_premult = fg.rgb * fg.a;
|
||||
float3 bg_premult = bg.rgb * bg.a;
|
||||
float ratio = contrast_ratio(fg_premult, bg_premult);
|
||||
float ratio = contrast_ratio(fg.rgb, bg.rgb);
|
||||
if (ratio < min) {
|
||||
float white_ratio = contrast_ratio(float3(1.0f), bg_premult);
|
||||
float black_ratio = contrast_ratio(float3(0.0f), bg_premult);
|
||||
float white_ratio = contrast_ratio(float3(1.0f), bg.rgb);
|
||||
float black_ratio = contrast_ratio(float3(0.0f), bg.rgb);
|
||||
if (white_ratio > black_ratio) {
|
||||
return float4(1.0f);
|
||||
} else {
|
||||
@ -70,6 +116,62 @@ float4 contrasted_color(float min, float4 fg, float4 bg) {
|
||||
return fg;
|
||||
}
|
||||
|
||||
// Load a 4 byte RGBA non-premultiplied color and linearize
|
||||
// and convert it as necessary depending on the provided info.
|
||||
//
|
||||
// Returns a color in the Display P3 color space.
|
||||
//
|
||||
// If `display_p3` is true, then the provided color is assumed to
|
||||
// already be in the Display P3 color space, otherwise it's treated
|
||||
// as an sRGB color and is appropriately converted to Display P3.
|
||||
//
|
||||
// `linear` controls whether the returned color is linear or gamma encoded.
|
||||
float4 load_color(
|
||||
uchar4 in_color,
|
||||
bool display_p3,
|
||||
bool linear
|
||||
) {
|
||||
// 0 .. 255 -> 0.0 .. 1.0
|
||||
float4 color = float4(in_color) / 255.0f;
|
||||
|
||||
// If our color is already in Display P3 and
|
||||
// we aren't doing linear blending, then we
|
||||
// already have the correct color here and
|
||||
// can premultiply and return it.
|
||||
if (display_p3 && !linear) {
|
||||
color.rgb *= color.a;
|
||||
return color;
|
||||
}
|
||||
|
||||
// The color is in either the sRGB or Display P3 color space,
|
||||
// so in either case, it's a color space which uses the sRGB
|
||||
// transfer function, so we can use one function in order to
|
||||
// linearize it in either case.
|
||||
//
|
||||
// Even if we aren't doing linear blending, the color
|
||||
// needs to be in linear space to convert color spaces.
|
||||
color = linearize(color);
|
||||
|
||||
// If we're *NOT* using display P3 colors, then we're dealing
|
||||
// with an sRGB color, in which case we need to convert it in
|
||||
// to the Display P3 color space, since our output is always
|
||||
// Display P3.
|
||||
if (!display_p3) {
|
||||
color.rgb = srgb_to_display_p3(color.rgb);
|
||||
}
|
||||
|
||||
// If we're not doing linear blending, then we need to
|
||||
// unlinearize after doing the color space conversion.
|
||||
if (!linear) {
|
||||
color = unlinearize(color);
|
||||
}
|
||||
|
||||
// Premultiply our color by its alpha.
|
||||
color.rgb *= color.a;
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Full Screen Vertex Shader
|
||||
//-------------------------------------------------------------------
|
||||
@ -112,25 +214,62 @@ vertex FullScreenVertexOut full_screen_vertex(
|
||||
//-------------------------------------------------------------------
|
||||
#pragma mark - Cell BG Shader
|
||||
|
||||
struct CellBgVertexOut {
|
||||
float4 position [[position]];
|
||||
float4 bg_color;
|
||||
};
|
||||
|
||||
vertex CellBgVertexOut cell_bg_vertex(
|
||||
uint vid [[vertex_id]],
|
||||
constant Uniforms& uniforms [[buffer(1)]]
|
||||
) {
|
||||
CellBgVertexOut out;
|
||||
|
||||
float4 position;
|
||||
position.x = (vid == 2) ? 3.0 : -1.0;
|
||||
position.y = (vid == 0) ? -3.0 : 1.0;
|
||||
position.zw = 1.0;
|
||||
out.position = position;
|
||||
|
||||
// Convert the background color to Display P3
|
||||
out.bg_color = load_color(
|
||||
uniforms.bg_color,
|
||||
uniforms.use_display_p3,
|
||||
uniforms.use_linear_blending
|
||||
);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
fragment float4 cell_bg_fragment(
|
||||
FullScreenVertexOut in [[stage_in]],
|
||||
CellBgVertexOut in [[stage_in]],
|
||||
constant uchar4 *cells [[buffer(0)]],
|
||||
constant Uniforms& uniforms [[buffer(1)]]
|
||||
) {
|
||||
int2 grid_pos = int2(floor((in.position.xy - uniforms.grid_padding.wx) / uniforms.cell_size));
|
||||
|
||||
float4 bg = float4(0.0);
|
||||
// If we have any background transparency then we render bg-colored cells as
|
||||
// fully transparent, since the background is handled by the layer bg color
|
||||
// and we don't want to double up our bg color, but if our bg color is fully
|
||||
// opaque then our layer is opaque and can't handle transparency, so we need
|
||||
// to return the bg color directly instead.
|
||||
if (uniforms.bg_color.a == 255) {
|
||||
bg = in.bg_color;
|
||||
}
|
||||
|
||||
// Clamp x position, extends edge bg colors in to padding on sides.
|
||||
if (grid_pos.x < 0) {
|
||||
if (uniforms.padding_extend & EXTEND_LEFT) {
|
||||
grid_pos.x = 0;
|
||||
} else {
|
||||
return float4(0.0);
|
||||
return bg;
|
||||
}
|
||||
} else if (grid_pos.x > uniforms.grid_size.x - 1) {
|
||||
if (uniforms.padding_extend & EXTEND_RIGHT) {
|
||||
grid_pos.x = uniforms.grid_size.x - 1;
|
||||
} else {
|
||||
return float4(0.0);
|
||||
return bg;
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,18 +278,40 @@ fragment float4 cell_bg_fragment(
|
||||
if (uniforms.padding_extend & EXTEND_UP) {
|
||||
grid_pos.y = 0;
|
||||
} else {
|
||||
return float4(0.0);
|
||||
return bg;
|
||||
}
|
||||
} else if (grid_pos.y > uniforms.grid_size.y - 1) {
|
||||
if (uniforms.padding_extend & EXTEND_DOWN) {
|
||||
grid_pos.y = uniforms.grid_size.y - 1;
|
||||
} else {
|
||||
return float4(0.0);
|
||||
return bg;
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve color for cell and return it.
|
||||
return float4(cells[grid_pos.y * uniforms.grid_size.x + grid_pos.x]) / 255.0;
|
||||
// Load the color for the cell.
|
||||
uchar4 cell_color = cells[grid_pos.y * uniforms.grid_size.x + grid_pos.x];
|
||||
|
||||
// We have special case handling for when the cell color matches the bg color.
|
||||
if (all(cell_color == uniforms.bg_color)) {
|
||||
return bg;
|
||||
}
|
||||
|
||||
// Convert the color and return it.
|
||||
//
|
||||
// TODO: We may want to blend the color with the background
|
||||
// color, rather than purely replacing it, this needs
|
||||
// some consideration about config options though.
|
||||
//
|
||||
// TODO: It might be a good idea to do a pass before this
|
||||
// to convert all of the bg colors, so we don't waste
|
||||
// a bunch of work converting the cell color in every
|
||||
// fragment of each cell. It's not the most epxensive
|
||||
// operation, but it is still wasted work.
|
||||
return load_color(
|
||||
cell_color,
|
||||
uniforms.use_display_p3,
|
||||
uniforms.use_linear_blending
|
||||
);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@ -222,7 +383,6 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
|
||||
CellTextVertexOut out;
|
||||
out.mode = in.mode;
|
||||
out.color = float4(in.color) / 255.0f;
|
||||
|
||||
// === Grid Cell ===
|
||||
// +X
|
||||
@ -277,6 +437,14 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
// be sampled with pixel coordinate mode.
|
||||
out.tex_coord = float2(in.glyph_pos) + float2(in.glyph_size) * corner;
|
||||
|
||||
// Get our color. We always fetch a linearized version to
|
||||
// make it easier to handle minimum contrast calculations.
|
||||
out.color = load_color(
|
||||
in.color,
|
||||
uniforms.use_display_p3,
|
||||
true
|
||||
);
|
||||
|
||||
// If we have a minimum contrast, we need to check if we need to
|
||||
// change the color of the text to ensure it has enough contrast
|
||||
// with the background.
|
||||
@ -285,7 +453,13 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
// and Powerline glyphs to be unaffected (else parts of the line would
|
||||
// have different colors as some parts are displayed via background colors).
|
||||
if (uniforms.min_contrast > 1.0f && in.mode == MODE_TEXT) {
|
||||
float4 bg_color = float4(bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x]) / 255.0f;
|
||||
// Get the BG color
|
||||
float4 bg_color = load_color(
|
||||
bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x],
|
||||
uniforms.use_display_p3,
|
||||
true
|
||||
);
|
||||
// Ensure our minimum contrast
|
||||
out.color = contrasted_color(uniforms.min_contrast, out.color, bg_color);
|
||||
}
|
||||
|
||||
@ -308,7 +482,8 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
fragment float4 cell_text_fragment(
|
||||
CellTextVertexOut in [[stage_in]],
|
||||
texture2d<float> textureGrayscale [[texture(0)]],
|
||||
texture2d<float> textureColor [[texture(1)]]
|
||||
texture2d<float> textureColor [[texture(1)]],
|
||||
constant Uniforms& uniforms [[buffer(2)]]
|
||||
) {
|
||||
constexpr sampler textureSampler(
|
||||
coord::pixel,
|
||||
@ -322,20 +497,63 @@ fragment float4 cell_text_fragment(
|
||||
case MODE_TEXT_CONSTRAINED:
|
||||
case MODE_TEXT_POWERLINE:
|
||||
case MODE_TEXT: {
|
||||
// We premult the alpha to our whole color since our blend function
|
||||
// uses One/OneMinusSourceAlpha to avoid blurry edges.
|
||||
// We first premult our given color.
|
||||
float4 premult = float4(in.color.rgb * in.color.a, in.color.a);
|
||||
// Our input color is always linear.
|
||||
float4 color = in.color;
|
||||
|
||||
// Then premult the texture color
|
||||
// If we're not doing linear blending, then we need to
|
||||
// re-apply the gamma encoding to our color manually.
|
||||
//
|
||||
// Since the alpha is premultiplied, we need to divide
|
||||
// it out before unlinearizing and re-multiply it after.
|
||||
if (!uniforms.use_linear_blending) {
|
||||
color.rgb /= color.a;
|
||||
color = unlinearize(color);
|
||||
color.rgb *= color.a;
|
||||
}
|
||||
|
||||
// Fetch our alpha mask for this pixel.
|
||||
float a = textureGrayscale.sample(textureSampler, in.tex_coord).r;
|
||||
premult = premult * a;
|
||||
|
||||
return premult;
|
||||
// Experimental linear blending weight correction.
|
||||
if (uniforms.use_experimental_linear_correction) {
|
||||
float l = luminance(color.rgb);
|
||||
|
||||
// TODO: This is a dynamic dilation term that biases
|
||||
// the alpha adjustment for small font sizes;
|
||||
// it should be computed by dividing the font
|
||||
// size in `pt`s by `13.0` and using that if
|
||||
// it's less than `1.0`, but for now it's
|
||||
// hard coded at 1.0, which has no effect.
|
||||
float d = 13.0 / 13.0;
|
||||
|
||||
a += pow(a, d + d * l) - pow(a, d + 1.0 - d * l);
|
||||
}
|
||||
|
||||
// Multiply our whole color by the alpha mask.
|
||||
// Since we use premultiplied alpha, this is
|
||||
// the correct way to apply the mask.
|
||||
color *= a;
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
case MODE_TEXT_COLOR: {
|
||||
return textureColor.sample(textureSampler, in.tex_coord);
|
||||
// For now, we assume that color glyphs are
|
||||
// already premultiplied Display P3 colors.
|
||||
float4 color = textureColor.sample(textureSampler, in.tex_coord);
|
||||
|
||||
// If we aren't doing linear blending, we can return this right away.
|
||||
if (!uniforms.use_linear_blending) {
|
||||
return color;
|
||||
}
|
||||
|
||||
// Otherwise we need to linearize the color. Since the alpha is
|
||||
// premultiplied, we need to divide it out before linearizing.
|
||||
color.rgb /= color.a;
|
||||
color = linearize(color);
|
||||
color.rgb *= color.a;
|
||||
|
||||
return color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -409,7 +627,8 @@ vertex ImageVertexOut image_vertex(
|
||||
|
||||
fragment float4 image_fragment(
|
||||
ImageVertexOut in [[stage_in]],
|
||||
texture2d<uint> image [[texture(0)]]
|
||||
texture2d<uint> image [[texture(0)]],
|
||||
constant Uniforms& uniforms [[buffer(1)]]
|
||||
) {
|
||||
constexpr sampler textureSampler(address::clamp_to_edge, filter::linear);
|
||||
|
||||
@ -418,10 +637,12 @@ fragment float4 image_fragment(
|
||||
// our texture to BGRA8Unorm.
|
||||
uint4 rgba = image.sample(textureSampler, in.tex_coord);
|
||||
|
||||
// Convert to float4 and premultiply the alpha. We should also probably
|
||||
// premultiply the alpha in the texture.
|
||||
float4 result = float4(rgba) / 255.0f;
|
||||
result.rgb *= result.a;
|
||||
return result;
|
||||
return load_color(
|
||||
uchar4(rgba),
|
||||
// We assume all images are sRGB regardless of the configured colorspace
|
||||
// TODO: Maybe support wide gamut images?
|
||||
false,
|
||||
uniforms.use_linear_blending
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ supports.
|
||||
|
||||
This README is meant as developer documentation and not as
|
||||
user documentation. For user documentation, see the main
|
||||
README.
|
||||
README or [ghostty.org](https://ghostty.org/docs)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
|
@ -250,10 +250,8 @@ __bp_preexec_invoke_exec() {
|
||||
fi
|
||||
|
||||
local this_command
|
||||
this_command=$(
|
||||
export LC_ALL=C
|
||||
HISTTIMEFORMAT='' builtin history 1 | sed '1 s/^ *[0-9][0-9]*[* ] //'
|
||||
)
|
||||
this_command=$(LC_ALL=C HISTTIMEFORMAT='' builtin history 1)
|
||||
this_command="${this_command#*[[:digit:]][* ] }"
|
||||
|
||||
# Sanity check to make sure we have something to invoke our function with.
|
||||
if [[ -z "$this_command" ]]; then
|
||||
@ -297,10 +295,8 @@ __bp_install() {
|
||||
trap '__bp_preexec_invoke_exec "$_"' DEBUG
|
||||
|
||||
# Preserve any prior DEBUG trap as a preexec function
|
||||
local prior_trap
|
||||
# we can't easily do this with variable expansion. Leaving as sed command.
|
||||
# shellcheck disable=SC2001
|
||||
prior_trap=$(sed "s/[^']*'\(.*\)'[^']*/\1/" <<<"${__bp_trap_string:-}")
|
||||
eval "local trap_argv=(${__bp_trap_string:-})"
|
||||
local prior_trap=${trap_argv[2]:-}
|
||||
unset __bp_trap_string
|
||||
if [[ -n "$prior_trap" ]]; then
|
||||
eval '__bp_original_debug_trap() {
|
||||
|
@ -20,14 +20,13 @@
|
||||
if [[ "$-" != *i* ]] ; then builtin return; fi
|
||||
if [ -z "$GHOSTTY_RESOURCES_DIR" ]; then builtin return; fi
|
||||
|
||||
# When automatic shell integration is active, we need to manually
|
||||
# load the normal bash startup files based on the injected state.
|
||||
# When automatic shell integration is active, we were started in POSIX
|
||||
# mode and need to manually recreate the bash startup sequence.
|
||||
if [ -n "$GHOSTTY_BASH_INJECT" ]; then
|
||||
builtin declare ghostty_bash_inject="$GHOSTTY_BASH_INJECT"
|
||||
builtin unset GHOSTTY_BASH_INJECT ENV
|
||||
|
||||
# At this point, we're in POSIX mode and rely on the injected
|
||||
# flags to guide is through the rest of the startup sequence.
|
||||
# Store a temporary copy of our startup flags and unset these global
|
||||
# environment variables so we can safely handle reentrancy.
|
||||
builtin declare __ghostty_bash_flags="$GHOSTTY_BASH_INJECT"
|
||||
builtin unset ENV GHOSTTY_BASH_INJECT
|
||||
|
||||
# Restore bash's default 'posix' behavior. Also reset 'inherit_errexit',
|
||||
# which doesn't happen as part of the 'posix' reset.
|
||||
@ -40,35 +39,34 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then
|
||||
builtin unset GHOSTTY_BASH_UNEXPORT_HISTFILE
|
||||
fi
|
||||
|
||||
# Manually source the startup files, respecting the injected flags like
|
||||
# --norc and --noprofile that we parsed with the shell integration code.
|
||||
#
|
||||
# See also: run_startup_files() in shell.c in the Bash source code
|
||||
# Manually source the startup files. See INVOCATION in bash(1) and
|
||||
# run_startup_files() in shell.c in the Bash source code.
|
||||
if builtin shopt -q login_shell; then
|
||||
if [[ $ghostty_bash_inject != *"--noprofile"* ]]; then
|
||||
if [[ $__ghostty_bash_flags != *"--noprofile"* ]]; then
|
||||
[ -r /etc/profile ] && builtin source "/etc/profile"
|
||||
for rcfile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
|
||||
[ -r "$rcfile" ] && { builtin source "$rcfile"; break; }
|
||||
for __ghostty_rcfile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
|
||||
[ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile"; break; }
|
||||
done
|
||||
fi
|
||||
else
|
||||
if [[ $ghostty_bash_inject != *"--norc"* ]]; then
|
||||
if [[ $__ghostty_bash_flags != *"--norc"* ]]; then
|
||||
# The location of the system bashrc is determined at bash build
|
||||
# time via -DSYS_BASHRC and can therefore vary across distros:
|
||||
# Arch, Debian, Ubuntu use /etc/bash.bashrc
|
||||
# Fedora uses /etc/bashrc sourced from ~/.bashrc instead of SYS_BASHRC
|
||||
# Void Linux uses /etc/bash/bashrc
|
||||
# Nixos uses /etc/bashrc
|
||||
for rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do
|
||||
[ -r "$rcfile" ] && { builtin source "$rcfile"; break; }
|
||||
for __ghostty_rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do
|
||||
[ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile"; break; }
|
||||
done
|
||||
if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE="$HOME/.bashrc"; fi
|
||||
[ -r "$GHOSTTY_BASH_RCFILE" ] && builtin source "$GHOSTTY_BASH_RCFILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
builtin unset __ghostty_rcfile
|
||||
builtin unset __ghostty_bash_flags
|
||||
builtin unset GHOSTTY_BASH_RCFILE
|
||||
builtin unset ghostty_bash_inject rcfile
|
||||
fi
|
||||
|
||||
# Sudo
|
||||
|
@ -71,11 +71,11 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||
and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x")
|
||||
# Wrap `sudo` command to ensure Ghostty terminfo is preserved
|
||||
function sudo -d "Wrap sudo to preserve terminfo"
|
||||
set --local sudo_has_sudoedit_flags "no"
|
||||
set --function sudo_has_sudoedit_flags "no"
|
||||
for arg in $argv
|
||||
# Check if argument is '-e' or '--edit' (sudoedit flags)
|
||||
if string match -q -- "-e" "$arg"; or string match -q -- "--edit" "$arg"
|
||||
set --local sudo_has_sudoedit_flags "yes"
|
||||
set --function sudo_has_sudoedit_flags "yes"
|
||||
break
|
||||
end
|
||||
# Check if argument is neither an option nor a key-value pair
|
||||
|
@ -4962,7 +4962,7 @@ static int stbi__expand_png_palette(stbi__png *a, stbi_uc *palette, int len, int
|
||||
p = (stbi_uc *) stbi__malloc_mad2(pixel_count, pal_img_n, 0);
|
||||
if (p == NULL) return stbi__err("outofmem", "Out of memory");
|
||||
|
||||
// between here and free(out) below, exitting would leak
|
||||
// between here and free(out) below, exiting would leak
|
||||
temp_out = p;
|
||||
|
||||
if (pal_img_n == 3) {
|
||||
|
@ -520,6 +520,7 @@ pub fn clone(
|
||||
assert(node.data.capacity.rows >= chunk.end - chunk.start);
|
||||
defer node.data.assertIntegrity();
|
||||
node.data.size.rows = chunk.end - chunk.start;
|
||||
node.data.size.cols = chunk.node.data.size.cols;
|
||||
try node.data.cloneFrom(
|
||||
&chunk.node.data,
|
||||
chunk.start,
|
||||
|
@ -6,6 +6,7 @@ const Parser = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const testing = std.testing;
|
||||
const table = @import("parse_table.zig").table;
|
||||
const osc = @import("osc.zig");
|
||||
@ -81,11 +82,15 @@ pub const Action = union(enum) {
|
||||
pub const CSI = struct {
|
||||
intermediates: []u8,
|
||||
params: []u16,
|
||||
params_sep: SepList,
|
||||
final: u8,
|
||||
sep: Sep,
|
||||
|
||||
/// The list of separators used for CSI params. The value of the
|
||||
/// bit can be mapped to Sep.
|
||||
pub const SepList = std.StaticBitSet(MAX_PARAMS);
|
||||
|
||||
/// The separator used for CSI params.
|
||||
pub const Sep = enum { semicolon, colon };
|
||||
pub const Sep = enum(u1) { semicolon = 0, colon = 1 };
|
||||
|
||||
// Implement formatter for logging
|
||||
pub fn format(
|
||||
@ -183,15 +188,6 @@ pub const Action = union(enum) {
|
||||
}
|
||||
};
|
||||
|
||||
/// Keeps track of the parameter sep used for CSI params. We allow colons
|
||||
/// to be used ONLY by the 'm' CSI action.
|
||||
pub const ParamSepState = enum(u8) {
|
||||
none = 0,
|
||||
semicolon = ';',
|
||||
colon = ':',
|
||||
mixed = 1,
|
||||
};
|
||||
|
||||
/// Maximum number of intermediate characters during parsing. This is
|
||||
/// 4 because we also use the intermediates array for UTF8 decoding which
|
||||
/// can be at most 4 bytes.
|
||||
@ -207,8 +203,8 @@ intermediates_idx: u8 = 0,
|
||||
|
||||
/// Param tracking, building
|
||||
params: [MAX_PARAMS]u16 = undefined,
|
||||
params_sep: Action.CSI.SepList = Action.CSI.SepList.initEmpty(),
|
||||
params_idx: u8 = 0,
|
||||
params_sep: ParamSepState = .none,
|
||||
param_acc: u16 = 0,
|
||||
param_acc_idx: u8 = 0,
|
||||
|
||||
@ -312,13 +308,9 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action {
|
||||
// Ignore too many parameters
|
||||
if (self.params_idx >= MAX_PARAMS) break :param null;
|
||||
|
||||
// If this is our first time seeing a parameter, we track
|
||||
// the separator used so that we can't mix separators later.
|
||||
if (self.params_idx == 0) self.params_sep = @enumFromInt(c);
|
||||
if (@as(ParamSepState, @enumFromInt(c)) != self.params_sep) self.params_sep = .mixed;
|
||||
|
||||
// Set param final value
|
||||
self.params[self.params_idx] = self.param_acc;
|
||||
if (c == ':') self.params_sep.set(self.params_idx);
|
||||
self.params_idx += 1;
|
||||
|
||||
// Reset current param value to 0
|
||||
@ -359,29 +351,18 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action {
|
||||
.csi_dispatch = .{
|
||||
.intermediates = self.intermediates[0..self.intermediates_idx],
|
||||
.params = self.params[0..self.params_idx],
|
||||
.params_sep = self.params_sep,
|
||||
.final = c,
|
||||
.sep = switch (self.params_sep) {
|
||||
.none, .semicolon => .semicolon,
|
||||
.colon => .colon,
|
||||
|
||||
// There is nothing that treats mixed separators specially
|
||||
// afaik so we just treat it as a semicolon.
|
||||
.mixed => .semicolon,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// We only allow colon or mixed separators for the 'm' command.
|
||||
switch (self.params_sep) {
|
||||
.none => {},
|
||||
.semicolon => {},
|
||||
.colon, .mixed => if (c != 'm') {
|
||||
if (c != 'm' and self.params_sep.count() > 0) {
|
||||
log.warn(
|
||||
"CSI colon or mixed separators only allowed for 'm' command, got: {}",
|
||||
.{result},
|
||||
);
|
||||
break :csi_dispatch null;
|
||||
},
|
||||
}
|
||||
|
||||
break :csi_dispatch result;
|
||||
@ -400,7 +381,7 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action {
|
||||
pub fn clear(self: *Parser) void {
|
||||
self.intermediates_idx = 0;
|
||||
self.params_idx = 0;
|
||||
self.params_sep = .none;
|
||||
self.params_sep = Action.CSI.SepList.initEmpty();
|
||||
self.param_acc = 0;
|
||||
self.param_acc_idx = 0;
|
||||
}
|
||||
@ -507,10 +488,11 @@ test "csi: SGR ESC [ 38 : 2 m" {
|
||||
|
||||
const d = a[1].?.csi_dispatch;
|
||||
try testing.expect(d.final == 'm');
|
||||
try testing.expect(d.sep == .colon);
|
||||
try testing.expect(d.params.len == 2);
|
||||
try testing.expectEqual(@as(u16, 38), d.params[0]);
|
||||
try testing.expect(d.params_sep.isSet(0));
|
||||
try testing.expectEqual(@as(u16, 2), d.params[1]);
|
||||
try testing.expect(!d.params_sep.isSet(1));
|
||||
}
|
||||
}
|
||||
|
||||
@ -581,13 +563,17 @@ test "csi: SGR ESC [ 48 : 2 m" {
|
||||
|
||||
const d = a[1].?.csi_dispatch;
|
||||
try testing.expect(d.final == 'm');
|
||||
try testing.expect(d.sep == .colon);
|
||||
try testing.expect(d.params.len == 5);
|
||||
try testing.expectEqual(@as(u16, 48), d.params[0]);
|
||||
try testing.expect(d.params_sep.isSet(0));
|
||||
try testing.expectEqual(@as(u16, 2), d.params[1]);
|
||||
try testing.expect(d.params_sep.isSet(1));
|
||||
try testing.expectEqual(@as(u16, 240), d.params[2]);
|
||||
try testing.expect(d.params_sep.isSet(2));
|
||||
try testing.expectEqual(@as(u16, 143), d.params[3]);
|
||||
try testing.expect(d.params_sep.isSet(3));
|
||||
try testing.expectEqual(@as(u16, 104), d.params[4]);
|
||||
try testing.expect(!d.params_sep.isSet(4));
|
||||
}
|
||||
}
|
||||
|
||||
@ -608,10 +594,11 @@ test "csi: SGR ESC [4:3m colon" {
|
||||
|
||||
const d = a[1].?.csi_dispatch;
|
||||
try testing.expect(d.final == 'm');
|
||||
try testing.expect(d.sep == .colon);
|
||||
try testing.expect(d.params.len == 2);
|
||||
try testing.expectEqual(@as(u16, 4), d.params[0]);
|
||||
try testing.expect(d.params_sep.isSet(0));
|
||||
try testing.expectEqual(@as(u16, 3), d.params[1]);
|
||||
try testing.expect(!d.params_sep.isSet(1));
|
||||
}
|
||||
}
|
||||
|
||||
@ -634,14 +621,71 @@ test "csi: SGR with many blank and colon" {
|
||||
|
||||
const d = a[1].?.csi_dispatch;
|
||||
try testing.expect(d.final == 'm');
|
||||
try testing.expect(d.sep == .colon);
|
||||
try testing.expect(d.params.len == 6);
|
||||
try testing.expectEqual(@as(u16, 58), d.params[0]);
|
||||
try testing.expect(d.params_sep.isSet(0));
|
||||
try testing.expectEqual(@as(u16, 2), d.params[1]);
|
||||
try testing.expect(d.params_sep.isSet(1));
|
||||
try testing.expectEqual(@as(u16, 0), d.params[2]);
|
||||
try testing.expect(d.params_sep.isSet(2));
|
||||
try testing.expectEqual(@as(u16, 240), d.params[3]);
|
||||
try testing.expect(d.params_sep.isSet(3));
|
||||
try testing.expectEqual(@as(u16, 143), d.params[4]);
|
||||
try testing.expect(d.params_sep.isSet(4));
|
||||
try testing.expectEqual(@as(u16, 104), d.params[5]);
|
||||
try testing.expect(!d.params_sep.isSet(5));
|
||||
}
|
||||
}
|
||||
|
||||
// This is from a Kakoune actual SGR sequence.
|
||||
test "csi: SGR mixed colon and semicolon with blank" {
|
||||
var p = init();
|
||||
_ = p.next(0x1B);
|
||||
for ("[;4:3;38;2;175;175;215;58:2::190:80:70") |c| {
|
||||
const a = p.next(c);
|
||||
try testing.expect(a[0] == null);
|
||||
try testing.expect(a[1] == null);
|
||||
try testing.expect(a[2] == null);
|
||||
}
|
||||
|
||||
{
|
||||
const a = p.next('m');
|
||||
try testing.expect(p.state == .ground);
|
||||
try testing.expect(a[0] == null);
|
||||
try testing.expect(a[1].? == .csi_dispatch);
|
||||
try testing.expect(a[2] == null);
|
||||
|
||||
const d = a[1].?.csi_dispatch;
|
||||
try testing.expect(d.final == 'm');
|
||||
try testing.expectEqual(14, d.params.len);
|
||||
try testing.expectEqual(@as(u16, 0), d.params[0]);
|
||||
try testing.expect(!d.params_sep.isSet(0));
|
||||
try testing.expectEqual(@as(u16, 4), d.params[1]);
|
||||
try testing.expect(d.params_sep.isSet(1));
|
||||
try testing.expectEqual(@as(u16, 3), d.params[2]);
|
||||
try testing.expect(!d.params_sep.isSet(2));
|
||||
try testing.expectEqual(@as(u16, 38), d.params[3]);
|
||||
try testing.expect(!d.params_sep.isSet(3));
|
||||
try testing.expectEqual(@as(u16, 2), d.params[4]);
|
||||
try testing.expect(!d.params_sep.isSet(4));
|
||||
try testing.expectEqual(@as(u16, 175), d.params[5]);
|
||||
try testing.expect(!d.params_sep.isSet(5));
|
||||
try testing.expectEqual(@as(u16, 175), d.params[6]);
|
||||
try testing.expect(!d.params_sep.isSet(6));
|
||||
try testing.expectEqual(@as(u16, 215), d.params[7]);
|
||||
try testing.expect(!d.params_sep.isSet(7));
|
||||
try testing.expectEqual(@as(u16, 58), d.params[8]);
|
||||
try testing.expect(d.params_sep.isSet(8));
|
||||
try testing.expectEqual(@as(u16, 2), d.params[9]);
|
||||
try testing.expect(d.params_sep.isSet(9));
|
||||
try testing.expectEqual(@as(u16, 0), d.params[10]);
|
||||
try testing.expect(d.params_sep.isSet(10));
|
||||
try testing.expectEqual(@as(u16, 190), d.params[11]);
|
||||
try testing.expect(d.params_sep.isSet(11));
|
||||
try testing.expectEqual(@as(u16, 80), d.params[12]);
|
||||
try testing.expect(d.params_sep.isSet(12));
|
||||
try testing.expectEqual(@as(u16, 70), d.params[13]);
|
||||
try testing.expect(!d.params_sep.isSet(13));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,17 @@
|
||||
//! SGR (Select Graphic Rendition) attrinvbute parsing and types.
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const testing = std.testing;
|
||||
const color = @import("color.zig");
|
||||
const SepList = @import("Parser.zig").Action.CSI.SepList;
|
||||
|
||||
/// Attribute type for SGR
|
||||
pub const Attribute = union(enum) {
|
||||
pub const Tag = std.meta.FieldEnum(Attribute);
|
||||
|
||||
/// Unset all attributes
|
||||
unset: void,
|
||||
unset,
|
||||
|
||||
/// Unknown attribute, the raw CSI command parameters are here.
|
||||
unknown: struct {
|
||||
@ -19,43 +23,43 @@ pub const Attribute = union(enum) {
|
||||
},
|
||||
|
||||
/// Bold the text.
|
||||
bold: void,
|
||||
reset_bold: void,
|
||||
bold,
|
||||
reset_bold,
|
||||
|
||||
/// Italic text.
|
||||
italic: void,
|
||||
reset_italic: void,
|
||||
italic,
|
||||
reset_italic,
|
||||
|
||||
/// Faint/dim text.
|
||||
/// Note: reset faint is the same SGR code as reset bold
|
||||
faint: void,
|
||||
faint,
|
||||
|
||||
/// Underline the text
|
||||
underline: Underline,
|
||||
reset_underline: void,
|
||||
reset_underline,
|
||||
underline_color: color.RGB,
|
||||
@"256_underline_color": u8,
|
||||
reset_underline_color: void,
|
||||
reset_underline_color,
|
||||
|
||||
// Overline the text
|
||||
overline: void,
|
||||
reset_overline: void,
|
||||
overline,
|
||||
reset_overline,
|
||||
|
||||
/// Blink the text
|
||||
blink: void,
|
||||
reset_blink: void,
|
||||
blink,
|
||||
reset_blink,
|
||||
|
||||
/// Invert fg/bg colors.
|
||||
inverse: void,
|
||||
reset_inverse: void,
|
||||
inverse,
|
||||
reset_inverse,
|
||||
|
||||
/// Invisible
|
||||
invisible: void,
|
||||
reset_invisible: void,
|
||||
invisible,
|
||||
reset_invisible,
|
||||
|
||||
/// Strikethrough the text.
|
||||
strikethrough: void,
|
||||
reset_strikethrough: void,
|
||||
strikethrough,
|
||||
reset_strikethrough,
|
||||
|
||||
/// Set foreground color as RGB values.
|
||||
direct_color_fg: color.RGB,
|
||||
@ -68,8 +72,8 @@ pub const Attribute = union(enum) {
|
||||
@"8_fg": color.Name,
|
||||
|
||||
/// Reset the fg/bg to their default values.
|
||||
reset_fg: void,
|
||||
reset_bg: void,
|
||||
reset_fg,
|
||||
reset_bg,
|
||||
|
||||
/// Set the background/foreground as a named bright color attribute.
|
||||
@"8_bright_bg": color.Name,
|
||||
@ -94,11 +98,9 @@ pub const Attribute = union(enum) {
|
||||
/// Parser parses the attributes from a list of SGR parameters.
|
||||
pub const Parser = struct {
|
||||
params: []const u16,
|
||||
params_sep: SepList = SepList.initEmpty(),
|
||||
idx: usize = 0,
|
||||
|
||||
/// True if the separator is a colon
|
||||
colon: bool = false,
|
||||
|
||||
/// Next returns the next attribute or null if there are no more attributes.
|
||||
pub fn next(self: *Parser) ?Attribute {
|
||||
if (self.idx > self.params.len) return null;
|
||||
@ -106,220 +108,261 @@ pub const Parser = struct {
|
||||
// Implicitly means unset
|
||||
if (self.params.len == 0) {
|
||||
self.idx += 1;
|
||||
return Attribute{ .unset = {} };
|
||||
return .unset;
|
||||
}
|
||||
|
||||
const slice = self.params[self.idx..self.params.len];
|
||||
const colon = self.params_sep.isSet(self.idx);
|
||||
self.idx += 1;
|
||||
|
||||
// Our last one will have an idx be the last value.
|
||||
if (slice.len == 0) return null;
|
||||
|
||||
// If we have a colon separator then we need to ensure we're
|
||||
// parsing a value that allows it.
|
||||
if (colon) switch (slice[0]) {
|
||||
4, 38, 48, 58 => {},
|
||||
|
||||
else => {
|
||||
// Consume all the colon separated values.
|
||||
const start = self.idx;
|
||||
while (self.params_sep.isSet(self.idx)) self.idx += 1;
|
||||
self.idx += 1;
|
||||
return .{ .unknown = .{
|
||||
.full = self.params,
|
||||
.partial = slice[0 .. self.idx - start + 1],
|
||||
} };
|
||||
},
|
||||
};
|
||||
|
||||
switch (slice[0]) {
|
||||
0 => return Attribute{ .unset = {} },
|
||||
0 => return .unset,
|
||||
|
||||
1 => return Attribute{ .bold = {} },
|
||||
1 => return .bold,
|
||||
|
||||
2 => return Attribute{ .faint = {} },
|
||||
2 => return .faint,
|
||||
|
||||
3 => return Attribute{ .italic = {} },
|
||||
3 => return .italic,
|
||||
|
||||
4 => blk: {
|
||||
if (self.colon) {
|
||||
switch (slice.len) {
|
||||
// 0 is unreachable because we're here and we read
|
||||
// an element to get here.
|
||||
0 => unreachable,
|
||||
4 => underline: {
|
||||
if (colon) {
|
||||
assert(slice.len >= 2);
|
||||
if (self.isColon()) {
|
||||
self.consumeUnknownColon();
|
||||
break :underline;
|
||||
}
|
||||
|
||||
// 1 is possible if underline is the last element.
|
||||
1 => return Attribute{ .underline = .single },
|
||||
|
||||
// 2 means we have a specific underline style.
|
||||
2 => {
|
||||
self.idx += 1;
|
||||
switch (slice[1]) {
|
||||
0 => return Attribute{ .reset_underline = {} },
|
||||
1 => return Attribute{ .underline = .single },
|
||||
2 => return Attribute{ .underline = .double },
|
||||
3 => return Attribute{ .underline = .curly },
|
||||
4 => return Attribute{ .underline = .dotted },
|
||||
5 => return Attribute{ .underline = .dashed },
|
||||
0 => return .reset_underline,
|
||||
1 => return .{ .underline = .single },
|
||||
2 => return .{ .underline = .double },
|
||||
3 => return .{ .underline = .curly },
|
||||
4 => return .{ .underline = .dotted },
|
||||
5 => return .{ .underline = .dashed },
|
||||
|
||||
// For unknown underline styles, just render
|
||||
// a single underline.
|
||||
else => return Attribute{ .underline = .single },
|
||||
else => return .{ .underline = .single },
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .underline = .single };
|
||||
},
|
||||
|
||||
// Colon-separated must only be 2.
|
||||
else => break :blk,
|
||||
}
|
||||
}
|
||||
5 => return .blink,
|
||||
|
||||
return Attribute{ .underline = .single };
|
||||
},
|
||||
6 => return .blink,
|
||||
|
||||
5 => return Attribute{ .blink = {} },
|
||||
7 => return .inverse,
|
||||
|
||||
6 => return Attribute{ .blink = {} },
|
||||
8 => return .invisible,
|
||||
|
||||
7 => return Attribute{ .inverse = {} },
|
||||
9 => return .strikethrough,
|
||||
|
||||
8 => return Attribute{ .invisible = {} },
|
||||
21 => return .{ .underline = .double },
|
||||
|
||||
9 => return Attribute{ .strikethrough = {} },
|
||||
22 => return .reset_bold,
|
||||
|
||||
21 => return Attribute{ .underline = .double },
|
||||
23 => return .reset_italic,
|
||||
|
||||
22 => return Attribute{ .reset_bold = {} },
|
||||
24 => return .reset_underline,
|
||||
|
||||
23 => return Attribute{ .reset_italic = {} },
|
||||
25 => return .reset_blink,
|
||||
|
||||
24 => return Attribute{ .reset_underline = {} },
|
||||
27 => return .reset_inverse,
|
||||
|
||||
25 => return Attribute{ .reset_blink = {} },
|
||||
28 => return .reset_invisible,
|
||||
|
||||
27 => return Attribute{ .reset_inverse = {} },
|
||||
29 => return .reset_strikethrough,
|
||||
|
||||
28 => return Attribute{ .reset_invisible = {} },
|
||||
|
||||
29 => return Attribute{ .reset_strikethrough = {} },
|
||||
|
||||
30...37 => return Attribute{
|
||||
30...37 => return .{
|
||||
.@"8_fg" = @enumFromInt(slice[0] - 30),
|
||||
},
|
||||
|
||||
38 => if (slice.len >= 2) switch (slice[1]) {
|
||||
// `2` indicates direct-color (r, g, b).
|
||||
// We need at least 3 more params for this to make sense.
|
||||
2 => if (slice.len >= 5) {
|
||||
self.idx += 4;
|
||||
// When a colon separator is used, there may or may not be
|
||||
// a color space identifier as the third param, which we
|
||||
// need to ignore (it has no standardized behavior).
|
||||
const rgb = if (slice.len == 5 or !self.colon)
|
||||
slice[2..5]
|
||||
else rgb: {
|
||||
self.idx += 1;
|
||||
break :rgb slice[3..6];
|
||||
};
|
||||
2 => if (self.parseDirectColor(
|
||||
.direct_color_fg,
|
||||
slice,
|
||||
colon,
|
||||
)) |v| return v,
|
||||
|
||||
// We use @truncate because the value should be 0 to 255. If
|
||||
// it isn't, the behavior is undefined so we just... truncate it.
|
||||
return Attribute{
|
||||
.direct_color_fg = .{
|
||||
.r = @truncate(rgb[0]),
|
||||
.g = @truncate(rgb[1]),
|
||||
.b = @truncate(rgb[2]),
|
||||
},
|
||||
};
|
||||
},
|
||||
// `5` indicates indexed color.
|
||||
5 => if (slice.len >= 3) {
|
||||
self.idx += 2;
|
||||
return Attribute{
|
||||
return .{
|
||||
.@"256_fg" = @truncate(slice[2]),
|
||||
};
|
||||
},
|
||||
else => {},
|
||||
},
|
||||
|
||||
39 => return Attribute{ .reset_fg = {} },
|
||||
39 => return .reset_fg,
|
||||
|
||||
40...47 => return Attribute{
|
||||
40...47 => return .{
|
||||
.@"8_bg" = @enumFromInt(slice[0] - 40),
|
||||
},
|
||||
|
||||
48 => if (slice.len >= 2) switch (slice[1]) {
|
||||
// `2` indicates direct-color (r, g, b).
|
||||
// We need at least 3 more params for this to make sense.
|
||||
2 => if (slice.len >= 5) {
|
||||
self.idx += 4;
|
||||
// When a colon separator is used, there may or may not be
|
||||
// a color space identifier as the third param, which we
|
||||
// need to ignore (it has no standardized behavior).
|
||||
const rgb = if (slice.len == 5 or !self.colon)
|
||||
slice[2..5]
|
||||
else rgb: {
|
||||
self.idx += 1;
|
||||
break :rgb slice[3..6];
|
||||
};
|
||||
2 => if (self.parseDirectColor(
|
||||
.direct_color_bg,
|
||||
slice,
|
||||
colon,
|
||||
)) |v| return v,
|
||||
|
||||
// We use @truncate because the value should be 0 to 255. If
|
||||
// it isn't, the behavior is undefined so we just... truncate it.
|
||||
return Attribute{
|
||||
.direct_color_bg = .{
|
||||
.r = @truncate(rgb[0]),
|
||||
.g = @truncate(rgb[1]),
|
||||
.b = @truncate(rgb[2]),
|
||||
},
|
||||
};
|
||||
},
|
||||
// `5` indicates indexed color.
|
||||
5 => if (slice.len >= 3) {
|
||||
self.idx += 2;
|
||||
return Attribute{
|
||||
return .{
|
||||
.@"256_bg" = @truncate(slice[2]),
|
||||
};
|
||||
},
|
||||
else => {},
|
||||
},
|
||||
|
||||
49 => return Attribute{ .reset_bg = {} },
|
||||
49 => return .reset_bg,
|
||||
|
||||
53 => return Attribute{ .overline = {} },
|
||||
55 => return Attribute{ .reset_overline = {} },
|
||||
53 => return .overline,
|
||||
55 => return .reset_overline,
|
||||
|
||||
58 => if (slice.len >= 2) switch (slice[1]) {
|
||||
// `2` indicates direct-color (r, g, b).
|
||||
// We need at least 3 more params for this to make sense.
|
||||
2 => if (slice.len >= 5) {
|
||||
self.idx += 4;
|
||||
// When a colon separator is used, there may or may not be
|
||||
// a color space identifier as the third param, which we
|
||||
// need to ignore (it has no standardized behavior).
|
||||
const rgb = if (slice.len == 5 or !self.colon)
|
||||
slice[2..5]
|
||||
else rgb: {
|
||||
self.idx += 1;
|
||||
break :rgb slice[3..6];
|
||||
};
|
||||
2 => if (self.parseDirectColor(
|
||||
.underline_color,
|
||||
slice,
|
||||
colon,
|
||||
)) |v| return v,
|
||||
|
||||
// We use @truncate because the value should be 0 to 255. If
|
||||
// it isn't, the behavior is undefined so we just... truncate it.
|
||||
return Attribute{
|
||||
.underline_color = .{
|
||||
.r = @truncate(rgb[0]),
|
||||
.g = @truncate(rgb[1]),
|
||||
.b = @truncate(rgb[2]),
|
||||
},
|
||||
};
|
||||
},
|
||||
// `5` indicates indexed color.
|
||||
5 => if (slice.len >= 3) {
|
||||
self.idx += 2;
|
||||
return Attribute{
|
||||
return .{
|
||||
.@"256_underline_color" = @truncate(slice[2]),
|
||||
};
|
||||
},
|
||||
else => {},
|
||||
},
|
||||
|
||||
59 => return Attribute{ .reset_underline_color = {} },
|
||||
59 => return .reset_underline_color,
|
||||
|
||||
90...97 => return Attribute{
|
||||
90...97 => return .{
|
||||
// 82 instead of 90 to offset to "bright" colors
|
||||
.@"8_bright_fg" = @enumFromInt(slice[0] - 82),
|
||||
},
|
||||
|
||||
100...107 => return Attribute{
|
||||
100...107 => return .{
|
||||
.@"8_bright_bg" = @enumFromInt(slice[0] - 92),
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
return Attribute{ .unknown = .{ .full = self.params, .partial = slice } };
|
||||
return .{ .unknown = .{ .full = self.params, .partial = slice } };
|
||||
}
|
||||
|
||||
fn parseDirectColor(
|
||||
self: *Parser,
|
||||
comptime tag: Attribute.Tag,
|
||||
slice: []const u16,
|
||||
colon: bool,
|
||||
) ?Attribute {
|
||||
// Any direct color style must have at least 5 values.
|
||||
if (slice.len < 5) return null;
|
||||
|
||||
// Only used for direct color sets (38, 48, 58) and subparam 2.
|
||||
assert(slice[1] == 2);
|
||||
|
||||
// Note: We use @truncate because the value should be 0 to 255. If
|
||||
// it isn't, the behavior is undefined so we just... truncate it.
|
||||
|
||||
// If we don't have a colon, then we expect exactly 3 semicolon
|
||||
// separated values.
|
||||
if (!colon) {
|
||||
self.idx += 4;
|
||||
return @unionInit(Attribute, @tagName(tag), .{
|
||||
.r = @truncate(slice[2]),
|
||||
.g = @truncate(slice[3]),
|
||||
.b = @truncate(slice[4]),
|
||||
});
|
||||
}
|
||||
|
||||
// We have a colon, we might have either 5 or 6 values depending
|
||||
// on if the colorspace is present.
|
||||
const count = self.countColon();
|
||||
switch (count) {
|
||||
3 => {
|
||||
self.idx += 4;
|
||||
return @unionInit(Attribute, @tagName(tag), .{
|
||||
.r = @truncate(slice[2]),
|
||||
.g = @truncate(slice[3]),
|
||||
.b = @truncate(slice[4]),
|
||||
});
|
||||
},
|
||||
|
||||
4 => {
|
||||
self.idx += 5;
|
||||
return @unionInit(Attribute, @tagName(tag), .{
|
||||
.r = @truncate(slice[3]),
|
||||
.g = @truncate(slice[4]),
|
||||
.b = @truncate(slice[5]),
|
||||
});
|
||||
},
|
||||
|
||||
else => {
|
||||
self.consumeUnknownColon();
|
||||
return null;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the present position has a colon separator.
|
||||
/// This always returns false for the last value since it has no
|
||||
/// separator.
|
||||
fn isColon(self: *Parser) bool {
|
||||
// The `- 1` here is because the last value has no separator.
|
||||
if (self.idx >= self.params.len - 1) return false;
|
||||
return self.params_sep.isSet(self.idx);
|
||||
}
|
||||
|
||||
fn countColon(self: *Parser) usize {
|
||||
var count: usize = 0;
|
||||
var idx = self.idx;
|
||||
while (idx < self.params.len - 1 and self.params_sep.isSet(idx)) : (idx += 1) {
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Consumes all the remaining parameters separated by a colon and
|
||||
/// returns an unknown attribute.
|
||||
fn consumeUnknownColon(self: *Parser) void {
|
||||
const count = self.countColon();
|
||||
self.idx += count + 1;
|
||||
}
|
||||
};
|
||||
|
||||
@ -329,7 +372,7 @@ fn testParse(params: []const u16) Attribute {
|
||||
}
|
||||
|
||||
fn testParseColon(params: []const u16) Attribute {
|
||||
var p: Parser = .{ .params = params, .colon = true };
|
||||
var p: Parser = .{ .params = params, .params_sep = SepList.initFull() };
|
||||
return p.next().?;
|
||||
}
|
||||
|
||||
@ -366,6 +409,35 @@ test "sgr: Parser multiple" {
|
||||
try testing.expect(p.next() == null);
|
||||
}
|
||||
|
||||
test "sgr: unsupported with colon" {
|
||||
var p: Parser = .{
|
||||
.params = &[_]u16{ 0, 4, 1 },
|
||||
.params_sep = sep: {
|
||||
var list = SepList.initEmpty();
|
||||
list.set(0);
|
||||
break :sep list;
|
||||
},
|
||||
};
|
||||
try testing.expect(p.next().? == .unknown);
|
||||
try testing.expect(p.next().? == .bold);
|
||||
try testing.expect(p.next() == null);
|
||||
}
|
||||
|
||||
test "sgr: unsupported with multiple colon" {
|
||||
var p: Parser = .{
|
||||
.params = &[_]u16{ 0, 4, 2, 1 },
|
||||
.params_sep = sep: {
|
||||
var list = SepList.initEmpty();
|
||||
list.set(0);
|
||||
list.set(1);
|
||||
break :sep list;
|
||||
},
|
||||
};
|
||||
try testing.expect(p.next().? == .unknown);
|
||||
try testing.expect(p.next().? == .bold);
|
||||
try testing.expect(p.next() == null);
|
||||
}
|
||||
|
||||
test "sgr: bold" {
|
||||
{
|
||||
const v = testParse(&[_]u16{1});
|
||||
@ -439,6 +511,37 @@ test "sgr: underline styles" {
|
||||
}
|
||||
}
|
||||
|
||||
test "sgr: underline style with more" {
|
||||
var p: Parser = .{
|
||||
.params = &[_]u16{ 4, 2, 1 },
|
||||
.params_sep = sep: {
|
||||
var list = SepList.initEmpty();
|
||||
list.set(0);
|
||||
break :sep list;
|
||||
},
|
||||
};
|
||||
|
||||
try testing.expect(p.next().? == .underline);
|
||||
try testing.expect(p.next().? == .bold);
|
||||
try testing.expect(p.next() == null);
|
||||
}
|
||||
|
||||
test "sgr: underline style with too many colons" {
|
||||
var p: Parser = .{
|
||||
.params = &[_]u16{ 4, 2, 3, 1 },
|
||||
.params_sep = sep: {
|
||||
var list = SepList.initEmpty();
|
||||
list.set(0);
|
||||
list.set(1);
|
||||
break :sep list;
|
||||
},
|
||||
};
|
||||
|
||||
try testing.expect(p.next().? == .unknown);
|
||||
try testing.expect(p.next().? == .bold);
|
||||
try testing.expect(p.next() == null);
|
||||
}
|
||||
|
||||
test "sgr: blink" {
|
||||
{
|
||||
const v = testParse(&[_]u16{5});
|
||||
@ -592,13 +695,13 @@ test "sgr: underline, bg, and fg" {
|
||||
|
||||
test "sgr: direct color fg missing color" {
|
||||
// This used to crash
|
||||
var p: Parser = .{ .params = &[_]u16{ 38, 5 }, .colon = false };
|
||||
var p: Parser = .{ .params = &[_]u16{ 38, 5 } };
|
||||
while (p.next()) |_| {}
|
||||
}
|
||||
|
||||
test "sgr: direct color bg missing color" {
|
||||
// This used to crash
|
||||
var p: Parser = .{ .params = &[_]u16{ 48, 5 }, .colon = false };
|
||||
var p: Parser = .{ .params = &[_]u16{ 48, 5 } };
|
||||
while (p.next()) |_| {}
|
||||
}
|
||||
|
||||
@ -608,7 +711,7 @@ test "sgr: direct fg/bg/underline ignore optional color space" {
|
||||
// Colon version should skip the optional color space identifier
|
||||
{
|
||||
// 3 8 : 2 : Pi : Pr : Pg : Pb
|
||||
const v = testParseColon(&[_]u16{ 38, 2, 0, 1, 2, 3, 4 });
|
||||
const v = testParseColon(&[_]u16{ 38, 2, 0, 1, 2, 3 });
|
||||
try testing.expect(v == .direct_color_fg);
|
||||
try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
|
||||
try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
|
||||
@ -616,7 +719,7 @@ test "sgr: direct fg/bg/underline ignore optional color space" {
|
||||
}
|
||||
{
|
||||
// 4 8 : 2 : Pi : Pr : Pg : Pb
|
||||
const v = testParseColon(&[_]u16{ 48, 2, 0, 1, 2, 3, 4 });
|
||||
const v = testParseColon(&[_]u16{ 48, 2, 0, 1, 2, 3 });
|
||||
try testing.expect(v == .direct_color_bg);
|
||||
try testing.expectEqual(@as(u8, 1), v.direct_color_bg.r);
|
||||
try testing.expectEqual(@as(u8, 2), v.direct_color_bg.g);
|
||||
@ -624,7 +727,7 @@ test "sgr: direct fg/bg/underline ignore optional color space" {
|
||||
}
|
||||
{
|
||||
// 5 8 : 2 : Pi : Pr : Pg : Pb
|
||||
const v = testParseColon(&[_]u16{ 58, 2, 0, 1, 2, 3, 4 });
|
||||
const v = testParseColon(&[_]u16{ 58, 2, 0, 1, 2, 3 });
|
||||
try testing.expect(v == .underline_color);
|
||||
try testing.expectEqual(@as(u8, 1), v.underline_color.r);
|
||||
try testing.expectEqual(@as(u8, 2), v.underline_color.g);
|
||||
@ -634,7 +737,7 @@ test "sgr: direct fg/bg/underline ignore optional color space" {
|
||||
// Semicolon version should not parse optional color space identifier
|
||||
{
|
||||
// 3 8 ; 2 ; Pr ; Pg ; Pb
|
||||
const v = testParse(&[_]u16{ 38, 2, 0, 1, 2, 3, 4 });
|
||||
const v = testParse(&[_]u16{ 38, 2, 0, 1, 2, 3 });
|
||||
try testing.expect(v == .direct_color_fg);
|
||||
try testing.expectEqual(@as(u8, 0), v.direct_color_fg.r);
|
||||
try testing.expectEqual(@as(u8, 1), v.direct_color_fg.g);
|
||||
@ -642,7 +745,7 @@ test "sgr: direct fg/bg/underline ignore optional color space" {
|
||||
}
|
||||
{
|
||||
// 4 8 ; 2 ; Pr ; Pg ; Pb
|
||||
const v = testParse(&[_]u16{ 48, 2, 0, 1, 2, 3, 4 });
|
||||
const v = testParse(&[_]u16{ 48, 2, 0, 1, 2, 3 });
|
||||
try testing.expect(v == .direct_color_bg);
|
||||
try testing.expectEqual(@as(u8, 0), v.direct_color_bg.r);
|
||||
try testing.expectEqual(@as(u8, 1), v.direct_color_bg.g);
|
||||
@ -650,10 +753,114 @@ test "sgr: direct fg/bg/underline ignore optional color space" {
|
||||
}
|
||||
{
|
||||
// 5 8 ; 2 ; Pr ; Pg ; Pb
|
||||
const v = testParse(&[_]u16{ 58, 2, 0, 1, 2, 3, 4 });
|
||||
const v = testParse(&[_]u16{ 58, 2, 0, 1, 2, 3 });
|
||||
try testing.expect(v == .underline_color);
|
||||
try testing.expectEqual(@as(u8, 0), v.underline_color.r);
|
||||
try testing.expectEqual(@as(u8, 1), v.underline_color.g);
|
||||
try testing.expectEqual(@as(u8, 2), v.underline_color.b);
|
||||
}
|
||||
}
|
||||
|
||||
test "sgr: direct fg colon with too many colons" {
|
||||
var p: Parser = .{
|
||||
.params = &[_]u16{ 38, 2, 0, 1, 2, 3, 4, 1 },
|
||||
.params_sep = sep: {
|
||||
var list = SepList.initEmpty();
|
||||
for (0..6) |idx| list.set(idx);
|
||||
break :sep list;
|
||||
},
|
||||
};
|
||||
|
||||
try testing.expect(p.next().? == .unknown);
|
||||
try testing.expect(p.next().? == .bold);
|
||||
try testing.expect(p.next() == null);
|
||||
}
|
||||
|
||||
test "sgr: direct fg colon with colorspace and extra param" {
|
||||
var p: Parser = .{
|
||||
.params = &[_]u16{ 38, 2, 0, 1, 2, 3, 1 },
|
||||
.params_sep = sep: {
|
||||
var list = SepList.initEmpty();
|
||||
for (0..5) |idx| list.set(idx);
|
||||
break :sep list;
|
||||
},
|
||||
};
|
||||
|
||||
{
|
||||
const v = p.next().?;
|
||||
std.log.warn("WHAT={}", .{v});
|
||||
try testing.expect(v == .direct_color_fg);
|
||||
try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
|
||||
try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
|
||||
try testing.expectEqual(@as(u8, 3), v.direct_color_fg.b);
|
||||
}
|
||||
|
||||
try testing.expect(p.next().? == .bold);
|
||||
try testing.expect(p.next() == null);
|
||||
}
|
||||
|
||||
test "sgr: direct fg colon no colorspace and extra param" {
|
||||
var p: Parser = .{
|
||||
.params = &[_]u16{ 38, 2, 1, 2, 3, 1 },
|
||||
.params_sep = sep: {
|
||||
var list = SepList.initEmpty();
|
||||
for (0..4) |idx| list.set(idx);
|
||||
break :sep list;
|
||||
},
|
||||
};
|
||||
|
||||
{
|
||||
const v = p.next().?;
|
||||
try testing.expect(v == .direct_color_fg);
|
||||
try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
|
||||
try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
|
||||
try testing.expectEqual(@as(u8, 3), v.direct_color_fg.b);
|
||||
}
|
||||
|
||||
try testing.expect(p.next().? == .bold);
|
||||
try testing.expect(p.next() == null);
|
||||
}
|
||||
|
||||
// Kakoune sent this complex SGR sequence that caused invalid behavior.
|
||||
test "sgr: kakoune input" {
|
||||
// This used to crash
|
||||
var p: Parser = .{
|
||||
.params = &[_]u16{ 0, 4, 3, 38, 2, 175, 175, 215, 58, 2, 0, 190, 80, 70 },
|
||||
.params_sep = sep: {
|
||||
var list = SepList.initEmpty();
|
||||
list.set(1);
|
||||
list.set(8);
|
||||
list.set(9);
|
||||
list.set(10);
|
||||
list.set(11);
|
||||
list.set(12);
|
||||
break :sep list;
|
||||
},
|
||||
};
|
||||
|
||||
{
|
||||
const v = p.next().?;
|
||||
try testing.expect(v == .unset);
|
||||
}
|
||||
{
|
||||
const v = p.next().?;
|
||||
try testing.expect(v == .underline);
|
||||
try testing.expectEqual(Attribute.Underline.curly, v.underline);
|
||||
}
|
||||
{
|
||||
const v = p.next().?;
|
||||
try testing.expect(v == .direct_color_fg);
|
||||
try testing.expectEqual(@as(u8, 175), v.direct_color_fg.r);
|
||||
try testing.expectEqual(@as(u8, 175), v.direct_color_fg.g);
|
||||
try testing.expectEqual(@as(u8, 215), v.direct_color_fg.b);
|
||||
}
|
||||
{
|
||||
const v = p.next().?;
|
||||
try testing.expect(v == .underline_color);
|
||||
try testing.expectEqual(@as(u8, 190), v.underline_color.r);
|
||||
try testing.expectEqual(@as(u8, 80), v.underline_color.g);
|
||||
try testing.expectEqual(@as(u8, 70), v.underline_color.b);
|
||||
}
|
||||
|
||||
//try testing.expect(p.next() == null);
|
||||
}
|
||||
|
@ -253,15 +253,11 @@ pub fn Stream(comptime Handler: type) type {
|
||||
// A parameter separator:
|
||||
':', ';' => if (self.parser.params_idx < 16) {
|
||||
self.parser.params[self.parser.params_idx] = self.parser.param_acc;
|
||||
if (c == ':') self.parser.params_sep.set(self.parser.params_idx);
|
||||
self.parser.params_idx += 1;
|
||||
|
||||
self.parser.param_acc = 0;
|
||||
self.parser.param_acc_idx = 0;
|
||||
|
||||
// Keep track of separator state.
|
||||
const sep: Parser.ParamSepState = @enumFromInt(c);
|
||||
if (self.parser.params_idx == 1) self.parser.params_sep = sep;
|
||||
if (self.parser.params_sep != sep) self.parser.params_sep = .mixed;
|
||||
},
|
||||
// Explicitly ignored:
|
||||
0x7F => {},
|
||||
@ -937,7 +933,10 @@ pub fn Stream(comptime Handler: type) type {
|
||||
'm' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "setAttribute")) {
|
||||
// log.info("parse SGR params={any}", .{action.params});
|
||||
var p: sgr.Parser = .{ .params = input.params, .colon = input.sep == .colon };
|
||||
var p: sgr.Parser = .{
|
||||
.params = input.params,
|
||||
.params_sep = input.params_sep,
|
||||
};
|
||||
while (p.next()) |attr| {
|
||||
// log.info("SGR attribute: {}", .{attr});
|
||||
try self.handler.setAttribute(attr);
|
||||
|
@ -179,8 +179,17 @@ pub fn threadExit(self: *Exec, td: *termio.Termio.ThreadData) void {
|
||||
// Quit our read thread after exiting the subprocess so that
|
||||
// we don't get stuck waiting for data to stop flowing if it is
|
||||
// a particularly noisy process.
|
||||
_ = posix.write(exec.read_thread_pipe, "x") catch |err|
|
||||
log.warn("error writing to read thread quit pipe err={}", .{err});
|
||||
_ = posix.write(exec.read_thread_pipe, "x") catch |err| switch (err) {
|
||||
// BrokenPipe means that our read thread is closed already,
|
||||
// which is completely fine since that is what we were trying
|
||||
// to achieve.
|
||||
error.BrokenPipe => {},
|
||||
|
||||
else => log.warn(
|
||||
"error writing to read thread quit pipe err={}",
|
||||
.{err},
|
||||
),
|
||||
};
|
||||
|
||||
if (comptime builtin.os.tag == .windows) {
|
||||
// Interrupt the blocking read so the thread can see the quit message
|
||||
@ -875,7 +884,11 @@ const Subprocess = struct {
|
||||
};
|
||||
|
||||
const force: ?shell_integration.Shell = switch (cfg.shell_integration) {
|
||||
.none => break :shell .{ null, default_shell_command },
|
||||
.none => {
|
||||
// Even if shell integration is none, we still want to set up the feature env vars
|
||||
try shell_integration.setupFeatures(&env, cfg.shell_integration_features);
|
||||
break :shell .{ null, default_shell_command };
|
||||
},
|
||||
.detect => null,
|
||||
.bash => .bash,
|
||||
.elvish => .elvish,
|
||||
@ -971,12 +984,12 @@ const Subprocess = struct {
|
||||
// which we may not want. If we specify "-l" then we can avoid
|
||||
// this behavior but now the shell isn't a login shell.
|
||||
//
|
||||
// There is another issue: `login(1)` only checks for ".hushlogin"
|
||||
// in the working directory. This means that if we specify "-l"
|
||||
// then we won't get hushlogin honored if its in the home
|
||||
// directory (which is standard). To get around this, we
|
||||
// check for hushlogin ourselves and if present specify the
|
||||
// "-q" flag to login(1).
|
||||
// There is another issue: `login(1)` on macOS 14.3 and earlier
|
||||
// checked for ".hushlogin" in the working directory. This means
|
||||
// that if we specify "-l" then we won't get hushlogin honored
|
||||
// if its in the home directory (which is standard). To get
|
||||
// around this, we check for hushlogin ourselves and if present
|
||||
// specify the "-q" flag to login(1).
|
||||
//
|
||||
// So to get all the behaviors we want, we specify "-l" but
|
||||
// execute "bash" (which is built-in to macOS). We then use
|
||||
@ -1094,6 +1107,10 @@ const Subprocess = struct {
|
||||
});
|
||||
self.pty = pty;
|
||||
errdefer {
|
||||
if (comptime builtin.os.tag != .windows) {
|
||||
_ = posix.close(pty.slave);
|
||||
}
|
||||
|
||||
pty.deinit();
|
||||
self.pty = null;
|
||||
}
|
||||
@ -1178,6 +1195,13 @@ const Subprocess = struct {
|
||||
log.info("subcommand cgroup={s}", .{self.linux_cgroup orelse "-"});
|
||||
}
|
||||
|
||||
if (comptime builtin.os.tag != .windows) {
|
||||
// Once our subcommand is started we can close the slave
|
||||
// side. This prevents the slave fd from being leaked to
|
||||
// future children.
|
||||
_ = posix.close(pty.slave);
|
||||
}
|
||||
|
||||
self.command = cmd;
|
||||
return switch (builtin.os.tag) {
|
||||
.windows => .{
|
||||
@ -1452,6 +1476,13 @@ pub const ReadThread = struct {
|
||||
log.info("read thread got quit signal", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
// If our pty fd is closed, then we're also done with our
|
||||
// read thread.
|
||||
if (pollfds[0].revents & posix.POLL.HUP != 0) {
|
||||
log.info("pty fd closed, read thread exiting", .{});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user