mkslide

[root]/ src / mkslide.zig

10.7KB

raw
const std = @import("std");
const c = @import("c.zig");
const cfg = @import("config.zig");

const Slide = struct {
    head: ?[*:0]const u8 = null,
    lines: std.ArrayList([*:0]const u8),

    fn init(a: std.mem.Allocator) Slide {
        return Slide{ .lines = std.ArrayList([*:0]const u8).init(a) };
    }

    fn deinit(self: *Slide) void {
        self.lines.deinit();
    }
};

const Trim = struct {
    start: usize,
    len: usize,
};

const Deck = struct {
    alloc: std.mem.Allocator,
    slides: std.ArrayList(Slide),
    mem: []u8 = &[_]u8{},
    len: usize = 0,
    idx: usize = 0,

    fn init(a: std.mem.Allocator) Deck {
        return Deck{ .alloc = a, .slides = std.ArrayList(Slide).init(a) };
    }

    fn deinit(self: *Deck) void {
        self.reset();
        self.slides.deinit();
    }

    fn reset(self: *Deck) void {
        for (self.slides.items) |*s| s.deinit();
        self.slides.clearRetainingCapacity();
        self.idx = 0;
        if (self.mem.len != 0) {
            self.alloc.free(self.mem);
            self.mem = &[_]u8{};
            self.len = 0;
        }
    }

    fn load(self: *Deck, path: []const u8) !void {
        self.reset();
        var f = try std.fs.cwd().openFile(path, .{});
        defer f.close();
        const sz = try f.getEndPos();
        var buf = try self.alloc.alloc(u8, sz + 1);
        errdefer {
            self.alloc.free(buf);
            self.mem = &[_]u8{};
            self.len = 0;
        }
        const read = try f.readAll(buf[0..sz]);
        buf[read] = 0;
        self.mem = buf;
        self.len = read;
        try self.parse();
        if (self.slides.items.len == 0) return error.NoSlides;
    }

    fn parse(self: *Deck) !void {
        var sl = Slide.init(self.alloc);
        var have = false;
        var start: usize = 0;
        var i: usize = 0;

        while (i <= self.len) : (i += 1) {
            if (i == self.len or self.mem[i] == '\n') {
                if (trim_line(self.mem, start, i)) |trim| {
                    const line_ptr = @as([*:0]const u8, @ptrCast(self.mem.ptr + trim.start));
                    if (self.mem[trim.start] == '#') {
                        if (have) {
                            try self.slides.append(sl);
                            sl = Slide.init(self.alloc);
                            have = false;
                        }
                        var head_start = trim.start + 1;
                        if (head_start - trim.start >= trim.len) {
                            start = i + 1;
                            continue;
                        }
                        var head_len = trim.len - 1;
                        while (head_len > 0 and is_space(self.mem[head_start])) {
                            head_start += 1;
                            head_len -= 1;
                        }
                        if (head_len == 0) {
                            start = i + 1;
                            continue;
                        }
                        sl.head = @as([*:0]const u8, @ptrCast(self.mem.ptr + head_start));
                        have = true;
                    } else {
                        try sl.lines.append(line_ptr);
                        have = true;
                    }
                }
                start = i + 1;
            }
        }

        if (have) {
            try self.slides.append(sl);
        } else {
            sl.deinit();
        }
    }

    fn next(self: *Deck) void {
        if (self.idx + 1 < self.slides.items.len) self.idx += 1;
    }

    fn prev(self: *Deck) void {
        if (self.idx > 0) self.idx -= 1;
    }

    fn peek(self: *Deck) ?*const Slide {
        if (self.slides.items.len == 0) return null;
        return &self.slides.items[self.idx];
    }
};

const State = struct {
    conn: *c.xcb_connection_t,
    win: c.xcb_window_t,
    surf: *c.cairo_surface_t,
    cr: *c.cairo_t,
    keys: *c.xcb_key_symbols_t,
    deck: Deck,

    fn init(a: std.mem.Allocator) !State {
        var scr_no: c_int = undefined;
        const conn = c.xcb_connect(null, &scr_no);
        if (conn == null or c.xcb_connection_has_error(conn) != 0) {
            return error.ConnectionFailed;
        }

        const setup = c.xcb_get_setup(conn);
        var it = c.xcb_setup_roots_iterator(setup);
        var n: c_int = 0;
        while (n < scr_no) : (n += 1) c.xcb_screen_next(&it);
        const scr = it.data;
        const win = c.xcb_generate_id(conn);
        const mask = c.XCB_CW_BACK_PIXEL | c.XCB_CW_EVENT_MASK;
        const vals = [_]u32{
            scr.*.white_pixel,
            c.XCB_EVENT_MASK_EXPOSURE | c.XCB_EVENT_MASK_KEY_PRESS,
        };
        _ = c.xcb_create_window(
            conn,
            c.XCB_COPY_FROM_PARENT,
            win,
            scr.*.root,
            0,
            0,
            cfg.win_w,
            cfg.win_h,
            0,
            c.XCB_WINDOW_CLASS_INPUT_OUTPUT,
            scr.*.root_visual,
            mask,
            &vals,
        );
        _ = c.xcb_change_property(
            conn,
            c.XCB_PROP_MODE_REPLACE,
            win,
            c.XCB_ATOM_WM_NAME,
            c.XCB_ATOM_STRING,
            8,
            cfg.title.len,
            cfg.title,
        );
        const visual = find_visual(scr, scr.*.root_visual) orelse return error.VisualNotFound;
        const surf_opt = c.cairo_xcb_surface_create(conn, win, visual, cfg.win_w, cfg.win_h);
        if (surf_opt == null) return error.CairoSurface;
        const surf = surf_opt.?;
        const cr_opt = c.cairo_create(surf);
        if (cr_opt == null) return error.CairoContext;
        const cr = cr_opt.?;
        const keys_opt = c.xcb_key_symbols_alloc(conn);
        if (keys_opt == null) return error.KeySyms;
        const keys = keys_opt.?;
        _ = c.xcb_map_window(conn, win);
        _ = c.xcb_flush(conn);
        return State{
            .conn = conn.?,
            .win = win,
            .surf = surf,
            .cr = cr,
            .keys = keys,
            .deck = Deck.init(a),
        };
    }

    fn deinit(self: *State) void {
        self.deck.deinit();
        c.cairo_destroy(self.cr);
        c.cairo_surface_destroy(self.surf);
        c.xcb_key_symbols_free(self.keys);
        c.xcb_disconnect(self.conn);
    }

    fn draw(self: *State) void {
        c.cairo_set_source_rgb(self.cr, cfg.colors.bg[0], cfg.colors.bg[1], cfg.colors.bg[2]);
        c.cairo_paint(self.cr);
        const slide = self.deck.peek() orelse return;
        var y: f64 = cfg.pad;
        var te: c.cairo_text_extents_t = undefined;
        if (slide.head) |head| {
            c.cairo_select_font_face(self.cr, cfg.header_font, c.CAIRO_FONT_SLANT_NORMAL, c.CAIRO_FONT_WEIGHT_BOLD);
            c.cairo_set_font_size(self.cr, cfg.header_font_size);
            c.cairo_set_source_rgb(self.cr, cfg.colors.fg[0], cfg.colors.fg[1], cfg.colors.fg[2]);
            var fe: c.cairo_font_extents_t = undefined;
            c.cairo_font_extents(self.cr, &fe);
            y += (fe.ascent + fe.descent) / 2;
            const x = if (cfg.centerh) blk: {
                c.cairo_text_extents(self.cr, head, &te);
                break :blk (cfg.win_w - te.width) / 2;
            } else cfg.pad;
            c.cairo_move_to(self.cr, x, y);
            c.cairo_show_text(self.cr, head);
            y += fe.descent + cfg.line_gap * 2;
        }
        c.cairo_select_font_face(self.cr, cfg.font, c.CAIRO_FONT_SLANT_NORMAL, c.CAIRO_FONT_WEIGHT_NORMAL);
        c.cairo_set_font_size(self.cr, cfg.font_size);
        var fe_body: c.cairo_font_extents_t = undefined;
        c.cairo_font_extents(self.cr, &fe_body);
        for (slide.lines.items) |ln| {
            y += fe_body.ascent;
            c.cairo_move_to(self.cr, if (cfg.centert) (cfg.winert - te.width) / 2 else cfg.pad, y);
            c.cairo_show_text(self.cr, ln);
            y += fe_body.descent + cfg.line_gap;
        }
        var info_buf: [32:0]u8 = undefined;
        const info = std.fmt.bufPrintZ(&info_buf, "{}/{}", .{ self.deck.idx + 1, self.deck.slides.items.len }) catch return;
        c.cairo_select_font_face(self.cr, cfg.font, c.CAIRO_FONT_SLANT_NORMAL, c.CAIRO_FONT_WEIGHT_NORMAL);
        c.cairo_set_font_size(self.cr, cfg.footer_font_size);
        c.cairo_text_extents(self.cr, info.ptr, &te);
        c.cairo_move_to(self.cr, (cfg.win_w - te.width) / 2, cfg.win_h - cfg.pad);
        c.cairo_show_text(self.cr, info.ptr);
        c.cairo_move_to(self.cr, cfg.pad, cfg.win_h - cfg.pad);
        c.cairo_show_text(self.cr, cfg.AUTHOR);
        c.cairo_surface_flush(self.surf);
        _ = c.xcb_flush(self.conn);
    }

    fn key(self: *State, code: c.xcb_keycode_t) bool {
        const sym = c.xcb_key_symbols_get_keysym(self.keys, code, 0);
        switch (sym) {
            c.XK_Escape, c.XK_q => return false,
            c.XK_Right, c.XK_space, c.XK_Return => self.deck.next(),
            c.XK_Left, c.XK_BackSpace => self.deck.prev(),
            else => {},
        }
        return true;
    }
};

fn die(comptime fmt: []const u8, args: anytype) noreturn {
    std.debug.print("state: " ++ fmt ++ "\n", args);
    std.process.exit(1);
}

fn find_visual(scr: *c.xcb_screen_t, id: c.xcb_visualid_t) ?*c.xcb_visualtype_t {
    var d = c.xcb_screen_allowed_depths_iterator(scr);
    while (d.rem != 0) : (c.xcb_depth_next(&d)) {
        var v = c.xcb_depth_visuals_iterator(d.data);
        while (v.rem != 0) : (c.xcb_visualtype_next(&v)) {
            const visual = @as(*c.xcb_visualtype_t, @ptrCast(v.data));
            if (visual.visual_id == id) return visual;
        }
    }
    return null;
}

fn trim_line(buf: []u8, start: usize, end: usize) ?Trim {
    if (start >= end) return null;
    var s = start;
    while (s < end and is_space(buf[s])) s += 1;
    var e = end;
    while (e > s and is_space(buf[e - 1])) e -= 1;
    if (s == e) return null;
    buf[e] = 0;
    return Trim{ .start = s, .len = e - s };
}

inline fn is_space(cu: u8) bool {
    return cu == ' ' or cu == '\t' or cu == '\r';
}

pub fn main() void {
    const a = std.heap.page_allocator;
    const args = std.process.argsAlloc(a) catch die("args", .{});
    defer std.process.argsFree(a, args);
    if (args.len < 2) die("usage: {s} <file>", .{args[0]});
    var state = State.init(a) catch |err| die("init: {}", .{err});
    defer state.deinit();
    state.deck.load(args[1]) catch |err| die("slides: {}", .{err});
    state.draw();
    while (true) {
        const ev = c.xcb_wait_for_event(state.conn);
        if (ev == null) continue;
        defer c.free(ev);
        switch (ev.*.response_type & ~@as(u8, 0x80)) {
            c.XCB_EXPOSE => state.draw(),
            c.XCB_KEY_PRESS => {
                const key = @as(*c.xcb_key_press_event_t, @ptrCast(ev));
                if (!state.key(key.detail)) break;
                state.draw();
            },
            else => {},
        }
    }
}