parent
24b9778432
commit
901708e8da
|
|
@ -700,6 +700,7 @@ typedef struct {
|
||||||
typedef enum {
|
typedef enum {
|
||||||
GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN,
|
GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN,
|
||||||
GHOSTTY_ACTION_OPEN_URL_KIND_TEXT,
|
GHOSTTY_ACTION_OPEN_URL_KIND_TEXT,
|
||||||
|
GHOSTTY_ACTION_OPEN_URL_KIND_HTML,
|
||||||
} ghostty_action_open_url_kind_e;
|
} ghostty_action_open_url_kind_e;
|
||||||
|
|
||||||
// apprt.action.OpenUrl.C
|
// apprt.action.OpenUrl.C
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,14 @@ extension Ghostty.Action {
|
||||||
enum Kind {
|
enum Kind {
|
||||||
case unknown
|
case unknown
|
||||||
case text
|
case text
|
||||||
|
case html
|
||||||
|
|
||||||
init(_ c: ghostty_action_open_url_kind_e) {
|
init(_ c: ghostty_action_open_url_kind_e) {
|
||||||
switch c {
|
switch c {
|
||||||
case GHOSTTY_ACTION_OPEN_URL_KIND_TEXT:
|
case GHOSTTY_ACTION_OPEN_URL_KIND_TEXT:
|
||||||
self = .text
|
self = .text
|
||||||
|
case GHOSTTY_ACTION_OPEN_URL_KIND_HTML:
|
||||||
|
self = .html
|
||||||
default:
|
default:
|
||||||
self = .unknown
|
self = .unknown
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -676,6 +676,10 @@ extension Ghostty {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case .html:
|
||||||
|
// The extension will be HTML and we do the right thing automatically.
|
||||||
|
break
|
||||||
|
|
||||||
case .unknown:
|
case .unknown:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5280,14 +5280,24 @@ const WriteScreenLoc = enum {
|
||||||
fn writeScreenFile(
|
fn writeScreenFile(
|
||||||
self: *Surface,
|
self: *Surface,
|
||||||
loc: WriteScreenLoc,
|
loc: WriteScreenLoc,
|
||||||
write_action: input.Binding.Action.WriteScreenAction,
|
write_screen: input.Binding.Action.WriteScreen,
|
||||||
) !void {
|
) !void {
|
||||||
// Create a temporary directory to store our scrollback.
|
// Create a temporary directory to store our scrollback.
|
||||||
var tmp_dir = try internal_os.TempDir.init();
|
var tmp_dir = try internal_os.TempDir.init();
|
||||||
errdefer tmp_dir.deinit();
|
errdefer tmp_dir.deinit();
|
||||||
|
|
||||||
var filename_buf: [std.fs.max_path_bytes]u8 = undefined;
|
var filename_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||||
const filename = try std.fmt.bufPrint(&filename_buf, "{s}.txt", .{@tagName(loc)});
|
const filename = try std.fmt.bufPrint(
|
||||||
|
&filename_buf,
|
||||||
|
"{s}.{s}",
|
||||||
|
.{
|
||||||
|
@tagName(loc),
|
||||||
|
switch (write_screen.emit) {
|
||||||
|
.plain, .vt => "txt",
|
||||||
|
.html => "html",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Open our scrollback file
|
// Open our scrollback file
|
||||||
var file = try tmp_dir.dir.createFile(
|
var file = try tmp_dir.dir.createFile(
|
||||||
|
|
@ -5347,18 +5357,24 @@ fn writeScreenFile(
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use topLeft and bottomRight to ensure correct coordinate ordering
|
const ScreenFormatter = terminal.formatter.ScreenFormatter;
|
||||||
const tl = sel.topLeft(&self.io.terminal.screen);
|
var formatter: ScreenFormatter = .init(&self.io.terminal.screen, .{
|
||||||
const br = sel.bottomRight(&self.io.terminal.screen);
|
.emit = switch (write_screen.emit) {
|
||||||
|
.plain => .plain,
|
||||||
try self.io.terminal.screen.dumpString(
|
.vt => .vt,
|
||||||
buf_writer,
|
.html => .html,
|
||||||
.{
|
|
||||||
.tl = tl,
|
|
||||||
.br = br,
|
|
||||||
.unwrap = true,
|
|
||||||
},
|
},
|
||||||
);
|
.unwrap = true,
|
||||||
|
.trim = false,
|
||||||
|
.background = self.io.terminal.colors.background.get(),
|
||||||
|
.foreground = self.io.terminal.colors.foreground.get(),
|
||||||
|
.palette = &self.io.terminal.colors.palette.current,
|
||||||
|
});
|
||||||
|
formatter.content = .{ .selection = sel.ordered(
|
||||||
|
&self.io.terminal.screen,
|
||||||
|
.forward,
|
||||||
|
) };
|
||||||
|
try formatter.format(buf_writer);
|
||||||
}
|
}
|
||||||
try buf_writer.flush();
|
try buf_writer.flush();
|
||||||
|
|
||||||
|
|
@ -5366,7 +5382,7 @@ fn writeScreenFile(
|
||||||
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||||
const path = try tmp_dir.dir.realpath(filename, &path_buf);
|
const path = try tmp_dir.dir.realpath(filename, &path_buf);
|
||||||
|
|
||||||
switch (write_action) {
|
switch (write_screen.action) {
|
||||||
.copy => {
|
.copy => {
|
||||||
const pathZ = try self.alloc.dupeZ(u8, path);
|
const pathZ = try self.alloc.dupeZ(u8, path);
|
||||||
defer self.alloc.free(pathZ);
|
defer self.alloc.free(pathZ);
|
||||||
|
|
@ -5375,7 +5391,13 @@ fn writeScreenFile(
|
||||||
.data = pathZ,
|
.data = pathZ,
|
||||||
}}, false);
|
}}, false);
|
||||||
},
|
},
|
||||||
.open => try self.openUrl(.{ .kind = .text, .url = path }),
|
.open => try self.openUrl(.{
|
||||||
|
.kind = switch (write_screen.emit) {
|
||||||
|
.plain, .vt => .text,
|
||||||
|
.html => .html,
|
||||||
|
},
|
||||||
|
.url = path,
|
||||||
|
}),
|
||||||
.paste => self.io.queueMessage(try termio.Message.writeReq(
|
.paste => self.io.queueMessage(try termio.Message.writeReq(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
path,
|
path,
|
||||||
|
|
|
||||||
|
|
@ -724,6 +724,9 @@ pub const OpenUrl = struct {
|
||||||
/// should try to open the URL in a text editor or viewer or
|
/// should try to open the URL in a text editor or viewer or
|
||||||
/// some equivalent, if possible.
|
/// some equivalent, if possible.
|
||||||
text,
|
text,
|
||||||
|
|
||||||
|
/// The URL is known to contain HTML content.
|
||||||
|
html,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sync with: ghostty_action_open_url_s
|
// Sync with: ghostty_action_open_url_s
|
||||||
|
|
|
||||||
|
|
@ -434,13 +434,13 @@ pub const Action = union(enum) {
|
||||||
/// The default OS editor is determined by using `open` on macOS
|
/// The default OS editor is determined by using `open` on macOS
|
||||||
/// and `xdg-open` on Linux.
|
/// and `xdg-open` on Linux.
|
||||||
///
|
///
|
||||||
write_scrollback_file: WriteScreenAction,
|
write_scrollback_file: WriteScreen,
|
||||||
|
|
||||||
/// Write the contents of the screen into a temporary file with the
|
/// Write the contents of the screen into a temporary file with the
|
||||||
/// specified action.
|
/// specified action.
|
||||||
///
|
///
|
||||||
/// See `write_scrollback_file` for possible actions.
|
/// See `write_scrollback_file` for possible actions.
|
||||||
write_screen_file: WriteScreenAction,
|
write_screen_file: WriteScreen,
|
||||||
|
|
||||||
/// Write the currently selected text into a temporary file with the
|
/// Write the currently selected text into a temporary file with the
|
||||||
/// specified action.
|
/// specified action.
|
||||||
|
|
@ -448,7 +448,7 @@ pub const Action = union(enum) {
|
||||||
/// See `write_scrollback_file` for possible actions.
|
/// See `write_scrollback_file` for possible actions.
|
||||||
///
|
///
|
||||||
/// Does nothing when no text is selected.
|
/// Does nothing when no text is selected.
|
||||||
write_selection_file: WriteScreenAction,
|
write_selection_file: WriteScreen,
|
||||||
|
|
||||||
/// Open a new window.
|
/// Open a new window.
|
||||||
///
|
///
|
||||||
|
|
@ -811,6 +811,15 @@ pub const Action = union(enum) {
|
||||||
.application = try alloc.dupe(u8, self.application),
|
.application = try alloc.dupe(u8, self.application),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn format(
|
||||||
|
self: CursorKey,
|
||||||
|
writer: *std.Io.Writer,
|
||||||
|
) std.Io.Writer.Error!void {
|
||||||
|
_ = self;
|
||||||
|
_ = writer;
|
||||||
|
@panic("formatting not supported");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const AdjustSelection = enum {
|
pub const AdjustSelection = enum {
|
||||||
|
|
@ -902,10 +911,64 @@ pub const Action = union(enum) {
|
||||||
pub const default: CopyToClipboard = .mixed;
|
pub const default: CopyToClipboard = .mixed;
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const WriteScreenAction = enum {
|
pub const WriteScreen = struct {
|
||||||
copy,
|
action: WriteScreen.Action,
|
||||||
paste,
|
emit: WriteScreen.Format,
|
||||||
open,
|
|
||||||
|
pub const copy: WriteScreen = .{ .action = .copy, .emit = .plain };
|
||||||
|
pub const paste: WriteScreen = .{ .action = .paste, .emit = .plain };
|
||||||
|
pub const open: WriteScreen = .{ .action = .open, .emit = .plain };
|
||||||
|
|
||||||
|
pub const Action = enum {
|
||||||
|
copy,
|
||||||
|
paste,
|
||||||
|
open,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Format = enum {
|
||||||
|
plain,
|
||||||
|
vt,
|
||||||
|
html,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn parse(param: []const u8) !WriteScreen {
|
||||||
|
// If we don't have a `,`, default to the plain format. This is
|
||||||
|
// also very important for backwards compatibility before Ghostty
|
||||||
|
// 1.3 which didn't support output formats.
|
||||||
|
const idx = std.mem.indexOfScalar(u8, param, ',') orelse return .{
|
||||||
|
.action = try Binding.Action.parseEnum(
|
||||||
|
WriteScreen.Action,
|
||||||
|
param,
|
||||||
|
),
|
||||||
|
.emit = .plain,
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.action = try Binding.Action.parseEnum(
|
||||||
|
WriteScreen.Action,
|
||||||
|
param[0..idx],
|
||||||
|
),
|
||||||
|
.emit = try Binding.Action.parseEnum(
|
||||||
|
WriteScreen.Format,
|
||||||
|
param[idx + 1 ..],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clone(
|
||||||
|
self: WriteScreen,
|
||||||
|
alloc: Allocator,
|
||||||
|
) Allocator.Error!WriteScreen {
|
||||||
|
_ = alloc;
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(self: WriteScreen, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
||||||
|
try writer.print("{t},{t}", .{
|
||||||
|
self.action,
|
||||||
|
self.emit,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extern because it is used in the embedded runtime ABI.
|
// Extern because it is used in the embedded runtime ABI.
|
||||||
|
|
@ -948,7 +1011,7 @@ pub const Action = union(enum) {
|
||||||
if (@hasDecl(field.type, "parse") and
|
if (@hasDecl(field.type, "parse") and
|
||||||
@typeInfo(@TypeOf(field.type.parse)) == .@"fn")
|
@typeInfo(@TypeOf(field.type.parse)) == .@"fn")
|
||||||
{
|
{
|
||||||
return field.type.parse(param);
|
return try field.type.parse(param);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1244,12 +1307,19 @@ pub const Action = union(enum) {
|
||||||
.@"enum" => try writer.print("{t}", .{value}),
|
.@"enum" => try writer.print("{t}", .{value}),
|
||||||
.float => try writer.print("{d}", .{value}),
|
.float => try writer.print("{d}", .{value}),
|
||||||
.int => try writer.print("{d}", .{value}),
|
.int => try writer.print("{d}", .{value}),
|
||||||
.@"struct" => |info| if (!info.is_tuple) {
|
.@"struct" => |info| format: {
|
||||||
try writer.print("{} (not configurable)", .{value});
|
if (@hasDecl(Value, "format")) {
|
||||||
} else {
|
try value.format(writer);
|
||||||
inline for (info.fields, 0..) |field, i| {
|
break :format;
|
||||||
try formatValue(writer, @field(value, field.name));
|
}
|
||||||
if (i + 1 < info.fields.len) try writer.writeAll(",");
|
|
||||||
|
if (!info.is_tuple) {
|
||||||
|
@compileError("unhandled struct type: " ++ @typeName(Value));
|
||||||
|
} else {
|
||||||
|
inline for (info.fields, 0..) |field, i| {
|
||||||
|
try formatValue(writer, @field(value, field.name));
|
||||||
|
if (i + 1 < info.fields.len) try writer.writeAll(",");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
else => @compileError("unhandled type: " ++ @typeName(Value)),
|
else => @compileError("unhandled type: " ++ @typeName(Value)),
|
||||||
|
|
@ -3274,6 +3344,62 @@ test "parse: copy to clipboard explicit" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "parse: write screen file no format" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
// parameter
|
||||||
|
{
|
||||||
|
const binding = try parseSingle("a=write_screen_file:copy");
|
||||||
|
try testing.expect(binding.action == .write_screen_file);
|
||||||
|
try testing.expectEqual(Action.WriteScreen.copy, binding.action.write_screen_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse: write screen file format" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
// parameter
|
||||||
|
{
|
||||||
|
const binding = try parseSingle("a=write_screen_file:copy,html");
|
||||||
|
try testing.expect(binding.action == .write_screen_file);
|
||||||
|
try testing.expectEqual(Action.WriteScreen{
|
||||||
|
.action = .copy,
|
||||||
|
.emit = .html,
|
||||||
|
}, binding.action.write_screen_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse: write screen file format as string" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
{
|
||||||
|
var buf: std.Io.Writer.Allocating = .init(alloc);
|
||||||
|
defer buf.deinit();
|
||||||
|
const binding = try parseSingle("a=write_screen_file:copy,html");
|
||||||
|
try binding.action.format(&buf.writer);
|
||||||
|
try testing.expectEqualStrings("write_screen_file:copy,html", buf.written());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse: write screen file invalid" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
// paramet r
|
||||||
|
try testing.expectError(Error.InvalidFormat, parseSingle(
|
||||||
|
"a=write_screen_file:",
|
||||||
|
));
|
||||||
|
try testing.expectError(Error.InvalidFormat, parseSingle(
|
||||||
|
"a=write_screen_file:,",
|
||||||
|
));
|
||||||
|
try testing.expectError(Error.InvalidFormat, parseSingle(
|
||||||
|
"a=write_screen_file:copy,",
|
||||||
|
));
|
||||||
|
try testing.expectError(Error.InvalidFormat, parseSingle(
|
||||||
|
"a=write_screen_file:copy,html,extra",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
test "action: format" {
|
test "action: format" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,56 @@ fn actionCommands(action: Action.Key) []const Command {
|
||||||
.title = "Copy Screen to Temporary File and Open",
|
.title = "Copy Screen to Temporary File and Open",
|
||||||
.description = "Copy the screen contents to a temporary file and open it.",
|
.description = "Copy the screen contents to a temporary file and open it.",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.{
|
||||||
|
.action = .{ .write_screen_file = .{
|
||||||
|
.action = .copy,
|
||||||
|
.emit = .html,
|
||||||
|
} },
|
||||||
|
.title = "Copy Screen as HTML to Temporary File and Copy Path",
|
||||||
|
.description = "Copy the screen contents as HTML to a temporary file and copy the path to the clipboard.",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.action = .{ .write_screen_file = .{
|
||||||
|
.action = .paste,
|
||||||
|
.emit = .html,
|
||||||
|
} },
|
||||||
|
.title = "Copy Screen as HTML to Temporary File and Paste Path",
|
||||||
|
.description = "Copy the screen contents as HTML to a temporary file and paste the path to the file.",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.action = .{ .write_screen_file = .{
|
||||||
|
.action = .open,
|
||||||
|
.emit = .html,
|
||||||
|
} },
|
||||||
|
.title = "Copy Screen as HTML to Temporary File and Open",
|
||||||
|
.description = "Copy the screen contents as HTML to a temporary file and open it.",
|
||||||
|
},
|
||||||
|
|
||||||
|
.{
|
||||||
|
.action = .{ .write_screen_file = .{
|
||||||
|
.action = .copy,
|
||||||
|
.emit = .vt,
|
||||||
|
} },
|
||||||
|
.title = "Copy Screen as ANSI Sequences to Temporary File and Copy Path",
|
||||||
|
.description = "Copy the screen contents as ANSI escape sequences to a temporary file and copy the path to the clipboard.",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.action = .{ .write_screen_file = .{
|
||||||
|
.action = .paste,
|
||||||
|
.emit = .vt,
|
||||||
|
} },
|
||||||
|
.title = "Copy Screen as ANSI Sequences to Temporary File and Paste Path",
|
||||||
|
.description = "Copy the screen contents as ANSI escape sequences to a temporary file and paste the path to the file.",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.action = .{ .write_screen_file = .{
|
||||||
|
.action = .open,
|
||||||
|
.emit = .vt,
|
||||||
|
} },
|
||||||
|
.title = "Copy Screen as ANSI Sequences to Temporary File and Open",
|
||||||
|
.description = "Copy the screen contents as ANSI escape sequences to a temporary file and open it.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
.write_selection_file => comptime &.{
|
.write_selection_file => comptime &.{
|
||||||
|
|
@ -257,6 +307,56 @@ fn actionCommands(action: Action.Key) []const Command {
|
||||||
.title = "Copy Selection to Temporary File and Open",
|
.title = "Copy Selection to Temporary File and Open",
|
||||||
.description = "Copy the selection contents to a temporary file and open it.",
|
.description = "Copy the selection contents to a temporary file and open it.",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.{
|
||||||
|
.action = .{ .write_selection_file = .{
|
||||||
|
.action = .copy,
|
||||||
|
.emit = .html,
|
||||||
|
} },
|
||||||
|
.title = "Copy Selection as HTML to Temporary File and Copy Path",
|
||||||
|
.description = "Copy the selection contents as HTML to a temporary file and copy the path to the clipboard.",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.action = .{ .write_selection_file = .{
|
||||||
|
.action = .paste,
|
||||||
|
.emit = .html,
|
||||||
|
} },
|
||||||
|
.title = "Copy Selection as HTML to Temporary File and Paste Path",
|
||||||
|
.description = "Copy the selection contents as HTML to a temporary file and paste the path to the file.",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.action = .{ .write_selection_file = .{
|
||||||
|
.action = .open,
|
||||||
|
.emit = .html,
|
||||||
|
} },
|
||||||
|
.title = "Copy Selection as HTML to Temporary File and Open",
|
||||||
|
.description = "Copy the selection contents as HTML to a temporary file and open it.",
|
||||||
|
},
|
||||||
|
|
||||||
|
.{
|
||||||
|
.action = .{ .write_selection_file = .{
|
||||||
|
.action = .copy,
|
||||||
|
.emit = .vt,
|
||||||
|
} },
|
||||||
|
.title = "Copy Selection as ANSI Sequences to Temporary File and Copy Path",
|
||||||
|
.description = "Copy the selection contents as ANSI escape sequences to a temporary file and copy the path to the clipboard.",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.action = .{ .write_selection_file = .{
|
||||||
|
.action = .paste,
|
||||||
|
.emit = .vt,
|
||||||
|
} },
|
||||||
|
.title = "Copy Selection as ANSI Sequences to Temporary File and Paste Path",
|
||||||
|
.description = "Copy the selection contents as ANSI escape sequences to a temporary file and paste the path to the file.",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.action = .{ .write_selection_file = .{
|
||||||
|
.action = .open,
|
||||||
|
.emit = .vt,
|
||||||
|
} },
|
||||||
|
.title = "Copy Selection as ANSI Sequences to Temporary File and Open",
|
||||||
|
.description = "Copy the selection contents as ANSI escape sequences to a temporary file and open it.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
.new_window => comptime &.{.{
|
.new_window => comptime &.{.{
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ pub fn open(
|
||||||
.macos => .init(
|
.macos => .init(
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
.text => &.{ "open", "-t", url },
|
.text => &.{ "open", "-t", url },
|
||||||
.unknown => &.{ "open", url },
|
.html, .unknown => &.{ "open", url },
|
||||||
},
|
},
|
||||||
alloc,
|
alloc,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue