Merge branch 'ghostty-org:main' into ms_MY

This commit is contained in:
яυzαιηι
2025-04-18 22:51:01 +08:00
committed by GitHub
13 changed files with 213 additions and 19 deletions

View File

@ -186,6 +186,12 @@ class AppDelegate: NSObject,
name: .ghosttyConfigDidChange, name: .ghosttyConfigDidChange,
object: nil object: nil
) )
NotificationCenter.default.addObserver(
self,
selector: #selector(ghosttyBellDidRing(_:)),
name: .ghosttyBellDidRing,
object: nil
)
// Configure user notifications // Configure user notifications
let actions = [ let actions = [
@ -502,6 +508,11 @@ class AppDelegate: NSObject,
ghosttyConfigDidChange(config: config) ghosttyConfigDidChange(config: config)
} }
@objc private func ghosttyBellDidRing(_ notification: Notification) {
// Bounce the dock icon if we're not focused.
NSApp.requestUserAttention(.informationalRequest)
}
private func ghosttyConfigDidChange(config: Ghostty.Config) { private func ghosttyConfigDidChange(config: Ghostty.Config) {
// Update the config we need to store // Update the config we need to store
self.derivedConfig = DerivedConfig(config) self.derivedConfig = DerivedConfig(config)

View File

@ -495,14 +495,20 @@ class QuickTerminalController: BaseTerminalController {
private func onToggleFullscreen() { private func onToggleFullscreen() {
// We ignore the configured fullscreen style and always use non-native // We ignore the configured fullscreen style and always use non-native
// because the way the quick terminal works doesn't support native. // because the way the quick terminal works doesn't support native.
// let mode: FullscreenMode
// An additional detail is that if the is NOT frontmost, then our if (NSApp.isFrontmost) {
// NSApp.presentationOptions will not take effect so we must always // If we're frontmost and we have a notch then we keep padding
// do the visible menu mode since we can't get rid of the menu. // so all lines of the terminal are visible.
let mode: FullscreenMode = if (NSApp.isFrontmost) { if (window?.screen?.hasNotch ?? false) {
.nonNative mode = .nonNativePaddedNotch
} else {
mode = .nonNative
}
} else { } else {
.nonNativeVisibleMenu // An additional detail is that if the is NOT frontmost, then our
// NSApp.presentationOptions will not take effect so we must always
// do the visible menu mode since we can't get rid of the menu.
mode = .nonNativeVisibleMenu
} }
toggleFullscreen(mode: mode) toggleFullscreen(mode: mode)

View File

@ -538,6 +538,9 @@ extension Ghostty {
case GHOSTTY_ACTION_COLOR_CHANGE: case GHOSTTY_ACTION_COLOR_CHANGE:
colorChange(app, target: target, change: action.action.color_change) colorChange(app, target: target, change: action.action.color_change)
case GHOSTTY_ACTION_RING_BELL:
ringBell(app, target: target)
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS: case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
fallthrough fallthrough
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
@ -747,6 +750,30 @@ extension Ghostty {
appDelegate.toggleVisibility(self) appDelegate.toggleVisibility(self)
} }
private static func ringBell(
_ app: ghostty_app_t,
target: ghostty_target_s) {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
// Technically we could still request app attention here but there
// are no known cases where the bell is rang with an app target so
// I think its better to warn.
Ghostty.logger.warning("ring bell does nothing with an app target")
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
NotificationCenter.default.post(
name: .ghosttyBellDidRing,
object: surfaceView
)
default:
assertionFailure()
}
}
private static func moveTab( private static func moveTab(
_ app: ghostty_app_t, _ app: ghostty_app_t,
target: ghostty_target_s, target: ghostty_target_s,

View File

@ -116,6 +116,14 @@ extension Ghostty {
/// details on what each means. We only add documentation if there is a strange conversion /// details on what each means. We only add documentation if there is a strange conversion
/// due to the embedded library and Swift. /// due to the embedded library and Swift.
var bellFeatures: BellFeatures {
guard let config = self.config else { return .init() }
var v: CUnsignedInt = 0
let key = "bell-features"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .init() }
return .init(rawValue: v)
}
var initialWindow: Bool { var initialWindow: Bool {
guard let config = self.config else { return true } guard let config = self.config else { return true }
var v = true; var v = true;
@ -543,6 +551,12 @@ extension Ghostty.Config {
case download case download
} }
struct BellFeatures: OptionSet {
let rawValue: CUnsignedInt
static let system = BellFeatures(rawValue: 1 << 0)
}
enum MacHidden : String { enum MacHidden : String {
case never case never
case always case always

View File

@ -253,6 +253,9 @@ extension Notification.Name {
/// Resize the window to a default size. /// Resize the window to a default size.
static let ghosttyResetWindowSize = Notification.Name("com.mitchellh.ghostty.resetWindowSize") static let ghosttyResetWindowSize = Notification.Name("com.mitchellh.ghostty.resetWindowSize")
/// Ring the bell
static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing")
} }
// NOTE: I am moving all of these to Notification.Name extensions over time. This // NOTE: I am moving all of these to Notification.Name extensions over time. This

View File

@ -59,6 +59,15 @@ extension Ghostty {
@EnvironmentObject private var ghostty: Ghostty.App @EnvironmentObject private var ghostty: Ghostty.App
var title: String {
var result = surfaceView.title
if (surfaceView.bell) {
result = "🔔 \(result)"
}
return result
}
var body: some View { var body: some View {
let center = NotificationCenter.default let center = NotificationCenter.default
@ -74,7 +83,7 @@ extension Ghostty {
Surface(view: surfaceView, size: geo.size) Surface(view: surfaceView, size: geo.size)
.focused($surfaceFocus) .focused($surfaceFocus)
.focusedValue(\.ghosttySurfaceTitle, surfaceView.title) .focusedValue(\.ghosttySurfaceTitle, title)
.focusedValue(\.ghosttySurfacePwd, surfaceView.pwd) .focusedValue(\.ghosttySurfacePwd, surfaceView.pwd)
.focusedValue(\.ghosttySurfaceView, surfaceView) .focusedValue(\.ghosttySurfaceView, surfaceView)
.focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize) .focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize)

View File

@ -63,6 +63,9 @@ extension Ghostty {
/// dynamically updated. Otherwise, the background color is the default background color. /// dynamically updated. Otherwise, the background color is the default background color.
@Published private(set) var backgroundColor: Color? = nil @Published private(set) var backgroundColor: Color? = nil
/// True when the bell is active. This is set inactive on focus or event.
@Published private(set) var bell: Bool = false
// An initial size to request for a window. This will only affect // An initial size to request for a window. This will only affect
// then the view is moved to a new window. // then the view is moved to a new window.
var initialSize: NSSize? = nil var initialSize: NSSize? = nil
@ -190,6 +193,11 @@ extension Ghostty {
selector: #selector(ghosttyColorDidChange(_:)), selector: #selector(ghosttyColorDidChange(_:)),
name: .ghosttyColorDidChange, name: .ghosttyColorDidChange,
object: self) object: self)
center.addObserver(
self,
selector: #selector(ghosttyBellDidRing(_:)),
name: .ghosttyBellDidRing,
object: self)
center.addObserver( center.addObserver(
self, self,
selector: #selector(windowDidChangeScreen), selector: #selector(windowDidChangeScreen),
@ -300,9 +308,12 @@ extension Ghostty {
SecureInput.shared.setScoped(ObjectIdentifier(self), focused: focused) SecureInput.shared.setScoped(ObjectIdentifier(self), focused: focused)
} }
// On macOS 13+ we can store our continuous clock...
if (focused) { if (focused) {
// On macOS 13+ we can store our continuous clock...
focusInstant = ContinuousClock.now focusInstant = ContinuousClock.now
// We unset our bell state if we gained focus
bell = false
} }
} }
@ -556,6 +567,11 @@ extension Ghostty {
} }
} }
@objc private func ghosttyBellDidRing(_ notification: SwiftUI.Notification) {
// Bell state goes to true
bell = true
}
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) { @objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
guard let window = self.window else { return } guard let window = self.window else { return }
guard let object = notification.object as? NSWindow, window == object else { return } guard let object = notification.object as? NSWindow, window == object else { return }
@ -855,6 +871,9 @@ extension Ghostty {
return return
} }
// On any keyDown event we unset our bell state
bell = false
// We need to translate the mods (maybe) to handle configs such as option-as-alt // We need to translate the mods (maybe) to handle configs such as option-as-alt
let translationModsGhostty = Ghostty.eventModifierFlags( let translationModsGhostty = Ghostty.eventModifierFlags(
mods: ghostty_surface_key_translation_mods( mods: ghostty_surface_key_translation_mods(

View File

@ -35,6 +35,9 @@ extension Ghostty {
// on supported platforms. // on supported platforms.
@Published var focusInstant: ContinuousClock.Instant? = nil @Published var focusInstant: ContinuousClock.Instant? = nil
/// True when the bell is active. This is set inactive on focus or event.
@Published var bell: Bool = false
// Returns sizing information for the surface. This is the raw C // Returns sizing information for the surface. This is the raw C
// structure because I'm lazy. // structure because I'm lazy.
var surfaceSize: ghostty_surface_size_s? { var surfaceSize: ghostty_surface_size_s? {

View File

@ -34,4 +34,11 @@ extension NSScreen {
return visibleFrame.height < (frame.height - max(menuHeight, notchInset) - boundaryAreaPadding) return visibleFrame.height < (frame.height - max(menuHeight, notchInset) - boundaryAreaPadding)
} }
/// Returns true if the screen has a visible notch (i.e., a non-zero safe area inset at the top).
var hasNotch: Bool {
// We assume that a top safe area means notch, since we don't currently
// know any other situation this is true.
return safeAreaInsets.top > 0
}
} }

View File

@ -1,5 +1,5 @@
# Japanese translations for com.mitchellh.ghostty package # Japanese translations for com.mitchellh.ghostty package
# com.mitchellh.ghostty パッケージに対する訳. # com.mitchellh.ghostty パッケージに対する訳.
# Copyright (C) 2025 Mitchell Hashimoto # Copyright (C) 2025 Mitchell Hashimoto
# This file is distributed under the same license as the com.mitchellh.ghostty package. # This file is distributed under the same license as the com.mitchellh.ghostty package.
# Lon Sagisawa <lon@sagisawa.me>, 2025. # Lon Sagisawa <lon@sagisawa.me>, 2025.
@ -37,7 +37,7 @@ msgstr "OK"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5
msgid "Configuration Errors" msgid "Configuration Errors"
msgstr "設定エラー" msgstr "設定ファイルにエラーがあります"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
msgid "" msgid ""
@ -192,7 +192,7 @@ msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 #: src/apprt/gtk/ui/1.5/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste" msgid "Warning: Potentially Unsafe Paste"
msgstr "警告: 危険な可能性のあるペースト" msgstr "警告: 危険な可能性のある貼り付け"
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 #: src/apprt/gtk/ui/1.5/ccw-paste.blp:7
msgid "" msgid ""
@ -232,19 +232,19 @@ msgstr "分割ウィンドウを閉じますか?"
#: src/apprt/gtk/CloseDialog.zig:96 #: src/apprt/gtk/CloseDialog.zig:96
msgid "All terminal sessions will be terminated." msgid "All terminal sessions will be terminated."
msgstr "すべてのターミナルセッションが終了されます。" msgstr "すべてのターミナルセッションが終了ます。"
#: src/apprt/gtk/CloseDialog.zig:97 #: src/apprt/gtk/CloseDialog.zig:97
msgid "All terminal sessions in this window will be terminated." msgid "All terminal sessions in this window will be terminated."
msgstr "ウィンドウ内のすべてのターミナルセッションが終了されます。" msgstr "ウィンドウ内のすべてのターミナルセッションが終了ます。"
#: src/apprt/gtk/CloseDialog.zig:98 #: src/apprt/gtk/CloseDialog.zig:98
msgid "All terminal sessions in this tab will be terminated." msgid "All terminal sessions in this tab will be terminated."
msgstr "タブ内のすべてのターミナルセッションが終了されます。" msgstr "タブ内のすべてのターミナルセッションが終了ます。"
#: src/apprt/gtk/CloseDialog.zig:99 #: src/apprt/gtk/CloseDialog.zig:99
msgid "The currently running process in this split will be terminated." msgid "The currently running process in this split will be terminated."
msgstr "分割ウィンドウ内のすべてのターミナルセッションが終了されます。" msgstr "分割ウィンドウ内のすべてのプロセスが終了します。"
#: src/apprt/gtk/Window.zig:200 #: src/apprt/gtk/Window.zig:200
msgid "Main Menu" msgid "Main Menu"

View File

@ -1874,7 +1874,13 @@ keybind: Keybinds = .{},
/// for instance under the "Sound > Alert Sound" setting in GNOME, /// for instance under the "Sound > Alert Sound" setting in GNOME,
/// or the "Accessibility > System Bell" settings in KDE Plasma. /// or the "Accessibility > System Bell" settings in KDE Plasma.
/// ///
/// Currently only implemented on Linux. /// On macOS this has no affect.
///
/// On macOS, if the app is unfocused, it will bounce the app icon in the dock
/// once. Additionally, the title of the window with the alerted terminal
/// surface will contain a bell emoji (🔔) until the terminal is focused
/// or a key is pressed. These are not currently configurable since they're
/// considered unobtrusive.
@"bell-features": BellFeatures = .{}, @"bell-features": BellFeatures = .{},
/// Control the in-app notifications that Ghostty shows. /// Control the in-app notifications that Ghostty shows.

View File

@ -88,6 +88,7 @@ fn writeSyntax(writer: anytype) !void {
\\let s:cpo_save = &cpo \\let s:cpo_save = &cpo
\\set cpo&vim \\set cpo&vim
\\ \\
\\syn iskeyword @,48-57,-
\\syn keyword ghosttyConfigKeyword \\syn keyword ghosttyConfigKeyword
); );

View File

@ -924,8 +924,8 @@ fn cursorScrollAboveRotate(self: *Screen) !void {
fastmem.rotateOnceR(Row, cur_rows[self.cursor.page_pin.y..cur_page.size.rows]); fastmem.rotateOnceR(Row, cur_rows[self.cursor.page_pin.y..cur_page.size.rows]);
self.clearCells( self.clearCells(
cur_page, cur_page,
&cur_rows[0], &cur_rows[self.cursor.page_pin.y],
cur_page.getCells(&cur_rows[0]), cur_page.getCells(&cur_rows[self.cursor.page_pin.y]),
); );
// Set all the rows we rotated and cleared dirty // Set all the rows we rotated and cleared dirty
@ -1256,6 +1256,17 @@ pub fn clearCells(
self.assertIntegrity(); self.assertIntegrity();
} }
if (comptime std.debug.runtime_safety) {
// Our row and cells should be within the page.
const page_rows = page.rows.ptr(page.memory.ptr);
assert(@intFromPtr(row) >= @intFromPtr(&page_rows[0]));
assert(@intFromPtr(row) <= @intFromPtr(&page_rows[page.size.rows - 1]));
const row_cells = page.getCells(row);
assert(@intFromPtr(&cells[0]) >= @intFromPtr(&row_cells[0]));
assert(@intFromPtr(&cells[cells.len - 1]) <= @intFromPtr(&row_cells[row_cells.len - 1]));
}
// If this row has graphemes, then we need go through a slow path // If this row has graphemes, then we need go through a slow path
// and delete the cell graphemes. // and delete the cell graphemes.
if (row.grapheme) { if (row.grapheme) {
@ -4818,6 +4829,83 @@ test "Screen: scroll above creates new page" {
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
} }
test "Screen: scroll above with cursor on non-final row" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 10, 4, 10);
defer s.deinit();
// Get the cursor to be 2 rows above a new page
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
s.pages.pages.first.?.data.pauseIntegrityChecks(true);
for (0..first_page_size - 3) |_| try s.testWriteString("\n");
s.pages.pages.first.?.data.pauseIntegrityChecks(false);
// Write 3 lines of text, forcing the last line into the first
// row of a new page. Move our cursor onto the previous page.
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
try s.testWriteString("1AB\n2BC\n3DE\n4FG");
s.cursorAbsolute(0, 1);
s.pages.clearDirty();
// Ensure we're still on the first page. So our cursor is on the first
// page but we have two pages of data.
try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?);
// +----------+ = PAGE 0
// ... : :
// +-------------+ ACTIVE
// 4305 |1AB0000000| | 0
// 4306 |2BC0000000| | 1
// :^ : : = PIN 0
// 4307 |3DE0000000| | 2
// +----------+ :
// +----------+ : = PAGE 1
// 0 |4FG0000000| | 3
// +----------+ :
// +-------------+
try s.cursorScrollAbove();
// +----------+ = PAGE 0
// ... : :
// 4305 |1AB0000000|
// +-------------+ ACTIVE
// 4306 |2BC0000000| | 0
// 4307 | | | 1
// :^ : : = PIN 0
// +----------+ :
// +----------+ : = PAGE 1
// 0 |3DE0000000| | 2
// 1 |4FG0000000| | 3
// +----------+ :
// +-------------+
// try s.pages.diagram(std.io.getStdErr().writer());
{
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
defer alloc.free(contents);
try testing.expectEqualStrings("2BC\n\n3DE\n4FG", contents);
}
{
const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?;
const cell = list_cell.cell;
try testing.expect(cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 155,
.g = 0,
.b = 0,
}, cell.content.color_rgb);
}
// Page 0's penultimate row is dirty because the cursor moved off of it.
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
// Page 0's final row is dirty because it was cleared.
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
// Page 1's row is dirty because it's new.
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
}
test "Screen: scroll above no scrollback bottom of page" { test "Screen: scroll above no scrollback bottom of page" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;