add TTY fallback

This commit is contained in:
Leon Henrik Plickat 2022-10-18 18:27:53 +02:00
parent 58020dea3a
commit ff0004a7a2
9 changed files with 337 additions and 83 deletions

3
.gitmodules vendored
View file

@ -10,3 +10,6 @@
[submodule "deps/zig-fcft"]
path = deps/zig-fcft
url = https://git.sr.ht/~novakane/zig-fcft
[submodule "deps/zig-spoon"]
path = deps/zig-spoon
url = https://git.sr.ht/~leon_plickat/zig-spoon

View file

@ -26,6 +26,8 @@ pub fn build(b: *zbs.Builder) !void {
wayprompt.setBuildMode(mode);
wayprompt.addOptions("build_options", options);
wayprompt.addPackagePath("spoon", "deps/zig-spoon/import.zig");
const pixman = std.build.Pkg{
.name = "pixman",
.path = .{ .path = "deps/zig-pixman/pixman.zig" },

1
deps/zig-spoon vendored Submodule

@ -0,0 +1 @@
Subproject commit a1ea3574526cdc51aaa3d324d86c533cde56acd0

47
src/Utf8String.zig Normal file
View file

@ -0,0 +1,47 @@
const std = @import("std");
const ascii = std.ascii;
const unicode = std.unicode;
const debug = std.debug;
const context = &@import("wayprompt.zig").context;
const Self = @This();
buffer: std.ArrayListUnmanaged(u8) = .{},
len: usize = 0,
pub fn appendSlice(self: *Self, str: []const u8) !void {
const len = try unicode.utf8CountCodepoints(str);
const alloc = context.gpa.allocator();
try self.buffer.appendSlice(alloc, str);
self.len += len;
}
pub fn deleteBackwards(self: *Self) void {
if (self.buffer.items.len == 0) return;
const alloc = context.gpa.allocator();
var i: usize = self.buffer.items.len - 1;
while (i >= 0) : (i -= 1) {
_ = unicode.utf8ByteSequenceLength(self.buffer.items[i]) catch continue;
self.buffer.shrinkAndFree(alloc, i);
self.len -= 1;
return;
}
unreachable;
}
pub fn toOwnedSlice(self: *Self) ?[]const u8 {
const alloc = context.gpa.allocator();
defer self.* = .{};
if (self.buffer.items.len > 0) {
return self.buffer.toOwnedSlice(alloc);
} else {
return null;
}
}
pub fn deinit(self: *Self) void {
const alloc = context.gpa.allocator();
self.buffer.deinit(alloc);
self.* = .{};
}

View file

@ -9,6 +9,7 @@ const fmt = std.fmt;
const debug = std.debug;
const wayland = @import("wayland.zig");
const tty = @import("tty.zig");
const context = &@import("wayprompt.zig").context;
@ -136,6 +137,9 @@ fn parseInput(writer: io.BufferedWriter(4096, fs.File.Writer).Writer, line: []co
if (getOption("putenv=WAYLAND_DISPLAY=", option, line)) |o| {
if (context.wayland_display) |w| alloc.free(w);
context.wayland_display = try alloc.dupeZ(u8, o);
} else if (getOption("ttyname=", option, line)) |o| {
if (context.tty_name) |w| alloc.free(w);
context.tty_name = try alloc.dupeZ(u8, o);
} else if (getOption("default-ok=", option, line)) |o| {
if (default_ok) |w| alloc.free(w);
default_ok = try pinentryDupe(o, true);
@ -221,9 +225,10 @@ fn getPin(writer: anytype) !void {
default_cancel = null;
}
const alloc = context.gpa.allocator();
if (wayland.run(true)) |pin| {
if (pin) |p| {
const alloc = context.gpa.allocator();
defer alloc.free(p);
try writer.print("D {s}\nEND\nOK\n", .{p});
} else {
@ -231,6 +236,7 @@ fn getPin(writer: anytype) !void {
try writer.writeAll("OK\n");
}
} else |err| {
// TODO error.UserNotOk should be handled here as well
// The client will ignore all messages starting with #, however they
// should still be logged by the gpg-agent, given that the right
// debug options are enabled. This means we can use this to insert
@ -242,16 +248,25 @@ fn getPin(writer: anytype) !void {
// treats both equally. We do it properly of course, because we're
// pedantic. Anyway, that's why error.UserAbort exists and why we
// don't print it because it's not /really/ an error.
if (err != error.UserAbort) {
try writer.print("# Error: {s}\n", .{@errorName(err)});
if (err == error.NoWaylandDisplay or err == error.ConnectFailed) {
if (tty.run(true)) |_pin| {
if (_pin) |p| {
defer alloc.free(p);
try writer.print("D {s}\nEND\nOK\n", .{p});
} else {
try writer.writeAll("OK\n");
}
} else |e| {
try errMessage(writer, e);
}
} else {
try errMessage(writer, err);
}
try writer.writeAll("ERR 83886179 Operation cancelled\n");
}
// The errormessage must automatically reset after every GETPIN or
// CONFIRM action.
if (context.errmessage) |e| {
const alloc = context.gpa.allocator();
alloc.free(e);
context.errmessage = null;
}
@ -267,8 +282,16 @@ fn message(writer: anytype) !void {
debug.assert(ret == null);
try writer.writeAll("OK\n");
} else |err| {
try writer.print("# Error: {s}\n", .{@errorName(err)});
try writer.writeAll("ERR 83886179 cancelled\n");
if (err == error.NoWaylandDisplay or err == error.ConnectFailed) {
if (tty.run(false)) |r| {
debug.assert(r == null);
try writer.writeAll("OK\n");
} else |e| {
try errMessage(writer, e);
}
} else {
try errMessage(writer, err);
}
}
}
@ -289,13 +312,15 @@ fn confirm(writer: anytype) !void {
debug.assert(ret == null);
try writer.writeAll("OK\n");
} else |err| {
switch (err) {
error.UserAbort => try writer.writeAll("ERR 83886179 cancelled\n"),
error.UserNotOk => try writer.writeAll("ERR 83886194 not confirmed\n"),
else => {
try writer.print("# Error: {s}\n", .{@errorName(err)});
try writer.writeAll("ERR 83886179 cancelled\n");
},
if (err == error.NoWaylandDisplay or err == error.ConnectFailed) {
if (tty.run(false)) |r| {
debug.assert(r == null);
try writer.writeAll("OK\n");
} else |e| {
try errMessage(writer, e);
}
} else {
try errMessage(writer, err);
}
}
@ -308,6 +333,17 @@ fn confirm(writer: anytype) !void {
}
}
fn errMessage(writer: anytype, err: anyerror) !void {
switch (err) {
error.UserAbort => try writer.writeAll("ERR 83886179 Operation cancelled\n"),
error.UserNotOk => try writer.writeAll("ERR 83886194 not confirmed\n"),
else => {
try writer.print("# Error: {s}\n", .{@errorName(err)});
try writer.writeAll("ERR 83886179 Operation cancelled\n");
},
}
}
fn setString(writer: anytype, comptime name: []const u8, value: []const u8) !void {
const alloc = context.gpa.allocator();
if (@field(context.*, name)) |f| {

219
src/tty.zig Normal file
View file

@ -0,0 +1,219 @@
const std = @import("std");
const debug = std.debug;
const os = std.os;
const io = std.io;
const math = std.math;
const unicode = std.unicode;
const spoon = @import("spoon");
const context = &@import("wayprompt.zig").context;
const Utf8String = @import("Utf8String.zig");
const LineIterator = struct {
in: ?[]const u8,
pub fn from(input: []const u8) LineIterator {
return .{ .in = input };
}
pub fn next(self: *LineIterator) ?[]const u8 {
if (self.in == null) return null;
if (self.in.?.len == 0) return null;
if (self.in.?.len == 1 and self.in.?[0] == '\n') return null;
var i: usize = 0;
for (self.in.?) |byte| {
if (byte == '\n') {
defer self.in = self.in.?[i + 1 ..];
return self.in.?[0..i];
}
i += 1;
}
defer self.in = null;
return self.in.?;
}
};
const TtyContext = struct {
term: spoon.Term = undefined,
loop: bool = true,
getpin: bool = false,
exit_reason: ?anyerror = null,
pin: Utf8String = .{},
pub fn run(self: *TtyContext, getpin: bool) !?[]const u8 {
errdefer self.pin.deinit();
self.getpin = getpin;
// Only try to fall back to TTY mode when a TTY is set.
if (context.tty_name) |name| {
try self.term.init(.{ .tty_name = name });
} else {
return error.NoTTYNameSet;
}
defer self.term.deinit();
os.sigaction(os.SIG.WINCH, &os.Sigaction{
.handler = .{ .handler = handleSigWinch },
.mask = os.empty_sigset,
.flags = 0,
}, null);
try self.term.uncook(.{});
defer self.term.cook() catch {};
try self.term.fetchSize();
if (context.title) |t| {
try self.term.setWindowTitle("wayprompt TTY fallback: {s}", .{t});
} else {
try self.term.setWindowTitle("wayprompt TTY fallback", .{});
}
try self.render();
var fds: [1]os.pollfd = undefined;
fds[0] = .{
.fd = self.term.tty.handle,
.events = os.POLL.IN,
.revents = undefined,
};
var buf: [32]u8 = undefined;
while (self.loop) {
_ = try os.poll(&fds, -1);
const read = try self.term.readInput(&buf);
var it = spoon.inputParser(buf[0..read]);
while (it.next()) |in| {
if (in.eqlDescription("enter")) {
self.loop = false;
} else if (in.eqlDescription("C-c")) {
if (context.notok == null) {
self.abort(error.UserAbort);
} else {
self.abort(error.UserNotOk);
}
} else if (in.eqlDescription("backspace")) {
self.pin.deleteBackwards();
try self.render();
} else if (in.eqlDescription("escape")) {
self.abort(error.UserAbort);
} else if (self.getpin and in.content == .codepoint) {
if (in.mod_alt or in.mod_ctrl or in.mod_super) continue;
const cp = in.content.codepoint;
// We can safely reuse the buffer here.
const len = unicode.utf8Encode(cp, &buf) catch continue;
self.pin.appendSlice(buf[0..len]) catch {};
try self.render();
}
}
}
if (self.exit_reason) |reason| {
return reason;
} else if (self.getpin) {
return self.pin.toOwnedSlice();
} else {
debug.assert(self.pin.len == 0);
return null;
}
}
fn abort(self: *TtyContext, reason: anyerror) void {
self.loop = false;
self.exit_reason = reason;
}
fn render(self: *TtyContext) !void {
var rc = try self.term.getRenderContext();
defer rc.done() catch {};
try rc.clear();
if (self.term.width < 5 or self.term.height < 5) {
try rc.setAttribute(.{ .fg = .red, .bold = true });
try rc.writeAllWrapping("Terminal too small!");
return;
}
var line: usize = 0;
if (context.title) |title| try self.renderContent(&rc, title, .{ .bg = .green, .bold = true, .fg = .black }, &line);
if (context.description) |description| try self.renderContent(&rc, description, .{}, &line);
if (context.prompt) |prompt| try self.renderContent(&rc, prompt, .{ .bold = true }, &line);
if (self.getpin) {
try rc.setAttribute(.{ .bold = true });
try rc.moveCursorTo(line, 0);
var rpw = rc.restrictedPaddingWriter(self.term.width);
const writer = rpw.writer();
try writer.writeAll(" > ");
try writer.writeByteNTimes('*', math.min(context.pin_square_amount, self.pin.len));
try writer.writeByteNTimes('_', context.pin_square_amount -| self.pin.len);
try rpw.finish();
line += 2;
}
if (context.errmessage) |errmessage| try self.renderContent(&rc, errmessage, .{ .bold = true, .fg = .red }, &line);
if (context.ok) |ok| try self.renderButton(&rc, "enter", ok, &line);
if (context.notok) |notok| try self.renderButton(&rc, "C-c", notok, &line);
if (context.cancel) |cancel| try self.renderButton(&rc, "escape", cancel, &line);
}
fn renderContent(self: *TtyContext, rc: *spoon.Term.RenderContext, str: []const u8, attr: spoon.Attribute, line: *usize) !void {
try rc.setAttribute(attr);
var it = LineIterator.from(str);
while (it.next()) |l| {
if (line.* >= self.term.height) return;
try rc.moveCursorTo(line.*, 0);
var rpw = rc.restrictedPaddingWriter(self.term.width);
const writer = rpw.writer();
try writer.writeByte(' ');
try writer.writeAll(l);
if (attr.bg != .none) {
try rpw.pad();
} else {
try rpw.finish();
}
line.* += 1;
}
line.* += 1;
}
fn renderButton(self: *TtyContext, rc: *spoon.Term.RenderContext, comptime button: []const u8, str: []const u8, line: *usize) !void {
var first = line.*;
try rc.setAttribute(.{});
try rc.moveCursorTo(line.*, 0);
var it = LineIterator.from(str);
while (it.next()) |l| {
if (line.* >= self.term.height) return;
try rc.moveCursorTo(line.*, 0);
var rpw = rc.restrictedPaddingWriter(self.term.width);
const writer = rpw.writer();
try writer.writeByte(' ');
if (line.* == first) {
try writer.writeAll(button);
try writer.writeAll(": ");
} else {
try writer.writeByteNTimes(' ', button.len + ": ".len);
}
try writer.writeAll(l);
try rpw.finish();
line.* += 1;
}
}
fn handleSigWinch(_: c_int) callconv(.C) void {
tty_context.term.fetchSize() catch {};
tty_context.render() catch {};
}
};
var tty_context: TtyContext = undefined;
/// Returned pin is owned by context.gpa.
pub fn run(getpin: bool) !?[]const u8 {
tty_context = .{};
return (try tty_context.run(getpin));
}

View file

@ -1,10 +0,0 @@
const std = @import("std");
const unicode = std.unicode;
pub fn unicodeLen(bytes: []const u8) !usize {
var view = try unicode.Utf8View.init(bytes);
var len: usize = 0;
var it = view.iterator();
while (it.nextCodepointSlice()) |_| : (len += 1) {}
return len;
}

View file

@ -15,50 +15,9 @@ const wayland = @import("wayland");
const wl = wayland.client.wl;
const zwlr = wayland.client.zwlr;
const util = @import("util.zig");
const context = &@import("wayprompt.zig").context;
const Utf8String = struct {
buffer: std.ArrayListUnmanaged(u8) = .{},
len: usize = 0,
pub fn appendSlice(self: *Utf8String, str: []const u8) !void {
const len = try unicode.utf8CountCodepoints(str);
const alloc = context.gpa.allocator();
try self.buffer.appendSlice(alloc, str);
self.len += len;
}
pub fn deleteBackwards(self: *Utf8String) void {
if (self.buffer.items.len == 0) return;
const alloc = context.gpa.allocator();
var i: usize = self.buffer.items.len - 1;
while (i >= 0) : (i -= 1) {
_ = unicode.utf8ByteSequenceLength(self.buffer.items[i]) catch continue;
self.buffer.shrinkAndFree(alloc, i);
self.len -= 1;
return;
}
unreachable;
}
pub fn toOwnedSlice(self: *Utf8String) ?[]const u8 {
const alloc = context.gpa.allocator();
defer self.* = .{};
if (self.buffer.items.len > 0) {
return self.buffer.toOwnedSlice(alloc);
} else {
return null;
}
}
pub fn deinit(self: *Utf8String) void {
const alloc = context.gpa.allocator();
self.buffer.deinit(alloc);
self.* = .{};
}
};
const Utf8String = @import("Utf8String.zig");
const HotSpot = struct {
const Effect = enum { cancel, ok, notok };
@ -931,7 +890,7 @@ const Buffer = struct {
}
};
pub const WaylandContext = struct {
const WaylandContext = struct {
title: ?TextView = null,
description: ?TextView = null,
prompt: ?TextView = null,
@ -957,8 +916,6 @@ pub const WaylandContext = struct {
pin: Utf8String = .{},
pub fn abort(self: *WaylandContext, reason: anyerror) void {
self.pin.deinit();
self.pin = .{};
self.loop = false;
self.exit_reason = reason;
}
@ -966,6 +923,12 @@ pub const WaylandContext = struct {
pub fn run(self: *WaylandContext, getpin: bool) !?[]const u8 {
self.getpin = getpin;
const wayland_display = blk: {
if (context.wayland_display) |wd| break :blk wd;
if (os.getenv("WAYLAND_DISPLAY")) |wd| break :blk wd;
return error.NoWaylandDisplay;
};
_ = fcft.init(.never, false, .none);
defer fcft.fini();
@ -992,12 +955,6 @@ pub const WaylandContext = struct {
if (context.cancel) |cancel| self.cancel = try TextView.new(mem.trim(u8, cancel, &ascii.spaces), font_regular);
defer if (self.cancel) |cancel| cancel.deinit();
const wayland_display = blk: {
if (context.wayland_display) |wd| break :blk wd;
if (os.getenv("WAYLAND_DISPLAY")) |wd| break :blk wd;
return error.NoWaylandDisplay;
};
const display = try wl.Display.connect(@ptrCast([*:0]const u8, wayland_display.ptr));
defer display.disconnect();
@ -1028,10 +985,7 @@ pub const WaylandContext = struct {
alloc.destroy(node);
}
}
errdefer {
self.pin.deinit();
self.pin = .{};
}
errdefer self.pin.deinit();
// Per pinentry protocol documentation, the client may not send us anything
// while it is waiting for a data response. So it's fine to just jump into
@ -1041,11 +995,8 @@ pub const WaylandContext = struct {
}
if (self.exit_reason) |reason| {
debug.assert(self.pin.len == 0);
return reason;
}
if (self.getpin) {
} else if (self.getpin) {
return self.pin.toOwnedSlice();
} else {
debug.assert(self.pin.len == 0);

View file

@ -50,6 +50,7 @@ const Context = struct {
// We may not have WAYLAND_DISPLAY in our env when we get started, or maybe
// even a bad one. However the gpg-agent will likely send us its own.
wayland_display: ?[:0]const u8 = null,
tty_name: ?[:0]const u8 = null,
/// Release all allocated objects.
pub fn reset(self: *Context) void {
@ -86,6 +87,10 @@ const Context = struct {
alloc.free(t);
self.wayland_display = null;
}
if (self.tty_name) |t| {
alloc.free(t);
self.tty_name = null;
}
}
};