mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-18 21:40:29 +00:00
Merge branch 'ghostty-org:main' into feat-list-themes-write-config
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const diags = @import("diagnostics.zig");
|
||||
@@ -604,7 +604,7 @@ pub fn parseAutoStruct(
|
||||
return result;
|
||||
}
|
||||
|
||||
fn parsePackedStruct(comptime T: type, v: []const u8) !T {
|
||||
pub fn parsePackedStruct(comptime T: type, v: []const u8) !T {
|
||||
const info = @typeInfo(T).@"struct";
|
||||
comptime assert(info.layout == .@"packed");
|
||||
|
||||
@@ -1427,7 +1427,12 @@ pub const LineIterator = struct {
|
||||
//
|
||||
// This will also optimize reads down the line as we're
|
||||
// more likely to beworking with buffered data.
|
||||
self.r.fillMore() catch {};
|
||||
//
|
||||
// fillMore asserts that the buffer has available capacity,
|
||||
// so skip this if it's full.
|
||||
if (self.r.bufferedLen() < self.r.buffer.len) {
|
||||
self.r.fillMore() catch {};
|
||||
}
|
||||
|
||||
var writer: std.Io.Writer = .fixed(self.entry[2..]);
|
||||
|
||||
@@ -1590,3 +1595,33 @@ test "LineIterator with CRLF line endings" {
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
}
|
||||
|
||||
test "LineIterator with buffered reader" {
|
||||
const testing = std.testing;
|
||||
var f: std.Io.Reader = .fixed("A\nB = C\n");
|
||||
var buf: [2]u8 = undefined;
|
||||
var r = f.limited(.unlimited, &buf);
|
||||
const reader = &r.interface;
|
||||
|
||||
var iter: LineIterator = .init(reader);
|
||||
try testing.expectEqualStrings("--A", iter.next().?);
|
||||
try testing.expectEqualStrings("--B=C", iter.next().?);
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
}
|
||||
|
||||
test "LineIterator with buffered and primed reader" {
|
||||
const testing = std.testing;
|
||||
var f: std.Io.Reader = .fixed("A\nB = C\n");
|
||||
var buf: [2]u8 = undefined;
|
||||
var r = f.limited(.unlimited, &buf);
|
||||
const reader = &r.interface;
|
||||
|
||||
try reader.fill(buf.len);
|
||||
|
||||
var iter: LineIterator = .init(reader);
|
||||
try testing.expectEqualStrings("--A", iter.next().?);
|
||||
try testing.expectEqualStrings("--B=C", iter.next().?);
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@ const builtin = @import("builtin");
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const help_strings = @import("help_strings");
|
||||
const vaxis = @import("vaxis");
|
||||
|
||||
const framedata = @embedFile("framedata");
|
||||
const framedata = @import("framedata").compressed;
|
||||
|
||||
const vxfw = vaxis.vxfw;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_config = @import("../build_config.zig");
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const args = @import("args.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
@@ -30,9 +30,9 @@ pub const Options = struct {
|
||||
/// this yet.
|
||||
///
|
||||
/// The filepath opened is the default user-specific configuration
|
||||
/// file, which is typically located at `$XDG_CONFIG_HOME/ghostty/config`.
|
||||
/// file, which is typically located at `$XDG_CONFIG_HOME/ghostty/config.ghostty`.
|
||||
/// On macOS, this may also be located at
|
||||
/// `~/Library/Application Support/com.mitchellh.ghostty/config`.
|
||||
/// `~/Library/Application Support/com.mitchellh.ghostty/config.ghostty`.
|
||||
/// On macOS, whichever path exists and is non-empty will be prioritized,
|
||||
/// prioritizing the Application Support directory if neither are
|
||||
/// non-empty.
|
||||
@@ -73,7 +73,7 @@ fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 {
|
||||
defer config.deinit();
|
||||
|
||||
// Find the preferred path.
|
||||
const path = try Config.preferredDefaultFilePath(alloc);
|
||||
const path = try configpkg.preferredDefaultFilePath(alloc);
|
||||
defer alloc.free(path);
|
||||
|
||||
// We don't currently support Windows because we use the exec syscall.
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
const std = @import("std");
|
||||
const inputpkg = @import("../input.zig");
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Config = @import("../config/Config.zig");
|
||||
const themepkg = @import("../config/theme.zig");
|
||||
const tui = @import("tui.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const global_state = &@import("../global.zig").state;
|
||||
|
||||
const vaxis = @import("vaxis");
|
||||
@@ -180,7 +178,13 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var theme_config = try Config.default(gpa_alloc);
|
||||
defer theme_config.deinit();
|
||||
for (themes.items) |theme| {
|
||||
try theme_config.loadFile(theme_config._arena.?.allocator(), theme.path);
|
||||
if (!shouldIncludeTheme(opts.color, theme_config)) {
|
||||
continue;
|
||||
}
|
||||
if (opts.path)
|
||||
try stdout.print("{s} ({t}) {s}\n", .{ theme.theme, theme.location, theme.path })
|
||||
else
|
||||
@@ -266,7 +270,7 @@ const Preview = struct {
|
||||
.hex = false,
|
||||
.mode = .normal,
|
||||
.color_scheme = .light,
|
||||
.text_input = .init(allocator, &self.vx.unicode),
|
||||
.text_input = .init(allocator),
|
||||
.theme_filter = theme_filter,
|
||||
};
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ const DiskCache = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const xdg = @import("../../os/main.zig").xdg;
|
||||
const TempDir = @import("../../os/main.zig").TempDir;
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
const xdg = internal_os.xdg;
|
||||
const TempDir = internal_os.TempDir;
|
||||
const Entry = @import("Entry.zig");
|
||||
|
||||
// 512KB - sufficient for approximately 10k entries
|
||||
@@ -69,7 +70,7 @@ pub fn add(
|
||||
|
||||
// Create cache directory if needed
|
||||
if (std.fs.path.dirname(self.path)) |dir| {
|
||||
std.fs.makeDirAbsolute(dir) catch |err| switch (err) {
|
||||
std.fs.cwd().makePath(dir) catch |err| switch (err) {
|
||||
error.PathAlreadyExists => {},
|
||||
else => return err,
|
||||
};
|
||||
@@ -180,13 +181,12 @@ pub fn contains(
|
||||
// Open our file
|
||||
const file = std.fs.openFileAbsolute(
|
||||
self.path,
|
||||
.{ .mode = .read_write },
|
||||
.{},
|
||||
) catch |err| switch (err) {
|
||||
error.FileNotFound => return false,
|
||||
else => return err,
|
||||
};
|
||||
defer file.close();
|
||||
try fixupPermissions(file);
|
||||
|
||||
// Read existing entries
|
||||
var entries = try readEntries(alloc, file);
|
||||
@@ -327,55 +327,34 @@ fn readEntries(
|
||||
|
||||
// Supports both standalone hostnames and user@hostname format
|
||||
fn isValidCacheKey(key: []const u8) bool {
|
||||
// 253 + 1 + 64 for user@hostname
|
||||
if (key.len == 0 or key.len > 320) return false;
|
||||
if (key.len == 0) return false;
|
||||
|
||||
// Check for user@hostname format
|
||||
if (std.mem.indexOf(u8, key, "@")) |at_pos| {
|
||||
if (std.mem.indexOfScalar(u8, key, '@')) |at_pos| {
|
||||
const user = key[0..at_pos];
|
||||
const hostname = key[at_pos + 1 ..];
|
||||
return isValidUser(user) and isValidHostname(hostname);
|
||||
return isValidUser(user) and isValidHost(hostname);
|
||||
}
|
||||
|
||||
return isValidHostname(key);
|
||||
return isValidHost(key);
|
||||
}
|
||||
|
||||
// Basic hostname validation - accepts domains and IPs
|
||||
// (including IPv6 in brackets)
|
||||
fn isValidHostname(host: []const u8) bool {
|
||||
if (host.len == 0 or host.len > 253) return false;
|
||||
|
||||
// Handle IPv6 addresses in brackets
|
||||
if (host.len >= 4 and host[0] == '[' and host[host.len - 1] == ']') {
|
||||
const ipv6_part = host[1 .. host.len - 1];
|
||||
if (ipv6_part.len == 0) return false;
|
||||
var has_colon = false;
|
||||
for (ipv6_part) |c| {
|
||||
switch (c) {
|
||||
'a'...'f', 'A'...'F', '0'...'9' => {},
|
||||
':' => has_colon = true,
|
||||
else => return false,
|
||||
}
|
||||
}
|
||||
return has_colon;
|
||||
// Checks if a host is a valid hostname or IP address
|
||||
fn isValidHost(host: []const u8) bool {
|
||||
// First check for valid hostnames because this is assumed to be the more
|
||||
// likely ssh host format.
|
||||
if (internal_os.hostname.isValid(host)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Standard hostname/domain validation
|
||||
for (host) |c| {
|
||||
switch (c) {
|
||||
'a'...'z', 'A'...'Z', '0'...'9', '.', '-' => {},
|
||||
else => return false,
|
||||
}
|
||||
}
|
||||
|
||||
// No leading/trailing dots or hyphens, no consecutive dots
|
||||
if (host[0] == '.' or host[0] == '-' or
|
||||
host[host.len - 1] == '.' or host[host.len - 1] == '-')
|
||||
{
|
||||
// We also accept valid IP addresses. In practice, IPv4 addresses are also
|
||||
// considered valid hostnames due to their overlapping syntax, so we can
|
||||
// simplify this check to be IPv6-specific.
|
||||
if (std.net.Address.parseIp6(host, 0)) |_| {
|
||||
return true;
|
||||
} else |_| {
|
||||
return false;
|
||||
}
|
||||
|
||||
return std.mem.indexOf(u8, host, "..") == null;
|
||||
}
|
||||
|
||||
fn isValidUser(user: []const u8) bool {
|
||||
@@ -474,98 +453,73 @@ test "disk cache operations" {
|
||||
);
|
||||
}
|
||||
|
||||
// Tests
|
||||
test "hostname validation - valid cases" {
|
||||
test isValidHost {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isValidHostname("example.com"));
|
||||
try testing.expect(isValidHostname("sub.example.com"));
|
||||
try testing.expect(isValidHostname("host-name.domain.org"));
|
||||
try testing.expect(isValidHostname("192.168.1.1"));
|
||||
try testing.expect(isValidHostname("a"));
|
||||
try testing.expect(isValidHostname("1"));
|
||||
|
||||
// Valid hostnames
|
||||
try testing.expect(isValidHost("localhost"));
|
||||
try testing.expect(isValidHost("example.com"));
|
||||
try testing.expect(isValidHost("sub.example.com"));
|
||||
|
||||
// IPv4 addresses
|
||||
try testing.expect(isValidHost("127.0.0.1"));
|
||||
try testing.expect(isValidHost("192.168.1.1"));
|
||||
|
||||
// IPv6 addresses
|
||||
try testing.expect(isValidHost("::1"));
|
||||
try testing.expect(isValidHost("2001:db8::1"));
|
||||
try testing.expect(isValidHost("2001:db8:0:1:1:1:1:1"));
|
||||
try testing.expect(!isValidHost("fe80::1%eth0")); // scopes not supported
|
||||
|
||||
// Invalid hosts
|
||||
try testing.expect(!isValidHost(""));
|
||||
try testing.expect(!isValidHost("host\nname"));
|
||||
try testing.expect(!isValidHost(".example.com"));
|
||||
try testing.expect(!isValidHost("host..domain"));
|
||||
try testing.expect(!isValidHost("-hostname"));
|
||||
try testing.expect(!isValidHost("hostname-"));
|
||||
try testing.expect(!isValidHost("host name"));
|
||||
try testing.expect(!isValidHost("host_name"));
|
||||
try testing.expect(!isValidHost("host@domain"));
|
||||
try testing.expect(!isValidHost("host:port"));
|
||||
}
|
||||
|
||||
test "hostname validation - IPv6 addresses" {
|
||||
test isValidUser {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isValidHostname("[::1]"));
|
||||
try testing.expect(isValidHostname("[2001:db8::1]"));
|
||||
try testing.expect(!isValidHostname("[fe80::1%eth0]")); // Interface notation not supported
|
||||
try testing.expect(!isValidHostname("[]")); // Empty IPv6
|
||||
try testing.expect(!isValidHostname("[invalid]")); // No colons
|
||||
}
|
||||
|
||||
test "hostname validation - invalid cases" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(!isValidHostname(""));
|
||||
try testing.expect(!isValidHostname("host\nname"));
|
||||
try testing.expect(!isValidHostname(".example.com"));
|
||||
try testing.expect(!isValidHostname("example.com."));
|
||||
try testing.expect(!isValidHostname("host..domain"));
|
||||
try testing.expect(!isValidHostname("-hostname"));
|
||||
try testing.expect(!isValidHostname("hostname-"));
|
||||
try testing.expect(!isValidHostname("host name"));
|
||||
try testing.expect(!isValidHostname("host_name"));
|
||||
try testing.expect(!isValidHostname("host@domain"));
|
||||
try testing.expect(!isValidHostname("host:port"));
|
||||
|
||||
// Too long
|
||||
const long_host = "a" ** 254;
|
||||
try testing.expect(!isValidHostname(long_host));
|
||||
}
|
||||
|
||||
test "user validation - valid cases" {
|
||||
const testing = std.testing;
|
||||
// Valid
|
||||
try testing.expect(isValidUser("user"));
|
||||
try testing.expect(isValidUser("deploy"));
|
||||
try testing.expect(isValidUser("test-user"));
|
||||
try testing.expect(isValidUser("user-user"));
|
||||
try testing.expect(isValidUser("user_name"));
|
||||
try testing.expect(isValidUser("user.name"));
|
||||
try testing.expect(isValidUser("user123"));
|
||||
try testing.expect(isValidUser("a"));
|
||||
}
|
||||
|
||||
test "user validation - complex realistic cases" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isValidUser("git"));
|
||||
try testing.expect(isValidUser("ubuntu"));
|
||||
try testing.expect(isValidUser("root"));
|
||||
try testing.expect(isValidUser("service.account"));
|
||||
try testing.expect(isValidUser("user-with-dashes"));
|
||||
}
|
||||
|
||||
test "user validation - invalid cases" {
|
||||
const testing = std.testing;
|
||||
// Invalid
|
||||
try testing.expect(!isValidUser(""));
|
||||
try testing.expect(!isValidUser("user name"));
|
||||
try testing.expect(!isValidUser("user@domain"));
|
||||
try testing.expect(!isValidUser("user@example"));
|
||||
try testing.expect(!isValidUser("user:group"));
|
||||
try testing.expect(!isValidUser("user\nname"));
|
||||
|
||||
// Too long
|
||||
const long_user = "a" ** 65;
|
||||
try testing.expect(!isValidUser(long_user));
|
||||
try testing.expect(!isValidUser("a" ** 65)); // too long
|
||||
}
|
||||
|
||||
test "cache key validation - hostname format" {
|
||||
test isValidCacheKey {
|
||||
const testing = std.testing;
|
||||
|
||||
// Valid
|
||||
try testing.expect(isValidCacheKey("example.com"));
|
||||
try testing.expect(isValidCacheKey("sub.example.com"));
|
||||
try testing.expect(isValidCacheKey("192.168.1.1"));
|
||||
try testing.expect(isValidCacheKey("[::1]"));
|
||||
try testing.expect(!isValidCacheKey(""));
|
||||
try testing.expect(!isValidCacheKey(".invalid.com"));
|
||||
}
|
||||
|
||||
test "cache key validation - user@hostname format" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isValidCacheKey("::1"));
|
||||
try testing.expect(isValidCacheKey("user@example.com"));
|
||||
try testing.expect(isValidCacheKey("deploy@prod.server.com"));
|
||||
try testing.expect(isValidCacheKey("test-user@192.168.1.1"));
|
||||
try testing.expect(isValidCacheKey("user_name@host.domain.org"));
|
||||
try testing.expect(isValidCacheKey("git@github.com"));
|
||||
try testing.expect(isValidCacheKey("ubuntu@[::1]"));
|
||||
try testing.expect(isValidCacheKey("user@192.168.1.1"));
|
||||
try testing.expect(isValidCacheKey("user@::1"));
|
||||
|
||||
// Invalid
|
||||
try testing.expect(!isValidCacheKey(""));
|
||||
try testing.expect(!isValidCacheKey(".example.com"));
|
||||
try testing.expect(!isValidCacheKey("@example.com"));
|
||||
try testing.expect(!isValidCacheKey("user@"));
|
||||
try testing.expect(!isValidCacheKey("user@@host"));
|
||||
try testing.expect(!isValidCacheKey("user@.invalid.com"));
|
||||
try testing.expect(!isValidCacheKey("user@@example"));
|
||||
try testing.expect(!isValidCacheKey("user@.example.com"));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const std = @import("std");
|
||||
const fs = std.fs;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const xdg = @import("../os/xdg.zig");
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
pub const Entry = @import("ssh-cache/Entry.zig");
|
||||
|
||||
@@ -3,7 +3,6 @@ const Allocator = std.mem.Allocator;
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Config = @import("../config.zig").Config;
|
||||
const cli = @import("../cli.zig");
|
||||
|
||||
pub const Options = struct {
|
||||
/// The path of the config file to validate. If this isn't specified,
|
||||
|
||||
Reference in New Issue
Block a user