This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209const 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("action.zig").Action;pub const Entry = @import("ssh-cache/Entry.zig");pub const DiskCache = @import("ssh-cache/DiskCache.zig");
pub const Options = struct { clear: bool = false, add: ?[]const u8 = null, remove: ?[]const u8 = null, host: ?[]const u8 = null, @"expire-days": ?u32 = null,
pub fn deinit(self: *Options) void { _ = self; }
pub fn help(self: Options) !void { _ = self; return Action.help_error; }};
/// Manage the SSH terminfo cache for automatic remote host setup.////// When SSH integration is enabled with `shell-integration-features = ssh-terminfo`,/// Ghostty automatically installs its terminfo on remote hosts. This command/// manages the cache of successful installations to avoid redundant uploads.////// The cache stores hostnames (or user@hostname combinations) along with timestamps./// Entries older than the expiration period are automatically removed during cache/// operations. By default, entries never expire.////// Only one of `--clear`, `--add`, `--remove`, or `--host` can be specified./// If multiple are specified, one of the actions will be executed but/// it isn't guaranteed which one. This is entirely unsafe so you should split/// multiple actions into separate commands.////// Examples:/// ghostty +ssh-cache # List all cached hosts/// ghostty +ssh-cache --host=example.com # Check if host is cached/// ghostty +ssh-cache --add=example.com # Manually add host to cache/// ghostty +ssh-cache --add=user@example.com # Add user@host combination/// ghostty +ssh-cache --remove=example.com # Remove host from cache/// ghostty +ssh-cache --clear # Clear entire cache/// ghostty +ssh-cache --expire-days=30 # Set custom expiration periodpub fn run(alloc_gpa: Allocator) !u8 { var arena = std.heap.ArenaAllocator.init(alloc_gpa); defer arena.deinit(); const alloc = arena.allocator();
var opts: Options = .{}; defer opts.deinit();
{ var iter = try args.argsIterator(alloc_gpa); defer iter.deinit(); try args.parse(Options, alloc_gpa, &opts, &iter); }
const stdout = std.io.getStdOut().writer(); const stderr = std.io.getStdErr().writer();
// Setup our disk cache to the standard location const cache_path = try DiskCache.defaultPath(alloc, "ghostty"); const cache: DiskCache = .{ .path = cache_path };
if (opts.clear) { try cache.clear(); try stdout.print("Cache cleared.\n", .{}); return 0; }
if (opts.add) |host| { const result = cache.add(alloc, host) catch |err| switch (err) { DiskCache.Error.HostnameIsInvalid => { try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); try stderr.print("Expected format: hostname or user@hostname\n", .{}); return 1; }, DiskCache.Error.CacheIsLocked => { try stderr.print("Error: Cache is busy, try again\n", .{}); return 1; }, else => { try stderr.print( "Error: Unable to add '{s}' to cache. Error: {}\n", .{ host, err }, ); return 1; }, };
switch (result) { .added => try stdout.print("Added '{s}' to cache.\n", .{host}), .updated => try stdout.print("Updated '{s}' cache entry.\n", .{host}), } return 0; }
if (opts.remove) |host| { cache.remove(alloc, host) catch |err| switch (err) { DiskCache.Error.HostnameIsInvalid => { try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); try stderr.print("Expected format: hostname or user@hostname\n", .{}); return 1; }, DiskCache.Error.CacheIsLocked => { try stderr.print("Error: Cache is busy, try again\n", .{}); return 1; }, else => { try stderr.print( "Error: Unable to remove '{s}' from cache. Error: {}\n", .{ host, err }, ); return 1; }, }; try stdout.print("Removed '{s}' from cache.\n", .{host}); return 0; }
if (opts.host) |host| { const cached = cache.contains(alloc, host) catch |err| switch (err) { error.HostnameIsInvalid => { try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); try stderr.print("Expected format: hostname or user@hostname\n", .{}); return 1; }, else => { try stderr.print( "Error: Unable to check host '{s}' in cache. Error: {}\n", .{ host, err }, ); return 1; }, };
if (cached) { try stdout.print( "'{s}' has Ghostty terminfo installed.\n", .{host}, ); return 0; } else { try stdout.print( "'{s}' does not have Ghostty terminfo installed.\n", .{host}, ); return 1; } }
// Default action: list all hosts var entries = try cache.list(alloc); defer DiskCache.deinitEntries(alloc, &entries); try listEntries(alloc, &entries, stdout); return 0;}
fn listEntries( alloc: Allocator, entries: *const std.StringHashMap(Entry), writer: anytype,) !void { if (entries.count() == 0) { try writer.print("No hosts in cache.\n", .{}); return; }
// Sort entries by hostname for consistent output var items = std.ArrayList(Entry).init(alloc); defer items.deinit();
var iter = entries.iterator(); while (iter.next()) |kv| { try items.append(kv.value_ptr.*); }
std.mem.sort(Entry, items.items, {}, struct { fn lessThan(_: void, a: Entry, b: Entry) bool { return std.mem.lessThan(u8, a.hostname, b.hostname); } }.lessThan);
try writer.print("Cached hosts ({d}):\n", .{items.items.len}); const now = std.time.timestamp();
for (items.items) |entry| { const age_days = @divTrunc(now - entry.timestamp, std.time.s_per_day); if (age_days == 0) { try writer.print(" {s} (today)\n", .{entry.hostname}); } else if (age_days == 1) { try writer.print(" {s} (yesterday)\n", .{entry.hostname}); } else { try writer.print(" {s} ({d} days ago)\n", .{ entry.hostname, age_days }); } }}
test { _ = DiskCache; _ = Entry;}