Skip to content

Commit

Permalink
feat: add pselect event loop
Browse files Browse the repository at this point in the history
Rather than rely on the nodelay/blocking nature of a TTY read(), this
introduces an event loop based on pselect(2).

It currently blocks until there is available TTY input, or a SIGWINCH
signal is received.

There is still some room for improvements, but this provides a solid
base for extension like handling input on subprocesses for a preview
split.

Closes #40
Closes #39
  • Loading branch information
natecraddock committed Jun 24, 2023
1 parent 4713559 commit 8ddc8fd
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 5 deletions.
2 changes: 2 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub fn build(b: *std.Build) void {
.target = target,
.optimize = optimize,
});
exe.addCSourceFile("src/loop.c", &.{});

exe.addModule("ziglyph", ziglyph.module("ziglyph"));
b.installArtifact(exe);

Expand Down
4 changes: 2 additions & 2 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
.version = "0.9.0",
.dependencies = .{
.ziglyph = .{
.url = "https://github.com/jecolon/ziglyph/archive/a737cf1808f35f7cb5eb54a9b0bc47d839bae506.tar.gz",
.hash = "1220c892b709f43fac99c3ea7876fd0333274c8bec5fee62f7a341b523687e85d722",
.url = "https://github.com/jecolon/ziglyph/archive/d87a243b2007d43755e11539b944075822fb17b5.tar.gz",
.hash = "12201f6b22da5996ff8c875638463285971fd86183d70827a6383146c05a620b8c9c",
}
},
}
59 changes: 59 additions & 0 deletions src/Loop.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! A simple event loop that tracks signals and tty input based on pselect(2)
//!
//! Because pselect is difficult to call from Zig, a portion of the code is written in C.
//! See loop.c for more details.

const os = std.os;
const std = @import("std");

const Loop = @This();

/// The file descriptor of the TTY
ttyfd: os.fd_t,

/// Because the default for SIGWINCH is to discard the signal, all this
/// handler needs to do is exist for the signal to no longer be ignored.
fn handler(_: c_int) align(1) callconv(.C) void {}

pub fn init(ttyfd: os.fd_t) !Loop {
// Block handling of SIGWINCH.
// This will be unblocked by the kernel within the pselect() call.
var sigset = os.system.empty_sigset;
os.system.sigaddset(&sigset, os.SIG.WINCH);
_ = os.system.sigprocmask(os.SIG._BLOCK, &sigset, null);

// Setup SIGWINCH signal handler
var sigaction: os.Sigaction = .{
.handler = .{ .handler = handler },
.mask = os.system.empty_sigset,
.flags = os.SA.RESTART,
};
try os.sigaction(os.SIG.WINCH, &sigaction, null);

return .{ .ttyfd = ttyfd };
}

pub fn deinit(loop: *Loop) void {
_ = loop;
}

/// This function is defined in loop.c
extern "c" fn wait_internal(ttyfd: c_int) c_int;

/// Wait for an event
///
/// An event is either a file descriptor available to read, or a resize.
pub fn wait(loop: *Loop) !Event {
const fd = wait_internal(loop.ttyfd);

if (fd == loop.ttyfd) {
return .tty;
} else if (fd == -1) {
return .resize;
} else unreachable;
}

const Event = union(enum) {
tty,
resize,
};
36 changes: 36 additions & 0 deletions src/loop.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Using pselect() from Zig is tricky due to macros not working with translate-c
// so we write the interface to pselect() in C and then call this function from Zig

#include <stdlib.h>

#include <signal.h>
#include <sys/errno.h>
#include <sys/select.h>

int wait_internal(int ttyfd) {

fd_set read_set;
FD_ZERO(&read_set);
FD_SET(ttyfd, &read_set);

// Create a set that only allows SIGWINCH through
sigset_t sigwinch_set;
sigfillset(&sigwinch_set);
sigdelset(&sigwinch_set, SIGWINCH);

if (pselect(ttyfd + 1, &read_set, NULL, NULL, NULL, &sigwinch_set) == -1) {
// The only signals that can get through are SIGKILL, SIGSTOP, and SIGWINCH.
// The first two will kill the program no matter what we do, so we assume
// the signal is SIGWINCH. If it is we redraw, otherwise zf will exit.
if (errno == EINTR) {
return -1;
}
}

if (FD_ISSET(ttyfd, &read_set)) {
return ttyfd;
}

// Some unreachable error occurred
return -2;
}
2 changes: 1 addition & 1 deletion src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ pub fn main() anyerror!void {
} else |_| .cyan;

var terminal = try Terminal.init(@min(candidates.len, config.lines), highlight_color, no_color);
terminal.nodelay(false);
terminal.nodelay(true);
var selected = ui.run(
allocator,
&terminal,
Expand Down
3 changes: 1 addition & 2 deletions src/term.zig
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pub const Terminal = struct {
};
}

// TODO: remove this
pub fn nodelay(self: *Terminal, state: bool) void {
self.raw_termios.cc[system.V.MIN] = if (state) 0 else 1;
std.os.tcsetattr(self.tty.handle, .NOW, self.raw_termios) catch unreachable;
Expand Down Expand Up @@ -177,7 +178,6 @@ pub const Terminal = struct {
// buffer. If that is the case this will need to be refactored.
pub fn read(self: *Terminal, buf: []u8) !InputBuffer {
const reader = self.tty.reader();
defer self.nodelay(false);

var index: usize = 0;
// Ensure at least 4 bytes of space in the buffer so it is safe
Expand All @@ -188,7 +188,6 @@ pub const Terminal = struct {
error.InvalidUtf8 => continue,
else => return err,
};
self.nodelay(true);
if (cp) |c| {
// An escape sequence start
if (ziglyph.isControl(c)) {
Expand Down
9 changes: 9 additions & 0 deletions src/ui.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const ArrayList = std.ArrayList;
const ArrayToggleSet = @import("array_toggle_set.zig").ArrayToggleSet;
const Candidate = filter.Candidate;
const EditBuffer = @import("EditBuffer.zig");
const Loop = @import("Loop.zig");
const Terminal = term.Terminal;

const sep = std.fs.path.sep;
Expand Down Expand Up @@ -364,6 +365,8 @@ pub fn run(

var selected_rows = ArrayToggleSet(usize).init(allocator);

var loop = try Loop.init(terminal.tty.handle);

while (true) {
// did the query change?
if (query.dirty) {
Expand All @@ -387,6 +390,12 @@ pub fn run(

const visible_rows = std.math.min(terminal.height, filtered.len);

const event = try loop.wait();
switch (event) {
.resize => redraw = true,
else => {},
}

var buf: [2048]u8 = undefined;
const input = try terminal.read(&buf);
const action = inputToAction(input, vi_mode);
Expand Down

0 comments on commit 8ddc8fd

Please sign in to comment.