1
0
Fork 0
zar/src/bar/bar.zig
namedkitten acd1c30a3d h
2020-08-25 11:36:21 +01:00

312 lines
13 KiB
Zig

const std = @import("std");
const Widget = @import("../types/widget.zig").Widget;
const Info = @import("../types/info.zig");
const MouseEvent = @import("../types/mouseevent.zig");
const os = std.os;
const log = std.log;
const terminal_version = @import("build_options").terminal_version;
const debug_allocator = @import("build_options").debug_allocator;
const disable_terminal_mouse = @import("build_options").disable_terminal_mouse;
const json = @import("json.zig");
fn readFromSignalFd(signal_fd: std.os.fd_t) !void {
var buf: [@sizeOf(os.linux.signalfd_siginfo)]u8 align(8) = undefined;
_ = try os.read(signal_fd, &buf);
return error.Shutdown;
}
fn sigemptyset(set: *std.os.sigset_t) void {
for (set) |*val| {
val.* = 0;
}
}
pub const Bar = struct {
allocator: *std.mem.Allocator,
widgets: []const *Widget,
running: bool,
infos: std.ArrayList(Info),
items_mutex: std.Mutex,
out_file: std.fs.File,
pub fn start(self: *Bar) !void {
self.running = true;
// i3bar/swaybar requires starting with this to get click events.
if (!terminal_version) try self.out_file.writer().writeAll("{\"version\": 1,\"click_events\": true}\n[\n");
for (self.widgets) |w| {
try self.infos.append(try self.dupe_info(w.initial_info()));
}
try self.print_infos(true);
var mask: std.os.sigset_t = undefined;
sigemptyset(&mask);
os.linux.sigaddset(&mask, std.os.SIGTERM);
os.linux.sigaddset(&mask, std.os.SIGINT);
_ = os.linux.sigprocmask(std.os.SIG_BLOCK, &mask, null);
const signal_fd = try os.signalfd(-1, &mask, 0);
defer os.close(signal_fd);
log.debug(.bar, "Spawning threads.\n", .{});
for (self.widgets) |w| {
log.debug(.bar, "Spawning thread=\"{}\"\n", .{w.name()});
var thread = try std.Thread.spawn(w, Widget.start);
}
_ = try std.Thread.spawn(self, Bar.process);
log.info(.bar, "Waiting for kill signal.\n", .{});
while (true) {
readFromSignalFd(signal_fd) catch |err| {
if (err == error.Shutdown) break else log.err(.bar, "failed to read from signal fd: {}\n", .{err});
};
}
log.info(.bar, "Shutting Down.\n", .{});
self.running = false;
const lock = self.items_mutex.acquire();
defer lock.release();
// Wait for most widgets to stop.
std.time.sleep(1000 * std.time.ns_per_ms);
for (self.infos.items) |info| {
try self.free_info(info);
}
self.infos.deinit();
log.info(.bar, "Shut Down.\n", .{});
if (terminal_version and !disable_terminal_mouse) {
try self.out_file.writer().writeAll("\u{001b}[?1000;1006;1015l");
}
}
inline fn print_i3bar_infos(self: *Bar) !void {
// Serialize all bar items and put on stdout.
try self.out_file.writer().writeAll("[");
for (self.infos.items) |info, i| {
try json.stringify(info, .{}, self.out_file.writer());
if (i < self.infos.items.len - 1) {
try self.out_file.writer().writeAll(",");
}
}
try self.out_file.writer().writeAll("],\n");
}
inline fn print_terminal_infos(self: *Bar) !void {
// For terminal we just need to directly print.
for (self.infos.items) |info, i| {
try self.out_file.writer().writeAll(info.full_text);
if (i < self.infos.items.len - 1) {
try self.out_file.writer().writeAll("|");
}
}
try self.out_file.writer().writeAll("\n");
}
fn print_infos(self: *Bar, should_lock: bool) !void {
if (should_lock) {
const lock = self.items_mutex.acquire();
defer lock.release();
}
if (terminal_version) {
self.print_terminal_infos() catch {};
} else {
self.print_i3bar_infos() catch {};
}
}
fn dispatch_click_event(self: *Bar, name: []const u8, event: MouseEvent) !void {
std.log.debug(.bar, "Dispatch! {} {}\n", .{ name, event });
for (self.widgets) |w| {
if (std.mem.eql(u8, w.name(), name)) {
try w.mouse_event(event);
}
}
}
inline fn terminal_input_process(self: *Bar) !void {
// TODO: make work on other OSes other than xterm compatable terminals.
// Write to stdout that we want to recieve all terminal click events.
try self.out_file.writer().writeAll("\u{001b}[?1000;1006;1015h");
var termios = try os.tcgetattr(0);
// Set terminal to raw mode so that all input goes directly into stdin.
termios.iflag &= ~@as(
os.tcflag_t,
os.IGNBRK | os.BRKINT | os.PARMRK | os.ISTRIP |
os.INLCR | os.IGNCR | os.ICRNL | os.IXON,
);
// Disable echo so that you don't see mouse events in terminal.
termios.lflag |= ~@as(os.tcflag_t, (os.ECHO | os.ICANON));
termios.lflag &= os.ISIG;
// Set terminal attributes.
// TODO: reset on bar end.
try os.tcsetattr(0, .FLUSH, termios);
while (self.running) {
var line_buffer: [128]u8 = undefined;
// 0x1b is the ESC key which is used for sending and recieving events to xterm terminals.
const line_opt = try std.io.getStdIn().inStream().readUntilDelimiterOrEof(&line_buffer, 0x1b);
if (line_opt) |l| {
// I honestly have no idea what this does but I assume that it checks
// that this is the right event?
if (l.len < 2) continue;
var it = std.mem.tokenize(l, ";");
// First number is just the mouse event, skip processing it for now.
// TODO: map mouse click and scroll events to the right enum value.
_ = it.next();
var x = it.next();
var y = it.next();
if (x == null) {
continue;
}
if (y == null) {
continue;
}
const click_x_position = try std.fmt.parseInt(u16, x.?, 10);
// This makes it so it only works on the end of a click not the start
// preventing a single click pressing the button twice.
if (y.?[y.?.len - 1] == 'm') continue;
var current_info_line_length: u16 = 0;
for (self.infos.items) |infoItem, index| {
// Because the terminal output contains colour codes, we need to strip them.
// To do this we only count the number of characters that are actually printed.
var isEscape: bool = false;
const previous_length = current_info_line_length;
for (infoItem.full_text) |char| {
// Skip all of the escape codes.
if (char == 0x1b) {
isEscape = true;
continue;
}
if (isEscape and char != 'm') {
continue;
}
if (char == 'm' and isEscape) {
isEscape = false;
continue;
}
// If we get here, it is the start of some amount of actual printed characters.
current_info_line_length = current_info_line_length + 1;
}
// Get the first widget that the click is in.
if (click_x_position <= current_info_line_length) {
self.dispatch_click_event(infoItem.name, .{ .button = .LeftClick, .x = click_x_position, .y = 0, .scale = 1, .height = 1, .relative_x = click_x_position - previous_length }) catch {};
break;
}
// Compensate for the | seporator on the terminal.
current_info_line_length = current_info_line_length + 1;
}
}
}
}
inline fn i3bar_input_process(self: *Bar) !void {
var line_buffer: [512]u8 = undefined;
while (self.running) {
const line_opt = try std.io.getStdIn().inStream().readUntilDelimiterOrEof(&line_buffer, '\n');
if (line_opt) |l| {
var line = l;
if (std.mem.eql(u8, line, "[")) continue;
// Prevention from crashing when running via `zig build run` when pressing enter key.
if (line.len == 0) continue;
// Why do you even do this i3bar?
// Why cant you just send one single {} without a comma at start.
// This is stupid and I don't get it, maybe if you were streaming the data
// instead of looping and getting it, maybe then it would make more sense?
// Anyway this just strips off the prefix of ',' so I can parse the json.
if (line[0] == ',') line = line[1..line.len];
const parseOptions = std.json.ParseOptions{ .allocator = self.allocator };
const data = try std.json.parse(MouseEvent, &std.json.TokenStream.init(line), parseOptions);
defer std.json.parseFree(MouseEvent, data, parseOptions);
self.dispatch_click_event(data.name, data) catch {};
// If mouse_event needs to store the event for after the call is finished,
// it should do it by itself, this just keeps the lifetime of the event to bare minimum.
// Free the memory allocated by the MouseEvent struct.
}
}
}
fn process(self: *Bar) !void {
// Right now this is what we do for the debug allocator for testing memory usage.
// If it the best code? Heck no but until we can gracefully ^C the program
// this is the best we can do.
// TODO: log errors.
while (self.running) {
if (terminal_version) {
if (!disable_terminal_mouse) {
self.terminal_input_process() catch |err| {
log.err(.bar, "failed to process terminal input: {}\n", .{err});
};
}
} else {
self.i3bar_input_process() catch |err| {
log.err(.bar, "failed to process i3bar input: {}\n", .{err});
};
}
}
}
pub fn keep_running(self: *Bar) bool {
// TODO: maybe rename this function to something more descriptive?
return self.running;
}
/// This frees the name and text fields of a Info struct.
fn free_info(self: *Bar, info: Info) !void {
const name_size = info.name.len * @sizeOf(u8);
const text_size = info.full_text.len * @sizeOf(u8);
const total_size = name_size + text_size;
//log.debug(.bar, "Freeing info for \"{}\", name_size={} text_size={} total={}\n", .{ info.name, name_size, text_size, total_size });
self.allocator.free(info.name);
self.allocator.free(info.full_text);
}
/// In order to store the info and have Widgets not need to care about
/// memory lifetime, we duplicate the info fields.
fn dupe_info(self: *Bar, info: Info) !Info {
// TODO: name should be comptime known, rework.
const name_size = info.name.len * @sizeOf(u8);
const text_size = info.full_text.len * @sizeOf(u8);
const total_size = name_size + text_size;
//log.debug(.bar, "Duping info for \"{}\", name_size={} text_size={} total={}\n", .{ info.name, name_size, text_size, total_size });
const new_name = try self.allocator.alloc(u8, info.name.len);
std.mem.copy(u8, new_name, info.name);
const new_text = try self.allocator.alloc(u8, info.full_text.len);
std.mem.copy(u8, new_text, info.full_text);
return Info{
.name = new_name,
.full_text = new_text,
.markup = "pango", // setting markup to pango all the time seems OK for perf, no reason not to.
};
}
/// Add a Info to the bar.
pub fn add(self: *Bar, info: Info) !void {
const lock = self.items_mutex.acquire();
defer lock.release();
if (!self.running) return;
for (self.infos.items) |infoItem, index| {
if (std.mem.eql(u8, infoItem.name, info.name)) {
if (std.mem.eql(u8, infoItem.full_text, info.full_text)) {
// OK so info is a dupe, we don't care about dupes so we don't do anything.
return;
}
// If we reach here then it changed.
try self.free_info(infoItem);
self.infos.items[index] = try self.dupe_info(info);
try self.print_infos(false);
}
}
}
};
pub fn initBar(allocator: *std.mem.Allocator) Bar {
return Bar{
.allocator = allocator,
.widgets = undefined,
.running = false,
.infos = std.ArrayList(Info).init(allocator),
.items_mutex = std.Mutex{},
.out_file = std.io.getStdOut(),
};
}