mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge branch 'ghostty-org:main' into main
This commit is contained in:
19
build.zig
19
build.zig
@ -21,17 +21,20 @@ const XCFrameworkStep = @import("src/build/XCFrameworkStep.zig");
|
||||
const Version = @import("src/build/Version.zig");
|
||||
const Command = @import("src/Command.zig");
|
||||
|
||||
// Do a comptime Zig version requirement. This is the minimum required
|
||||
// Zig version. We don't check a maximum so that devs can try newer
|
||||
// versions but this is the only version we guarantee to work.
|
||||
comptime {
|
||||
// This is the required Zig version for building this project. We allow
|
||||
// any patch version but the major and minor must match exactly.
|
||||
const required_zig = "0.13.0";
|
||||
const current_zig = builtin.zig_version;
|
||||
const min_zig = std.SemanticVersion.parse(required_zig) catch unreachable;
|
||||
if (current_zig.order(min_zig) == .lt) {
|
||||
|
||||
// Fail compilation if the current Zig version doesn't meet requirements.
|
||||
const current_vsn = builtin.zig_version;
|
||||
const required_vsn = std.SemanticVersion.parse(required_zig) catch unreachable;
|
||||
if (current_vsn.major != required_vsn.major or
|
||||
current_vsn.minor != required_vsn.minor)
|
||||
{
|
||||
@compileError(std.fmt.comptimePrint(
|
||||
"Your Zig version v{} does not meet the minimum build requirement of v{}",
|
||||
.{ current_zig, min_zig },
|
||||
"Your Zig version v{} does not meet the required build version of v{}",
|
||||
.{ current_vsn, required_vsn },
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ extern "C" {
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Macros
|
||||
@ -379,6 +380,11 @@ typedef struct {
|
||||
ghostty_action_resize_split_direction_e direction;
|
||||
} ghostty_action_resize_split_s;
|
||||
|
||||
// apprt.action.MoveTab
|
||||
typedef struct {
|
||||
ssize_t amount;
|
||||
} ghostty_action_move_tab_s;
|
||||
|
||||
// apprt.action.GotoTab
|
||||
typedef enum {
|
||||
GHOSTTY_GOTO_TAB_PREVIOUS = -1,
|
||||
@ -517,6 +523,7 @@ typedef enum {
|
||||
GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS,
|
||||
GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL,
|
||||
GHOSTTY_ACTION_TOGGLE_VISIBILITY,
|
||||
GHOSTTY_ACTION_MOVE_TAB,
|
||||
GHOSTTY_ACTION_GOTO_TAB,
|
||||
GHOSTTY_ACTION_GOTO_SPLIT,
|
||||
GHOSTTY_ACTION_RESIZE_SPLIT,
|
||||
@ -543,6 +550,7 @@ typedef enum {
|
||||
typedef union {
|
||||
ghostty_action_split_direction_e new_split;
|
||||
ghostty_action_fullscreen_e toggle_fullscreen;
|
||||
ghostty_action_move_tab_s move_tab;
|
||||
ghostty_action_goto_tab_e goto_tab;
|
||||
ghostty_action_goto_split_e goto_split;
|
||||
ghostty_action_resize_split_s resize_split;
|
||||
|
@ -32,6 +32,7 @@
|
||||
A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */; };
|
||||
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */; };
|
||||
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; };
|
||||
A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */; };
|
||||
A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
|
||||
A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C932B53B43700305CE6 /* iOSApp.swift */; };
|
||||
A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
@ -115,6 +116,7 @@
|
||||
A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_AppKit.swift; sourceTree = "<group>"; };
|
||||
A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
||||
A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Action.swift; sourceTree = "<group>"; };
|
||||
A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
|
||||
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = "<group>"; };
|
||||
A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = "<group>"; };
|
||||
@ -317,6 +319,7 @@
|
||||
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */,
|
||||
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */,
|
||||
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */,
|
||||
A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */,
|
||||
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */,
|
||||
A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */,
|
||||
A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */,
|
||||
@ -601,6 +604,7 @@
|
||||
A57D79272C9C879B001D522E /* SecureInput.swift in Sources */,
|
||||
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
|
||||
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
|
||||
A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */,
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
|
||||
A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */,
|
||||
|
@ -197,7 +197,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// Run the animation that moves our window into the proper place and makes
|
||||
// it visible.
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = 0.2
|
||||
context.duration = ghostty.config.quickTerminalAnimationDuration
|
||||
context.timingFunction = .init(name: .easeIn)
|
||||
position.setFinal(in: window.animator(), on: screen)
|
||||
}, completionHandler: {
|
||||
@ -287,7 +287,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = 0.2
|
||||
context.duration = ghostty.config.quickTerminalAnimationDuration
|
||||
context.timingFunction = .init(name: .easeIn)
|
||||
position.setInitial(in: window.animator(), on: screen)
|
||||
}, completionHandler: {
|
||||
|
@ -13,6 +13,14 @@ class QuickTerminalWindow: NSWindow {
|
||||
// but I prefer to do it programmatically because the properties we
|
||||
// care about are less hidden.
|
||||
|
||||
// Add a custom identifier so third party apps can use the Accessibility
|
||||
// API to apply special rules to the quick terminal.
|
||||
self.identifier = .init(rawValue: "com.mitchellh.ghostty.quickTerminal")
|
||||
|
||||
// Set the correct AXSubrole of kAXFloatingWindowSubrole (allows
|
||||
// AeroSpace to treat the Quick Terminal as a floating window)
|
||||
self.setAccessibilitySubrole(.floatingWindow)
|
||||
|
||||
// Remove the title completely. This will make the window square. One
|
||||
// downside is it also hides the cursor indications of resize but the
|
||||
// window remains resizable.
|
||||
|
@ -40,6 +40,11 @@ class TerminalController: BaseTerminalController {
|
||||
selector: #selector(onToggleFullscreen),
|
||||
name: Ghostty.Notification.ghosttyToggleFullscreen,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onMoveTab),
|
||||
name: .ghosttyMoveTab,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onGotoTab),
|
||||
@ -482,6 +487,44 @@ class TerminalController: BaseTerminalController {
|
||||
|
||||
//MARK: - Notifications
|
||||
|
||||
@objc private func onMoveTab(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard target == self.focusedSurface else { return }
|
||||
guard let window = self.window else { return }
|
||||
|
||||
// Get the move action
|
||||
guard let action = notification.userInfo?[Notification.Name.GhosttyMoveTabKey] as? Ghostty.Action.MoveTab else { return }
|
||||
guard action.amount != 0 else { return }
|
||||
|
||||
// Determine our current selected index
|
||||
guard let windowController = window.windowController else { return }
|
||||
guard let tabGroup = windowController.window?.tabGroup else { return }
|
||||
guard let selectedWindow = tabGroup.selectedWindow else { return }
|
||||
let tabbedWindows = tabGroup.windows
|
||||
guard tabbedWindows.count > 0 else { return }
|
||||
guard let selectedIndex = tabbedWindows.firstIndex(where: { $0 == selectedWindow }) else { return }
|
||||
|
||||
// Determine the final index we want to insert our tab
|
||||
let finalIndex: Int
|
||||
if action.amount < 0 {
|
||||
finalIndex = selectedIndex - min(selectedIndex, -action.amount)
|
||||
} else {
|
||||
let remaining: Int = tabbedWindows.count - 1 - selectedIndex
|
||||
finalIndex = selectedIndex + min(remaining, action.amount)
|
||||
}
|
||||
|
||||
// If our index is the same we do nothing
|
||||
guard finalIndex != selectedIndex else { return }
|
||||
|
||||
// Get our parent
|
||||
let parent = tabbedWindows[finalIndex]
|
||||
|
||||
// Move our current selected window to the proper index
|
||||
tabGroup.removeWindow(selectedWindow)
|
||||
parent.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above)
|
||||
selectedWindow.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
@objc private func onGotoTab(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard target == self.focusedSurface else { return }
|
||||
|
15
macos/Sources/Ghostty/Ghostty.Action.swift
Normal file
15
macos/Sources/Ghostty/Ghostty.Action.swift
Normal file
@ -0,0 +1,15 @@
|
||||
import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
struct Action {}
|
||||
}
|
||||
|
||||
extension Ghostty.Action {
|
||||
struct MoveTab {
|
||||
let amount: Int
|
||||
|
||||
init(c: ghostty_action_move_tab_s) {
|
||||
self.amount = c.amount
|
||||
}
|
||||
}
|
||||
}
|
@ -458,6 +458,9 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_TOGGLE_FULLSCREEN:
|
||||
toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen)
|
||||
|
||||
case GHOSTTY_ACTION_MOVE_TAB:
|
||||
moveTab(app, target: target, move: action.action.move_tab)
|
||||
|
||||
case GHOSTTY_ACTION_GOTO_TAB:
|
||||
gotoTab(app, target: target, tab: action.action.goto_tab)
|
||||
|
||||
@ -666,6 +669,31 @@ extension Ghostty {
|
||||
appDelegate.toggleVisibility(self)
|
||||
}
|
||||
|
||||
private static func moveTab(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
move: ghostty_action_move_tab_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("move tab 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: .ghosttyMoveTab,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
SwiftUI.Notification.Name.GhosttyMoveTabKey: Action.MoveTab(c: move),
|
||||
]
|
||||
)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func gotoTab(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
|
@ -354,6 +354,14 @@ extension Ghostty {
|
||||
let str = String(cString: ptr)
|
||||
return QuickTerminalScreen(fromGhosttyConfig: str) ?? .main
|
||||
}
|
||||
|
||||
var quickTerminalAnimationDuration: Double {
|
||||
guard let config = self.config else { return 0.2 }
|
||||
var v: Double = 0.2
|
||||
let key = "quick-terminal-animation-duration"
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||
return v
|
||||
}
|
||||
#endif
|
||||
|
||||
var resizeOverlay: ResizeOverlay {
|
||||
|
@ -203,8 +203,16 @@ extension Ghostty {
|
||||
|
||||
}
|
||||
|
||||
// MARK: Surface Notifications
|
||||
// MARK: Surface Notification
|
||||
|
||||
extension Notification.Name {
|
||||
/// Goto tab. Has tab index in the userinfo.
|
||||
static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab")
|
||||
static let GhosttyMoveTabKey = ghosttyMoveTab.rawValue
|
||||
}
|
||||
|
||||
// NOTE: I am moving all of these to Notification.Name extensions over time. This
|
||||
// namespace was the old namespace.
|
||||
extension Ghostty.Notification {
|
||||
/// Used to pass a configuration along when creating a new tab/window/split.
|
||||
static let NewSurfaceConfigKey = "com.mitchellh.ghostty.newSurfaceConfig"
|
||||
|
@ -160,6 +160,10 @@ pub const Font = opaque {
|
||||
return @ptrFromInt(@intFromPtr(c.CTFontCopyDisplayName(@ptrCast(self))));
|
||||
}
|
||||
|
||||
pub fn copyPostScriptName(self: *Font) *foundation.String {
|
||||
return @ptrFromInt(@intFromPtr(c.CTFontCopyPostScriptName(@ptrCast(self))));
|
||||
}
|
||||
|
||||
pub fn getSymbolicTraits(self: *Font) text.FontSymbolicTraits {
|
||||
return @bitCast(c.CTFontGetSymbolicTraits(@ptrCast(self)));
|
||||
}
|
||||
|
@ -3913,6 +3913,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
},
|
||||
),
|
||||
|
||||
.move_tab => |position| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.move_tab,
|
||||
.{ .amount = position },
|
||||
),
|
||||
|
||||
.new_split => |direction| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.new_split,
|
||||
@ -4112,9 +4118,13 @@ fn writeScreenFile(
|
||||
var tmp_dir = try internal_os.TempDir.init();
|
||||
errdefer tmp_dir.deinit();
|
||||
|
||||
var filename_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const filename = try std.fmt.bufPrint(&filename_buf, "{s}.txt", .{@tagName(loc)});
|
||||
|
||||
// Open our scrollback file
|
||||
var file = try tmp_dir.dir.createFile(@tagName(loc), .{});
|
||||
var file = try tmp_dir.dir.createFile(filename, .{});
|
||||
defer file.close();
|
||||
|
||||
// Screen.dumpString writes byte-by-byte, so buffer it
|
||||
var buf_writer = std.io.bufferedWriter(file.writer());
|
||||
|
||||
@ -4173,7 +4183,7 @@ fn writeScreenFile(
|
||||
|
||||
// Get the final path
|
||||
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const path = try tmp_dir.dir.realpath(@tagName(loc), &path_buf);
|
||||
const path = try tmp_dir.dir.realpath(filename, &path_buf);
|
||||
|
||||
switch (write_action) {
|
||||
.open => try internal_os.open(self.alloc, path),
|
||||
|
@ -100,6 +100,13 @@ pub const Action = union(Key) {
|
||||
/// Toggle the visibility of all Ghostty terminal windows.
|
||||
toggle_visibility,
|
||||
|
||||
/// Moves a tab by a relative offset.
|
||||
///
|
||||
/// Adjusts the tab position based on `offset` (e.g., -1 for left, +1
|
||||
/// for right). If the new position is out of bounds, it wraps around
|
||||
/// cyclically within the tab range.
|
||||
move_tab: MoveTab,
|
||||
|
||||
/// Jump to a specific tab. Must handle the scenario that the tab
|
||||
/// value is invalid.
|
||||
goto_tab: GotoTab,
|
||||
@ -190,6 +197,7 @@ pub const Action = union(Key) {
|
||||
toggle_window_decorations,
|
||||
toggle_quick_terminal,
|
||||
toggle_visibility,
|
||||
move_tab,
|
||||
goto_tab,
|
||||
goto_split,
|
||||
resize_split,
|
||||
@ -308,6 +316,10 @@ pub const ResizeSplit = extern struct {
|
||||
};
|
||||
};
|
||||
|
||||
pub const MoveTab = extern struct {
|
||||
amount: isize,
|
||||
};
|
||||
|
||||
/// The tab to jump to. This is non-exhaustive so that integer values represent
|
||||
/// the index (zero-based) of the tab to jump to. Negative values are special
|
||||
/// values.
|
||||
|
@ -213,6 +213,7 @@ pub const App = struct {
|
||||
.toggle_quick_terminal,
|
||||
.toggle_visibility,
|
||||
.goto_tab,
|
||||
.move_tab,
|
||||
.inspector,
|
||||
.render_inspector,
|
||||
.quit_timer,
|
||||
|
@ -456,6 +456,7 @@ pub fn performAction(
|
||||
|
||||
.new_tab => try self.newTab(target),
|
||||
.goto_tab => self.gotoTab(target, value),
|
||||
.move_tab => self.moveTab(target, value),
|
||||
.new_split => try self.newSplit(target, value),
|
||||
.resize_split => self.resizeSplit(target, value),
|
||||
.equalize_splits => self.equalizeSplits(target),
|
||||
@ -527,6 +528,23 @@ fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) void {
|
||||
}
|
||||
}
|
||||
|
||||
fn moveTab(_: *App, target: apprt.Target, move_tab: apprt.action.MoveTab) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"moveTab invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.moveTab(v.rt_surface, @intCast(move_tab.amount));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn newSplit(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
|
@ -456,6 +456,15 @@ pub fn gotoNextTab(self: *Window, surface: *Surface) void {
|
||||
self.focusCurrentTab();
|
||||
}
|
||||
|
||||
/// Move the current tab for a surface.
|
||||
pub fn moveTab(self: *Window, surface: *Surface, position: c_int) void {
|
||||
const tab = surface.container.tab() orelse {
|
||||
log.info("surface is not attached to a tab bar, cannot navigate", .{});
|
||||
return;
|
||||
};
|
||||
self.notebook.moveTab(tab, position);
|
||||
}
|
||||
|
||||
/// Go to the next tab for a surface.
|
||||
pub fn gotoLastTab(self: *Window) void {
|
||||
const max = self.notebook.nPages() -| 1;
|
||||
|
@ -183,6 +183,35 @@ pub const Notebook = union(enum) {
|
||||
self.gotoNthTab(next_idx);
|
||||
}
|
||||
|
||||
pub fn moveTab(self: Notebook, tab: *Tab, position: c_int) void {
|
||||
const page_idx = self.getTabPosition(tab) orelse return;
|
||||
|
||||
const max = self.nPages() -| 1;
|
||||
var new_position: c_int = page_idx + position;
|
||||
|
||||
if (new_position < 0) {
|
||||
new_position = max + new_position + 1;
|
||||
} else if (new_position > max) {
|
||||
new_position = new_position - max - 1;
|
||||
}
|
||||
|
||||
if (new_position == page_idx) return;
|
||||
self.reorderPage(tab, new_position);
|
||||
}
|
||||
|
||||
pub fn reorderPage(self: Notebook, tab: *Tab, position: c_int) void {
|
||||
switch (self) {
|
||||
.gtk_notebook => |notebook| {
|
||||
c.gtk_notebook_reorder_child(notebook, @ptrCast(tab.box), position);
|
||||
},
|
||||
.adw_tab_view => |tab_view| {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box));
|
||||
_ = c.adw_tab_view_reorder_page(tab_view, page, position);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setTabLabel(self: Notebook, tab: *Tab, title: [:0]const u8) void {
|
||||
switch (self) {
|
||||
.adw_tab_view => |tab_view| {
|
||||
|
@ -515,7 +515,10 @@ const Preview = struct {
|
||||
}
|
||||
if (theme_list.hasMouse(mouse)) |_| {
|
||||
if (mouse.button == .left and mouse.type == .release) {
|
||||
self.current = self.window + mouse.row;
|
||||
const selection = self.window + mouse.row;
|
||||
if (selection < self.filtered.items.len) {
|
||||
self.current = selection;
|
||||
}
|
||||
}
|
||||
highlight = mouse.row;
|
||||
}
|
||||
|
@ -1270,6 +1270,11 @@ keybind: Keybinds = .{},
|
||||
/// by the operating system.
|
||||
@"quick-terminal-screen": QuickTerminalScreen = .main,
|
||||
|
||||
/// Duration (in seconds) of the quick terminal enter and exit animation.
|
||||
/// Set it to 0 to disable animation completely. This can be changed at
|
||||
/// runtime.
|
||||
@"quick-terminal-animation-duration": f64 = 0.2,
|
||||
|
||||
/// Whether to enable shell integration auto-injection or not. Shell integration
|
||||
/// greatly enhances the terminal experience by enabling a number of features:
|
||||
///
|
||||
|
@ -424,7 +424,30 @@ pub const CoreText = struct {
|
||||
};
|
||||
}
|
||||
|
||||
return try self.discover(alloc, desc);
|
||||
const it = try self.discover(alloc, desc);
|
||||
|
||||
// If our normal discovery doesn't find anything and we have a specific
|
||||
// codepoint, then fallback to using CTFontCreateForString to find a
|
||||
// matching font CoreText wants to use. See:
|
||||
// https://github.com/ghostty-org/ghostty/issues/2499
|
||||
if (it.list.len == 0 and desc.codepoint > 0) codepoint: {
|
||||
const ct_desc = try self.discoverCodepoint(
|
||||
collection,
|
||||
desc,
|
||||
) orelse break :codepoint;
|
||||
|
||||
const list = try alloc.alloc(*macos.text.FontDescriptor, 1);
|
||||
errdefer alloc.free(list);
|
||||
list[0] = ct_desc;
|
||||
|
||||
return DiscoverIterator{
|
||||
.alloc = alloc,
|
||||
.list = list,
|
||||
.i = 0,
|
||||
};
|
||||
}
|
||||
|
||||
return it;
|
||||
}
|
||||
|
||||
/// Discover a font for a specific codepoint using the CoreText
|
||||
@ -491,16 +514,45 @@ pub const CoreText = struct {
|
||||
);
|
||||
defer str.release();
|
||||
|
||||
// Get our range length for CTFontCreateForString. It looks like
|
||||
// the range uses UTF-16 codepoints and not UTF-32 codepoints.
|
||||
const range_len: usize = range_len: {
|
||||
var unichars: [2]u16 = undefined;
|
||||
const pair = macos.foundation.stringGetSurrogatePairForLongCharacter(
|
||||
desc.codepoint,
|
||||
&unichars,
|
||||
);
|
||||
break :range_len if (pair) 2 else 1;
|
||||
};
|
||||
|
||||
// Get our font
|
||||
const font = original.font.createForString(
|
||||
str,
|
||||
macos.foundation.Range.init(0, 1),
|
||||
macos.foundation.Range.init(0, range_len),
|
||||
) orelse return null;
|
||||
defer font.release();
|
||||
|
||||
// Do not allow the last resort font to go through. This is the
|
||||
// last font used by CoreText if it can't find anything else and
|
||||
// only contains replacement characters.
|
||||
last_resort: {
|
||||
const name_str = font.copyPostScriptName();
|
||||
defer name_str.release();
|
||||
|
||||
// If the name doesn't fit in our buffer, then it can't
|
||||
// be the last resort font so we break out.
|
||||
var name_buf: [64]u8 = undefined;
|
||||
const name: []const u8 = name_str.cstring(&name_buf, .utf8) orelse
|
||||
break :last_resort;
|
||||
|
||||
// If the name is "LastResort" then we don't want to use it.
|
||||
if (std.mem.eql(u8, "LastResort", name)) return null;
|
||||
}
|
||||
|
||||
// Get the descriptor
|
||||
return font.copyDescriptor();
|
||||
}
|
||||
|
||||
fn copyMatchingDescriptors(
|
||||
alloc: Allocator,
|
||||
list: *macos.foundation.Array,
|
||||
|
@ -301,6 +301,11 @@ pub const Action = union(enum) {
|
||||
/// is higher than the number of tabs, this will go to the last tab.
|
||||
goto_tab: usize,
|
||||
|
||||
/// Moves a tab by a relative offset.
|
||||
/// Adjusts the tab position based on `offset` (e.g., -1 for left, +1 for right).
|
||||
/// If the new position is out of bounds, it wraps around cyclically within the tab range.
|
||||
move_tab: isize,
|
||||
|
||||
/// Toggle the tab overview.
|
||||
/// This only works with libadwaita enabled currently.
|
||||
toggle_tab_overview: void,
|
||||
@ -312,7 +317,8 @@ pub const Action = union(enum) {
|
||||
/// Focus on a split in a given direction.
|
||||
goto_split: SplitFocusDirection,
|
||||
|
||||
/// zoom/unzoom the current split.
|
||||
/// zoom/unzoom the current split. This is currently only supported
|
||||
/// on macOS. Contributions welcome for other platforms.
|
||||
toggle_split_zoom: void,
|
||||
|
||||
/// Resize the current split by moving the split divider in the given
|
||||
@ -647,6 +653,7 @@ pub const Action = union(enum) {
|
||||
.next_tab,
|
||||
.last_tab,
|
||||
.goto_tab,
|
||||
.move_tab,
|
||||
.toggle_tab_overview,
|
||||
.new_split,
|
||||
.goto_split,
|
||||
|
@ -896,7 +896,7 @@ const ReflowCursor = struct {
|
||||
|
||||
// If our page can't support an additional cell with
|
||||
// graphemes then we create a new page for this row.
|
||||
if (self.page.graphemeCount() >= self.page.graphemeCapacity() - 1) {
|
||||
if (self.page.graphemeCount() >= self.page.graphemeCapacity()) {
|
||||
try self.moveLastRowToNewPage(list, cap);
|
||||
} else {
|
||||
// Attempt to allocate the space that would be required for
|
||||
@ -924,7 +924,7 @@ const ReflowCursor = struct {
|
||||
|
||||
// If our page can't support an additional cell with
|
||||
// a hyperlink ID then we create a new page for this row.
|
||||
if (self.page.hyperlinkCount() >= self.page.hyperlinkCapacity() - 1) {
|
||||
if (self.page.hyperlinkCount() >= self.page.hyperlinkCapacity()) {
|
||||
try self.moveLastRowToNewPage(list, cap);
|
||||
}
|
||||
|
||||
|
@ -125,7 +125,7 @@ pub const Cursor = struct {
|
||||
/// the cursor page pin changes. We can't get it from the old screen
|
||||
/// state because the page may be cleared. This is heap allocated
|
||||
/// because its most likely null.
|
||||
hyperlink: ?*Hyperlink = null,
|
||||
hyperlink: ?*hyperlink.Hyperlink = null,
|
||||
|
||||
/// The pointers into the page list where the cursor is currently
|
||||
/// located. This makes it faster to move the cursor.
|
||||
@ -134,7 +134,10 @@ pub const Cursor = struct {
|
||||
page_cell: *pagepkg.Cell,
|
||||
|
||||
pub fn deinit(self: *Cursor, alloc: Allocator) void {
|
||||
if (self.hyperlink) |link| link.destroy(alloc);
|
||||
if (self.hyperlink) |link| {
|
||||
link.deinit(alloc);
|
||||
alloc.destroy(link);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -182,31 +185,6 @@ pub const CharsetState = struct {
|
||||
const CharsetArray = std.EnumArray(charsets.Slots, charsets.Charset);
|
||||
};
|
||||
|
||||
pub const Hyperlink = struct {
|
||||
id: ?[]const u8,
|
||||
uri: []const u8,
|
||||
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
uri: []const u8,
|
||||
id: ?[]const u8,
|
||||
) !*Hyperlink {
|
||||
const self = try alloc.create(Hyperlink);
|
||||
errdefer alloc.destroy(self);
|
||||
self.id = if (id) |v| try alloc.dupe(u8, v) else null;
|
||||
errdefer if (self.id) |v| alloc.free(v);
|
||||
self.uri = try alloc.dupe(u8, uri);
|
||||
errdefer alloc.free(self.uri);
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *Hyperlink, alloc: Allocator) void {
|
||||
if (self.id) |id| alloc.free(id);
|
||||
alloc.free(self.uri);
|
||||
alloc.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
/// Initialize a new screen.
|
||||
///
|
||||
/// max_scrollback is the amount of scrollback to keep in bytes. This
|
||||
@ -471,10 +449,11 @@ pub fn adjustCapacity(
|
||||
self.cursor.hyperlink = null;
|
||||
|
||||
// Re-add
|
||||
self.startHyperlinkOnce(link.uri, link.id) catch unreachable;
|
||||
self.startHyperlinkOnce(link.*) catch unreachable;
|
||||
|
||||
// Remove our old link
|
||||
link.destroy(self.alloc);
|
||||
link.deinit(self.alloc);
|
||||
self.alloc.destroy(link);
|
||||
}
|
||||
|
||||
// Reload the cursor information because the pin changed.
|
||||
@ -1023,7 +1002,10 @@ fn cursorChangePin(self: *Screen, new: Pin) void {
|
||||
self.cursor.hyperlink = null;
|
||||
|
||||
// Re-add
|
||||
self.startHyperlink(link.uri, link.id) catch |err| {
|
||||
self.startHyperlink(link.uri, switch (link.id) {
|
||||
.explicit => |v| v,
|
||||
.implicit => null,
|
||||
}) catch |err| {
|
||||
// This shouldn't happen because startHyperlink should handle
|
||||
// resizing. This only happens if we're truly out of RAM. Degrade
|
||||
// to forgetting the hyperlink.
|
||||
@ -1031,7 +1013,8 @@ fn cursorChangePin(self: *Screen, new: Pin) void {
|
||||
};
|
||||
|
||||
// Remove our old link
|
||||
link.destroy(self.alloc);
|
||||
link.deinit(self.alloc);
|
||||
self.alloc.destroy(link);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1550,7 +1533,10 @@ fn resizeInternal(
|
||||
|
||||
// Fix up our hyperlink if we had one.
|
||||
if (hyperlink_) |link| {
|
||||
self.startHyperlink(link.uri, link.id) catch |err| {
|
||||
self.startHyperlink(link.uri, switch (link.id) {
|
||||
.explicit => |v| v,
|
||||
.implicit => null,
|
||||
}) catch |err| {
|
||||
// This shouldn't happen because startHyperlink should handle
|
||||
// resizing. This only happens if we're truly out of RAM. Degrade
|
||||
// to forgetting the hyperlink.
|
||||
@ -1558,7 +1544,8 @@ fn resizeInternal(
|
||||
};
|
||||
|
||||
// Remove our old link
|
||||
link.destroy(self.alloc);
|
||||
link.deinit(self.alloc);
|
||||
self.alloc.destroy(link);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1805,6 +1792,8 @@ pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void {
|
||||
};
|
||||
}
|
||||
|
||||
pub const StartHyperlinkError = Allocator.Error || PageList.AdjustCapacityError;
|
||||
|
||||
/// Start the hyperlink state. Future cells will be marked as hyperlinks with
|
||||
/// this state. Note that various terminal operations may clear the hyperlink
|
||||
/// state, such as switching screens (alt screen).
|
||||
@ -1812,14 +1801,29 @@ pub fn startHyperlink(
|
||||
self: *Screen,
|
||||
uri: []const u8,
|
||||
id_: ?[]const u8,
|
||||
) !void {
|
||||
) StartHyperlinkError!void {
|
||||
// Create our pending entry.
|
||||
const link: hyperlink.Hyperlink = .{
|
||||
.uri = uri,
|
||||
.id = if (id_) |id| .{
|
||||
.explicit = id,
|
||||
} else implicit: {
|
||||
defer self.cursor.hyperlink_implicit_id += 1;
|
||||
break :implicit .{ .implicit = self.cursor.hyperlink_implicit_id };
|
||||
},
|
||||
};
|
||||
errdefer switch (link.id) {
|
||||
.explicit => {},
|
||||
.implicit => self.cursor.hyperlink_implicit_id -= 1,
|
||||
};
|
||||
|
||||
// Loop until we have enough page memory to add the hyperlink
|
||||
while (true) {
|
||||
if (self.startHyperlinkOnce(uri, id_)) {
|
||||
if (self.startHyperlinkOnce(link)) {
|
||||
return;
|
||||
} else |err| switch (err) {
|
||||
// An actual self.alloc OOM is a fatal error.
|
||||
error.RealOutOfMemory => return error.OutOfMemory,
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
|
||||
// strings table is out of memory, adjust it up
|
||||
error.StringsOutOfMemory => _ = try self.adjustCapacity(
|
||||
@ -1849,74 +1853,21 @@ pub fn startHyperlink(
|
||||
/// all the previous state and try again.
|
||||
fn startHyperlinkOnce(
|
||||
self: *Screen,
|
||||
uri: []const u8,
|
||||
id_: ?[]const u8,
|
||||
) !void {
|
||||
source: hyperlink.Hyperlink,
|
||||
) (Allocator.Error || Page.InsertHyperlinkError)!void {
|
||||
// End any prior hyperlink
|
||||
self.endHyperlink();
|
||||
|
||||
// Create our hyperlink state.
|
||||
const link = Hyperlink.create(self.alloc, uri, id_) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.RealOutOfMemory,
|
||||
};
|
||||
errdefer link.destroy(self.alloc);
|
||||
// Allocate our new Hyperlink entry in non-page memory. This
|
||||
// lets us quickly get access to URI, ID.
|
||||
const link = try self.alloc.create(hyperlink.Hyperlink);
|
||||
errdefer self.alloc.destroy(link);
|
||||
link.* = try source.dupe(self.alloc);
|
||||
errdefer link.deinit(self.alloc);
|
||||
|
||||
// Copy our URI into the page memory.
|
||||
// Insert the hyperlink into page memory
|
||||
var page = &self.cursor.page_pin.page.data;
|
||||
const string_alloc = &page.string_alloc;
|
||||
const page_uri: Offset(u8).Slice = uri: {
|
||||
const buf = string_alloc.alloc(u8, page.memory, uri.len) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.StringsOutOfMemory,
|
||||
};
|
||||
errdefer string_alloc.free(page.memory, buf);
|
||||
@memcpy(buf, uri);
|
||||
|
||||
break :uri .{
|
||||
.offset = size.getOffset(u8, page.memory, &buf[0]),
|
||||
.len = uri.len,
|
||||
};
|
||||
};
|
||||
errdefer string_alloc.free(
|
||||
page.memory,
|
||||
page_uri.offset.ptr(page.memory)[0..page_uri.len],
|
||||
);
|
||||
|
||||
// Copy our ID into page memory or create an implicit ID via the counter
|
||||
const page_id: hyperlink.Hyperlink.Id = if (id_) |id| explicit: {
|
||||
const buf = string_alloc.alloc(u8, page.memory, id.len) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.StringsOutOfMemory,
|
||||
};
|
||||
errdefer string_alloc.free(page.memory, buf);
|
||||
@memcpy(buf, id);
|
||||
|
||||
break :explicit .{
|
||||
.explicit = .{
|
||||
.offset = size.getOffset(u8, page.memory, &buf[0]),
|
||||
.len = id.len,
|
||||
},
|
||||
};
|
||||
} else implicit: {
|
||||
defer self.cursor.hyperlink_implicit_id += 1;
|
||||
break :implicit .{ .implicit = self.cursor.hyperlink_implicit_id };
|
||||
};
|
||||
errdefer switch (page_id) {
|
||||
.implicit => self.cursor.hyperlink_implicit_id -= 1,
|
||||
.explicit => |slice| string_alloc.free(
|
||||
page.memory,
|
||||
slice.offset.ptr(page.memory)[0..slice.len],
|
||||
),
|
||||
};
|
||||
|
||||
// Put our hyperlink into the hyperlink set to get an ID
|
||||
const id = page.hyperlink_set.addContext(
|
||||
page.memory,
|
||||
.{ .id = page_id, .uri = page_uri },
|
||||
.{ .page = page },
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.SetOutOfMemory,
|
||||
error.NeedsRehash => return error.SetNeedsRehash,
|
||||
};
|
||||
errdefer page.hyperlink_set.release(page.memory, id);
|
||||
const id: hyperlink.Id = try page.insertHyperlink(link.*);
|
||||
|
||||
// Save it all
|
||||
self.cursor.hyperlink = link;
|
||||
@ -1944,7 +1895,8 @@ pub fn endHyperlink(self: *Screen) void {
|
||||
// will be called.
|
||||
var page = &self.cursor.page_pin.page.data;
|
||||
page.hyperlink_set.release(page.memory, self.cursor.hyperlink_id);
|
||||
self.cursor.hyperlink.?.destroy(self.alloc);
|
||||
self.cursor.hyperlink.?.deinit(self.alloc);
|
||||
self.alloc.destroy(self.cursor.hyperlink.?);
|
||||
self.cursor.hyperlink_id = 0;
|
||||
self.cursor.hyperlink = null;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const hash_map = @import("hash_map.zig");
|
||||
const AutoOffsetHashMap = hash_map.AutoOffsetHashMap;
|
||||
@ -21,9 +22,63 @@ pub const Id = size.CellCountInt;
|
||||
// the hyperlink ID in the cell itself.
|
||||
pub const Map = AutoOffsetHashMap(Offset(Cell), Id);
|
||||
|
||||
/// The main entry for hyperlinks.
|
||||
/// A fully decoded hyperlink that may or may not have its
|
||||
/// memory within a page. The memory location of this is dependent
|
||||
/// on the context so users should check with the source of the
|
||||
/// hyperlink.
|
||||
pub const Hyperlink = struct {
|
||||
id: Hyperlink.Id,
|
||||
uri: []const u8,
|
||||
|
||||
/// See PageEntry.Id
|
||||
pub const Id = union(enum) {
|
||||
explicit: []const u8,
|
||||
implicit: size.OffsetInt,
|
||||
};
|
||||
|
||||
/// Deinit and deallocate all the pointers using the given
|
||||
/// allocator.
|
||||
///
|
||||
/// WARNING: This should only be called if the hyperlink was
|
||||
/// heap-allocated. This DOES NOT need to be unconditionally
|
||||
/// called.
|
||||
pub fn deinit(self: *const Hyperlink, alloc: Allocator) void {
|
||||
alloc.free(self.uri);
|
||||
switch (self.id) {
|
||||
.implicit => {},
|
||||
.explicit => |v| alloc.free(v),
|
||||
}
|
||||
}
|
||||
|
||||
/// Duplicate a hyperlink by allocating all values with the
|
||||
/// given allocator. The returned hyperlink should have deinit
|
||||
/// called.
|
||||
pub fn dupe(
|
||||
self: *const Hyperlink,
|
||||
alloc: Allocator,
|
||||
) Allocator.Error!Hyperlink {
|
||||
const uri = try alloc.dupe(u8, self.uri);
|
||||
errdefer alloc.free(uri);
|
||||
|
||||
const id: Hyperlink.Id = switch (self.id) {
|
||||
.implicit => self.id,
|
||||
.explicit => |v| .{ .explicit = try alloc.dupe(u8, v) },
|
||||
};
|
||||
errdefer switch (id) {
|
||||
.implicit => {},
|
||||
.explicit => |v| alloc.free(v),
|
||||
};
|
||||
|
||||
return .{ .id = id, .uri = uri };
|
||||
}
|
||||
};
|
||||
|
||||
/// A hyperlink that has been committed to page memory. This
|
||||
/// is a "page entry" because while it represents a hyperlink,
|
||||
/// some decoding (pointer chasing) is still necessary to get the
|
||||
/// fully realized ID, URI, etc.
|
||||
pub const PageEntry = struct {
|
||||
id: PageEntry.Id,
|
||||
uri: Offset(u8).Slice,
|
||||
|
||||
pub const Id = union(enum) {
|
||||
@ -37,10 +92,10 @@ pub const Hyperlink = struct {
|
||||
|
||||
/// Duplicate this hyperlink from one page to another.
|
||||
pub fn dupe(
|
||||
self: *const Hyperlink,
|
||||
self: *const PageEntry,
|
||||
self_page: *const Page,
|
||||
dst_page: *Page,
|
||||
) error{OutOfMemory}!Hyperlink {
|
||||
) error{OutOfMemory}!PageEntry {
|
||||
var copy = self.*;
|
||||
|
||||
// If the pages are the same then we can return a shallow copy.
|
||||
@ -85,7 +140,7 @@ pub const Hyperlink = struct {
|
||||
return copy;
|
||||
}
|
||||
|
||||
pub fn hash(self: *const Hyperlink, base: anytype) u64 {
|
||||
pub fn hash(self: *const PageEntry, base: anytype) u64 {
|
||||
var hasher = Wyhash.init(0);
|
||||
autoHash(&hasher, std.meta.activeTag(self.id));
|
||||
switch (self.id) {
|
||||
@ -105,9 +160,9 @@ pub const Hyperlink = struct {
|
||||
}
|
||||
|
||||
pub fn eql(
|
||||
self: *const Hyperlink,
|
||||
self: *const PageEntry,
|
||||
self_base: anytype,
|
||||
other: *const Hyperlink,
|
||||
other: *const PageEntry,
|
||||
other_base: anytype,
|
||||
) bool {
|
||||
if (std.meta.activeTag(self.id) != std.meta.activeTag(other.id)) return false;
|
||||
@ -135,21 +190,21 @@ pub const Hyperlink = struct {
|
||||
/// The set of hyperlinks. This is ref-counted so that a set of cells
|
||||
/// can share the same hyperlink without duplicating the data.
|
||||
pub const Set = RefCountedSet(
|
||||
Hyperlink,
|
||||
PageEntry,
|
||||
Id,
|
||||
size.CellCountInt,
|
||||
struct {
|
||||
page: ?*Page = null,
|
||||
|
||||
pub fn hash(self: *const @This(), link: Hyperlink) u64 {
|
||||
pub fn hash(self: *const @This(), link: PageEntry) u64 {
|
||||
return link.hash(self.page.?.memory);
|
||||
}
|
||||
|
||||
pub fn eql(self: *const @This(), a: Hyperlink, b: Hyperlink) bool {
|
||||
pub fn eql(self: *const @This(), a: PageEntry, b: PageEntry) bool {
|
||||
return a.eql(self.page.?.memory, &b, self.page.?.memory);
|
||||
}
|
||||
|
||||
pub fn deleted(self: *const @This(), link: Hyperlink) void {
|
||||
pub fn deleted(self: *const @This(), link: PageEntry) void {
|
||||
const page = self.page.?;
|
||||
const alloc = &page.string_alloc;
|
||||
switch (link.id) {
|
||||
|
@ -808,7 +808,7 @@ pub const Page = struct {
|
||||
|
||||
// If our page can't support an additional cell with
|
||||
// a hyperlink then we have to return an error.
|
||||
if (self.hyperlinkCount() >= self.hyperlinkCapacity() - 1) {
|
||||
if (self.hyperlinkCount() >= self.hyperlinkCapacity()) {
|
||||
// The hyperlink map capacity needs to be increased.
|
||||
return error.HyperlinkMapOutOfMemory;
|
||||
}
|
||||
@ -1142,6 +1142,101 @@ pub const Page = struct {
|
||||
row.hyperlink = false;
|
||||
}
|
||||
|
||||
pub const InsertHyperlinkError = error{
|
||||
/// string_alloc errors
|
||||
StringsOutOfMemory,
|
||||
|
||||
/// hyperlink_set errors
|
||||
SetOutOfMemory,
|
||||
SetNeedsRehash,
|
||||
};
|
||||
|
||||
/// Convert a hyperlink into a page entry, returning the ID.
|
||||
///
|
||||
/// This does not de-dupe any strings, so if the URI, explicit ID,
|
||||
/// etc. is already in the strings table this will duplicate it.
|
||||
///
|
||||
/// To release the memory associated with the given hyperlink,
|
||||
/// release the ID from the `hyperlink_set`. If the refcount reaches
|
||||
/// zero and the slot is needed then the context will reap the
|
||||
/// memory.
|
||||
pub fn insertHyperlink(
|
||||
self: *Page,
|
||||
link: hyperlink.Hyperlink,
|
||||
) InsertHyperlinkError!hyperlink.Id {
|
||||
// Insert our URI into the page strings table.
|
||||
const page_uri: Offset(u8).Slice = uri: {
|
||||
const buf = self.string_alloc.alloc(
|
||||
u8,
|
||||
self.memory,
|
||||
link.uri.len,
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.StringsOutOfMemory,
|
||||
};
|
||||
errdefer self.string_alloc.free(self.memory, buf);
|
||||
@memcpy(buf, link.uri);
|
||||
|
||||
break :uri .{
|
||||
.offset = size.getOffset(u8, self.memory, &buf[0]),
|
||||
.len = link.uri.len,
|
||||
};
|
||||
};
|
||||
errdefer self.string_alloc.free(
|
||||
self.memory,
|
||||
page_uri.offset.ptr(self.memory)[0..page_uri.len],
|
||||
);
|
||||
|
||||
// Allocate an ID for our page memory if we have to.
|
||||
const page_id: hyperlink.PageEntry.Id = switch (link.id) {
|
||||
.explicit => |id| explicit: {
|
||||
const buf = self.string_alloc.alloc(
|
||||
u8,
|
||||
self.memory,
|
||||
id.len,
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.StringsOutOfMemory,
|
||||
};
|
||||
errdefer self.string_alloc.free(self.memory, buf);
|
||||
@memcpy(buf, id);
|
||||
|
||||
break :explicit .{
|
||||
.explicit = .{
|
||||
.offset = size.getOffset(u8, self.memory, &buf[0]),
|
||||
.len = id.len,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
.implicit => |id| .{ .implicit = id },
|
||||
};
|
||||
errdefer switch (page_id) {
|
||||
.implicit => {},
|
||||
.explicit => |slice| self.string_alloc.free(
|
||||
self.memory,
|
||||
slice.offset.ptr(self.memory)[0..slice.len],
|
||||
),
|
||||
};
|
||||
|
||||
// Build our entry
|
||||
const entry: hyperlink.PageEntry = .{
|
||||
.id = page_id,
|
||||
.uri = page_uri,
|
||||
};
|
||||
|
||||
// Put our hyperlink into the hyperlink set to get an ID
|
||||
const id = self.hyperlink_set.addContext(
|
||||
self.memory,
|
||||
entry,
|
||||
.{ .page = self },
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.SetOutOfMemory,
|
||||
error.NeedsRehash => return error.SetNeedsRehash,
|
||||
};
|
||||
errdefer self.hyperlink_set.release(self.memory, id);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Set the hyperlink for the given cell. If the cell already has a
|
||||
/// hyperlink, then this will handle memory management and refcount
|
||||
/// update for the prior hyperlink.
|
||||
@ -2237,6 +2332,50 @@ test "Page cloneFrom partial" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Page cloneFrom hyperlinks exact capacity" {
|
||||
var page = try Page.init(.{
|
||||
.cols = 50,
|
||||
.rows = 50,
|
||||
});
|
||||
defer page.deinit();
|
||||
|
||||
// Ensure our page can accommodate the capacity.
|
||||
const hyperlink_cap = page.hyperlinkCapacity();
|
||||
try testing.expect(hyperlink_cap <= page.size.cols * page.size.rows);
|
||||
|
||||
// Create a hyperlink.
|
||||
const hyperlink_id = try page.insertHyperlink(.{
|
||||
.id = .{ .implicit = 0 },
|
||||
.uri = "https://example.com",
|
||||
});
|
||||
|
||||
// Fill the exact cap with cells.
|
||||
fill: for (0..page.size.cols) |x| {
|
||||
for (0..page.size.rows) |y| {
|
||||
const rac = page.getRowAndCell(x, y);
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = 42 },
|
||||
};
|
||||
try page.setHyperlink(rac.row, rac.cell, hyperlink_id);
|
||||
page.hyperlink_set.use(page.memory, hyperlink_id);
|
||||
|
||||
if (page.hyperlinkCount() == hyperlink_cap) {
|
||||
break :fill;
|
||||
}
|
||||
}
|
||||
}
|
||||
try testing.expectEqual(page.hyperlinkCount(), page.hyperlinkCapacity());
|
||||
|
||||
// Clone the full page
|
||||
var page2 = try Page.init(page.capacity);
|
||||
defer page2.deinit();
|
||||
try page2.cloneFrom(&page, 0, page.size.rows);
|
||||
|
||||
// We should have the same number of hyperlinks
|
||||
try testing.expectEqual(page2.hyperlinkCount(), page.hyperlinkCount());
|
||||
}
|
||||
|
||||
test "Page cloneFrom graphemes" {
|
||||
var page = try Page.init(.{
|
||||
.cols = 10,
|
||||
|
Reference in New Issue
Block a user