~jamii/focus

3036cc5a38b6177ae941502632364a53761cff2f — Jamie Brandon 8 months ago 7ab6ece stable
Remove memory
1 files changed, 0 insertions(+), 310 deletions(-)

D lib/focus/memory.zig
D lib/focus/memory.zig => lib/focus/memory.zig +0 -310
@@ 1,310 0,0 @@
const focus = @import("../focus.zig");
usingnamespace focus.common;
const atlas = focus.atlas;
const UI = focus.UI;

pub const Memory = struct {
    allocator: *Allocator,
    data_arena: *ArenaAllocator,
    frame_arena: *ArenaAllocator,
    clozes: []Cloze,
    logs: ArrayList(Log),
    queue: []Cloze.WithState,
    state: State,

    const State = enum {
        Prepare,
        Prompt,
        Reveal,
    };

    pub fn init(allocator: *Allocator) !Memory {
        var data_arena = try allocator.create(ArenaAllocator);
        data_arena.* = ArenaAllocator.init(allocator);
        var frame_arena = try allocator.create(ArenaAllocator);
        frame_arena.* = ArenaAllocator.init(allocator);
        const clozes = try loadClozes(data_arena);
        var logs = ArrayList(Log).init(&data_arena.allocator);
        try logs.appendSlice(try loadLogs(data_arena));
        const queue = try sortByUrgency(data_arena, clozes, logs.items);
        return Memory{
            .allocator = allocator,
            .data_arena = data_arena,
            .frame_arena = frame_arena,
            .clozes = clozes,
            .logs = logs,
            .queue = queue,
            .state = .Prepare,
        };
    }

    pub fn deinit(self: *Memory) void {
        self.data_arena.deinit();
        self.frame_arena.deinit();
        self.allocator.destroy(self.data_arena);
        self.allocator.destroy(self.frame_arena);
    }

    pub fn frame(self: *Memory, ui: *UI, rect: UI.Rect) !void {
        self.frame_arena.deinit();
        self.frame_arena.* = ArenaAllocator.init(self.allocator);
        const allocator = &self.frame_arena.allocator;

        assert(self.queue.len > 0);

        if (ui.key orelse 0 == 'q') {
            try saveLogs(self.logs.items);
            std.os.exit(0);
        }

        const white = UI.Color{ .r = 255, .g = 255, .b = 255, .a = 255 };
        const margin = 5;
        var screen_rect = rect;
        var app_rect = screen_rect.shrink(margin);
        var button_rect = app_rect.splitBottom(UI.buttonHeight(margin), margin);
        var text_rect = app_rect;

        switch (self.state) {
            .Prepare => {
                try ui.text(text_rect, white, try format(allocator, "{} pending", .{self.queue.len}));
                if (try ui.button(button_rect, white, margin, "go")) {
                    self.state = .Prompt;
                }
            },
            .Prompt => {
                const next = self.queue[0];
                try ui.text(text_rect, white, try format(allocator, "{}\n\n(urgency={}, interval={})", .{ next.cloze.renders[next.state.render_ix], next.state.urgency, next.state.interval_ns }));
                if (try ui.button(button_rect, white, margin, "show")) {
                    self.state = .Reveal;
                }
            },
            .Reveal => {
                const next = self.queue[0];
                try ui.text(rect, white, try format(allocator, "{}", .{next.cloze.text}));
                var event_o: ?Log.Event = null;
                var hit_rect = button_rect.splitRight(@divTrunc(button_rect.w, 2), margin);
                var miss_rect = button_rect;
                if (try ui.button(miss_rect, white, margin, "miss")) {
                    event_o = .Miss;
                }
                if (try ui.button(hit_rect, white, margin, "hit")) {
                    event_o = .Hit;
                }
                if (event_o) |event| {
                    try self.logs.append(.{
                        .at_ns = std.time.milliTimestamp() * 1_000_000,
                        .cloze_text = next.cloze.text,
                        .render_ix = next.state.render_ix,
                        .event = event,
                    });
                    self.queue = self.queue[1..];
                    if (self.queue.len == 0) {
                        self.queue = try sortByUrgency(self.frame_arena, self.clozes, self.logs.items);
                        self.state = .Prepare;
                    } else {
                        self.state = .Prompt;
                    }
                }
            },
        }
    }
};

const Cloze = struct {
    filename: []const u8,
    heading: []const u8,
    text: []const u8,
    renders: [][]const u8,

    const State = struct {
        render_ix: usize,
        interval_ns: u64,
        last_hit_ns: u64,
        urgency: f64,
    };

    const WithState = struct {
        cloze: Cloze,
        state: State,
    };
};

const Log = struct {
    at_ns: u64,
    cloze_text: []const u8,
    render_ix: usize,
    event: Event,

    const Event = enum {
        Hit,
        Miss,

        pub fn jsonStringify(self: Event, options: std.json.StringifyOptions, out_stream: anytype) !void {
            try std.fmt.format(out_stream, "\"{}\"", .{@tagName(self)});
        }
    };
};

/// List of clozes written in simple subset of markdown
fn loadClozes(arena: *ArenaAllocator) ![]Cloze {
    const filename = "/home/jamie/exo-secret/memory.md";
    const contents = try std.fs.cwd().readFileAlloc(&arena.allocator, filename, std.math.maxInt(usize));
    var clozes = ArrayList(Cloze).init(&arena.allocator);
    var cloze_strs = std.mem.split(std.fmt.trim(contents), "\n\n");
    var heading: []const u8 = "";
    while (cloze_strs.next()) |cloze_str| {
        if (std.mem.startsWith(u8, cloze_str, "#")) {
            // is a heading
            heading = cloze_str[2..];
        } else if (std.mem.indexOf(u8, cloze_str, "\n")) |line_end| {
            // multi line, blanks are lines 1+
            var render = ArrayList(u8).init(&arena.allocator);
            try render.appendSlice(cloze_str[0..line_end]);
            try render.appendSlice("\n___");
            var renders: [][]const u8 = try arena.allocator.alloc([]const u8, 1);
            renders[0] = render.items;
            try clozes.append(.{
                .filename = filename,
                .heading = heading,
                .text = cloze_str,
                .renders = renders,
            });
        } else {
            // single line, blanks look like *...*
            // even numbers are text, odd numbers are blanks
            var sections = ArrayList([]const u8).init(&arena.allocator);
            var is_code = false;
            var is_blank = false;
            var last_read: usize = 0;
            for (cloze_str) |char, char_ix| {
                switch (char) {
                    '*' => if (!is_code) {
                        try sections.append(cloze_str[last_read..char_ix]);
                        last_read = char_ix + 1; // skip '*'
                        is_blank = !is_blank;
                    },
                    '`' => {
                        is_code = !is_code;
                    },
                    else => {},
                }
            }
            try sections.append(cloze_str[last_read..]);
            assert(is_code == false);
            assert(is_blank == false);
            var text = ArrayList(u8).init(&arena.allocator);
            for (sections.items) |section| {
                try text.appendSlice(section);
            }
            var renders = ArrayList([]const u8).init(&arena.allocator);
            var render_ix: usize = 1;
            while (render_ix < sections.items.len) : (render_ix += 2) {
                var render = ArrayList(u8).init(&arena.allocator);
                for (sections.items) |section, section_ix| {
                    if (section_ix == render_ix) {
                        try render.appendSlice("___");
                    } else {
                        try render.appendSlice(section);
                    }
                }
                try renders.append(render.items);
            }
            try clozes.append(.{
                .filename = filename,
                .heading = heading,
                .text = text.items,
                .renders = renders.items,
            });
        }
    }
    return clozes.items;
}

fn loadLogs(arena: *ArenaAllocator) ![]Log {
    const filename = "/home/jamie/exo-secret/memory.log";
    const contents = try std.fs.cwd().readFileAlloc(&arena.allocator, filename, std.math.maxInt(usize));
    const logs = try std.json.parse([]Log, &std.json.TokenStream.init(contents), std.json.ParseOptions{ .allocator = &arena.allocator });
    return logs;
}

fn saveLogs(logs: []Log) !void {
    const filename = "/home/jamie/exo-secret/memory.log";
    var file = try std.fs.cwd().createFile(filename, .{});
    defer file.close();
    try std.json.stringify(logs, std.json.StringifyOptions{}, file.outStream());
}

fn sortByUrgency(arena: *ArenaAllocator, clozes: []Cloze, logs: []Log) ![]Cloze.WithState {
    const Key = struct {
        cloze_text: []const u8,
        render_ix: usize,
    };
    var states = DeepHashMap(Key, Cloze.WithState).init(&arena.allocator);
    for (clozes) |cloze| {
        for (cloze.renders) |_, render_ix| {
            const key = Key{
                .cloze_text = cloze.text,
                .render_ix = render_ix,
            };
            const value = Cloze.WithState{
                .cloze = cloze,
                .state = Cloze.State{
                    .render_ix = render_ix,
                    .interval_ns = 12 * std.time.hour, // ie 1 day after first hit
                    .last_hit_ns = 0,
                    .urgency = 0,
                },
            };
            try states.putNoClobber(key, value);
        }
    }

    for (logs) |log| {
        const key = Key{
            .cloze_text = log.cloze_text,
            .render_ix = log.render_ix,
        };
        if (states.get(key)) |kv| {
            const state = &kv.value.state;
            switch (log.event) {
                .Hit => {
                    state.interval_ns *= 2;
                    state.last_hit_ns = log.at_ns;
                },
                .Miss => {
                    state.interval_ns /= 2;
                },
            }
        } else {
            warn("Can't find key: {}\n", .{key});
        }
    }
    const now_ns = std.time.milliTimestamp() * 1_000_000;
    var random = std.rand.DefaultPrng.init(42).random;
    var sorted_clozes = ArrayList(Cloze.WithState).init(&arena.allocator);
    var states_iter = states.iterator();
    var new_clozes: isize = 0;
    while (states_iter.next()) |kv| {
        var state = &kv.value.state;
        const since_hit_ns = now_ns - state.last_hit_ns;
        state.urgency = (@intToFloat(f64, since_hit_ns) / @intToFloat(f64, state.interval_ns));
        if (state.last_hit_ns == 0) {
            new_clozes += 1;
            if (new_clozes > 10) {
                continue;
            }
        }
        if (state.urgency < 1) {
            continue;
        }
        // random tiebreaker to avoid always seeing stuff in the same order
        state.urgency *= 1 + ((random.float(f64) - 0.5) / 10);
        try sorted_clozes.append(kv.value);
    }
    std.sort.sort(Cloze.WithState, sorted_clozes.items, moreUrgent);
    return sorted_clozes.items;
}

fn moreUrgent(c0: Cloze.WithState, c1: Cloze.WithState) bool {
    return (c0.state.urgency > c1.state.urgency);
}