aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/cli/ssh_cache.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/cli/ssh_cache.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (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.zig228
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;
+}