diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/cli/ssh_cache.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/cli/ssh_cache.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/cli/ssh_cache.zig | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/cli/ssh_cache.zig b/vendor/libghostty-vt/src/cli/ssh_cache.zig new file mode 100644 index 00000000..d3ee658a --- /dev/null +++ b/vendor/libghostty-vt/src/cli/ssh_cache.zig @@ -0,0 +1,228 @@ +const std = @import("std"); +const fs = std.fs; +const Allocator = std.mem.Allocator; +const args = @import("args.zig"); +const Action = @import("ghostty.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 period +pub 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); + } + + var stdout_buffer: [1024]u8 = undefined; + var stdout_file: std.fs.File = .stdout(); + var stdout_writer = stdout_file.writer(&stdout_buffer); + const stdout = &stdout_writer.interface; + + var stderr_buffer: [1024]u8 = undefined; + var stderr_file: std.fs.File = .stderr(); + var stderr_writer = stderr_file.writer(&stderr_buffer); + const stderr = &stderr_writer.interface; + + const result = runInner(alloc, opts, stdout, stderr); + + // Flushing *shouldn't* fail but... + stdout.flush() catch {}; + stderr.flush() catch {}; + return result; +} + +pub fn runInner( + alloc: Allocator, + opts: Options, + stdout: *std.Io.Writer, + stderr: *std.Io.Writer, +) !u8 { + // 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: *std.Io.Writer, +) !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) = .empty; + defer items.deinit(alloc); + + var iter = entries.iterator(); + while (iter.next()) |kv| { + try items.append(alloc, 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; +} |
