diff --git a/src/cli/Pager.zig b/src/cli/Pager.zig new file mode 100644 index 000000000..73b8e1f26 --- /dev/null +++ b/src/cli/Pager.zig @@ -0,0 +1,89 @@ +//! A pager wraps output to an external pager program (like `less`) when +//! stdout is a TTY. The pager command is determined by `$PAGER`, falling +//! back to `less` if `$PAGER` isn't set. +//! +//! If stdout is not a TTY, writes go directly to stdout. +const Pager = @This(); +const std = @import("std"); +const Allocator = std.mem.Allocator; +const internal_os = @import("../os/main.zig"); + +/// The pager child process, if one was spawned. +child: ?std.process.Child = null, + +/// The buffered file writer used for both the pager pipe and direct +/// stdout paths. +file_writer: std.fs.File.Writer = undefined, + +/// The write buffer. +buffer: [4096]u8 = undefined, + +/// Initialize the pager. If stdout is a TTY, this spawns the pager +/// process. Otherwise, output goes directly to stdout. +pub fn init(alloc: Allocator) Pager { + return .{ .child = initPager(alloc) }; +} + +/// Writes to the pager process if available; otherwise, stdout. +pub fn writer(self: *Pager) *std.Io.Writer { + if (self.child) |child| { + self.file_writer = child.stdin.?.writer(&self.buffer); + } else { + self.file_writer = std.fs.File.stdout().writer(&self.buffer); + } + return &self.file_writer.interface; +} + +/// Deinitialize the pager. Waits for the spawned process to exit. +pub fn deinit(self: *Pager) void { + if (self.child) |*child| { + // Flush any remaining buffered data, close the pipe so the + // pager sees EOF, then wait for it to exit. + self.file_writer.interface.flush() catch {}; + if (child.stdin) |stdin| { + stdin.close(); + child.stdin = null; + } + _ = child.wait() catch {}; + } + + self.* = undefined; +} + +fn initPager(alloc: Allocator) ?std.process.Child { + const stdout_file: std.fs.File = .stdout(); + if (!stdout_file.isTty()) return null; + + // Resolve the pager command: $PAGER > "less". + // An empty $PAGER disables paging. + const env_result = internal_os.getenv(alloc, "PAGER") catch null; + const cmd: ?[]const u8 = cmd: { + const r = env_result orelse break :cmd "less"; + break :cmd if (r.value.len > 0) r.value else null; + }; + defer if (env_result) |r| r.deinit(alloc); + + if (cmd == null) return null; + + var child: std.process.Child = .init(&.{cmd.?}, alloc); + child.stdin_behavior = .Pipe; + child.stdout_behavior = .Inherit; + child.stderr_behavior = .Inherit; + + child.spawn() catch return null; + return child; +} + +test "pager: non-tty" { + var pager: Pager = .init(std.testing.allocator); + defer pager.deinit(); + try std.testing.expect(pager.child == null); +} + +test "pager: default writer" { + var pager: Pager = .{}; + defer pager.deinit(); + try std.testing.expect(pager.child == null); + const w = pager.writer(); + try w.writeAll("hello"); +} diff --git a/src/cli/explain_config.zig b/src/cli/explain_config.zig index 32a9e9ceb..d1add0765 100644 --- a/src/cli/explain_config.zig +++ b/src/cli/explain_config.zig @@ -6,6 +6,7 @@ const help_strings = @import("help_strings"); const Config = @import("../config/Config.zig"); const ConfigKey = @import("../config/key.zig").Key; const KeybindAction = @import("../input/Binding.zig").Action; +const Pager = @import("Pager.zig"); pub const Options = struct { /// The config option to explain. For example: @@ -43,10 +44,13 @@ pub const Options = struct { /// /// * `--option`: The name of the configuration option to explain. /// * `--keybind`: The name of the keybind action to explain. +/// * `--no-pager`: Disable automatic paging of output. pub fn run(alloc: Allocator) !u8 { var option_name: ?[]const u8 = null; var keybind_name: ?[]const u8 = null; var positional: ?[]const u8 = null; + var no_pager: bool = false; + var iter = try args.argsIterator(alloc); defer iter.deinit(); defer if (option_name) |s| alloc.free(s); @@ -58,6 +62,8 @@ pub fn run(alloc: Allocator) !u8 { option_name = try alloc.dupe(u8, arg["--option=".len..]); } else if (std.mem.startsWith(u8, arg, "--keybind=")) { keybind_name = try alloc.dupe(u8, arg["--keybind=".len..]); + } else if (std.mem.eql(u8, arg, "--no-pager")) { + no_pager = true; } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { return Action.help_error; } else if (!std.mem.startsWith(u8, arg, "-")) { @@ -86,10 +92,9 @@ pub fn run(alloc: Allocator) !u8 { else explainOption(name) orelse explainKeybind(name); - var stdout: std.fs.File = .stdout(); - var buffer: [4096]u8 = undefined; - var stdout_writer = stdout.writer(&buffer); - const writer = &stdout_writer.interface; + var pager: Pager = if (!no_pager) .init(alloc) else .{}; + defer pager.deinit(); + const writer = pager.writer(); if (text) |t| { try writer.writeAll(t); @@ -98,11 +103,11 @@ pub fn run(alloc: Allocator) !u8 { try writer.writeAll("Unknown: '"); try writer.writeAll(name); try writer.writeAll("'.\n"); - try stdout_writer.end(); + try writer.flush(); return 1; } - try stdout_writer.end(); + try writer.flush(); return 0; }