mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-16 14:56:10 +00:00
input
configuration to pass input as stdin on startup
This adds a new configuration `input` that allows passing either raw text or file contents as stdin when starting the terminal. The input is sent byte-for-byte to the terminal, so control characters such as `\n` will be interpreted by the shell and can be used to run programs in the context of the loaded shell. Example: `ghostty --input="hello, world\n"` will start the your default shell, run `echo hello, world`, and then show the prompt.
This commit is contained in:
@@ -414,7 +414,7 @@ pub fn parseIntoField(
|
|||||||
return error.InvalidField;
|
return error.InvalidField;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T {
|
pub fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T {
|
||||||
const info = @typeInfo(T).@"union";
|
const info = @typeInfo(T).@"union";
|
||||||
assert(@typeInfo(info.tag_type.?) == .@"enum");
|
assert(@typeInfo(info.tag_type.?) == .@"enum");
|
||||||
|
|
||||||
@@ -1090,6 +1090,7 @@ test "parseIntoField: tagged union" {
|
|||||||
b: u8,
|
b: u8,
|
||||||
c: void,
|
c: void,
|
||||||
d: []const u8,
|
d: []const u8,
|
||||||
|
e: [:0]const u8,
|
||||||
} = undefined,
|
} = undefined,
|
||||||
} = .{};
|
} = .{};
|
||||||
|
|
||||||
@@ -1108,6 +1109,10 @@ test "parseIntoField: tagged union" {
|
|||||||
// Set string field
|
// Set string field
|
||||||
try parseIntoField(@TypeOf(data), alloc, &data, "value", "d:hello");
|
try parseIntoField(@TypeOf(data), alloc, &data, "value", "d:hello");
|
||||||
try testing.expectEqualStrings("hello", data.value.d);
|
try testing.expectEqualStrings("hello", data.value.d);
|
||||||
|
|
||||||
|
// Set sentinel string field
|
||||||
|
try parseIntoField(@TypeOf(data), alloc, &data, "value", "e:hello");
|
||||||
|
try testing.expectEqualStrings("hello", data.value.e);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseIntoField: tagged union unknown filed" {
|
test "parseIntoField: tagged union unknown filed" {
|
||||||
|
@@ -3,6 +3,7 @@ const builtin = @import("builtin");
|
|||||||
const formatter = @import("config/formatter.zig");
|
const formatter = @import("config/formatter.zig");
|
||||||
pub const Config = @import("config/Config.zig");
|
pub const Config = @import("config/Config.zig");
|
||||||
pub const conditional = @import("config/conditional.zig");
|
pub const conditional = @import("config/conditional.zig");
|
||||||
|
pub const io = @import("config/io.zig");
|
||||||
pub const string = @import("config/string.zig");
|
pub const string = @import("config/string.zig");
|
||||||
pub const edit = @import("config/edit.zig");
|
pub const edit = @import("config/edit.zig");
|
||||||
pub const url = @import("config/url.zig");
|
pub const url = @import("config/url.zig");
|
||||||
|
@@ -34,6 +34,7 @@ const ErrorList = @import("ErrorList.zig");
|
|||||||
const MetricModifier = fontpkg.Metrics.Modifier;
|
const MetricModifier = fontpkg.Metrics.Modifier;
|
||||||
const help_strings = @import("help_strings");
|
const help_strings = @import("help_strings");
|
||||||
pub const Command = @import("command.zig").Command;
|
pub const Command = @import("command.zig").Command;
|
||||||
|
const RepeatableReadableIO = @import("io.zig").RepeatableReadableIO;
|
||||||
const RepeatableStringMap = @import("RepeatableStringMap.zig");
|
const RepeatableStringMap = @import("RepeatableStringMap.zig");
|
||||||
pub const Path = @import("path.zig").Path;
|
pub const Path = @import("path.zig").Path;
|
||||||
pub const RepeatablePath = @import("path.zig").RepeatablePath;
|
pub const RepeatablePath = @import("path.zig").RepeatablePath;
|
||||||
@@ -807,6 +808,47 @@ command: ?Command = null,
|
|||||||
/// browser.
|
/// browser.
|
||||||
env: RepeatableStringMap = .{},
|
env: RepeatableStringMap = .{},
|
||||||
|
|
||||||
|
/// Data to send as input to the command on startup.
|
||||||
|
///
|
||||||
|
/// The configured `command` will be launched using the typical rules,
|
||||||
|
/// then the data specified as this input will be written to the pty
|
||||||
|
/// before any other input can be provided.
|
||||||
|
///
|
||||||
|
/// The bytes are sent as-is with no additional encoding. Therefore, be
|
||||||
|
/// cautious about input that can contain control characters, because this
|
||||||
|
/// can be used to execute programs in a shell.
|
||||||
|
///
|
||||||
|
/// The format of this value is:
|
||||||
|
///
|
||||||
|
/// * `raw:<string>` - Send raw text as-is. This uses Zig string literal
|
||||||
|
/// syntax so you can specify control characters and other standard
|
||||||
|
/// escapes.
|
||||||
|
///
|
||||||
|
/// * `path:<path>` - Read a filepath and send the contents. The path
|
||||||
|
/// must be to a file with finite length. e.g. don't use a device
|
||||||
|
/// such as `/dev/stdin` or `/dev/urandom` as these will block
|
||||||
|
/// terminal startup indefinitely. Files are limited to 10MB
|
||||||
|
/// in size to prevent excessive memory usage. If you have files
|
||||||
|
/// larger than this you should write a script to read the file
|
||||||
|
/// and send it to the terminal.
|
||||||
|
///
|
||||||
|
/// If no valid prefix is found, it is assumed to be a `raw:` input.
|
||||||
|
/// This is an ergonomic choice to allow you to simply write
|
||||||
|
/// `input = "Hello, world!"` (a common case) without needing to prefix
|
||||||
|
/// every value with `raw:`.
|
||||||
|
///
|
||||||
|
/// This can be repeated multiple times to send more data. The data
|
||||||
|
/// is concatenated directly with no separator characters in between
|
||||||
|
/// (e.g. no newline).
|
||||||
|
///
|
||||||
|
/// If any of the input sources do not exist, then none of the input
|
||||||
|
/// will be sent. Input sources are not verified until the terminal
|
||||||
|
/// is starting, so missing paths will not show up in config validation.
|
||||||
|
///
|
||||||
|
/// Changing this configuration at runtime will only affect new
|
||||||
|
/// terminals.
|
||||||
|
input: RepeatableReadableIO = .{},
|
||||||
|
|
||||||
/// If true, keep the terminal open after the command exits. Normally, the
|
/// If true, keep the terminal open after the command exits. Normally, the
|
||||||
/// terminal window closes when the running command (such as a shell) exits.
|
/// terminal window closes when the running command (such as a shell) exits.
|
||||||
/// With this true, the terminal window will stay open until any keypress is
|
/// With this true, the terminal window will stay open until any keypress is
|
||||||
|
256
src/config/io.zig
Normal file
256
src/config/io.zig
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
const string = @import("string.zig");
|
||||||
|
const formatterpkg = @import("formatter.zig");
|
||||||
|
const cli = @import("../cli.zig");
|
||||||
|
|
||||||
|
/// ReadableIO is some kind of IO source that is readable.
|
||||||
|
///
|
||||||
|
/// It can be either a direct string or a filepath. The filepath will
|
||||||
|
/// be deferred and read later, so it won't be checked for existence
|
||||||
|
/// or readability at configuration time. This allows using a path that
|
||||||
|
/// might be produced in an intermediate state.
|
||||||
|
pub const ReadableIO = union(enum) {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
raw: [:0]const u8,
|
||||||
|
path: [:0]const u8,
|
||||||
|
|
||||||
|
pub fn parseCLI(
|
||||||
|
self: *Self,
|
||||||
|
alloc: Allocator,
|
||||||
|
input_: ?[]const u8,
|
||||||
|
) !void {
|
||||||
|
const input = input_ orelse return error.ValueRequired;
|
||||||
|
if (input.len == 0) return error.ValueRequired;
|
||||||
|
|
||||||
|
// We create a buffer only to do string parsing and validate
|
||||||
|
// it works. We store the value as raw so that our formatting
|
||||||
|
// can recreate it.
|
||||||
|
{
|
||||||
|
const buf = try alloc.alloc(u8, input.len);
|
||||||
|
defer alloc.free(buf);
|
||||||
|
_ = try string.parse(buf, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, parse the tagged union using normal rules.
|
||||||
|
self.* = cli.args.parseTaggedUnion(
|
||||||
|
Self,
|
||||||
|
alloc,
|
||||||
|
input,
|
||||||
|
) catch |err| switch (err) {
|
||||||
|
// Invalid values in the tagged union are interpreted as
|
||||||
|
// raw values. This lets users pass in simple string values
|
||||||
|
// without needing to tag them.
|
||||||
|
error.InvalidValue => .{ .raw = try alloc.dupeZ(u8, input) },
|
||||||
|
else => return err,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clone(self: Self, alloc: Allocator) Allocator.Error!Self {
|
||||||
|
return switch (self) {
|
||||||
|
.raw => |v| .{ .raw = try alloc.dupeZ(u8, v) },
|
||||||
|
.path => |v| .{ .path = try alloc.dupeZ(u8, v) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as clone but also parses the values as Zig strings in
|
||||||
|
/// the final resulting value all at once so we can avoid extra
|
||||||
|
/// allocations.
|
||||||
|
pub fn cloneParsed(
|
||||||
|
self: Self,
|
||||||
|
alloc: Allocator,
|
||||||
|
) Allocator.Error!Self {
|
||||||
|
switch (self) {
|
||||||
|
inline else => |v, tag| {
|
||||||
|
// Parsing can't fail because we validate it in parseCLI
|
||||||
|
const copied = try alloc.dupeZ(u8, v);
|
||||||
|
const parsed = string.parse(copied, v) catch unreachable;
|
||||||
|
assert(copied.ptr == parsed.ptr);
|
||||||
|
|
||||||
|
// If we parsed less than our original length we need
|
||||||
|
// to keep it null-terminated.
|
||||||
|
if (parsed.len < copied.len) copied[parsed.len] = 0;
|
||||||
|
|
||||||
|
return @unionInit(
|
||||||
|
Self,
|
||||||
|
@tagName(tag),
|
||||||
|
copied[0..parsed.len :0],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn equal(self: Self, other: Self) bool {
|
||||||
|
if (std.meta.activeTag(self) != std.meta.activeTag(other)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (self) {
|
||||||
|
.raw => |v| std.mem.eql(u8, v, other.raw),
|
||||||
|
.path => |v| std.mem.eql(u8, v, other.path),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn formatEntry(self: Self, formatter: anytype) !void {
|
||||||
|
var buf: [4096]u8 = undefined;
|
||||||
|
var fbs = std.io.fixedBufferStream(&buf);
|
||||||
|
const writer = fbs.writer();
|
||||||
|
switch (self) {
|
||||||
|
inline else => |v, tag| {
|
||||||
|
writer.writeAll(@tagName(tag)) catch return error.OutOfMemory;
|
||||||
|
writer.writeByte(':') catch return error.OutOfMemory;
|
||||||
|
writer.writeAll(v) catch return error.OutOfMemory;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const written = fbs.getWritten();
|
||||||
|
try formatter.formatEntry(
|
||||||
|
[]const u8,
|
||||||
|
written,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseCLI" {
|
||||||
|
const testing = std.testing;
|
||||||
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
{
|
||||||
|
var io: Self = undefined;
|
||||||
|
try Self.parseCLI(&io, alloc, "foo");
|
||||||
|
try testing.expect(io == .raw);
|
||||||
|
try testing.expectEqualStrings("foo", io.raw);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
var io: Self = undefined;
|
||||||
|
try Self.parseCLI(&io, alloc, "raw:foo");
|
||||||
|
try testing.expect(io == .raw);
|
||||||
|
try testing.expectEqualStrings("foo", io.raw);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
var io: Self = undefined;
|
||||||
|
try Self.parseCLI(&io, alloc, "path:foo");
|
||||||
|
try testing.expect(io == .path);
|
||||||
|
try testing.expectEqualStrings("foo", io.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatEntry" {
|
||||||
|
const testing = std.testing;
|
||||||
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
|
var buf = std.ArrayList(u8).init(alloc);
|
||||||
|
defer buf.deinit();
|
||||||
|
|
||||||
|
var v: Self = undefined;
|
||||||
|
try v.parseCLI(alloc, "raw:foo");
|
||||||
|
try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||||
|
try std.testing.expectEqualSlices(u8, "a = raw:foo\n", buf.items);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RepeatableReadableIO = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
// Allocator for the list is the arena for the parent config.
|
||||||
|
list: std.ArrayListUnmanaged(ReadableIO) = .{},
|
||||||
|
|
||||||
|
pub fn parseCLI(
|
||||||
|
self: *Self,
|
||||||
|
alloc: Allocator,
|
||||||
|
input: ?[]const u8,
|
||||||
|
) !void {
|
||||||
|
const value = input orelse return error.ValueRequired;
|
||||||
|
|
||||||
|
// Empty value resets the list
|
||||||
|
if (value.len == 0) {
|
||||||
|
self.list.clearRetainingCapacity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var io: ReadableIO = undefined;
|
||||||
|
try ReadableIO.parseCLI(&io, alloc, value);
|
||||||
|
try self.list.append(alloc, io);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deep copy of the struct. Required by Config.
|
||||||
|
pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self {
|
||||||
|
var list = try std.ArrayListUnmanaged(ReadableIO).initCapacity(
|
||||||
|
alloc,
|
||||||
|
self.list.items.len,
|
||||||
|
);
|
||||||
|
for (self.list.items) |item| {
|
||||||
|
const copy = try item.clone(alloc);
|
||||||
|
list.appendAssumeCapacity(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{ .list = list };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See ReadableIO.cloneParsed
|
||||||
|
pub fn cloneParsed(
|
||||||
|
self: *const Self,
|
||||||
|
alloc: Allocator,
|
||||||
|
) Allocator.Error!Self {
|
||||||
|
var list = try std.ArrayListUnmanaged(ReadableIO).initCapacity(
|
||||||
|
alloc,
|
||||||
|
self.list.items.len,
|
||||||
|
);
|
||||||
|
for (self.list.items) |item| {
|
||||||
|
const copy = try item.cloneParsed(alloc);
|
||||||
|
list.appendAssumeCapacity(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{ .list = list };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare if two of our value are requal. Required by Config.
|
||||||
|
pub fn equal(self: Self, other: Self) bool {
|
||||||
|
const itemsA = self.list.items;
|
||||||
|
const itemsB = other.list.items;
|
||||||
|
if (itemsA.len != itemsB.len) return false;
|
||||||
|
for (itemsA, itemsB) |a, b| {
|
||||||
|
if (!a.equal(b)) return false;
|
||||||
|
} else return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used by Formatter
|
||||||
|
pub fn formatEntry(
|
||||||
|
self: Self,
|
||||||
|
formatter: anytype,
|
||||||
|
) !void {
|
||||||
|
if (self.list.items.len == 0) {
|
||||||
|
try formatter.formatEntry(void, {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (self.list.items) |value| {
|
||||||
|
try formatter.formatEntry(ReadableIO, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseCLI" {
|
||||||
|
const testing = std.testing;
|
||||||
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
|
var list: Self = .{};
|
||||||
|
try list.parseCLI(alloc, "raw:A");
|
||||||
|
try list.parseCLI(alloc, "path:B");
|
||||||
|
try testing.expectEqual(@as(usize, 2), list.list.items.len);
|
||||||
|
|
||||||
|
try list.parseCLI(alloc, "");
|
||||||
|
try testing.expectEqual(@as(usize, 0), list.list.items.len);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test {
|
||||||
|
_ = ReadableIO;
|
||||||
|
_ = RepeatableReadableIO;
|
||||||
|
}
|
@@ -3,7 +3,7 @@ const std = @import("std");
|
|||||||
/// Parse a string literal into a byte array. The string can contain
|
/// Parse a string literal into a byte array. The string can contain
|
||||||
/// any valid Zig string literal escape sequences.
|
/// any valid Zig string literal escape sequences.
|
||||||
///
|
///
|
||||||
/// The output buffer never needs sto be larger than the input buffer.
|
/// The output buffer never needs to be larger than the input buffer.
|
||||||
/// The buffers may alias.
|
/// The buffers may alias.
|
||||||
pub fn parse(out: []u8, bytes: []const u8) ![]u8 {
|
pub fn parse(out: []u8, bytes: []const u8) ![]u8 {
|
||||||
var dst_i: usize = 0;
|
var dst_i: usize = 0;
|
||||||
|
@@ -70,6 +70,89 @@ terminal_stream: terminalpkg.Stream(StreamHandler),
|
|||||||
/// flooding with cursor resets.
|
/// flooding with cursor resets.
|
||||||
last_cursor_reset: ?std.time.Instant = null,
|
last_cursor_reset: ?std.time.Instant = null,
|
||||||
|
|
||||||
|
/// State we have for thread enter. This may be null if we don't need
|
||||||
|
/// to keep track of any state or if its already been freed.
|
||||||
|
thread_enter_state: ?*ThreadEnterState = null,
|
||||||
|
|
||||||
|
/// The state we need to keep around only until we enter the IO
|
||||||
|
/// thread. Then we can throw it all away.
|
||||||
|
const ThreadEnterState = struct {
|
||||||
|
arena: ArenaAllocator,
|
||||||
|
|
||||||
|
/// Initial input to send to the subprocess after starting. This
|
||||||
|
/// memory is freed once the subprocess start is attempted, even
|
||||||
|
/// if it fails, because Exec only starts once.
|
||||||
|
input: configpkg.io.RepeatableReadableIO,
|
||||||
|
|
||||||
|
pub fn create(
|
||||||
|
alloc: Allocator,
|
||||||
|
config: *const configpkg.Config,
|
||||||
|
) !?*ThreadEnterState {
|
||||||
|
// If we have no input then we have no thread enter state
|
||||||
|
if (config.input.list.items.len == 0) return null;
|
||||||
|
|
||||||
|
// Create our arena allocator
|
||||||
|
var arena = ArenaAllocator.init(alloc);
|
||||||
|
errdefer arena.deinit();
|
||||||
|
const arena_alloc = arena.allocator();
|
||||||
|
|
||||||
|
// Allocate our ThreadEnterState
|
||||||
|
const ptr = try arena_alloc.create(ThreadEnterState);
|
||||||
|
|
||||||
|
// Copy the input from the config
|
||||||
|
const input = try config.input.cloneParsed(arena_alloc);
|
||||||
|
|
||||||
|
// Return the initialized state
|
||||||
|
ptr.* = .{
|
||||||
|
.arena = arena,
|
||||||
|
.input = input,
|
||||||
|
};
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn destroy(self: *ThreadEnterState) void {
|
||||||
|
self.arena.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare the inputs for use. Allocations happen on the arena.
|
||||||
|
pub fn prepareInput(
|
||||||
|
self: *ThreadEnterState,
|
||||||
|
) (Allocator.Error || error{InputNotFound})![]const Input {
|
||||||
|
const alloc = self.arena.allocator();
|
||||||
|
|
||||||
|
var input = try alloc.alloc(
|
||||||
|
Input,
|
||||||
|
self.input.list.items.len,
|
||||||
|
);
|
||||||
|
for (self.input.list.items, 0..) |item, i| {
|
||||||
|
input[i] = switch (item) {
|
||||||
|
.raw => |v| .{ .string = try alloc.dupe(u8, v) },
|
||||||
|
.path => |path| file: {
|
||||||
|
const f = std.fs.cwd().openFile(
|
||||||
|
path,
|
||||||
|
.{},
|
||||||
|
) catch |err| {
|
||||||
|
log.warn("failed to open input file={s} err={}", .{
|
||||||
|
path,
|
||||||
|
err,
|
||||||
|
});
|
||||||
|
return error.InputNotFound;
|
||||||
|
};
|
||||||
|
|
||||||
|
break :file .{ .file = f };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input = union(enum) {
|
||||||
|
string: []const u8,
|
||||||
|
file: std.fs.File,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/// The configuration for this IO that is derived from the main
|
/// The configuration for this IO that is derived from the main
|
||||||
/// configuration. This must be exported so that we don't need to
|
/// configuration. This must be exported so that we don't need to
|
||||||
/// pass around Config pointers which makes memory management a pain.
|
/// pass around Config pointers which makes memory management a pain.
|
||||||
@@ -211,6 +294,11 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const thread_enter_state = try ThreadEnterState.create(
|
||||||
|
alloc,
|
||||||
|
opts.full_config,
|
||||||
|
);
|
||||||
|
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.terminal = term,
|
.terminal = term,
|
||||||
@@ -232,6 +320,7 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
.thread_enter_state = thread_enter_state,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,9 +333,30 @@ pub fn deinit(self: *Termio) void {
|
|||||||
// Clear any StreamHandler state
|
// Clear any StreamHandler state
|
||||||
self.terminal_stream.handler.deinit();
|
self.terminal_stream.handler.deinit();
|
||||||
self.terminal_stream.deinit();
|
self.terminal_stream.deinit();
|
||||||
|
|
||||||
|
// Clear any initial state if we have it
|
||||||
|
if (self.thread_enter_state) |v| v.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn threadEnter(self: *Termio, thread: *termio.Thread, data: *ThreadData) !void {
|
pub fn threadEnter(
|
||||||
|
self: *Termio,
|
||||||
|
thread: *termio.Thread,
|
||||||
|
data: *ThreadData,
|
||||||
|
) !void {
|
||||||
|
// Always free our thread enter state when we're done.
|
||||||
|
defer if (self.thread_enter_state) |v| {
|
||||||
|
v.destroy();
|
||||||
|
self.thread_enter_state = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we have thread enter state then we're going to validate
|
||||||
|
// and set that all up now so that we can error before we actually
|
||||||
|
// start the command and pty.
|
||||||
|
const inputs: ?[]const ThreadEnterState.Input = if (self.thread_enter_state) |v|
|
||||||
|
try v.prepareInput()
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
|
||||||
data.* = .{
|
data.* = .{
|
||||||
.alloc = self.alloc,
|
.alloc = self.alloc,
|
||||||
.loop = &thread.loop,
|
.loop = &thread.loop,
|
||||||
@@ -258,6 +368,29 @@ pub fn threadEnter(self: *Termio, thread: *termio.Thread, data: *ThreadData) !vo
|
|||||||
|
|
||||||
// Setup our backend
|
// Setup our backend
|
||||||
try self.backend.threadEnter(self.alloc, self, data);
|
try self.backend.threadEnter(self.alloc, self, data);
|
||||||
|
errdefer self.backend.threadExit(data);
|
||||||
|
|
||||||
|
// If we have inputs, then queue them all up.
|
||||||
|
for (inputs orelse &.{}) |input| switch (input) {
|
||||||
|
.string => |v| self.queueWrite(data, v, false) catch |err| {
|
||||||
|
log.warn("failed to queue input string err={}", .{err});
|
||||||
|
return error.InputFailed;
|
||||||
|
},
|
||||||
|
.file => |f| self.queueWrite(
|
||||||
|
data,
|
||||||
|
f.readToEndAlloc(
|
||||||
|
self.alloc,
|
||||||
|
10 * 1024 * 1024, // 10 MiB max
|
||||||
|
) catch |err| {
|
||||||
|
log.warn("failed to read input file err={}", .{err});
|
||||||
|
return error.InputFailed;
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
) catch |err| {
|
||||||
|
log.warn("failed to queue input file err={}", .{err});
|
||||||
|
return error.InputFailed;
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn threadExit(self: *Termio, data: *ThreadData) void {
|
pub fn threadExit(self: *Termio, data: *ThreadData) void {
|
||||||
|
@@ -146,6 +146,8 @@ pub fn threadMain(self: *Thread, io: *termio.Termio) void {
|
|||||||
// have "OpenptyFailed".
|
// have "OpenptyFailed".
|
||||||
const Err = @TypeOf(err) || error{
|
const Err = @TypeOf(err) || error{
|
||||||
OpenptyFailed,
|
OpenptyFailed,
|
||||||
|
InputNotFound,
|
||||||
|
InputFailed,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (@as(Err, @errorCast(err))) {
|
switch (@as(Err, @errorCast(err))) {
|
||||||
@@ -165,6 +167,24 @@ pub fn threadMain(self: *Thread, io: *termio.Termio) void {
|
|||||||
t.printString(str) catch {};
|
t.printString(str) catch {};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
error.InputNotFound,
|
||||||
|
error.InputFailed,
|
||||||
|
=> {
|
||||||
|
const str =
|
||||||
|
\\A configured `input` path was not found, was not readable,
|
||||||
|
\\was too large, or the underlying pty failed to accept
|
||||||
|
\\the write.
|
||||||
|
\\
|
||||||
|
\\Ghostty can't continue since it can't guarantee that
|
||||||
|
\\initial terminal state will be as desired. Please review
|
||||||
|
\\the value of `input` in your configuration file and
|
||||||
|
\\ensure that all the path values exist and are readable.
|
||||||
|
;
|
||||||
|
|
||||||
|
t.eraseDisplay(.complete, false);
|
||||||
|
t.printString(str) catch {};
|
||||||
|
},
|
||||||
|
|
||||||
else => {
|
else => {
|
||||||
const str = std.fmt.allocPrint(
|
const str = std.fmt.allocPrint(
|
||||||
alloc,
|
alloc,
|
||||||
|
Reference in New Issue
Block a user