pull/11498/merge
Damien Mehala 2026-06-02 04:05:00 +08:00 committed by GitHub
commit 6f9a8b7485
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 101 additions and 13 deletions

View File

@ -787,6 +787,7 @@ typedef enum {
GHOSTTY_ACTION_COLOR_KIND_FOREGROUND = -1,
GHOSTTY_ACTION_COLOR_KIND_BACKGROUND = -2,
GHOSTTY_ACTION_COLOR_KIND_CURSOR = -3,
GHOSTTY_ACTION_COLOR_KIND_TAB = -4,
} ghostty_action_color_kind_e;
// apprt.action.ColorChange

View File

@ -140,7 +140,6 @@ struct TerminalCommandPaletteView: View {
guard let window = controller.window else { return [] }
let color = (window as? TerminalWindow)?.tabColor
let displayColor = color != TerminalTabColor.none ? color : nil
return controller.surfaceTree.map { surface in
let terminalTitle = surface.title.isEmpty ? window.title : surface.title
@ -163,7 +162,7 @@ struct TerminalCommandPaletteView: View {
title: "Focus: \(displayTitle)",
subtitle: subtitle,
leadingIcon: "rectangle.on.rectangle",
leadingColor: displayColor?.displayColor.map { Color($0) },
leadingColor: color?.color.map { Color($0) },
sortKey: AnySortKey(ObjectIdentifier(surface))
) {
NotificationCenter.default.post(

View File

@ -1,7 +1,7 @@
import AppKit
import SwiftUI
enum TerminalTabColor: Int, CaseIterable, Codable {
enum TerminalTabColorPreset: Int, CaseIterable, Codable {
case none
case blue
case purple
@ -67,6 +67,8 @@ enum TerminalTabColor: Int, CaseIterable, Codable {
}
}
var tabColor: TerminalTabColor { TerminalTabColor(color: displayColor) }
func swatchImage(selected: Bool) -> NSImage {
let size = NSSize(width: 18, height: 18)
return NSImage(size: size, flipped: false) { rect in
@ -105,15 +107,57 @@ enum TerminalTabColor: Int, CaseIterable, Codable {
}
}
struct TerminalTabColor: Equatable, Codable {
let color: NSColor?
static let none = TerminalTabColor(color: nil)
init(color: NSColor?) {
self.color = color
}
init(color: Color) {
self.color = NSColor(color)
}
// MARK: Codable
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// Backward compatibility: attempt to decode the previously stored preset index.
if let preset = try? container.decode(TerminalTabColorPreset.self) {
self.color = preset.displayColor
return
}
let hex = try container.decode(String.self)
self.color = NSColor(hex: hex)
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(color?.hexString ?? "")
}
// MARK: Equatable
static func == (lhs: TerminalTabColor, rhs: TerminalTabColor) -> Bool {
lhs.color == rhs.color
}
var matchingPreset: TerminalTabColorPreset {
TerminalTabColorPreset.allCases.first { $0.displayColor == color } ?? .none
}
}
// MARK: - Menu View
/// A SwiftUI view displaying a color palette for tab color selection.
/// Used as a custom view inside an NSMenuItem in the tab context menu.
struct TabColorMenuView: View {
@State private var currentSelection: TerminalTabColor
let onSelect: (TerminalTabColor) -> Void
@State private var currentSelection: TerminalTabColorPreset
let onSelect: (TerminalTabColorPreset) -> Void
init(selectedColor: TerminalTabColor, onSelect: @escaping (TerminalTabColor) -> Void) {
init(selectedColor: TerminalTabColorPreset, onSelect: @escaping (TerminalTabColorPreset) -> Void) {
self._currentSelection = State(initialValue: selectedColor)
self.onSelect = onSelect
}
@ -143,7 +187,7 @@ struct TabColorMenuView: View {
.padding(.bottom, 4)
}
static let paletteRows: [[TerminalTabColor]] = [
static let paletteRows: [[TerminalTabColorPreset]] = [
[.none, .blue, .purple, .pink, .red],
[.orange, .yellow, .green, .teal, .graphite],
]
@ -161,7 +205,7 @@ struct TabColorMenuView: View {
/// A single color swatch button in the tab color palette.
private struct TabColorSwatch: View {
let color: TerminalTabColor
let color: TerminalTabColorPreset
let isSelected: Bool
let action: () -> Void

View File

@ -686,7 +686,7 @@ private struct TabColorIndicatorView: View {
let tabColor: TerminalTabColor
var body: some View {
if let color = tabColor.displayColor {
if let color = tabColor.color {
Circle()
.fill(Color(color))
.frame(width: 6, height: 6)
@ -777,17 +777,17 @@ extension TerminalWindow {
let paletteItem = NSMenuItem()
paletteItem.identifier = Self.tabColorPaletteIdentifier
paletteItem.view = makeTabColorPaletteView(
selectedColor: (target?.window as? TerminalWindow)?.tabColor ?? .none
selectedColor: (target?.window as? TerminalWindow)?.tabColor.matchingPreset ?? .none
) { [weak target] color in
(target?.window as? TerminalWindow)?.tabColor = color
(target?.window as? TerminalWindow)?.tabColor = color.tabColor
}
menu.addItem(paletteItem)
}
}
private func makeTabColorPaletteView(
selectedColor: TerminalTabColor,
selectionHandler: @escaping (TerminalTabColor) -> Void
selectedColor: TerminalTabColorPreset,
selectionHandler: @escaping (TerminalTabColorPreset) -> Void
) -> NSView {
let hostingView = NSHostingView(rootView: TabColorMenuView(
selectedColor: selectedColor,

View File

@ -14,6 +14,7 @@ extension Ghostty.Action {
case foreground
case background
case cursor
case tab
case palette(index: UInt8)
}
@ -25,6 +26,8 @@ extension Ghostty.Action {
self.kind = .background
case GHOSTTY_ACTION_COLOR_KIND_CURSOR:
self.kind = .cursor
case GHOSTTY_ACTION_COLOR_KIND_TAB:
self.kind = .tab
default:
self.kind = .palette(index: UInt8(c.kind.rawValue))
}

View File

@ -751,6 +751,11 @@ extension Ghostty {
self?.backgroundColor = change.color
}
case .tab:
DispatchQueue.main.async { [weak self] in
(self?.window as? TerminalWindow)?.tabColor = TerminalTabColor(color: change.color)
}
default:
// We don't do anything for the other colors yet.
break

View File

@ -5547,6 +5547,16 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.io => self.queueIo(.{ .crash = {} }, .unlocked),
},
.colorize_tab => |hex| {
const color = try terminal.color.RGB.parse(hex);
return try self.rt_app.performAction(.{ .surface = self }, .color_change, .{
.kind = .tab,
.r = color.r,
.g = color.g,
.b = color.b,
});
},
.adjust_selection => |direction| {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();

View File

@ -845,6 +845,7 @@ pub const ColorKind = enum(c_int) {
foreground = -1,
background = -2,
cursor = -3,
tab = -4,
// 0+ values indicate a palette index
_,

View File

@ -970,6 +970,29 @@ pub const Action = union(enum) {
///
crash: CrashThread,
/// Mark the current tab with the color defined.
/// This is equivalent to right-click a tab and selecting one of the predefine color.
///
/// For example, `colorize_tab:#632CA6`
///
/// Any of the following forms are accepted:
///
/// - rgb:<red>/<green>/<blue>
/// <red>, <green>, <blue> := h | hh | hhh | hhhh
/// where `h` is a single hexadecimal digit.
///
/// - rgbi:<red>/<green>/<blue>
/// where <red>, <green>, and <blue> are floating point values between
/// 0.0 and 1.0 (inclusive).
///
/// - #rgb, #rrggbb, #rrrgggbbb #rrrrggggbbbb
/// where `r`, `g`, and `b` are a single hexadecimal digit.
/// These specify a color with 4, 8, 12, and 16 bits of precision
/// per color channel.
///
/// Only implemented on macOS.
colorize_tab: []const u8,
pub const Key = @typeInfo(Action).@"union".tag_type.?;
/// Make this a valid gobject if we're in a GTK environment.
@ -1427,6 +1450,7 @@ pub const Action = union(enum) {
.resize_split,
.equalize_splits,
.inspector,
.colorize_tab,
=> .surface,
};
}

View File

@ -706,6 +706,7 @@ fn actionCommands(action: Action.Key) []const Command {
.deactivate_all_key_tables,
.end_key_sequence,
.crash,
.colorize_tab,
=> comptime &.{},
// No commands because I'm not sure they make sense in a command