lib/sys/src/terminal.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const builtin = @import("builtin");
3
4 const capabilities = @import("capabilities.zig");
5 const env = @import("env.zig");
6 const fd = @import("fd.zig");
7 const posix = std.posix;
8
9 pub const required_capabilities = capabilities.host(&.{ .descriptors, .environment, .terminal });
10
11 pub const Size = struct {
12 rows: u16,
13 cols: u16,
14 x_pixel: u16,
15 y_pixel: u16,
16 };
17
18 pub const Attributes = posix.termios;
19 pub const AttributeAction = posix.TCSA;
20 pub const AttributeQueryError = posix.TermiosGetError || error{UnsupportedPlatform};
21 pub const AttributeApplyError = posix.TermiosSetError || error{UnsupportedPlatform};
22 pub const SizeError = error{ UnsupportedPlatform, QueryFailed, ApplyFailed };
23
24 pub fn querySize(file: std.Io.File) ?Size {
25 return queryDescriptorSize(file.handle);
26 }
27
28 pub fn isTty(file: std.Io.File) bool {
29 if (file.isTty(std.Options.debug_io) catch false) return true;
30 return isDescriptorTty(file.handle);
31 }
32
33 pub fn isDescriptorTty(descriptor: fd.Descriptor) bool {
34 if (!supportsPosixTerminalIo()) return false;
35 _ = posix.tcgetattr(descriptor) catch return false;
36 return true;
37 }
38
39 pub fn queryDescriptorSize(descriptor: fd.Descriptor) ?Size {
40 if (!supportsPosixTerminalIo()) return null;
41
42 var winsize: posix.winsize = .{
43 .row = 0,
44 .col = 0,
45 .xpixel = 0,
46 .ypixel = 0,
47 };
48 const err = posix.system.ioctl(descriptor, posix.T.IOCGWINSZ, @intFromPtr(&winsize));
49 if (posix.errno(err) != .SUCCESS) return null;
50
51 return .{
52 .rows = winsize.row,
53 .cols = winsize.col,
54 .x_pixel = winsize.xpixel,
55 .y_pixel = winsize.ypixel,
56 };
57 }
58
59 pub fn setDescriptorSize(descriptor: fd.Descriptor, size: Size) SizeError!void {
60 if (!supportsPosixTerminalIo()) return error.UnsupportedPlatform;
61 var winsize = posix.winsize{
62 .row = size.rows,
63 .col = size.cols,
64 .xpixel = size.x_pixel,
65 .ypixel = size.y_pixel,
66 };
67 const result = posix.system.ioctl(
68 descriptor,
69 posix.T.IOCSWINSZ,
70 @intFromPtr(&winsize),
71 );
72 if (posix.errno(result) != .SUCCESS) return error.ApplyFailed;
73 }
74
75 pub fn getAttributes(descriptor: fd.Descriptor) AttributeQueryError!Attributes {
76 if (!supportsPosixTerminalIo()) return error.UnsupportedPlatform;
77 return posix.tcgetattr(descriptor);
78 }
79
80 pub fn setAttributes(
81 descriptor: fd.Descriptor,
82 action: AttributeAction,
83 attributes: Attributes,
84 ) AttributeApplyError!void {
85 if (!supportsPosixTerminalIo()) return error.UnsupportedPlatform;
86 return posix.tcsetattr(descriptor, action, attributes);
87 }
88
89 pub fn setReadTiming(
90 attributes: *Attributes,
91 minimum_bytes: u8,
92 timeout_deciseconds: u8,
93 ) void {
94 attributes.cc[@backingInt(posix.V.MIN)] = minimum_bytes;
95 attributes.cc[@backingInt(posix.V.TIME)] = timeout_deciseconds;
96 }
97
98 pub const KeyboardInterruptError = error{
99 UnsupportedPlatform,
100 QueryFailed,
101 ApplyFailed,
102 };
103
104 pub fn enableKeyboardInterrupt(descriptor: fd.Descriptor) KeyboardInterruptError!void {
105 if (!supportsPosixTerminalIo()) return error.UnsupportedPlatform;
106
107 var raw = posix.tcgetattr(descriptor) catch return error.QueryFailed;
108 raw.lflag.ISIG = true;
109 posix.tcsetattr(descriptor, .FLUSH, raw) catch return error.ApplyFailed;
110 }
111
112 pub fn columnsFromEnvironment() ?usize {
113 if (!supportsPosixTerminalEnvironment()) return null;
114 const text = env.getConstant("COLUMNS") orelse return null;
115 return std.fmt.parseInt(usize, text, 10) catch null;
116 }
117
118 pub fn capabilityEnvironmentValue(comptime key: []const u8) ?[]const u8 {
119 if (!supportsPosixTerminalEnvironment()) return null;
120 return env.getConstant(key);
121 }
122
123 fn supportsPosixTerminalIo() bool {
124 return switch (builtin.os.tag) {
125 .windows, .wasi => false,
126 else => true,
127 };
128 }
129
130 fn supportsPosixTerminalEnvironment() bool {
131 return switch (builtin.os.tag) {
132 .windows, .wasi => false,
133 else => true,
134 };
135 }
136
137 test "columnsFromEnvironment ignores missing or unparsable values" {
138 if (env.getConstant("TINY_SYS_TERMINAL_TEST_COLUMNS")) |_| {
139 return error.SkipZigTest;
140 }
141 try std.testing.expect(columnsFromEnvironment() == null or columnsFromEnvironment().? > 0);
142 }