Initial public release

This commit is contained in:
CJ van den Berg 2024-02-29 00:00:15 +01:00
parent 3c3f068914
commit 4ece4babad
63 changed files with 15101 additions and 0 deletions

1158
src/Buffer.zig Normal file

File diff suppressed because it is too large Load diff

220
src/Cursor.zig Normal file
View file

@ -0,0 +1,220 @@
const std = @import("std");
const cbor = @import("cbor");
const Buffer = @import("Buffer.zig");
const View = @import("View.zig");
const Selection = @import("Selection.zig");
row: usize = 0,
col: usize = 0,
target: usize = 0,
const Self = @This();
pub inline fn invalid() Self {
return .{
.row = std.math.maxInt(u32),
.col = std.math.maxInt(u32),
.target = std.math.maxInt(u32),
};
}
pub inline fn eql(self: Self, other: Self) bool {
return self.row == other.row and self.col == other.col;
}
pub inline fn right_of(self: Self, other: Self) bool {
return if (self.row > other.row) true else if (self.row == other.row and self.col > other.col) true else false;
}
pub fn clamp_to_buffer(self: *Self, root: Buffer.Root) void {
self.row = @min(self.row, root.lines() - 1);
self.col = @min(self.col, root.line_width(self.row) catch 0);
}
fn follow_target(self: *Self, root: Buffer.Root) void {
self.col = @min(self.target, root.line_width(self.row) catch 0);
}
fn move_right_no_target(self: *Self, root: Buffer.Root) !void {
const lines = root.lines();
if (lines <= self.row) return error.Stop;
if (self.col < root.line_width(self.row) catch 0) {
_, const wcwidth, const offset = root.ecg_at(self.row, self.col) catch return error.Stop;
self.col += wcwidth - offset;
} else if (self.row < lines - 1) {
self.col = 0;
self.row += 1;
} else return error.Stop;
}
pub fn move_right(self: *Self, root: Buffer.Root) !void {
try self.move_right_no_target(root);
self.target = self.col;
}
fn move_left_no_target(self: *Self, root: Buffer.Root) !void {
if (self.col == 0) {
if (self.row == 0) return error.Stop;
self.row -= 1;
self.col = root.line_width(self.row) catch 0;
} else {
_, const wcwidth, _ = root.ecg_at(self.row, self.col - 1) catch return error.Stop;
if (self.col > wcwidth) self.col -= wcwidth else self.col = 0;
}
}
pub fn move_left(self: *Self, root: Buffer.Root) !void {
try self.move_left_no_target(root);
self.target = self.col;
}
pub fn move_up(self: *Self, root: Buffer.Root) !void {
if (self.row > 0) {
self.row -= 1;
self.follow_target(root);
self.move_left_no_target(root) catch return;
try self.move_right_no_target(root);
} else return error.Stop;
}
pub fn move_down(self: *Self, root: Buffer.Root) !void {
if (self.row < root.lines() - 1) {
self.row += 1;
self.follow_target(root);
self.move_left_no_target(root) catch return;
try self.move_right_no_target(root);
} else return error.Stop;
}
pub fn move_page_up(self: *Self, root: Buffer.Root, view: *const View) void {
self.row = if (self.row > view.rows) self.row - view.rows else 0;
self.follow_target(root);
self.move_left_no_target(root) catch return;
self.move_right_no_target(root) catch return;
}
pub fn move_page_down(self: *Self, root: Buffer.Root, view: *const View) void {
if (root.lines() < view.rows) {
self.move_buffer_last(root);
} else if (self.row < root.lines() - view.rows - 1) {
self.row += view.rows;
} else self.row = root.lines() - 1;
self.follow_target(root);
self.move_left_no_target(root) catch return;
self.move_right_no_target(root) catch return;
}
pub fn move_to(self: *Self, root: Buffer.Root, row: usize, col: usize) !void {
if (row < root.lines()) {
self.row = row;
self.col = @min(col, root.line_width(self.row) catch return error.Stop);
self.target = self.col;
} else return error.Stop;
}
pub fn move_abs(self: *Self, root: Buffer.Root, v: *View, y: usize, x: usize) !void {
self.row = v.row + y;
self.col = v.col + x;
self.clamp_to_buffer(root);
self.target = self.col;
}
pub fn move_begin(self: *Self) void {
self.col = 0;
self.target = self.col;
}
pub fn move_end(self: *Self, root: Buffer.Root) void {
if (self.row < root.lines()) self.col = root.line_width(self.row) catch 0;
self.target = std.math.maxInt(u32);
}
pub fn move_buffer_begin(self: *Self) void {
self.row = 0;
self.col = 0;
self.target = 0;
}
pub fn move_buffer_end(self: *Self, root: Buffer.Root) void {
self.row = root.lines() - 1;
self.move_end(root);
if (self.col == 0) self.target = 0;
}
fn move_buffer_first(self: *Self, root: Buffer.Root) void {
self.row = 0;
self.follow_target(root);
}
fn move_buffer_last(self: *Self, root: Buffer.Root) void {
self.row = root.lines() - 1;
self.follow_target(root);
}
fn is_at_begin(self: *const Self) bool {
return self.col == 0;
}
fn is_at_end(self: *const Self, root: Buffer.Root) bool {
return if (self.row < root.lines()) self.col == root.line_width(self.row) catch 0 else true;
}
pub fn test_at(self: *const Self, root: Buffer.Root, pred: *const fn (c: []const u8) bool) bool {
return root.test_at(pred, self.row, self.col);
}
pub fn write(self: *const Self, writer: Buffer.MetaWriter) !void {
try cbor.writeValue(writer, .{
self.row,
self.col,
self.target,
});
}
pub fn extract(self: *Self, iter: *[]const u8) !bool {
return cbor.matchValue(iter, .{
cbor.extract(&self.row),
cbor.extract(&self.col),
cbor.extract(&self.target),
});
}
pub fn nudge_insert(self: *Self, nudge: Selection) void {
if (self.row < nudge.begin.row or (self.row == nudge.begin.row and self.col < nudge.begin.col)) return;
const rows = nudge.end.row - nudge.begin.row;
if (self.row == nudge.begin.row) {
if (nudge.begin.row < nudge.end.row) {
self.row += rows;
self.col = self.col - nudge.begin.col + nudge.end.col;
} else {
self.col += nudge.end.col - nudge.begin.col;
}
} else {
self.row += rows;
}
}
pub fn nudge_delete(self: *Self, nudge: Selection) bool {
if (self.row < nudge.begin.row or (self.row == nudge.begin.row and self.col < nudge.begin.col)) return true;
if (self.row == nudge.begin.row) {
if (nudge.begin.row < nudge.end.row) {
return false;
} else {
if (self.col < nudge.end.col) {
return false;
}
self.col -= nudge.end.col - nudge.begin.col;
return true;
}
}
if (self.row < nudge.end.row) return false;
if (self.row == nudge.end.row) {
if (self.col < nudge.end.col) return false;
self.row -= nudge.end.row - nudge.begin.row;
self.col -= nudge.end.col;
return true;
}
self.row -= nudge.end.row - nudge.begin.row;
return true;
}

61
src/Selection.zig Normal file
View file

@ -0,0 +1,61 @@
const std = @import("std");
const Buffer = @import("Buffer.zig");
const Cursor = @import("Cursor.zig");
begin: Cursor = Cursor{},
end: Cursor = Cursor{},
const Self = @This();
pub inline fn eql(self: Self, other: Self) bool {
return self.begin.eql(other.begin) and self.end.eql(other.end);
}
pub fn from_cursor(cursor: *const Cursor) Self {
return .{ .begin = cursor.*, .end = cursor.* };
}
pub fn line_from_cursor(cursor: Cursor, root: Buffer.Root) Self {
var begin = cursor;
var end = cursor;
begin.move_begin();
end.move_end(root);
end.move_right(root) catch {};
return .{ .begin = begin, .end = end };
}
pub fn empty(self: *const Self) bool {
return self.begin.eql(self.end);
}
pub fn reverse(self: *Self) void {
const tmp = self.begin;
self.begin = self.end;
self.end = tmp;
}
pub fn normalize(self: *Self) void {
if (self.begin.right_of(self.end))
self.reverse();
}
pub fn write(self: *const Self, writer: Buffer.MetaWriter) !void {
try self.begin.write(writer);
try self.end.write(writer);
}
pub fn extract(self: *Self, iter: *[]const u8) !bool {
if (!try self.begin.extract(iter)) return false;
return self.end.extract(iter);
}
pub fn nudge_insert(self: *Self, nudge: Self) void {
self.begin.nudge_insert(nudge);
self.end.nudge_insert(nudge);
}
pub fn nudge_delete(self: *Self, nudge: Self) bool {
if (!self.begin.nudge_delete(nudge))
return false;
return self.end.nudge_delete(nudge);
}

142
src/View.zig Normal file
View file

@ -0,0 +1,142 @@
const std = @import("std");
const cbor = @import("cbor");
const Buffer = @import("Buffer.zig");
const Cursor = @import("Cursor.zig");
const Selection = @import("Selection.zig");
row: usize = 0,
col: usize = 0,
rows: usize = 0,
cols: usize = 0,
const scroll_cursor_min_border_distance = 5;
const scroll_cursor_min_border_distance_mouse = 1;
const Self = @This();
pub inline fn invalid() Self {
return .{
.row = std.math.maxInt(u32),
.col = std.math.maxInt(u32),
};
}
inline fn reset(self: *Self) void {
self.* = .{};
}
pub inline fn eql(self: Self, other: Self) bool {
return self.row == other.row and self.col == other.col and self.rows == other.rows and self.cols == other.cols;
}
pub fn move_left(self: *Self) !void {
if (self.col > 0) {
self.col -= 1;
} else return error.Stop;
}
pub fn move_right(self: *Self) !void {
self.col += 1;
}
pub fn move_up(self: *Self) !void {
if (!self.is_at_top()) {
self.row -= 1;
} else return error.Stop;
}
pub fn move_down(self: *Self, root: Buffer.Root) !void {
if (!self.is_at_bottom(root)) {
self.row += 1;
} else return error.Stop;
}
pub fn move_to(self: *Self, root: Buffer.Root, row: usize) !void {
if (row < root.lines() - self.rows - 1) {
self.row = row;
} else return error.Stop;
}
inline fn is_at_top(self: *const Self) bool {
return self.row == 0;
}
inline fn is_at_bottom(self: *const Self, root: Buffer.Root) bool {
if (root.lines() < self.rows) return true;
return self.row >= root.lines() - scroll_cursor_min_border_distance;
}
pub inline fn is_visible(self: *const Self, cursor: *const Cursor) bool {
const row_min = self.row;
const row_max = row_min + self.rows;
const col_min = self.col;
const col_max = col_min + self.cols;
return row_min <= cursor.row and cursor.row <= row_max and
col_min <= cursor.col and cursor.col < col_max;
}
inline fn is_visible_selection(self: *const Self, sel: *const Selection) bool {
const row_min = self.row;
const row_max = row_min + self.rows;
return self.is_visible(sel.begin) or is_visible(sel.end) or
(sel.begin.row < row_min and sel.end.row > row_max);
}
inline fn to_cursor_top(self: *const Self) Cursor {
return .{ .row = self.row, .col = 0 };
}
inline fn to_cursor_bottom(self: *const Self, root: Buffer.Root) Cursor {
const bottom = @min(root.lines(), self.row + self.rows + 1);
return .{ .row = bottom, .col = 0 };
}
fn clamp_row(self: *Self, cursor: *const Cursor, abs: bool) void {
const min_border_distance: usize = if (abs) scroll_cursor_min_border_distance_mouse else scroll_cursor_min_border_distance;
if (cursor.row < min_border_distance) {
self.row = 0;
return;
}
if (self.row > 0 and cursor.row >= min_border_distance) {
if (cursor.row < self.row + min_border_distance) {
self.row = cursor.row - min_border_distance;
return;
}
}
if (cursor.row < self.row) {
self.row = 0;
} else if (cursor.row > self.row + self.rows - min_border_distance) {
self.row = cursor.row + min_border_distance - self.rows;
}
}
fn clamp_col(self: *Self, cursor: *const Cursor, _: bool) void {
if (cursor.col < self.col) {
self.col = cursor.col;
} else if (cursor.col > self.col + self.cols - 1) {
self.col = cursor.col - self.cols + 1;
}
}
pub fn clamp(self: *Self, cursor: *const Cursor, abs: bool) void {
self.clamp_row(cursor, abs);
self.clamp_col(cursor, abs);
}
pub fn write(self: *const Self, writer: Buffer.MetaWriter) !void {
try cbor.writeValue(writer, .{
self.row,
self.col,
self.rows,
self.cols,
});
}
pub fn extract(self: *Self, iter: *[]const u8) !bool {
return cbor.matchValue(iter, .{
cbor.extract(&self.row),
cbor.extract(&self.col),
cbor.extract(&self.rows),
cbor.extract(&self.cols),
});
}

62
src/color.zig Normal file
View file

@ -0,0 +1,62 @@
const pow = @import("std").math.pow;
pub const RGB = struct {
r: u8,
g: u8,
b: u8,
pub inline fn from_u24(v: u24) RGB {
const r = @as(u8, @intCast(v >> 16 & 0xFF));
const g = @as(u8, @intCast(v >> 8 & 0xFF));
const b = @as(u8, @intCast(v & 0xFF));
return .{ .r = r, .g = g, .b = b };
}
pub inline fn to_u24(v: RGB) u24 {
const r = @as(u24, @intCast(v.r)) << 16;
const g = @as(u24, @intCast(v.g)) << 8;
const b = @as(u24, @intCast(v.b));
return r | b | g;
}
pub fn contrast(a_: RGB, b_: RGB) f32 {
const a = RGBf.from_RGB(a_).luminance();
const b = RGBf.from_RGB(b_).luminance();
return (@max(a, b) + 0.05) / (@min(a, b) + 0.05);
}
pub fn max_contrast(v: RGB, a: RGB, b: RGB) RGB {
return if (contrast(v, a) > contrast(v, b)) a else b;
}
};
pub const RGBf = struct {
r: f32,
g: f32,
b: f32,
pub inline fn from_RGB(v: RGB) RGBf {
return .{ .r = tof(v.r), .g = tof(v.g), .b = tof(v.b) };
}
pub fn luminance(v: RGBf) f32 {
return linear(v.r) * RED + linear(v.g) * GREEN + linear(v.b) * BLUE;
}
inline fn tof(c: u8) f32 {
return @as(f32, @floatFromInt(c)) / 255.0;
}
inline fn linear(v: f32) f32 {
return if (v <= 0.03928) v / 12.92 else pow(f32, (v + 0.055) / 1.055, GAMMA);
}
const RED = 0.2126;
const GREEN = 0.7152;
const BLUE = 0.0722;
const GAMMA = 2.4;
};
pub fn max_contrast(v: u24, a: u24, b: u24) u24 {
return RGB.max_contrast(RGB.from_u24(v), RGB.from_u24(a), RGB.from_u24(b)).to_u24();
}

15
src/config.zig Normal file
View file

@ -0,0 +1,15 @@
frame_rate: usize = 60,
theme: []const u8 = "default",
input_mode: []const u8 = "flow",
modestate_show: bool = true,
selectionstate_show: bool = true,
modstate_show: bool = false,
keystate_show: bool = false,
gutter_line_numbers: bool = true,
gutter_line_numbers_relative: bool = false,
enable_terminal_cursor: bool = false,
highlight_current_line: bool = true,
highlight_current_line_gutter: bool = true,
show_whitespace: bool = true,
animation_min_lag: usize = 0, //milliseconds
animation_max_lag: usize = 150, //milliseconds

165
src/diff.zig Normal file
View file

@ -0,0 +1,165 @@
const std = @import("std");
const tp = @import("thespian");
const dizzy = @import("dizzy");
const Buffer = @import("Buffer");
const tracy = @import("tracy");
const cbor = @import("cbor");
const Self = @This();
const module_name = @typeName(Self);
pub const Error = error{ OutOfMemory, Exit };
pub const Kind = enum { insert, delete };
pub const Edit = struct {
kind: Kind,
line: usize,
offset: usize,
bytes: []const u8,
};
pid: ?tp.pid,
pub fn create() Error!Self {
return .{ .pid = try Process.create() };
}
pub fn deinit(self: *Self) void {
if (self.pid) |pid| {
pid.send(.{"shutdown"}) catch {};
pid.deinit();
self.pid = null;
}
}
const Process = struct {
arena: std.heap.ArenaAllocator,
a: std.mem.Allocator,
receiver: Receiver,
const Receiver = tp.Receiver(*Process);
const outer_a = std.heap.page_allocator;
pub fn create() Error!tp.pid {
const self = try outer_a.create(Process);
self.* = .{
.arena = std.heap.ArenaAllocator.init(outer_a),
.a = self.arena.allocator(),
.receiver = Receiver.init(Process.receive, self),
};
return tp.spawn_link(self.a, self, Process.start, module_name) catch |e| tp.exit_error(e);
}
fn start(self: *Process) tp.result {
errdefer self.deinit();
tp.receive(&self.receiver);
}
fn deinit(self: *Process) void {
self.arena.deinit();
outer_a.destroy(self);
}
fn receive(self: *Process, from: tp.pid_ref, m: tp.message) tp.result {
errdefer self.deinit();
var cb: usize = 0;
var root_dst: usize = 0;
var root_src: usize = 0;
return if (try m.match(.{ "D", tp.extract(&cb), tp.extract(&root_dst), tp.extract(&root_src) }))
self.diff(from, cb, root_dst, root_src) catch |e| tp.exit_error(e)
else if (try m.match(.{"shutdown"}))
tp.exit_normal();
}
fn diff(self: *Process, from: tp.pid_ref, cb_addr: usize, root_new_addr: usize, root_old_addr: usize) !void {
const frame = tracy.initZone(@src(), .{ .name = "diff" });
defer frame.deinit();
const cb: *CallBack = if (cb_addr == 0) return else @ptrFromInt(cb_addr);
const root_dst: Buffer.Root = if (root_new_addr == 0) return else @ptrFromInt(root_new_addr);
const root_src: Buffer.Root = if (root_old_addr == 0) return else @ptrFromInt(root_old_addr);
var dizzy_edits = std.ArrayListUnmanaged(dizzy.Edit){};
var dst = std.ArrayList(u8).init(self.a);
var src = std.ArrayList(u8).init(self.a);
var scratch = std.ArrayListUnmanaged(u32){};
var edits = std.ArrayList(Edit).init(self.a);
defer {
dst.deinit();
src.deinit();
scratch.deinit(self.a);
dizzy_edits.deinit(self.a);
}
try root_dst.store(dst.writer());
try root_src.store(src.writer());
const scratch_len = 4 * (dst.items.len + src.items.len) + 2;
try scratch.ensureTotalCapacity(self.a, scratch_len);
scratch.items.len = scratch_len;
try dizzy.PrimitiveSliceDiffer(u8).diff(self.a, &dizzy_edits, src.items, dst.items, scratch.items);
if (dizzy_edits.items.len > 2)
try edits.ensureTotalCapacity((dizzy_edits.items.len - 1) / 2);
var lines_dst: usize = 0;
var pos_src: usize = 0;
var pos_dst: usize = 0;
var last_offset: usize = 0;
for (dizzy_edits.items) |dizzy_edit| {
switch (dizzy_edit.kind) {
.equal => {
const dist = dizzy_edit.range.end - dizzy_edit.range.start;
pos_src += dist;
pos_dst += dist;
scan_char(src.items[dizzy_edit.range.start..dizzy_edit.range.end], &lines_dst, '\n', &last_offset);
},
.insert => {
const dist = dizzy_edit.range.end - dizzy_edit.range.start;
pos_src += 0;
pos_dst += dist;
const line_start_dst: usize = lines_dst;
scan_char(dst.items[dizzy_edit.range.start..dizzy_edit.range.end], &lines_dst, '\n', null);
(try edits.addOne()).* = .{
.kind = .insert,
.line = line_start_dst,
.offset = last_offset,
.bytes = dst.items[dizzy_edit.range.start..dizzy_edit.range.end],
};
},
.delete => {
const dist = dizzy_edit.range.end - dizzy_edit.range.start;
pos_src += dist;
pos_dst += 0;
(try edits.addOne()).* = .{
.kind = .delete,
.line = lines_dst,
.offset = last_offset,
.bytes = src.items[dizzy_edit.range.start..dizzy_edit.range.end],
};
},
}
}
cb(from, edits.items);
}
fn scan_char(chars: []const u8, lines: *usize, char: u8, last_offset: ?*usize) void {
var pos = chars;
while (pos.len > 0) {
if (pos[0] == char) {
if (last_offset) |off| off.* = pos.len - 1;
lines.* += 1;
}
pos = pos[1..];
}
}
};
pub const CallBack = fn (from: tp.pid_ref, edits: []Edit) void;
pub fn diff(self: Self, cb: *const CallBack, root_dst: Buffer.Root, root_src: Buffer.Root) tp.result {
if (self.pid) |pid| try pid.send(.{ "D", @intFromPtr(cb), @intFromPtr(root_dst), @intFromPtr(root_src) });
}

127
src/file_type.zig Normal file
View file

@ -0,0 +1,127 @@
const std = @import("std");
const treez = @import("treez");
pub const FileType = @This();
color: u24,
icon: []const u8,
name: []const u8,
lang_fn: LangFn,
extensions: []const []const u8,
highlights: [:0]const u8,
injections: ?[:0]const u8,
first_line_matches: ?FirstLineMatch = null,
comment: []const u8,
pub fn get_by_name(name: []const u8) ?*const FileType {
for (file_types) |*file_type|
if (std.mem.eql(u8, file_type.name, name))
return file_type;
return null;
}
pub fn guess(file_path: ?[]const u8, content: []const u8) ?*const FileType {
if (guess_first_line(content)) |ft| return ft;
for (file_types) |*file_type|
if (file_path) |fp| if (match_file_type(file_type, fp))
return file_type;
return null;
}
fn guess_first_line(content: []const u8) ?*const FileType {
const first_line = if (std.mem.indexOf(u8, content, "\n")) |pos| content[0..pos] else content;
for (file_types) |*file_type|
if (file_type.first_line_matches) |match|
if (match_first_line(match, first_line))
return file_type;
return null;
}
fn match_first_line(match: FirstLineMatch, first_line: []const u8) bool {
if (match.prefix) |prefix|
if (prefix.len > first_line.len or !std.mem.eql(u8, first_line[0..prefix.len], prefix))
return false;
if (match.content) |content|
if (std.mem.indexOf(u8, first_line, content)) |_| {} else return false;
return true;
}
fn match_file_type(file_type: *const FileType, file_path: []const u8) bool {
const basename = std.fs.path.basename(file_path);
const extension = std.fs.path.extension(file_path);
return for (file_type.extensions) |ext| {
if (ext.len == basename.len and std.mem.eql(u8, ext, basename))
return true;
if (extension.len > 0 and ext.len == extension.len - 1 and std.mem.eql(u8, ext, extension[1..]))
return true;
} else false;
}
pub fn Parser(comptime lang: []const u8) LangFn {
return get_parser(lang);
}
fn get_parser(comptime lang: []const u8) LangFn {
const language_name = ft_func_name(lang);
return @extern(?LangFn, .{ .name = "tree_sitter_" ++ language_name }) orelse @compileError(std.fmt.comptimePrint("Cannot find extern tree_sitter_{s}", .{language_name}));
}
fn ft_func_name(comptime lang: []const u8) []const u8 {
var func_name: [lang.len]u8 = undefined;
for (lang, 0..) |c, i|
func_name[i] = if (c == '-') '_' else c;
return &func_name;
}
const LangFn = *const fn () callconv(.C) ?*const treez.Language;
const FirstLineMatch = struct {
prefix: ?[]const u8 = null,
content: ?[]const u8 = null,
};
const FileTypeOptions = struct {
extensions: []const []const u8 = &[_][]const u8{},
comment: []const u8,
icon: ?[]const u8 = null,
color: ?u24 = null,
highlights: ?[:0]const u8 = null,
injections: ?[:0]const u8 = null,
first_line_matches: ?FirstLineMatch = null,
parser: ?LangFn = null,
};
fn DeclLang(comptime lang: []const u8, comptime args: FileTypeOptions) FileType {
return .{
.color = args.color orelse 0xffffff,
.icon = args.icon orelse "󱀫",
.name = lang,
.lang_fn = if (args.parser) |p| p else get_parser(lang),
.extensions = args.extensions,
.comment = args.comment,
.highlights = if (args.highlights) |h| h else @embedFile("tree-sitter-" ++ lang ++ "/queries/highlights.scm"),
.injections = args.injections,
.first_line_matches = args.first_line_matches,
};
}
pub const file_types = load_file_types(@import("file_types.zig"));
fn load_file_types(comptime Namespace: type) []FileType {
comptime switch (@typeInfo(Namespace)) {
.Struct => |info| {
var count = 0;
for (info.decls) |_| {
// @compileLog(decl.name, @TypeOf(@field(Namespace, decl.name)));
count += 1;
}
var cmds: [count]FileType = undefined;
var i = 0;
for (info.decls) |decl| {
cmds[i] = DeclLang(decl.name, @field(Namespace, decl.name));
i += 1;
}
return &cmds;
},
else => @compileError("expected tuple or struct type"),
};
}

294
src/file_types.zig Normal file
View file

@ -0,0 +1,294 @@
pub const agda = .{
.extensions = &[_][]const u8{"agda"},
.comment = "--",
};
pub const bash = .{
.color = 0x3e474a,
.icon = "󱆃",
.extensions = &[_][]const u8{ "sh", "bash" },
.comment = "#",
.first_line_matches = .{ .prefix = "#!", .content = "sh" },
};
pub const c = .{
.icon = "󰙱",
.extensions = &[_][]const u8{ "c", "h" },
.comment = "//",
};
pub const @"c-sharp" = .{
.color = 0x68217a,
.icon = "󰌛",
.extensions = &[_][]const u8{"cs"},
.comment = "//",
};
pub const conf = .{
.color = 0x000000,
.icon = "",
.extensions = &[_][]const u8{ "conf", "config", ".gitconfig" },
.highlights = fish.highlights,
.comment = "#",
.parser = fish.parser,
};
pub const cpp = .{
.color = 0x9c033a,
.icon = "",
.extensions = &[_][]const u8{ "cc", "cpp", "cxx", "hpp", "hxx", "h", "ipp", "ixx" },
.comment = "//",
.injections = @embedFile("tree-sitter-cpp/queries/injections.scm"),
};
pub const css = .{
.color = 0x3d8fc6,
.icon = "󰌜",
.extensions = &[_][]const u8{"css"},
.comment = "//",
};
pub const diff = .{
.extensions = &[_][]const u8{ "diff", "patch" },
.comment = "#",
};
pub const dockerfile = .{
.color = 0x019bc6,
.icon = "",
.extensions = &[_][]const u8{ "Dockerfile", "dockerfile", "docker", "Containerfile", "container" },
.comment = "#",
};
pub const dtd = .{
.icon = "󰗀",
.extensions = &[_][]const u8{"dtd"},
.comment = "<!--",
.highlights = @embedFile("tree-sitter-xml/dtd/queries/highlights.scm"),
};
pub const fish = .{
.extensions = &[_][]const u8{"fish"},
.comment = "#",
.parser = @import("file_type.zig").Parser("fish"),
.highlights = @embedFile("tree-sitter-fish/queries/highlights.scm"),
};
pub const @"git-rebase" = .{
.color = 0xf34f29,
.icon = "",
.extensions = &[_][]const u8{"git-rebase-todo"},
.comment = "#",
};
pub const gitcommit = .{
.color = 0xf34f29,
.icon = "",
.extensions = &[_][]const u8{"COMMIT_EDITMSG"},
.comment = "#",
.injections = @embedFile("tree-sitter-gitcommit/queries/injections.scm"),
};
pub const go = .{
.color = 0x00acd7,
.icon = "󰟓",
.extensions = &[_][]const u8{"go"},
.comment = "//",
};
pub const haskell = .{
.color = 0x5E5185,
.icon = "󰲒",
.extensions = &[_][]const u8{"hs"},
.comment = "--",
};
pub const html = .{
.color = 0xe54d26,
.icon = "󰌝",
.extensions = &[_][]const u8{"html"},
.comment = "<!--",
.injections = @embedFile("tree-sitter-html/queries/injections.scm"),
};
pub const java = .{
.color = 0xEA2D2E,
.icon = "",
.extensions = &[_][]const u8{"java"},
.comment = "//",
};
pub const javascript = .{
.color = 0xf0db4f,
.icon = "󰌞",
.extensions = &[_][]const u8{"js"},
.comment = "//",
.injections = @embedFile("tree-sitter-javascript/queries/injections.scm"),
};
pub const json = .{
.extensions = &[_][]const u8{"json"},
.comment = "//",
};
pub const lua = .{
.color = 0x000080,
.icon = "󰢱",
.extensions = &[_][]const u8{"lua"},
.comment = "--",
.injections = @embedFile("tree-sitter-lua/queries/injections.scm"),
.first_line_matches = .{ .prefix = "--", .content = "lua" },
};
pub const make = .{
.extensions = &[_][]const u8{ "makefile", "Makefile", "MAKEFILE", "GNUmakefile", "mk", "mak", "dsp" },
.comment = "#",
};
pub const markdown = .{
.color = 0x000000,
.icon = "󰍔",
.extensions = &[_][]const u8{"md"},
.comment = "<!--",
.highlights = @embedFile("tree-sitter-markdown/tree-sitter-markdown/queries/highlights.scm"),
.injections = @embedFile("tree-sitter-markdown/tree-sitter-markdown/queries/injections.scm"),
};
pub const @"markdown-inline" = .{
.color = 0x000000,
.icon = "󰍔",
.extensions = &[_][]const u8{},
.comment = "<!--",
.highlights = @embedFile("tree-sitter-markdown/tree-sitter-markdown-inline/queries/highlights.scm"),
.injections = @embedFile("tree-sitter-markdown/tree-sitter-markdown-inline/queries/injections.scm"),
};
pub const nasm = .{
.extensions = &[_][]const u8{ "asm", "nasm" },
.comment = "#",
.injections = @embedFile("tree-sitter-nasm/queries/injections.scm"),
};
pub const ninja = .{
.extensions = &[_][]const u8{"ninja"},
.comment = "#",
};
pub const nix = .{
.color = 0x5277C3,
.icon = "󱄅",
.extensions = &[_][]const u8{"nix"},
.comment = "#",
.injections = @embedFile("tree-sitter-nix/queries/injections.scm"),
};
pub const ocaml = .{
.color = 0xF18803,
.icon = "",
.extensions = &[_][]const u8{ "ml", "mli" },
.comment = "(*",
};
pub const openscad = .{
.color = 0x000000,
.icon = "󰻫",
.extensions = &[_][]const u8{"scad"},
.comment = "//",
.injections = @embedFile("tree-sitter-openscad/queries/injections.scm"),
};
pub const org = .{
.icon = "",
.extensions = &[_][]const u8{"org"},
.comment = "#",
};
pub const php = .{
.color = 0x6181b6,
.icon = "󰌟",
.extensions = &[_][]const u8{"php"},
.comment = "//",
.injections = @embedFile("tree-sitter-php/queries/injections.scm"),
};
pub const purescript = .{
.color = 0x14161a,
.icon = "",
.extensions = &[_][]const u8{"purs"},
.comment = "--",
.injections = @embedFile("tree-sitter-purescript/queries/injections.scm"),
};
pub const python = .{
.color = 0xffd845,
.icon = "󰌠",
.extensions = &[_][]const u8{"py"},
.comment = "#",
.first_line_matches = .{ .prefix = "#!", .content = "/bin/bash" },
};
pub const regex = .{
.extensions = &[_][]const u8{},
.comment = "#",
};
pub const ruby = .{
.color = 0xd91404,
.icon = "󰴭",
.extensions = &[_][]const u8{"rb"},
.comment = "#",
};
pub const rust = .{
.color = 0x000000,
.icon = "󱘗",
.extensions = &[_][]const u8{"rs"},
.comment = "//",
.injections = @embedFile("tree-sitter-rust/queries/injections.scm"),
};
pub const scheme = .{
.extensions = &[_][]const u8{ "scm", "ss", "el" },
.comment = ";",
};
pub const @"ssh-config" = .{
.extensions = &[_][]const u8{".ssh/config"},
.comment = "#",
};
pub const toml = .{
.extensions = &[_][]const u8{ "toml" },
.comment = "#",
};
pub const typescript = .{
.color = 0x007acc,
.icon = "󰛦",
.extensions = &[_][]const u8{ "ts", "tsx" },
.comment = "//",
};
pub const xml = .{
.icon = "󰗀",
.extensions = &[_][]const u8{"xml"},
.comment = "<!--",
.highlights = @embedFile("tree-sitter-xml/xml/queries/highlights.scm"),
.first_line_matches = .{ .prefix = "<?xml " },
};
pub const zig = .{
.color = 0xf7a41d,
.icon = "",
.extensions = &[_][]const u8{ "zig", "zon" },
.comment = "//",
.injections = @embedFile("tree-sitter-zig/queries/injections.scm"),
};
pub const ziggy = .{
.color = 0xf7a41d,
.icon = "",
.extensions = &[_][]const u8{ "ziggy" },
.comment = "//",
.highlights = @embedFile("tree-sitter-ziggy/tree-sitter-ziggy/queries/highlights.scm"),
};

147
src/location_history.zig Normal file
View file

@ -0,0 +1,147 @@
const std = @import("std");
const tp = @import("thespian");
const Self = @This();
const module_name = @typeName(Self);
pub const Error = error{ OutOfMemory, Exit };
pid: ?tp.pid,
pub const Cursor = struct {
row: usize = 0,
col: usize = 0,
};
pub const Selection = struct {
begin: Cursor = Cursor{},
end: Cursor = Cursor{},
};
pub fn create() Error!Self {
return .{ .pid = try Process.create() };
}
pub fn deinit(self: *Self) void {
if (self.pid) |pid| {
pid.send(.{"shutdown"}) catch {};
pid.deinit();
self.pid = null;
}
}
const Process = struct {
arena: std.heap.ArenaAllocator,
a: std.mem.Allocator,
pos: usize = 0,
records: std.ArrayList(Entry),
receiver: Receiver,
const Receiver = tp.Receiver(*Process);
const outer_a = std.heap.page_allocator;
const Entry = struct {
cursor: Cursor,
selection: ?Selection = null,
};
pub fn create() Error!tp.pid {
const self = try outer_a.create(Process);
self.* = .{
.arena = std.heap.ArenaAllocator.init(outer_a),
.a = self.arena.allocator(),
.records = std.ArrayList(Entry).init(self.a),
.receiver = Receiver.init(Process.receive, self),
};
return tp.spawn_link(self.a, self, Process.start, module_name) catch |e| tp.exit_error(e);
}
fn start(self: *Process) tp.result {
errdefer self.deinit();
tp.receive(&self.receiver);
}
fn deinit(self: *Process) void {
self.records.deinit();
self.arena.deinit();
outer_a.destroy(self);
}
fn receive(self: *Process, from: tp.pid_ref, m: tp.message) tp.result {
errdefer self.deinit();
var c: Cursor = .{};
var s: Selection = .{};
var cb: usize = 0;
return if (try m.match(.{ "A", tp.extract(&c.col), tp.extract(&c.row) }))
self.add(.{ .cursor = c })
else if (try m.match(.{ "A", tp.extract(&c.col), tp.extract(&c.row), tp.extract(&s.begin.row), tp.extract(&s.begin.col), tp.extract(&s.end.row), tp.extract(&s.end.col) }))
self.add(.{ .cursor = c, .selection = s })
else if (try m.match(.{ "B", tp.extract(&cb) }))
self.back(from, cb)
else if (try m.match(.{ "F", tp.extract(&cb) }))
self.forward(from, cb)
else if (try m.match(.{"shutdown"}))
tp.exit_normal();
}
fn add(self: *Process, entry: Entry) tp.result {
if (self.records.items.len == 0)
return self.records.append(entry) catch |e| tp.exit_error(e);
if (entry.cursor.row == self.records.items[self.pos].cursor.row) {
self.records.items[self.pos] = entry;
return;
}
if (self.records.items.len > self.pos + 1) {
if (entry.cursor.row == self.records.items[self.pos + 1].cursor.row)
return;
}
if (self.pos > 0) {
if (entry.cursor.row == self.records.items[self.pos - 1].cursor.row)
return;
}
self.records.append(entry) catch |e| return tp.exit_error(e);
self.pos = self.records.items.len - 1;
}
fn back(self: *Process, from: tp.pid_ref, cb_addr: usize) void {
const cb: *CallBack = if (cb_addr == 0) return else @ptrFromInt(cb_addr);
if (self.pos == 0)
return;
self.pos -= 1;
const entry = self.records.items[self.pos];
cb(from, entry.cursor, entry.selection);
}
fn forward(self: *Process, from: tp.pid_ref, cb_addr: usize) void {
const cb: *CallBack = if (cb_addr == 0) return else @ptrFromInt(cb_addr);
if (self.pos == self.records.items.len - 1)
return;
self.pos += 1;
const entry = self.records.items[self.pos];
cb(from, entry.cursor, entry.selection);
}
};
pub fn add(self: Self, cursor: Cursor, selection: ?Selection) void {
if (self.pid) |pid| {
if (selection) |sel|
pid.send(.{ "A", cursor.col, cursor.row, sel.begin.row, sel.begin.col, sel.end.row, sel.end.col }) catch {}
else
pid.send(.{ "A", cursor.col, cursor.row }) catch {};
}
}
pub const CallBack = fn (from: tp.pid_ref, cursor: Cursor, selection: ?Selection) void;
pub fn back(self: Self, cb: *const CallBack) tp.result {
if (self.pid) |pid| try pid.send(.{ "B", @intFromPtr(cb) });
}
pub fn forward(self: Self, cb: *const CallBack) tp.result {
if (self.pid) |pid| try pid.send(.{ "F", @intFromPtr(cb) });
}

158
src/log.zig Normal file
View file

@ -0,0 +1,158 @@
const std = @import("std");
const tp = @import("thespian");
const deque = std.TailQueue;
const fba = std.heap.FixedBufferAllocator;
const Self = @This();
pub const max_log_message = tp.max_message_size - 128;
a: std.mem.Allocator,
receiver: Receiver,
subscriber: ?tp.pid,
heap: [32 + 1024]u8,
fba: fba,
msg_store: MsgStoreT,
const MsgStoreT = std.DoublyLinkedList([]u8);
const Receiver = tp.Receiver(*Self);
const StartArgs = struct {
a: std.mem.Allocator,
};
pub fn spawn(ctx: *tp.context, a: std.mem.Allocator, env: ?*const tp.env) !tp.pid {
return try ctx.spawn_link(StartArgs{ .a = a }, Self.start, "log", null, env);
}
fn start(args: StartArgs) tp.result {
_ = tp.set_trap(true);
var this = Self.init(args) catch |e| return tp.exit_error(e);
errdefer this.deinit();
tp.receive(&this.receiver);
}
fn init(args: StartArgs) !*Self {
var p = try args.a.create(Self);
p.* = .{
.a = args.a,
.receiver = Receiver.init(Self.receive, p),
.subscriber = null,
.heap = undefined,
.fba = fba.init(&p.heap),
.msg_store = MsgStoreT{},
};
return p;
}
fn deinit(self: *const Self) void {
if (self.subscriber) |*s| s.deinit();
self.a.destroy(self);
}
fn log(msg: []const u8) void {
tp.self_pid().send(.{ "log", "log", msg }) catch {};
}
fn store(self: *Self, m: tp.message) void {
const a: std.mem.Allocator = self.fba.allocator();
const buf: []u8 = a.alloc(u8, m.len()) catch return;
var node: *MsgStoreT.Node = a.create(MsgStoreT.Node) catch return;
node.data = buf;
@memcpy(buf, m.buf);
self.msg_store.append(node);
}
fn store_send(self: *Self) void {
var node = self.msg_store.first;
if (self.subscriber) |sub| {
while (node) |node_| {
sub.send_raw(tp.message{ .buf = node_.data }) catch return;
node = node_.next;
}
}
self.store_reset();
}
fn store_reset(self: *Self) void {
self.msg_store = MsgStoreT{};
self.fba.reset();
}
fn receive(self: *Self, from: tp.pid_ref, m: tp.message) tp.result {
errdefer self.deinit();
if (try m.match(.{ "log", tp.more })) {
if (self.subscriber) |subscriber| {
subscriber.send_raw(m) catch {};
} else {
self.store(m);
}
} else if (try m.match(.{"subscribe"})) {
// log("subscribed");
if (self.subscriber) |*s| s.deinit();
self.subscriber = from.clone();
self.store_send();
} else if (try m.match(.{"unsubscribe"})) {
// log("unsubscribed");
if (self.subscriber) |*s| s.deinit();
self.subscriber = null;
self.store_reset();
} else if (try m.match(.{"shutdown"})) {
return tp.exit_normal();
}
}
pub const Logger = struct {
proc: tp.pid_ref,
tag: []const u8,
const Self_ = @This();
pub fn write(self: Self_, value: anytype) void {
self.proc.send(.{ "log", self.tag } ++ value) catch {};
}
pub fn print(self: Self_, comptime fmt: anytype, args: anytype) void {
var buf: [max_log_message]u8 = undefined;
const output = std.fmt.bufPrint(&buf, fmt, args) catch "MESSAGE TOO LARGE";
self.proc.send(.{ "log", self.tag, output }) catch {};
}
pub fn err(self: Self_, context: []const u8, e: anyerror) void {
defer tp.reset_error();
var buf: [max_log_message]u8 = undefined;
var msg: []const u8 = "UNKNOWN";
switch (e) {
error.Exit => {
const msg_: tp.message = .{ .buf = tp.error_message() };
var msg__: []const u8 = undefined;
if (!(msg_.match(.{ "exit", tp.extract(&msg__) }) catch false))
msg__ = msg_.buf;
if (msg__.len > buf.len) {
self.proc.send(.{ "log", "error", self.tag, context, "->", "MESSAGE TOO LARGE" }) catch {};
return;
}
const msg___ = buf[0..msg__.len];
@memcpy(msg___, msg__);
msg = msg___;
},
else => {
msg = @errorName(e);
},
}
self.proc.send(.{ "log", "error", self.tag, context, "->", msg }) catch {};
}
};
pub fn logger(tag: []const u8) Logger {
return .{ .proc = tp.env.get().proc("log"), .tag = tag };
}
pub fn subscribe() tp.result {
return tp.env.get().proc("log").send(.{"subscribe"});
}
pub fn unsubscribe() tp.result {
return tp.env.get().proc("log").send(.{"unsubscribe"});
}

168
src/lsp_process.zig Normal file
View file

@ -0,0 +1,168 @@
const std = @import("std");
const tp = @import("thespian");
const cbor = @import("cbor");
const log = @import("log");
pid: ?tp.pid,
const Self = @This();
const module_name = @typeName(Self);
const sp_tag = "LSP";
pub const Error = error{ OutOfMemory, Exit };
pub fn open(a: std.mem.Allocator, cmd: tp.message, tag: [:0]const u8) Error!Self {
return .{ .pid = try Process.create(a, cmd, tag) };
}
pub fn deinit(self: *Self) void {
if (self.pid) |pid| {
pid.send(.{"close"}) catch {};
self.pid = null;
pid.deinit();
}
}
pub fn send(self: *Self, message: []const u8) tp.result {
const pid = if (self.pid) |pid| pid else return tp.exit_error(error.Closed);
try pid.send(.{ "M", message });
}
pub fn close(self: *Self) void {
self.deinit();
}
const Process = struct {
a: std.mem.Allocator,
cmd: tp.message,
receiver: Receiver,
sp: ?tp.subprocess = null,
recv_buf: std.ArrayList(u8),
parent: tp.pid,
tag: [:0]const u8,
logger: log.Logger,
const Receiver = tp.Receiver(*Process);
pub fn create(a: std.mem.Allocator, cmd: tp.message, tag: [:0]const u8) Error!tp.pid {
const self = try a.create(Process);
self.* = .{
.a = a,
.cmd = try cmd.clone(a),
.receiver = Receiver.init(receive, self),
.recv_buf = std.ArrayList(u8).init(a),
.parent = tp.self_pid().clone(),
.tag = try a.dupeZ(u8, tag),
.logger = log.logger(@typeName(Self)),
};
return tp.spawn_link(self.a, self, Process.start, tag) catch |e| tp.exit_error(e);
}
fn deinit(self: *Process) void {
self.recv_buf.deinit();
self.a.free(self.cmd.buf);
self.close() catch {};
}
fn close(self: *Process) tp.result {
if (self.sp) |*sp| {
defer self.sp = null;
try sp.close();
}
}
fn start(self: *Process) tp.result {
_ = tp.set_trap(true);
self.sp = tp.subprocess.init(self.a, self.cmd, sp_tag, self.stdin_behavior) catch |e| return tp.exit_error(e);
tp.receive(&self.receiver);
}
fn receive(self: *Process, _: tp.pid_ref, m: tp.message) tp.result {
errdefer self.deinit();
var bytes: []u8 = "";
if (try m.match(.{ "S", tp.extract(&bytes) })) {
const sp = if (self.sp) |sp| sp else return tp.exit_error(error.Closed);
try sp.send(bytes);
} else if (try m.match(.{"close"})) {
try self.close();
} else if (try m.match(.{ sp_tag, "stdout", tp.extract(&bytes) })) {
self.handle_output(bytes) catch |e| return tp.exit_error(e);
} else if (try m.match(.{ sp_tag, "term", tp.more })) {
self.handle_terminated() catch |e| return tp.exit_error(e);
} else if (try m.match(.{ sp_tag, "stderr", tp.extract(&bytes) })) {
self.logger.print("ERR: {s}", .{bytes});
} else if (try m.match(.{ "exit", "normal" })) {
return tp.exit_normal();
} else {
self.logger.err("receive", tp.unexpected(m));
return tp.unexpected(m);
}
}
fn handle_output(self: *Process, bytes: []u8) !void {
try self.recv_buf.appendSlice(bytes);
@import("log").logger(module_name).print("{s}", .{bytes}) catch {};
const message = try self.frame_message() orelse return;
_ = message;
}
fn handle_terminated(self: *Process) !void {
const recv_buf = try self.recv_buf.toOwnedSlice();
var it = std.mem.splitScalar(u8, recv_buf, '\n');
while (it.next()) |json| {
if (json.len == 0) continue;
var msg_buf: [tp.max_message_size]u8 = undefined;
const msg: tp.message = .{ .buf = try cbor.fromJson(json, &msg_buf) };
try self.dispatch(msg);
// var buf: [tp.max_message_size]u8 = undefined;
// @import("log").logger(module_name).print("json: {s}", .{try msg.to_json(&buf)}) catch {};
}
@import("log").logger(module_name).print("done", .{}) catch {};
try self.parent.send(.{ self.tag, "done" });
}
fn frame_message(self: *Self) !?Message {
const end = std.mem.indexOf(u8, self.recv_buf, "\r\n\r\n") orelse return null;
const headers = try Headers.parse(self.recv_buf[0..end]);
const body = self.recv_buf[end + 2 ..];
if (body.len < headers.content_length) return null;
return .{ .body = body };
}
};
const Message = struct {
body: []const u8,
};
const Headers = struct {
content_length: usize = 0,
content_type: ?[]const u8 = null,
fn parse(buf_: []const u8) !Headers {
var buf = buf_;
var ret: Headers = .{};
while (true) {
const sep = std.mem.indexOf(u8, buf, ":") orelse return error.InvalidSyntax;
const name = buf[0..sep];
const end = std.mem.indexOf(u8, buf, "\r\n") orelse buf.len;
const vstart = if (buf.len > sep + 1)
if (buf[sep + 1] == ' ')
sep + 2
else
sep + 1
else
sep + 1;
const value = buf[vstart..end];
ret.parse_one(name, value);
buf = if (end < buf.len - 2) buf[end + 2 ..] else return ret;
}
}
fn parse_one(self: *Headers, name: []const u8, value: []const u8) void {
if (std.mem.eql(u8, "Content-Length", name)) {
self.content_length = std.fmt.parseInt(@TypeOf(self.content_length), value, 10);
} else if (std.mem.eql(u8, "Content-Type", name)) {
self.content_type = value;
}
}
};

319
src/main.zig Normal file
View file

@ -0,0 +1,319 @@
const std = @import("std");
const tui = @import("tui");
const thespian = @import("thespian");
const clap = @import("clap");
const builtin = @import("builtin");
const c = @cImport({
@cInclude("locale.h");
});
const build_options = @import("build_options");
const log = @import("log");
pub const application_name = "flow";
pub const application_logo = "󱞏 ";
pub fn main() anyerror!void {
const params = comptime clap.parseParamsComptime(
\\-h, --help Display this help and exit.
\\-f, --frame-rate <usize> Set target frame rate. (default: 60)
\\--debug-wait Wait for key press before starting UI.
\\--debug-dump-on-error Dump stack traces on errors.
\\--no-sleep Do not sleep the main loop when idle.
\\--no-alternate Do not use the alternate terminal screen.
\\--no-trace Do not enable internal tracing.
\\--restore-session Restore restart session.
\\<str>... File to open.
\\ Add +<LINE> to the command line or append
\\ :LINE or :LINE:COL to the file name to jump
\\ to a location in the file.
\\
);
if (builtin.os.tag == .linux) {
// drain stdin so we don't pickup junk from previous application/shell
_ = std.os.linux.syscall3(.ioctl, @as(usize, @bitCast(@as(isize, std.os.STDIN_FILENO))), std.os.linux.T.CFLSH, 0);
}
const a = std.heap.c_allocator;
var diag = clap.Diagnostic{};
var res = clap.parse(clap.Help, &params, clap.parsers.default, .{
.diagnostic = &diag,
.allocator = a,
}) catch |err| {
diag.report(std.io.getStdErr().writer(), err) catch {};
clap.help(std.io.getStdErr().writer(), clap.Help, &params, .{}) catch {};
exit(1);
return err;
};
defer res.deinit();
if (res.args.help != 0)
return clap.help(std.io.getStdErr().writer(), clap.Help, &params, .{});
if (std.os.getenv("JITDEBUG")) |_| thespian.install_debugger();
if (res.args.@"debug-wait" != 0) {
std.debug.print("press return to start", .{});
var buf: [10]u8 = undefined;
_ = std.c.read(0, &buf, @sizeOf(@TypeOf(buf)));
}
if (c.setlocale(c.LC_ALL, "") == null) {
return error.SetLocaleFailed;
}
var ctx = try thespian.context.init(a);
defer ctx.deinit();
const env = thespian.env.init();
defer env.deinit();
if (build_options.enable_tracy) {
if (res.args.@"no-trace" == 0) {
env.enable_all_channels();
env.on_trace(trace);
}
}
const log_proc = try log.spawn(&ctx, a, &env);
defer log_proc.deinit();
env.set("restore-session", (res.args.@"restore-session" != 0));
env.set("no-alternate", (res.args.@"no-alternate" != 0));
env.set("no-sleep", (res.args.@"no-sleep" != 0));
env.set("dump-stack-trace", (res.args.@"debug-dump-on-error" != 0));
if (res.args.@"frame-rate") |frame_rate|
env.num_set("frame-rate", @intCast(frame_rate));
env.proc_set("log", log_proc.ref());
var eh = thespian.make_exit_handler({}, print_exit_status);
const tui_proc = try tui.spawn(a, &ctx, &eh, &env);
defer tui_proc.deinit();
const Dest = struct {
file: []const u8 = "",
line: ?usize = null,
column: ?usize = null,
end_column: ?usize = null,
};
var dests = std.ArrayList(Dest).init(a);
defer dests.deinit();
var prev: ?*Dest = null;
var line_next: ?usize = null;
for (res.positionals) |arg| {
if (arg.len == 0) continue;
if (arg[0] == '+') {
const line = try std.fmt.parseInt(usize, arg[1..], 10);
if (prev) |p| {
p.line = line;
} else {
line_next = line;
}
continue;
}
const curr = try dests.addOne();
curr.* = .{};
prev = curr;
if (line_next) |line| {
curr.line = line;
line_next = null;
}
var it = std.mem.splitScalar(u8, arg, ':');
curr.file = it.first();
if (it.next()) |line_|
curr.line = std.fmt.parseInt(usize, line_, 10) catch null;
if (it.next()) |col_|
curr.column = std.fmt.parseInt(usize, col_, 10) catch null;
if (it.next()) |col_|
curr.end_column = std.fmt.parseInt(usize, col_, 10) catch null;
}
for (dests.items) |dest| {
if (dest.file.len == 0) continue;
if (dest.line) |l| {
if (dest.column) |col| {
try tui_proc.send(.{ "cmd", "navigate", .{ .file = dest.file, .line = l, .column = col } });
if (dest.end_column) |end|
try tui_proc.send(.{ "A", l, col - 1, end - 1 });
} else {
try tui_proc.send(.{ "cmd", "navigate", .{ .file = dest.file, .line = l } });
}
} else {
try tui_proc.send(.{ "cmd", "navigate", .{ .file = dest.file } });
}
} else {
try tui_proc.send(.{ "cmd", "show_home" });
}
ctx.run();
if (want_restart) restart();
exit(final_exit_status);
}
var final_exit_status: u8 = 0;
var want_restart: bool = false;
fn print_exit_status(_: void, msg: []const u8) void {
if (std.mem.eql(u8, msg, "normal")) {
return;
} else if (std.mem.eql(u8, msg, "restart")) {
want_restart = true;
} else {
std.io.getStdErr().writer().print("\n" ++ application_name ++ " ERROR: {s}\n", .{msg}) catch {};
final_exit_status = 1;
}
}
fn count_args() usize {
var args = std.process.args();
_ = args.next();
var count: usize = 0;
while (args.next()) |_| {
count += 1;
}
return count;
}
fn trace(m: thespian.message.c_buffer_type) callconv(.C) void {
thespian.message.from(m).to_json_cb(trace_json);
}
fn trace_json(json: thespian.message.json_string_view) callconv(.C) void {
const callstack_depth = 10;
___tracy_emit_message(json.base, json.len, callstack_depth);
}
extern fn ___tracy_emit_message(txt: [*]const u8, size: usize, callstack: c_int) void;
fn exit(status: u8) noreturn {
if (builtin.os.tag == .linux) {
// drain stdin so we don't leave junk at the next prompt
_ = std.os.linux.syscall3(.ioctl, @as(usize, @bitCast(@as(isize, std.os.STDIN_FILENO))), std.os.linux.T.CFLSH, 0);
}
std.os.exit(status);
}
const config = @import("config");
pub fn read_config(a: std.mem.Allocator, buf: *?[]const u8) config {
const file_name = get_app_config_file_name(application_name) catch return .{};
return read_json_config_file(a, file_name, buf) catch .{};
}
fn read_json_config_file(a: std.mem.Allocator, file_name: []const u8, buf: *?[]const u8) !config {
const cbor = @import("cbor");
var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch |e| switch (e) {
error.FileNotFound => return .{},
else => return e,
};
defer file.close();
const json = try file.readToEndAlloc(a, 64 * 1024);
defer a.free(json);
const cbor_buf: []u8 = try a.alloc(u8, json.len);
buf.* = cbor_buf;
const cb = try cbor.fromJson(json, cbor_buf);
var iter = cb;
var len = try cbor.decodeMapHeader(&iter);
var data: config = .{};
while (len > 0) : (len -= 1) {
var field_name: []const u8 = undefined;
if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidConfig;
inline for (@typeInfo(config).Struct.fields) |field_info| {
if (std.mem.eql(u8, field_name, field_info.name)) {
var value: field_info.type = undefined;
if (!(try cbor.matchValue(&iter, cbor.extract(&value)))) return error.InvalidConfig;
@field(data, field_info.name) = value;
}
}
}
return data;
}
pub fn write_config(conf: config, a: std.mem.Allocator) !void {
return write_json_file(config, conf, a, try get_app_config_file_name(application_name));
}
fn write_json_file(comptime T: type, data: T, a: std.mem.Allocator, file_name: []const u8) !void {
const cbor = @import("cbor");
var file = try std.fs.createFileAbsolute(file_name, .{ .truncate = true });
defer file.close();
var cb = std.ArrayList(u8).init(a);
defer cb.deinit();
try cbor.writeValue(cb.writer(), data);
var s = std.json.writeStream(file.writer(), .{ .whitespace = .indent_4 });
var iter: []const u8 = cb.items;
try cbor.JsonStream(std.fs.File).jsonWriteValue(&s, &iter);
}
pub fn get_config_dir() ![]const u8 {
return get_app_config_dir(application_name);
}
fn get_app_config_dir(appname: []const u8) ![]const u8 {
const local = struct {
var config_dir_buffer: [std.os.PATH_MAX]u8 = undefined;
var config_dir: ?[]const u8 = null;
};
const config_dir = if (local.config_dir) |dir|
dir
else if (std.os.getenv("XDG_CONFIG_HOME")) |xdg|
try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/{s}", .{ xdg, appname })
else if (std.os.getenv("HOME")) |home|
try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/.config/{s}", .{ home, appname })
else
return error.AppConfigDirUnavailable;
local.config_dir = config_dir;
std.fs.makeDirAbsolute(config_dir) catch |e| switch (e) {
error.PathAlreadyExists => {},
else => return e,
};
return config_dir;
}
fn get_app_config_file_name(appname: []const u8) ![]const u8 {
const local = struct {
var config_file_buffer: [std.os.PATH_MAX]u8 = undefined;
var config_file: ?[]const u8 = null;
};
const config_file_name = "config.json";
const config_file = if (local.config_file) |file|
file
else
try std.fmt.bufPrint(&local.config_file_buffer, "{s}/{s}", .{ try get_app_config_dir(appname), config_file_name });
local.config_file = config_file;
return config_file;
}
pub fn get_config_file_name() ![]const u8 {
return get_app_config_file_name(application_name);
}
pub fn get_restore_file_name() ![]const u8 {
const local = struct {
var restore_file_buffer: [std.os.PATH_MAX]u8 = undefined;
var restore_file: ?[]const u8 = null;
};
const restore_file_name = "restore";
const restore_file = if (local.restore_file) |file|
file
else
try std.fmt.bufPrint(&local.restore_file_buffer, "{s}/{s}", .{ try get_app_config_dir(application_name), restore_file_name });
local.restore_file = restore_file;
return restore_file;
}
fn restart() noreturn {
const argv = [_]?[*:0]const u8{
std.os.argv[0],
"--restore-session",
null,
};
const ret = std.c.execve(std.os.argv[0], @ptrCast(&argv), @ptrCast(std.os.environ));
std.io.getStdErr().writer().print("\nrestart failed: {d}", .{ret}) catch {};
exit(234);
}

235
src/ripgrep.zig Normal file
View file

@ -0,0 +1,235 @@
const std = @import("std");
const tp = @import("thespian");
const cbor = @import("cbor");
const log = @import("log");
pub const ripgrep_binary = "rg";
pid: ?tp.pid,
stdin_behavior: std.ChildProcess.StdIo,
const Self = @This();
const module_name = @typeName(Self);
pub const max_chunk_size = tp.subprocess.max_chunk_size;
pub const Writer = std.io.Writer(*Self, error{Exit}, write);
pub const BufferedWriter = std.io.BufferedWriter(max_chunk_size, Writer);
pub const Error = error{ OutOfMemory, Exit };
pub const FindF = fn (a: std.mem.Allocator, query: []const u8, tag: [:0]const u8) Error!Self;
pub fn find_in_stdin(a: std.mem.Allocator, query: []const u8, tag: [:0]const u8) Error!Self {
return create(a, query, tag, .Pipe);
}
pub fn find_in_files(a: std.mem.Allocator, query: []const u8, tag: [:0]const u8) Error!Self {
return create(a, query, tag, .Close);
}
fn create(a: std.mem.Allocator, query: []const u8, tag: [:0]const u8, stdin_behavior: std.ChildProcess.StdIo) Error!Self {
return .{ .pid = try Process.create(a, query, tag, stdin_behavior), .stdin_behavior = stdin_behavior };
}
pub fn deinit(self: *Self) void {
if (self.pid) |pid| {
if (self.stdin_behavior == .Pipe)
pid.send(.{"close"}) catch {};
self.pid = null;
pid.deinit();
}
}
pub fn write(self: *Self, bytes: []const u8) error{Exit}!usize {
try self.input(bytes);
return bytes.len;
}
pub fn input(self: *const Self, bytes: []const u8) tp.result {
const pid = if (self.pid) |pid| pid else return tp.exit_error(error.Closed);
var remaining = bytes;
while (remaining.len > 0)
remaining = loop: {
if (remaining.len > max_chunk_size) {
try pid.send(.{ "input", remaining[0..max_chunk_size] });
break :loop remaining[max_chunk_size..];
} else {
try pid.send(.{ "input", remaining });
break :loop &[_]u8{};
}
};
}
pub fn close(self: *Self) void {
self.deinit();
}
pub fn writer(self: *Self) Writer {
return .{ .context = self };
}
pub fn bufferedWriter(self: *Self) BufferedWriter {
return .{ .unbuffered_writer = self.writer() };
}
const Process = struct {
a: std.mem.Allocator,
query: []const u8,
receiver: Receiver,
sp: ?tp.subprocess = null,
output: std.ArrayList(u8),
parent: tp.pid,
tag: [:0]const u8,
logger: log.Logger,
stdin_behavior: std.ChildProcess.StdIo,
const Receiver = tp.Receiver(*Process);
pub fn create(a: std.mem.Allocator, query: []const u8, tag: [:0]const u8, stdin_behavior: std.ChildProcess.StdIo) Error!tp.pid {
const self = try a.create(Process);
self.* = .{
.a = a,
.query = try a.dupe(u8, query),
.receiver = Receiver.init(receive, self),
.output = std.ArrayList(u8).init(a),
.parent = tp.self_pid().clone(),
.tag = try a.dupeZ(u8, tag),
.logger = log.logger(@typeName(Self)),
.stdin_behavior = stdin_behavior,
};
return tp.spawn_link(self.a, self, Process.start, tag) catch |e| tp.exit_error(e);
}
fn deinit(self: *Process) void {
self.output.deinit();
self.a.free(self.query);
self.close() catch {};
}
fn close(self: *Process) tp.result {
if (self.sp) |*sp| {
defer self.sp = null;
try sp.close();
}
}
fn start(self: *Process) tp.result {
_ = tp.set_trap(true);
const args = tp.message.fmt(.{
ripgrep_binary,
// "--line-buffered",
"--fixed-strings",
"--json",
self.query,
});
self.sp = tp.subprocess.init(self.a, args, module_name, self.stdin_behavior) catch |e| return tp.exit_error(e);
tp.receive(&self.receiver);
}
fn receive(self: *Process, _: tp.pid_ref, m: tp.message) tp.result {
errdefer self.deinit();
var bytes: []u8 = "";
if (try m.match(.{ "input", tp.extract(&bytes) })) {
const sp = if (self.sp) |sp| sp else return tp.exit_error(error.Closed);
try sp.send(bytes);
} else if (try m.match(.{"close"})) {
try self.close();
} else if (try m.match(.{ module_name, "stdout", tp.extract(&bytes) })) {
self.handle_output(bytes) catch |e| return tp.exit_error(e);
} else if (try m.match(.{ module_name, "term", tp.more })) {
self.handle_terminated() catch |e| return tp.exit_error(e);
} else if (try m.match(.{ module_name, "stderr", tp.extract(&bytes) })) {
self.logger.print("ERR: {s}", .{bytes});
} else if (try m.match(.{ "exit", "normal" })) {
return tp.exit_normal();
} else {
self.logger.err("receive", tp.unexpected(m));
return tp.unexpected(m);
}
}
fn handle_output(self: *Process, bytes: []u8) !void {
try self.output.appendSlice(bytes);
// @import("log").logger(module_name).print("{s}", .{bytes}) catch {};
}
fn handle_terminated(self: *Process) !void {
const output = try self.output.toOwnedSlice();
var it = std.mem.splitScalar(u8, output, '\n');
while (it.next()) |json| {
if (json.len == 0) continue;
var msg_buf: [tp.max_message_size]u8 = undefined;
const msg: tp.message = .{ .buf = try cbor.fromJson(json, &msg_buf) };
try self.dispatch(msg);
// var buf: [tp.max_message_size]u8 = undefined;
// @import("log").logger(module_name).print("json: {s}", .{try msg.to_json(&buf)}) catch {};
}
// @import("log").logger(module_name).print("done", .{}) catch {};
try self.parent.send(.{ self.tag, "done" });
}
fn dispatch(self: *Process, m: tp.message) !void {
var obj = std.json.ObjectMap.init(self.a);
defer obj.deinit();
if (try m.match(tp.extract(&obj))) {
if (obj.get("type")) |*val| {
if (std.mem.eql(u8, "match", val.string))
if (obj.get("data")) |*data| switch (data.*) {
.object => |*o| try self.dispatch_match(o),
else => {},
};
}
}
}
fn get_match_string(obj: *const std.json.ObjectMap, name: []const u8) ?[]const u8 {
return if (obj.get(name)) |*val| switch (val.*) {
.object => |*o| if (o.get("text")) |*val_| switch (val_.*) {
.string => |s| if (std.mem.eql(u8, "<stdin>", s)) null else s,
else => null,
} else null,
else => null,
} else null;
}
fn dispatch_match(self: *Process, obj: *const std.json.ObjectMap) !void {
const path: ?[]const u8 = get_match_string(obj, "path");
const lines: ?[]const u8 = get_match_string(obj, "lines");
const line = if (obj.get("line_number")) |*val| switch (val.*) {
.integer => |i| i,
else => return,
} else return;
if (obj.get("submatches")) |*val| switch (val.*) {
.array => |*a| try self.dispatch_submatches(path, line, a, lines),
else => return,
};
}
fn dispatch_submatches(self: *Process, path: ?[]const u8, line: i64, arr: *const std.json.Array, lines: ?[]const u8) !void {
for (arr.items) |*item| switch (item.*) {
.object => |*o| try self.dispatch_submatch(path, line, o, lines),
else => {},
};
}
fn dispatch_submatch(self: *Process, path: ?[]const u8, line: i64, obj: *const std.json.ObjectMap, lines: ?[]const u8) !void {
const begin = if (obj.get("start")) |*val| switch (val.*) {
.integer => |i| i,
else => return,
} else return;
const end = if (obj.get("end")) |*val| switch (val.*) {
.integer => |i| i,
else => return,
} else return;
if (path) |p| {
const match_text = if (lines) |l|
if (l[l.len - 1] == '\n') l[0 .. l.len - 2] else l
else
"";
try self.parent.send(.{ self.tag, p, line, begin, line, end, match_text });
} else {
try self.parent.send(.{ self.tag, line, begin, line, end });
}
}
};

112
src/syntax.zig Normal file
View file

@ -0,0 +1,112 @@
const std = @import("std");
const treez = @import("treez");
const Self = @This();
pub const Edit = treez.InputEdit;
pub const FileType = @import("file_type.zig");
pub const Range = treez.Range;
pub const Point = treez.Point;
const Language = treez.Language;
const Parser = treez.Parser;
const Query = treez.Query;
const Tree = treez.Tree;
a: std.mem.Allocator,
lang: *const Language,
file_type: *const FileType,
parser: *Parser,
query: *Query,
injections: *Query,
tree: ?*Tree = null,
pub fn create(file_type: *const FileType, a: std.mem.Allocator, content: []const u8) !*Self {
const self = try a.create(Self);
self.* = .{
.a = a,
.lang = file_type.lang_fn() orelse std.debug.panic("tree-sitter parser function failed for language: {d}", .{file_type.name}),
.file_type = file_type,
.parser = try Parser.create(),
.query = try Query.create(self.lang, file_type.highlights),
.injections = try Query.create(self.lang, file_type.highlights),
};
errdefer self.destroy();
try self.parser.setLanguage(self.lang);
try self.parse(content);
return self;
}
pub fn create_file_type(a: std.mem.Allocator, content: []const u8, lang_name: []const u8) !*Self {
const file_type = FileType.get_by_name(lang_name) orelse return error.NotFound;
return create(file_type, a, content);
}
pub fn create_guess_file_type(a: std.mem.Allocator, content: []const u8, file_path: ?[]const u8) !*Self {
const file_type = FileType.guess(file_path, content) orelse return error.NotFound;
return create(file_type, a, content);
}
pub fn destroy(self: *Self) void {
if (self.tree) |tree| tree.destroy();
self.query.destroy();
self.parser.destroy();
self.a.destroy(self);
}
fn parse(self: *Self, content: []const u8) !void {
if (self.tree) |tree| tree.destroy();
self.tree = try self.parser.parseString(null, content);
}
pub fn refresh_full(self: *Self, content: []const u8) !void {
return self.parse(content);
}
pub fn edit(self: *Self, ed: Edit) void {
if (self.tree) |tree| tree.edit(&ed);
}
pub fn refresh(self: *Self, content: []const u8) !void {
const old_tree = self.tree;
defer if (old_tree) |tree| tree.destroy();
self.tree = try self.parser.parseString(old_tree, content);
}
fn CallBack(comptime T: type) type {
return fn (ctx: T, sel: Range, scope: []const u8, id: u32, capture_idx: usize) error{Stop}!void;
}
pub fn render(self: *const Self, ctx: anytype, comptime cb: CallBack(@TypeOf(ctx)), range: ?Range) !void {
const cursor = try Query.Cursor.create();
defer cursor.destroy();
const tree = if (self.tree) |p| p else return;
cursor.execute(self.query, tree.getRootNode());
if (range) |r| cursor.setPointRange(r.start_point, r.end_point);
while (cursor.nextMatch()) |match| {
var idx: usize = 0;
for (match.captures()) |capture| {
try cb(ctx, capture.node.getRange(), self.query.getCaptureNameForId(capture.id), capture.id, idx);
idx += 1;
}
}
}
pub fn highlights_at_point(self: *const Self, ctx: anytype, comptime cb: CallBack(@TypeOf(ctx)), point: Point) void {
const cursor = Query.Cursor.create() catch return;
defer cursor.destroy();
const tree = if (self.tree) |p| p else return;
cursor.execute(self.query, tree.getRootNode());
cursor.setPointRange(.{ .row = point.row, .column = 0 }, .{ .row = point.row + 1, .column = 0 });
while (cursor.nextMatch()) |match| {
for (match.captures()) |capture| {
const range = capture.node.getRange();
const start = range.start_point;
const end = range.end_point;
const scope = self.query.getCaptureNameForId(capture.id);
if (start.row == point.row and start.row <= point.column and point.column < end.column)
cb(ctx, range, scope, capture.id, 0) catch return;
break;
}
}
return;
}

63
src/text_manip.zig Normal file
View file

@ -0,0 +1,63 @@
const std = @import("std");
const TextWriter = std.ArrayList(u8).Writer;
pub fn find_first_non_ws(text: []const u8) ?usize {
for (text, 0..) |c, i| if (c == ' ' or c == '\t') continue else return i;
return null;
}
pub fn find_prefix(prefix: []const u8, text: []const u8) ?usize {
var start: usize = 0;
var pos: usize = 0;
var in_prefix: bool = false;
for (text, 0..) |c, i| {
if (!in_prefix) {
if (c == ' ' or c == '\t')
continue
else {
in_prefix = true;
start = i;
}
}
if (in_prefix) {
if (c == prefix[pos]) {
pos += 1;
if (prefix.len > pos) continue else return start;
} else return null;
}
}
return null;
}
fn toggle_prefix_in_line(prefix: []const u8, text: []const u8, writer: TextWriter) !void {
if (find_prefix(prefix, text)) |pos| {
_ = try writer.write(text[0..pos]);
if (text.len > pos + prefix.len) {
_ = try if (text[pos + prefix.len] == ' ')
writer.write(text[pos + 1 + prefix.len ..])
else
writer.write(text[pos + prefix.len ..]);
}
} else if (find_first_non_ws(text)) |pos| {
_ = try writer.write(text[0..pos]);
_ = try writer.write(prefix);
_ = try writer.write(" ");
_ = try writer.write(text[pos..]);
} else {
_ = try writer.write(prefix);
_ = try writer.write(text);
}
}
pub fn toggle_prefix_in_text(prefix: []const u8, text: []const u8, a: std.mem.Allocator) ![]const u8 {
var result = try std.ArrayList(u8).initCapacity(a, prefix.len + text.len);
const writer = result.writer();
var pos: usize = 0;
while (std.mem.indexOfScalarPos(u8, text, pos, '\n')) |next| {
try toggle_prefix_in_line(prefix, text[pos..next], writer);
_ = try writer.write("\n");
pos = next + 1;
}
return result.toOwnedSlice();
}

9
src/tracy_noop.zig Normal file
View file

@ -0,0 +1,9 @@
pub fn initZone(_: anytype, _: anytype) Zone {
return .{};
}
pub const Zone = struct {
pub fn deinit(_: @This()) void {}
};
pub fn frameMark() void {}

40
src/tui/Box.zig Normal file
View file

@ -0,0 +1,40 @@
const Plane = @import("notcurses").Plane;
const Self = @This();
y: usize = 0,
x: usize = 0,
h: usize = 1,
w: usize = 1,
pub fn opts(self: Self, name_: [:0]const u8) Plane.Options {
return self.opts_flags(name_, 0);
}
pub fn opts_vscroll(self: Self, name_: [:0]const u8) Plane.Options {
return self.opts_flags(name_, Plane.option.VSCROLL);
}
fn opts_flags(self: Self, name_: [:0]const u8, flags: u64) Plane.Options {
return Plane.Options{
.y = @intCast(self.y),
.x = @intCast(self.x),
.rows = @intCast(self.h),
.cols = @intCast(self.w),
.name = name_,
.flags = flags,
};
}
pub fn from(n: Plane) Self {
return .{
.y = @intCast(n.abs_y()),
.x = @intCast(n.abs_x()),
.h = @intCast(n.dim_y()),
.w = @intCast(n.dim_x()),
};
}
pub fn is_abs_coord_inside(self: Self, y: usize, x: usize) bool {
return y >= self.y and y < self.y + self.h and x >= self.x and x < self.x + self.w;
}

175
src/tui/EventHandler.zig Normal file
View file

@ -0,0 +1,175 @@
const std = @import("std");
const tp = @import("thespian");
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const Self = @This();
const EventHandler = Self;
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
deinit: *const fn (ctx: *anyopaque) void,
send: *const fn (ctx: *anyopaque, from: tp.pid_ref, m: tp.message) tp.result,
type_name: []const u8,
};
pub fn to_owned(pimpl: anytype) Self {
const impl = @typeInfo(@TypeOf(pimpl));
const child: type = impl.Pointer.child;
return .{
.ptr = pimpl,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(ctx: *anyopaque) void {
return child.deinit(@as(*child, @ptrCast(@alignCast(ctx))));
}
}.deinit,
.send = struct {
pub fn receive(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) tp.result {
_ = try child.receive(@as(*child, @ptrCast(@alignCast(ctx))), from_, m);
}
}.receive,
},
};
}
pub fn to_unowned(pimpl: anytype) Self {
const impl = @typeInfo(@TypeOf(pimpl));
const child: type = impl.Pointer.child;
return .{
.ptr = pimpl,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(_: *anyopaque) void {}
}.deinit,
.send = if (@hasDecl(child, "send")) struct {
pub fn send(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) tp.result {
_ = try child.send(@as(*child, @ptrCast(@alignCast(ctx))), from_, m);
}
}.send else struct {
pub fn receive(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) tp.result {
_ = try child.receive(@as(*child, @ptrCast(@alignCast(ctx))), from_, m);
}
}.receive,
},
};
}
pub fn bind(pimpl: anytype, comptime f: *const fn (ctx: @TypeOf(pimpl), from: tp.pid_ref, m: tp.message) tp.result) Self {
const impl = @typeInfo(@TypeOf(pimpl));
const child: type = impl.Pointer.child;
return .{
.ptr = pimpl,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(_: *anyopaque) void {}
}.deinit,
.send = struct {
pub fn receive(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) tp.result {
return @call(.auto, f, .{ @as(*child, @ptrCast(@alignCast(ctx))), from_, m });
}
}.receive,
},
};
}
pub fn deinit(self: Self) void {
return self.vtable.deinit(self.ptr);
}
pub fn dynamic_cast(self: Self, comptime T: type) ?*T {
return if (std.mem.eql(u8, self.vtable.type_name, @typeName(T)))
@as(*T, @ptrCast(@alignCast(self.ptr)))
else
null;
}
pub fn msg(self: Self, m: anytype) tp.result {
return self.vtable.send(self.ptr, tp.self_pid(), tp.message.fmt(m));
}
pub fn send(self: Self, from_: tp.pid_ref, m: tp.message) tp.result {
return self.vtable.send(self.ptr, from_, m);
}
pub fn empty(a: Allocator) !Self {
const child: type = struct {};
const widget = try a.create(child);
widget.* = .{};
return .{
.ptr = widget,
.plane = &widget.plane,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(ctx: *anyopaque, a_: Allocator) void {
return a_.destroy(@as(*child, @ptrCast(@alignCast(ctx))));
}
}.deinit,
.send = struct {
pub fn receive(_: *anyopaque, _: tp.pid_ref, _: tp.message) tp.result {
return false;
}
}.receive,
},
};
}
pub const List = struct {
a: Allocator,
list: ArrayList(EventHandler),
recursion_check: bool = false,
pub fn init(a: Allocator) List {
return .{
.a = a,
.list = ArrayList(EventHandler).init(a),
};
}
pub fn deinit(self: *List) void {
for (self.list.items) |*i|
i.deinit();
self.list.deinit();
}
pub fn add(self: *List, h: EventHandler) !void {
(try self.list.addOne()).* = h;
}
pub fn remove(self: *List, h: EventHandler) !void {
return self.remove_ptr(h.ptr);
}
pub fn remove_ptr(self: *List, p_: *anyopaque) void {
for (self.list.items, 0..) |*p, i|
if (p.ptr == p_)
self.list.orderedRemove(i).deinit();
}
pub fn msg(self: *const List, m: anytype) tp.result {
return self.send(tp.self_pid(), tp.message.fmt(m));
}
pub fn send(self: *const List, from: tp.pid_ref, m: tp.message) tp.result {
if (self.recursion_check)
unreachable;
const self_nonconst = @constCast(self);
self_nonconst.recursion_check = true;
defer self_nonconst.recursion_check = false;
tp.trace(tp.channel.event, m);
var buf: [tp.max_message_size]u8 = undefined;
@memcpy(buf[0..m.buf.len], m.buf);
const m_: tp.message = .{ .buf = buf[0..m.buf.len] };
var e: ?error{Exit} = null;
for (self.list.items) |*i|
i.send(from, m_) catch |e_| {
e = e_;
};
return if (e) |e_| e_;
}
};

138
src/tui/MessageFilter.zig Normal file
View file

@ -0,0 +1,138 @@
const std = @import("std");
const tp = @import("thespian");
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const Self = @This();
const MessageFilter = Self;
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
deinit: *const fn (ctx: *anyopaque) void,
filter: *const fn (ctx: *anyopaque, from: tp.pid_ref, m: tp.message) error{Exit}!bool,
type_name: []const u8,
};
pub fn to_owned(pimpl: anytype) Self {
const impl = @typeInfo(@TypeOf(pimpl));
const child: type = impl.Pointer.child;
return .{
.ptr = pimpl,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(ctx: *anyopaque) void {
return child.deinit(@as(*child, @ptrCast(@alignCast(ctx))));
}
}.deinit,
.filter = struct {
pub fn filter(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
return child.filter(@as(*child, @ptrCast(@alignCast(ctx))), from_, m);
}
}.filter,
},
};
}
pub fn to_unowned(pimpl: anytype) Self {
const impl = @typeInfo(@TypeOf(pimpl));
const child: type = impl.Pointer.child;
return .{
.ptr = pimpl,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(_: *anyopaque) void {}
}.deinit,
.filter = struct {
pub fn filter(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
return child.filter(@as(*child, @ptrCast(@alignCast(ctx))), from_, m);
}
}.filter,
},
};
}
pub fn bind(pimpl: anytype, comptime f: *const fn (ctx: @TypeOf(pimpl), from: tp.pid_ref, m: tp.message) error{Exit}!bool) Self {
const impl = @typeInfo(@TypeOf(pimpl));
const child: type = impl.Pointer.child;
return .{
.ptr = pimpl,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(_: *anyopaque) void {}
}.deinit,
.filter = struct {
pub fn filter(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
return @call(.auto, f, .{ @as(*child, @ptrCast(@alignCast(ctx))), from_, m });
}
}.filter,
},
};
}
pub fn deinit(self: Self) void {
return self.vtable.deinit(self.ptr);
}
pub fn dynamic_cast(self: Self, comptime T: type) ?*T {
return if (std.mem.eql(u8, self.vtable.type_name, @typeName(T)))
@as(*T, @ptrCast(@alignCast(self.ptr)))
else
null;
}
pub fn filter(self: Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
return self.vtable.filter(self.ptr, from_, m);
}
pub const List = struct {
a: Allocator,
list: ArrayList(MessageFilter),
pub fn init(a: Allocator) List {
return .{
.a = a,
.list = ArrayList(MessageFilter).init(a),
};
}
pub fn deinit(self: *List) void {
for (self.list.items) |*i|
i.deinit();
self.list.deinit();
}
pub fn add(self: *List, h: MessageFilter) !void {
(try self.list.addOne()).* = h;
// @import("log").logger("MessageFilter").print("add: {d} {s}", .{ self.list.items.len, self.list.items[self.list.items.len - 1].vtable.type_name });
}
pub fn remove(self: *List, h: MessageFilter) !void {
return self.remove_ptr(h.ptr);
}
pub fn remove_ptr(self: *List, p_: *anyopaque) void {
for (self.list.items, 0..) |*p, i|
if (p.ptr == p_)
self.list.orderedRemove(i).deinit();
}
pub fn filter(self: *const List, from: tp.pid_ref, m: tp.message) error{Exit}!bool {
var buf: [tp.max_message_size]u8 = undefined;
@memcpy(buf[0..m.buf.len], m.buf);
const m_: tp.message = .{ .buf = buf[0..m.buf.len] };
var e: ?error{Exit} = null;
for (self.list.items) |*i| {
const consume = i.filter(from, m_) catch |e_| ret: {
e = e_;
break :ret false;
};
if (consume)
return true;
}
return if (e) |e_| e_ else false;
}
};

273
src/tui/Widget.zig Normal file
View file

@ -0,0 +1,273 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
pub const Box = @import("Box.zig");
pub const EventHandler = @import("EventHandler.zig");
pub const Theme = @import("theme");
pub const themes = @import("themes").themes;
pub const scopes = @import("themes").scopes;
ptr: *anyopaque,
plane: *nc.Plane,
vtable: *const VTable,
const Self = @This();
pub const WalkFn = *const fn (ctx: *anyopaque, w: *Self) bool;
pub const Direction = enum { horizontal, vertical };
pub const Layout = union(enum) {
dynamic,
static: usize,
pub inline fn eql(self: Layout, other: Layout) bool {
return switch (self) {
.dynamic => switch (other) {
.dynamic => true,
.static => false,
},
.static => |s| switch (other) {
.dynamic => false,
.static => |o| s == o,
},
};
}
};
pub const VTable = struct {
deinit: *const fn (ctx: *anyopaque, a: Allocator) void,
send: *const fn (ctx: *anyopaque, from: tp.pid_ref, m: tp.message) error{Exit}!bool,
update: *const fn (ctx: *anyopaque) void,
render: *const fn (ctx: *anyopaque, theme: *const Theme) bool,
resize: *const fn (ctx: *anyopaque, pos: Box) void,
layout: *const fn (ctx: *anyopaque) Layout,
subscribe: *const fn (ctx: *anyopaque, h: EventHandler) error{NotSupported}!void,
unsubscribe: *const fn (ctx: *anyopaque, h: EventHandler) error{NotSupported}!void,
get: *const fn (ctx: *anyopaque, name_: []const u8) ?*Self,
walk: *const fn (ctx: *anyopaque, walk_ctx: *anyopaque, f: WalkFn) bool,
type_name: []const u8,
};
pub fn to(pimpl: anytype) Self {
const impl = @typeInfo(@TypeOf(pimpl));
const child: type = impl.Pointer.child;
return .{
.ptr = pimpl,
.plane = &pimpl.plane,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(ctx: *anyopaque, a: Allocator) void {
return child.deinit(@as(*child, @ptrCast(@alignCast(ctx))), a);
}
}.deinit,
.send = if (@hasDecl(child, "receive")) struct {
pub fn f(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
return child.receive(@as(*child, @ptrCast(@alignCast(ctx))), from_, m);
}
}.f else struct {
pub fn f(_: *anyopaque, _: tp.pid_ref, _: tp.message) error{Exit}!bool {
return false;
}
}.f,
.update = if (@hasDecl(child, "update")) struct {
pub fn f(ctx: *anyopaque) void {
return child.update(@as(*child, @ptrCast(@alignCast(ctx))));
}
}.f else struct {
pub fn f(_: *anyopaque) void {}
}.f,
.render = if (@hasDecl(child, "render")) struct {
pub fn f(ctx: *anyopaque, theme: *const Theme) bool {
return child.render(@as(*child, @ptrCast(@alignCast(ctx))), theme);
}
}.f else struct {
pub fn f(_: *anyopaque, _: *const Theme) bool {
return false;
}
}.f,
.resize = if (@hasDecl(child, "handle_resize")) struct {
pub fn f(ctx: *anyopaque, pos: Box) void {
return child.handle_resize(@as(*child, @ptrCast(@alignCast(ctx))), pos);
}
}.f else struct {
pub fn f(ctx: *anyopaque, pos: Box) void {
const self: *child = @ptrCast(@alignCast(ctx));
self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return;
self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return;
}
}.f,
.layout = if (@hasDecl(child, "layout")) struct {
pub fn f(ctx: *anyopaque) Layout {
return child.layout(@as(*child, @ptrCast(@alignCast(ctx))));
}
}.f else struct {
pub fn f(_: *anyopaque) Layout {
return .dynamic;
}
}.f,
.subscribe = struct {
pub fn subscribe(ctx: *anyopaque, h: EventHandler) error{NotSupported}!void {
return if (comptime @hasDecl(child, "subscribe"))
child.subscribe(@as(*child, @ptrCast(@alignCast(ctx))), h)
else
error.NotSupported;
}
}.subscribe,
.unsubscribe = struct {
pub fn unsubscribe(ctx: *anyopaque, h: EventHandler) error{NotSupported}!void {
return if (comptime @hasDecl(child, "unsubscribe"))
child.unsubscribe(@as(*child, @ptrCast(@alignCast(ctx))), h)
else
error.NotSupported;
}
}.unsubscribe,
.get = struct {
pub fn get(ctx: *anyopaque, name_: []const u8) ?*Self {
return if (comptime @hasDecl(child, "get")) child.get(@as(*child, @ptrCast(@alignCast(ctx))), name_) else null;
}
}.get,
.walk = struct {
pub fn walk(ctx: *anyopaque, walk_ctx: *anyopaque, f: WalkFn) bool {
return if (comptime @hasDecl(child, "walk")) child.walk(@as(*child, @ptrCast(@alignCast(ctx))), walk_ctx, f) else false;
}
}.walk,
},
};
}
pub fn dynamic_cast(self: Self, comptime T: type) ?*T {
return if (std.mem.eql(u8, self.vtable.type_name, @typeName(T)))
@as(*T, @ptrCast(@alignCast(self.ptr)))
else
null;
}
pub fn need_render() void {
tp.self_pid().send(.{"render"}) catch {};
}
pub fn need_reflow() void {
tp.self_pid().send(.{"reflow"}) catch {};
}
pub fn name(self: Self, buf: []u8) []u8 {
return self.plane.name(buf);
}
pub fn box(self: Self) Box {
return Box.from(self.plane.*);
}
pub fn deinit(self: Self, a: Allocator) void {
return self.vtable.deinit(self.ptr, a);
}
pub fn msg(self: *const Self, m: anytype) error{Exit}!bool {
return self.vtable.send(self.ptr, tp.self_pid(), tp.message.fmt(m));
}
pub fn send(self: *const Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
return self.vtable.send(self.ptr, from_, m);
}
pub fn update(self: Self) void {
return self.vtable.update(self.ptr);
}
pub fn render(self: Self, theme: *const Theme) bool {
return self.vtable.render(self.ptr, theme);
}
pub fn resize(self: Self, pos: Box) void {
return self.vtable.resize(self.ptr, pos);
}
pub fn layout(self: Self) Layout {
return self.vtable.layout(self.ptr);
}
pub fn subscribe(self: Self, h: EventHandler) !void {
return self.vtable.subscribe(self.ptr, h);
}
pub fn unsubscribe(self: Self, h: EventHandler) !void {
return self.vtable.unsubscribe(self.ptr, h);
}
pub fn get(self: *Self, name_: []const u8) ?*Self {
var buf: [256]u8 = undefined;
return if (std.mem.eql(u8, self.plane.name(&buf), name_))
self
else
self.vtable.get(self.ptr, name_);
}
pub fn walk(self: *Self, walk_ctx: *anyopaque, f: WalkFn) bool {
return if (self.vtable.walk(self.ptr, walk_ctx, f)) true else f(walk_ctx, self);
}
pub fn empty(a: Allocator, parent: nc.Plane, layout_: Layout) !Self {
const child: type = struct { plane: nc.Plane, layout: Layout };
const widget = try a.create(child);
const n = try nc.Plane.init(&(Box{}).opts("empty"), parent);
widget.* = .{ .plane = n, .layout = layout_ };
return .{
.ptr = widget,
.plane = &widget.plane,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(ctx: *anyopaque, a_: Allocator) void {
const self: *child = @ptrCast(@alignCast(ctx));
self.plane.deinit();
a_.destroy(self);
}
}.deinit,
.send = struct {
pub fn receive(_: *anyopaque, _: tp.pid_ref, _: tp.message) error{Exit}!bool {
return false;
}
}.receive,
.update = struct {
pub fn update(_: *anyopaque) void {}
}.update,
.render = struct {
pub fn render(_: *anyopaque, _: *const Theme) bool {
return false;
}
}.render,
.resize = struct {
pub fn resize(_: *anyopaque, _: Box) void {}
}.resize,
.layout = struct {
pub fn layout(ctx: *anyopaque) Layout {
const self: *child = @ptrCast(@alignCast(ctx));
return self.layout;
}
}.layout,
.subscribe = struct {
pub fn subscribe(_: *anyopaque, _: EventHandler) error{NotSupported}!void {
return error.NotSupported;
}
}.subscribe,
.unsubscribe = struct {
pub fn unsubscribe(_: *anyopaque, _: EventHandler) error{NotSupported}!void {
return error.NotSupported;
}
}.unsubscribe,
.get = struct {
pub fn get(_: *anyopaque, _: []const u8) ?*Self {
return null;
}
}.get,
.walk = struct {
pub fn walk(_: *anyopaque, _: *anyopaque, _: WalkFn) bool {
return false;
}
}.walk,
},
};
}

223
src/tui/WidgetList.zig Normal file
View file

@ -0,0 +1,223 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const nc = @import("notcurses");
const tp = @import("thespian");
const Widget = @import("Widget.zig");
const Box = @import("Box.zig");
const Self = @This();
pub const Direction = Widget.Direction;
pub const Layout = Widget.Layout;
const WidgetState = struct {
widget: Widget,
layout: Layout = .{},
};
plane: nc.Plane,
parent: nc.Plane,
a: Allocator,
widgets: ArrayList(WidgetState),
layout: Layout,
direction: Direction,
box: ?Widget.Box = null,
pub fn createH(a: Allocator, parent: Widget, name: [:0]const u8, layout_: Layout) !*Self {
const self: *Self = try a.create(Self);
self.* = try init(a, parent, name, .horizontal, layout_);
return self;
}
pub fn createV(a: Allocator, parent: Widget, name: [:0]const u8, layout_: Layout) !*Self {
const self: *Self = try a.create(Self);
self.* = try init(a, parent, name, .vertical, layout_);
return self;
}
fn init(a: Allocator, parent: Widget, name: [:0]const u8, dir: Direction, layout_: Layout) !Self {
return .{
.plane = try nc.Plane.init(&(Box{}).opts(name), parent.plane.*),
.parent = parent.plane.*,
.a = a,
.widgets = ArrayList(WidgetState).init(a),
.layout = layout_,
.direction = dir,
};
}
pub fn widget(self: *Self) Widget {
return Widget.to(self);
}
pub fn layout(self: *Self) Widget.Layout {
return self.layout;
}
pub fn deinit(self: *Self, a: std.mem.Allocator) void {
for (self.widgets.items) |*w|
w.widget.deinit(self.a);
self.widgets.deinit();
self.plane.deinit();
a.destroy(self);
}
pub fn add(self: *Self, w_: Widget) !void {
_ = try self.addP(w_);
}
pub fn addP(self: *Self, w_: Widget) !*Widget {
var w: *WidgetState = try self.widgets.addOne();
w.widget = w_;
w.layout = w_.layout();
return &w.widget;
}
pub fn remove(self: *Self, w: Widget) void {
for (self.widgets.items, 0..) |p, i| if (p.widget.ptr == w.ptr)
self.widgets.orderedRemove(i).widget.deinit(self.a);
}
pub fn empty(self: *const Self) bool {
return self.widgets.items.len == 0;
}
pub fn swap(self: *Self, n: usize, w: Widget) Widget {
const old = self.widgets.items[n];
self.widgets.items[n].widget = w;
self.widgets.items[n].layout = w.layout();
return old.widget;
}
pub fn replace(self: *Self, n: usize, w: Widget) void {
const old = self.swap(n, w);
old.deinit(self.a);
}
pub fn send(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool {
for (self.widgets.items) |*w|
if (try w.widget.send(from, m))
return true;
return false;
}
pub fn update(self: *Self) void {
for (self.widgets.items) |*w|
w.widget.update();
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
for (self.widgets.items) |*w| if (!w.layout.eql(w.widget.layout())) {
self.refresh_layout();
break;
};
var more = false;
for (self.widgets.items) |*w|
if (w.widget.render(theme)) {
more = true;
};
return more;
}
pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
for (self.widgets.items) |*w|
if (try w.widget.send(from_, m))
return true;
return false;
}
fn get_size_a(self: *Self, pos: *Widget.Box) *usize {
return switch (self.direction) {
.vertical => &pos.h,
.horizontal => &pos.w,
};
}
fn get_size_b(self: *Self, pos: *Widget.Box) *usize {
return switch (self.direction) {
.vertical => &pos.w,
.horizontal => &pos.h,
};
}
fn get_loc_a(self: *Self, pos: *Widget.Box) *usize {
return switch (self.direction) {
.vertical => &pos.y,
.horizontal => &pos.x,
};
}
fn get_loc_b(self: *Self, pos: *Widget.Box) *usize {
return switch (self.direction) {
.vertical => &pos.x,
.horizontal => &pos.y,
};
}
pub fn resize(self: *Self, pos: Widget.Box) void {
return self.handle_resize(pos);
}
fn refresh_layout(self: *Self) void {
return if (self.box) |box| self.handle_resize(box);
}
pub fn handle_resize(self: *Self, pos_: Widget.Box) void {
self.box = pos_;
var pos = pos_;
const total = self.get_size_a(&pos).*;
var avail = total;
var statics: usize = 0;
var dynamics: usize = 0;
for (self.widgets.items) |*w| {
w.layout = w.widget.layout();
switch (w.layout) {
.dynamic => {
dynamics += 1;
},
.static => |val| {
statics += 1;
avail = if (avail > val) avail - val else 0;
},
}
}
const dyn_size = avail / if (dynamics > 0) dynamics else 1;
const rounded: usize = if (dyn_size * dynamics < avail) avail - dyn_size * dynamics else 0;
var cur_loc: usize = self.get_loc_a(&pos).*;
var first = true;
for (self.widgets.items) |*w| {
var w_pos: Box = .{};
const size = switch (w.layout) {
.dynamic => if (first) val: {
first = false;
break :val dyn_size + rounded;
} else dyn_size,
.static => |val| val,
};
self.get_size_a(&w_pos).* = size;
self.get_loc_a(&w_pos).* = cur_loc;
cur_loc += size;
self.get_size_b(&w_pos).* = self.get_size_b(&pos).*;
self.get_loc_b(&w_pos).* = self.get_loc_b(&pos).*;
w.widget.resize(w_pos);
}
}
pub fn get(self: *Self, name_: []const u8) ?*Widget {
for (self.widgets.items) |*w|
if (w.widget.get(name_)) |p|
return p;
return null;
}
pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool {
for (self.widgets.items) |*w|
if (w.widget.walk(walk_ctx, f)) return true;
return false;
}

94
src/tui/WidgetStack.zig Normal file
View file

@ -0,0 +1,94 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const eql = std.mem.eql;
const nc = @import("notcurses");
const tp = @import("thespian");
const Widget = @import("Widget.zig");
const Self = @This();
a: Allocator,
widgets: ArrayList(Widget),
pub fn init(a_: Allocator) Self {
return .{
.a = a_,
.widgets = ArrayList(Widget).init(a_),
};
}
pub fn deinit(self: *Self) void {
for (self.widgets.items) |*widget|
widget.deinit(self.a);
self.widgets.deinit();
}
pub fn addWidget(self: *Self, widget: Widget) !void {
(try self.widgets.addOne()).* = widget;
}
pub fn swapWidget(self: *Self, n: usize, widget: Widget) Widget {
const old = self.widgets.items[n];
self.widgets.items[n] = widget;
return old;
}
pub fn replaceWidget(self: *Self, n: usize, widget: Widget) void {
const old = self.swapWidget(n, widget);
old.deinit(self.a);
}
pub fn deleteWidget(self: *Self, name: []const u8) bool {
for (self.widgets.items, 0..) |*widget, i| {
var buf: [64]u8 = undefined;
const wname = widget.name(&buf);
if (eql(u8, wname, name)) {
self.widgets.orderedRemove(i).deinit(self.a);
return true;
}
}
return false;
}
pub fn findWidget(self: *Self, name: []const u8) ?*Widget {
for (self.widgets.items) |*widget| {
var buf: [64]u8 = undefined;
const wname = widget.name(&buf);
if (eql(u8, wname, name))
return widget;
}
return null;
}
pub fn send(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool {
for (self.widgets.items) |*widget|
if (try widget.send(from, m))
return true;
return false;
}
pub fn update(self: *Self) void {
for (self.widgets.items) |*widget| widget.update();
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
var more = false;
for (self.widgets.items) |*widget|
if (widget.render(theme)) {
more = true;
};
return more;
}
pub fn resize(self: *Self, pos: Widget.Box) void {
for (self.widgets.items) |*widget|
widget.resize(pos);
}
pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool {
for (self.widgets.items) |*w|
if (w.walk(walk_ctx, f)) return true;
return false;
}

194
src/tui/command.zig Normal file
View file

@ -0,0 +1,194 @@
const std = @import("std");
const tp = @import("thespian");
const log = @import("log");
const tui = @import("tui.zig");
pub const ID = usize;
pub const Context = struct {
args: tp.message = .{},
pub fn fmt(value: anytype) Context {
return .{ .args = tp.message.fmtbuf(&context_buffer, value) catch unreachable };
}
};
threadlocal var context_buffer: [tp.max_message_size]u8 = undefined;
pub const fmt = Context.fmt;
const Vtable = struct {
id: ID = 0,
name: []const u8,
run: *const fn (self: *Vtable, ctx: Context) tp.result,
};
pub fn Closure(comptime T: type) type {
return struct {
vtbl: Vtable,
f: FunT,
data: T,
const FunT: type = *const fn (T, ctx: Context) tp.result;
const Self = @This();
pub fn init(f: FunT, data: T, name: []const u8) Self {
return .{
.vtbl = .{
.run = run,
.name = name,
},
.f = f,
.data = data,
};
}
pub fn register(self: *Self) !void {
self.vtbl.id = try addCommand(&self.vtbl);
// try log.logger("cmd").print("addCommand({s}) => {d}", .{ self.vtbl.name, self.vtbl.id });
}
pub fn unregister(self: *Self) void {
removeCommand(self.vtbl.id);
}
fn run(vtbl: *Vtable, ctx: Context) tp.result {
const self: *Self = fromVtable(vtbl);
return self.f(self.data, ctx);
}
fn fromVtable(vtbl: *Vtable) *Self {
return @fieldParentPtr(Self, "vtbl", vtbl);
}
};
}
const CommandTable = std.ArrayList(?*Vtable);
var commands: CommandTable = CommandTable.init(std.heap.page_allocator);
fn addCommand(cmd: *Vtable) !ID {
try commands.append(cmd);
return commands.items.len - 1;
}
pub fn removeCommand(id: ID) void {
commands.items[id] = null;
}
pub fn execute(id: ID, ctx: Context) tp.result {
_ = tui.current(); // assert we are in tui thread scope
if (id >= commands.items.len)
return tp.exit_fmt("CommandNotFound: {d}", .{id});
const cmd = commands.items[id];
if (cmd) |p| {
// var buf: [tp.max_message_size]u8 = undefined;
// log.logger("cmd").print("execute({s}) {s}", .{ p.name, ctx.args.to_json(&buf) catch "" }) catch |e| return tp.exit_error(e);
return p.run(p, ctx);
} else {
return tp.exit_fmt("CommandNotAvailable: {d}", .{id});
}
}
pub fn getId(name: []const u8) ?ID {
for (commands.items) |cmd| {
if (cmd) |p|
if (std.mem.eql(u8, p.name, name))
return p.id;
}
return null;
}
pub fn get_id_cache(name: []const u8, id: *?ID) ?ID {
for (commands.items) |cmd| {
if (cmd) |p|
if (std.mem.eql(u8, p.name, name)) {
id.* = p.id;
return p.id;
};
}
return null;
}
pub fn executeName(name: []const u8, ctx: Context) tp.result {
return execute(getId(name) orelse return tp.exit_fmt("CommandNotFound: {s}", .{name}), ctx);
}
fn CmdDef(comptime T: type) type {
return struct {
const Fn = fn (T, Context) tp.result;
name: [:0]const u8,
f: *const Fn,
};
}
fn getTargetType(comptime Namespace: type) type {
return @field(Namespace, "Target");
}
fn getCommands(comptime Namespace: type) []CmdDef(*getTargetType(Namespace)) {
comptime switch (@typeInfo(Namespace)) {
.Struct => |info| {
var count = 0;
const Target = getTargetType(Namespace);
// @compileLog(Namespace, Target);
for (info.decls) |decl| {
// @compileLog(decl.name, @TypeOf(@field(Namespace, decl.name)));
if (@TypeOf(@field(Namespace, decl.name)) == CmdDef(*Target).Fn)
count += 1;
}
var cmds: [count]CmdDef(*Target) = undefined;
var i = 0;
for (info.decls) |decl| {
if (@TypeOf(@field(Namespace, decl.name)) == CmdDef(*Target).Fn) {
cmds[i] = .{ .f = &@field(Namespace, decl.name), .name = decl.name };
i += 1;
}
}
return &cmds;
},
else => @compileError("expected tuple or struct type"),
};
}
pub fn Collection(comptime Namespace: type) type {
const cmds = comptime getCommands(Namespace);
const Target = getTargetType(Namespace);
const Clsr = Closure(*Target);
var fields: [cmds.len]std.builtin.Type.StructField = undefined;
inline for (cmds, 0..) |cmd, i| {
@setEvalBranchQuota(10_000);
fields[i] = .{
.name = cmd.name,
.type = Clsr,
.default_value = null,
.is_comptime = false,
.alignment = if (@sizeOf(Clsr) > 0) @alignOf(Clsr) else 0,
};
}
const Fields = @Type(.{
.Struct = .{
.is_tuple = false,
.layout = .Auto,
.decls = &.{},
.fields = &fields,
},
});
return struct {
fields: Fields,
const Self = @This();
pub fn init(self: *Self, targetPtr: *Target) !void {
if (cmds.len == 0)
@compileError("no commands found in type " ++ @typeName(Target) ++ " (did you mark them public?)");
inline for (cmds) |cmd| {
@field(self.fields, cmd.name) = Closure(*Target).init(cmd.f, targetPtr, cmd.name);
try @field(self.fields, cmd.name).register();
}
}
pub fn deinit(self: *Self) void {
inline for (cmds) |cmd|
Closure(*Target).unregister(&@field(self.fields, cmd.name));
}
};
}

3290
src/tui/editor.zig Normal file

File diff suppressed because it is too large Load diff

327
src/tui/editor_gutter.zig Normal file
View file

@ -0,0 +1,327 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const diff = @import("diff");
const cbor = @import("cbor");
const root = @import("root");
const Widget = @import("Widget.zig");
const WidgetList = @import("WidgetList.zig");
const EventHandler = @import("EventHandler.zig");
const MessageFilter = @import("MessageFilter.zig");
const tui = @import("tui.zig");
const command = @import("command.zig");
const ed = @import("editor.zig");
a: Allocator,
plane: nc.Plane,
parent: Widget,
lines: u32 = 0,
rows: u32 = 1,
row: u32 = 1,
line: usize = 0,
linenum: bool,
relative: bool,
highlight: bool,
width: usize = 4,
editor: *ed.Editor,
diff: diff,
diff_symbols: std.ArrayList(Symbol),
const Self = @This();
const Kind = enum { insert, modified, delete };
const Symbol = struct { kind: Kind, line: usize };
pub fn create(a: Allocator, parent: Widget, event_source: Widget, editor: *ed.Editor) !Widget {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.plane = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent.plane.*),
.parent = parent,
.linenum = tui.current().config.gutter_line_numbers,
.relative = tui.current().config.gutter_line_numbers_relative,
.highlight = tui.current().config.highlight_current_line_gutter,
.editor = editor,
.diff = try diff.create(),
.diff_symbols = std.ArrayList(Symbol).init(a),
};
try tui.current().message_filters.add(MessageFilter.bind(self, filter_receive));
try event_source.subscribe(EventHandler.bind(self, handle_event));
return self.widget();
}
pub fn widget(self: *Self) Widget {
return Widget.to(self);
}
pub fn deinit(self: *Self, a: Allocator) void {
self.diff_symbols_clear();
self.diff_symbols.deinit();
tui.current().message_filters.remove_ptr(self);
self.plane.deinit();
a.destroy(self);
}
fn diff_symbols_clear(self: *Self) void {
self.diff_symbols.clearRetainingCapacity();
}
pub fn handle_event(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
if (try m.match(.{ "E", "update", tp.more }))
return self.diff_update() catch |e| return tp.exit_error(e);
if (try m.match(.{ "E", "view", tp.extract(&self.lines), tp.extract(&self.rows), tp.extract(&self.row) }))
return self.update_width();
if (try m.match(.{ "E", "pos", tp.extract(&self.lines), tp.extract(&self.line), tp.more }))
return self.update_width();
if (try m.match(.{ "E", "close" })) {
self.lines = 0;
self.line = 0;
}
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var y: i32 = undefined;
var ypx: i32 = undefined;
if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) }))
return self.primary_click(y);
if (try m.match(.{ "D", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) }))
return self.primary_drag(y);
if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON4, tp.more }))
return self.mouse_click_button4();
if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON5, tp.more }))
return self.mouse_click_button5();
return false;
}
fn update_width(self: *Self) void {
if (!self.linenum) return;
var buf: [31]u8 = undefined;
const tmp = std.fmt.bufPrint(&buf, " {d} ", .{self.lines}) catch return;
self.width = if (self.relative and tmp.len > 6) 6 else @max(tmp.len, 4);
}
pub fn layout(self: *Self) Widget.Layout {
return .{ .static = self.get_width() };
}
inline fn get_width(self: *Self) usize {
return if (self.linenum) self.width else 1;
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
const frame = tracy.initZone(@src(), .{ .name = "gutter render" });
defer frame.deinit();
tui.set_base_style(&self.plane, " ", theme.editor_gutter);
self.plane.erase();
if (self.linenum) {
const relative = self.relative or std.mem.eql(u8, tui.get_mode(), root.application_logo ++ "NOR"); // TODO: move to mode
if (relative)
self.render_relative(theme)
else
self.render_linear(theme);
}
return false;
}
pub fn render_linear(self: *Self, theme: *const Widget.Theme) void {
var pos: usize = 0;
var linenum = self.row + 1;
var rows = self.rows;
var diff_symbols = self.diff_symbols.items;
var buf: [31:0]u8 = undefined;
while (rows > 0) : (rows -= 1) {
if (linenum > self.lines) return;
if (linenum == self.line + 1) {
tui.set_base_style(&self.plane, " ", theme.editor_gutter_active);
self.plane.on_styles(nc.style.bold);
} else {
tui.set_base_style(&self.plane, " ", theme.editor_gutter);
self.plane.off_styles(nc.style.bold);
}
_ = self.plane.putstr_aligned(@intCast(pos), nc.Align.right, std.fmt.bufPrintZ(&buf, "{d} ", .{linenum}) catch return) catch {};
if (self.highlight and linenum == self.line + 1)
self.render_line_highlight(pos, theme);
self.render_diff_symbols(&diff_symbols, pos, linenum, theme);
pos += 1;
linenum += 1;
}
}
pub fn render_relative(self: *Self, theme: *const Widget.Theme) void {
const row: isize = @intCast(self.row + 1);
const line: isize = @intCast(self.line + 1);
var pos: usize = 0;
var linenum: isize = row - line;
var rows = self.rows;
var buf: [31:0]u8 = undefined;
while (rows > 0) : (rows -= 1) {
if (pos > self.lines - row) return;
tui.set_base_style(&self.plane, " ", if (linenum == 0) theme.editor_gutter_active else theme.editor_gutter);
const val = @abs(if (linenum == 0) line else linenum);
const fmt = std.fmt.bufPrintZ(&buf, "{d} ", .{val}) catch return;
_ = self.plane.putstr_aligned(@intCast(pos), nc.Align.right, if (fmt.len > 6) "==> " else fmt) catch {};
if (self.highlight and linenum == 0)
self.render_line_highlight(pos, theme);
pos += 1;
linenum += 1;
}
}
inline fn render_line_highlight(self: *Self, pos: usize, theme: *const Widget.Theme) void {
for (0..self.get_width()) |i| {
self.plane.cursor_move_yx(@intCast(pos), @intCast(i)) catch return;
var cell = self.plane.cell_init();
_ = self.plane.at_cursor_cell(&cell) catch return;
tui.set_cell_style_bg(&cell, theme.editor_line_highlight);
_ = self.plane.putc(&cell) catch {};
}
}
inline fn render_diff_symbols(self: *Self, diff_symbols: *[]Symbol, pos: usize, linenum_: usize, theme: *const Widget.Theme) void {
const linenum = linenum_ - 1;
if (diff_symbols.len == 0) return;
while ((diff_symbols.*)[0].line < linenum) {
diff_symbols.* = (diff_symbols.*)[1..];
if (diff_symbols.len == 0) return;
}
if ((diff_symbols.*)[0].line > linenum) return;
const sym = (diff_symbols.*)[0];
const char = switch (sym.kind) {
.insert => "",
.modified => "",
.delete => "",
};
self.plane.cursor_move_yx(@intCast(pos), @intCast(self.get_width() - 1)) catch return;
var cell = self.plane.cell_init();
_ = self.plane.at_cursor_cell(&cell) catch return;
tui.set_cell_style_fg(&cell, switch (sym.kind) {
.insert => theme.editor_gutter_added,
.modified => theme.editor_gutter_modified,
.delete => theme.editor_gutter_deleted,
});
_ = self.plane.cell_load(&cell, char) catch {};
_ = self.plane.putc(&cell) catch {};
}
fn primary_click(self: *const Self, y: i32) error{Exit}!bool {
var line = self.row + 1;
line += @intCast(y);
try command.executeName("goto_line", command.fmt(.{line}));
try command.executeName("goto_column", command.fmt(.{1}));
try command.executeName("select_end", .{});
try command.executeName("select_right", .{});
return true;
}
fn primary_drag(_: *const Self, y: i32) error{Exit}!bool {
try command.executeName("drag_to", command.fmt(.{ y + 1, 0 }));
return true;
}
fn mouse_click_button4(_: *Self) error{Exit}!bool {
try command.executeName("scroll_up_pageup", .{});
return true;
}
fn mouse_click_button5(_: *Self) error{Exit}!bool {
try command.executeName("scroll_down_pagedown", .{});
return true;
}
fn diff_update(self: *Self) !void {
const editor = self.editor;
const new = if (editor.get_current_root()) |new| new else return;
const old = if (editor.buffer) |buffer| if (buffer.last_save) |old| old else return else return;
return self.diff.diff(diff_result, new, old);
}
fn diff_result(from: tp.pid_ref, edits: []diff.Edit) void {
diff_result_send(from, edits) catch |e| @import("log").logger(@typeName(Self)).err("diff", e);
}
fn diff_result_send(from: tp.pid_ref, edits: []diff.Edit) !void {
var buf: [tp.max_message_size]u8 = undefined;
var stream = std.io.fixedBufferStream(&buf);
const writer = stream.writer();
try cbor.writeArrayHeader(writer, 2);
try cbor.writeValue(writer, "DIFF");
try cbor.writeArrayHeader(writer, edits.len);
for (edits) |edit| {
try cbor.writeArrayHeader(writer, 4);
try cbor.writeValue(writer, switch (edit.kind) {
.insert => "I",
.delete => "D",
});
try cbor.writeValue(writer, edit.line);
try cbor.writeValue(writer, edit.offset);
try cbor.writeValue(writer, edit.bytes);
}
from.send_raw(tp.message{ .buf = stream.getWritten() }) catch return;
}
pub fn process_diff(self: *Self, cb: []const u8) !void {
var iter = cb;
self.diff_symbols_clear();
var count = try cbor.decodeArrayHeader(&iter);
while (count > 0) : (count -= 1) {
var line: usize = undefined;
var offset: usize = undefined;
var bytes: []const u8 = undefined;
if (try cbor.matchValue(&iter, .{ "I", cbor.extract(&line), cbor.extract(&offset), cbor.extract(&bytes) })) {
var pos: usize = 0;
var ln: usize = line;
while (std.mem.indexOfScalarPos(u8, bytes, pos, '\n')) |next| {
const end = if (next < bytes.len) next + 1 else next;
try self.process_edit(.insert, ln, offset, bytes[pos..end]);
pos = next + 1;
ln += 1;
offset = 0;
}
try self.process_edit(.insert, ln, offset, bytes[pos..]);
continue;
}
if (try cbor.matchValue(&iter, .{ "D", cbor.extract(&line), cbor.extract(&offset), cbor.extract(&bytes) })) {
try self.process_edit(.delete, line, offset, bytes);
continue;
}
}
}
fn process_edit(self: *Self, kind: Kind, line: usize, offset: usize, bytes: []const u8) !void {
const change = if (self.diff_symbols.items.len > 0) self.diff_symbols.items[self.diff_symbols.items.len - 1].line == line else false;
if (change) {
self.diff_symbols.items[self.diff_symbols.items.len - 1].kind = .modified;
return;
}
(try self.diff_symbols.addOne()).* = switch (kind) {
.insert => ret: {
if (offset > 0)
break :ret .{ .kind = .modified, .line = line };
if (bytes.len == 0)
return;
if (bytes[bytes.len - 1] == '\n')
break :ret .{ .kind = .insert, .line = line };
break :ret .{ .kind = .modified, .line = line };
},
.delete => .{ .kind = .delete, .line = line },
else => unreachable,
};
}
pub fn filter_receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var cb: []const u8 = undefined;
if (try m.match(.{ "DIFF", tp.extract_cbor(&cb) })) {
self.process_diff(cb) catch |e| return tp.exit_error(e);
return true;
}
return false;
}

185
src/tui/fonts.zig Normal file
View file

@ -0,0 +1,185 @@
const nc = @import("notcurses");
pub fn print_string_large(n: nc.Plane, s: []const u8) !void {
for (s) |c|
print_char_large(n, c) catch break;
}
pub fn print_char_large(n: nc.Plane, char: u8) !void {
const bitmap = font8x8[char];
for (0..8) |y| {
for (0..8) |x| {
const set = bitmap[y] & @as(usize, 1) << @intCast(x);
if (set != 0) {
_ = try n.putstr("");
} else {
n.cursor_move_rel(0, 1) catch {};
}
}
n.cursor_move_rel(1, -8) catch {};
}
n.cursor_move_rel(-8, 8) catch {};
}
pub fn print_string_medium(n: nc.Plane, s: []const u8) !void {
for (s) |c|
print_char_medium(n, c) catch break;
}
const QUADBLOCKS = [_][:0]const u8{ " ", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" };
pub fn print_char_medium(n: nc.Plane, char: u8) !void {
const bitmap = font8x8[char];
for (0..4) |y| {
for (0..4) |x| {
const yt = 2 * y;
const yb = 2 * y + 1;
const xl = 2 * x;
const xr = 2 * x + 1;
const settl: u4 = if (bitmap[yt] & @as(usize, 1) << @intCast(xl) != 0) 1 else 0;
const settr: u4 = if (bitmap[yt] & @as(usize, 1) << @intCast(xr) != 0) 2 else 0;
const setbl: u4 = if (bitmap[yb] & @as(usize, 1) << @intCast(xl) != 0) 4 else 0;
const setbr: u4 = if (bitmap[yb] & @as(usize, 1) << @intCast(xr) != 0) 8 else 0;
const quadidx: u4 = setbr | setbl | settr | settl;
const c = QUADBLOCKS[quadidx];
if (quadidx != 0) {
_ = try n.putstr(c);
} else {
n.cursor_move_rel(0, 1) catch {};
}
}
n.cursor_move_rel(1, -4) catch {};
}
n.cursor_move_rel(-4, 4) catch {};
}
pub const font8x8: [128][8]u8 = [128][8]u8{
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 24, 60, 60, 24, 24, 0, 24, 0 },
[8]u8{ 54, 54, 0, 0, 0, 0, 0, 0 },
[8]u8{ 54, 54, 127, 54, 127, 54, 54, 0 },
[8]u8{ 12, 62, 3, 30, 48, 31, 12, 0 },
[8]u8{ 0, 99, 51, 24, 12, 102, 99, 0 },
[8]u8{ 28, 54, 28, 110, 59, 51, 110, 0 },
[8]u8{ 6, 6, 3, 0, 0, 0, 0, 0 },
[8]u8{ 24, 12, 6, 6, 6, 12, 24, 0 },
[8]u8{ 6, 12, 24, 24, 24, 12, 6, 0 },
[8]u8{ 0, 102, 60, 255, 60, 102, 0, 0 },
[8]u8{ 0, 12, 12, 63, 12, 12, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 12, 12, 6 },
[8]u8{ 0, 0, 0, 63, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 12, 12, 0 },
[8]u8{ 96, 48, 24, 12, 6, 3, 1, 0 },
[8]u8{ 62, 99, 115, 123, 111, 103, 62, 0 },
[8]u8{ 12, 14, 12, 12, 12, 12, 63, 0 },
[8]u8{ 30, 51, 48, 28, 6, 51, 63, 0 },
[8]u8{ 30, 51, 48, 28, 48, 51, 30, 0 },
[8]u8{ 56, 60, 54, 51, 127, 48, 120, 0 },
[8]u8{ 63, 3, 31, 48, 48, 51, 30, 0 },
[8]u8{ 28, 6, 3, 31, 51, 51, 30, 0 },
[8]u8{ 63, 51, 48, 24, 12, 12, 12, 0 },
[8]u8{ 30, 51, 51, 30, 51, 51, 30, 0 },
[8]u8{ 30, 51, 51, 62, 48, 24, 14, 0 },
[8]u8{ 0, 12, 12, 0, 0, 12, 12, 0 },
[8]u8{ 0, 12, 12, 0, 0, 12, 12, 6 },
[8]u8{ 24, 12, 6, 3, 6, 12, 24, 0 },
[8]u8{ 0, 0, 63, 0, 0, 63, 0, 0 },
[8]u8{ 6, 12, 24, 48, 24, 12, 6, 0 },
[8]u8{ 30, 51, 48, 24, 12, 0, 12, 0 },
[8]u8{ 62, 99, 123, 123, 123, 3, 30, 0 },
[8]u8{ 12, 30, 51, 51, 63, 51, 51, 0 },
[8]u8{ 63, 102, 102, 62, 102, 102, 63, 0 },
[8]u8{ 60, 102, 3, 3, 3, 102, 60, 0 },
[8]u8{ 31, 54, 102, 102, 102, 54, 31, 0 },
[8]u8{ 127, 70, 22, 30, 22, 70, 127, 0 },
[8]u8{ 127, 70, 22, 30, 22, 6, 15, 0 },
[8]u8{ 60, 102, 3, 3, 115, 102, 124, 0 },
[8]u8{ 51, 51, 51, 63, 51, 51, 51, 0 },
[8]u8{ 30, 12, 12, 12, 12, 12, 30, 0 },
[8]u8{ 120, 48, 48, 48, 51, 51, 30, 0 },
[8]u8{ 103, 102, 54, 30, 54, 102, 103, 0 },
[8]u8{ 15, 6, 6, 6, 70, 102, 127, 0 },
[8]u8{ 99, 119, 127, 127, 107, 99, 99, 0 },
[8]u8{ 99, 103, 111, 123, 115, 99, 99, 0 },
[8]u8{ 28, 54, 99, 99, 99, 54, 28, 0 },
[8]u8{ 63, 102, 102, 62, 6, 6, 15, 0 },
[8]u8{ 30, 51, 51, 51, 59, 30, 56, 0 },
[8]u8{ 63, 102, 102, 62, 54, 102, 103, 0 },
[8]u8{ 30, 51, 7, 14, 56, 51, 30, 0 },
[8]u8{ 63, 45, 12, 12, 12, 12, 30, 0 },
[8]u8{ 51, 51, 51, 51, 51, 51, 63, 0 },
[8]u8{ 51, 51, 51, 51, 51, 30, 12, 0 },
[8]u8{ 99, 99, 99, 107, 127, 119, 99, 0 },
[8]u8{ 99, 99, 54, 28, 28, 54, 99, 0 },
[8]u8{ 51, 51, 51, 30, 12, 12, 30, 0 },
[8]u8{ 127, 99, 49, 24, 76, 102, 127, 0 },
[8]u8{ 30, 6, 6, 6, 6, 6, 30, 0 },
[8]u8{ 3, 6, 12, 24, 48, 96, 64, 0 },
[8]u8{ 30, 24, 24, 24, 24, 24, 30, 0 },
[8]u8{ 8, 28, 54, 99, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 255 },
[8]u8{ 12, 12, 24, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 30, 48, 62, 51, 110, 0 },
[8]u8{ 7, 6, 6, 62, 102, 102, 59, 0 },
[8]u8{ 0, 0, 30, 51, 3, 51, 30, 0 },
[8]u8{ 56, 48, 48, 62, 51, 51, 110, 0 },
[8]u8{ 0, 0, 30, 51, 63, 3, 30, 0 },
[8]u8{ 28, 54, 6, 15, 6, 6, 15, 0 },
[8]u8{ 0, 0, 110, 51, 51, 62, 48, 31 },
[8]u8{ 7, 6, 54, 110, 102, 102, 103, 0 },
[8]u8{ 12, 0, 14, 12, 12, 12, 30, 0 },
[8]u8{ 48, 0, 48, 48, 48, 51, 51, 30 },
[8]u8{ 7, 6, 102, 54, 30, 54, 103, 0 },
[8]u8{ 14, 12, 12, 12, 12, 12, 30, 0 },
[8]u8{ 0, 0, 51, 127, 127, 107, 99, 0 },
[8]u8{ 0, 0, 31, 51, 51, 51, 51, 0 },
[8]u8{ 0, 0, 30, 51, 51, 51, 30, 0 },
[8]u8{ 0, 0, 59, 102, 102, 62, 6, 15 },
[8]u8{ 0, 0, 110, 51, 51, 62, 48, 120 },
[8]u8{ 0, 0, 59, 110, 102, 6, 15, 0 },
[8]u8{ 0, 0, 62, 3, 30, 48, 31, 0 },
[8]u8{ 8, 12, 62, 12, 12, 44, 24, 0 },
[8]u8{ 0, 0, 51, 51, 51, 51, 110, 0 },
[8]u8{ 0, 0, 51, 51, 51, 30, 12, 0 },
[8]u8{ 0, 0, 99, 107, 127, 127, 54, 0 },
[8]u8{ 0, 0, 99, 54, 28, 54, 99, 0 },
[8]u8{ 0, 0, 51, 51, 51, 62, 48, 31 },
[8]u8{ 0, 0, 63, 25, 12, 38, 63, 0 },
[8]u8{ 56, 12, 12, 7, 12, 12, 56, 0 },
[8]u8{ 24, 24, 24, 0, 24, 24, 24, 0 },
[8]u8{ 7, 12, 12, 56, 12, 12, 7, 0 },
[8]u8{ 110, 59, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
};

263
src/tui/home.zig Normal file
View file

@ -0,0 +1,263 @@
const std = @import("std");
const nc = @import("notcurses");
const tp = @import("thespian");
const Widget = @import("Widget.zig");
const tui = @import("tui.zig");
const command = @import("command.zig");
const fonts = @import("fonts.zig");
a: std.mem.Allocator,
plane: nc.Plane,
parent: nc.Plane,
fire: ?Fire = null,
commands: Commands = undefined,
const Self = @This();
pub fn create(a: std.mem.Allocator, parent: Widget) !Widget {
const self: *Self = try a.create(Self);
var n = try nc.Plane.init(&(Widget.Box{}).opts("editor"), parent.plane.*);
errdefer n.deinit();
self.* = .{
.a = a,
.parent = parent.plane.*,
.plane = n,
};
try self.commands.init(self);
command.executeName("enter_mode", command.Context.fmt(.{"home"})) catch {};
return Widget.to(self);
}
pub fn deinit(self: *Self, a: std.mem.Allocator) void {
self.commands.deinit();
self.plane.deinit();
if (self.fire) |*fire| fire.deinit();
a.destroy(self);
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
tui.set_base_style(&self.plane, " ", theme.editor);
self.plane.erase();
self.plane.home();
if (self.fire) |*fire| fire.render() catch unreachable;
const style_title = if (tui.find_scope_style(theme, "function")) |sty| sty.style else theme.editor;
const style_subtext = if (tui.find_scope_style(theme, "comment")) |sty| sty.style else theme.editor;
const style_text = if (tui.find_scope_style(theme, "keyword")) |sty| sty.style else theme.editor;
const style_keybind = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else theme.editor;
const title = "Flow Control";
const subtext = "a programmer's text editor";
if (self.plane.dim_x() > 120 and self.plane.dim_y() > 22) {
self.set_style(style_title);
self.plane.cursor_move_yx(2, 4) catch return false;
fonts.print_string_large(self.plane, title) catch return false;
self.set_style(style_subtext);
self.plane.cursor_move_yx(10, 8) catch return false;
fonts.print_string_medium(self.plane, subtext) catch return false;
self.plane.cursor_move_yx(15, 10) catch return false;
} else if (self.plane.dim_x() > 55 and self.plane.dim_y() > 16) {
self.set_style(style_title);
self.plane.cursor_move_yx(2, 4) catch return false;
fonts.print_string_medium(self.plane, title) catch return false;
self.set_style(style_subtext);
self.plane.cursor_move_yx(7, 6) catch return false;
_ = self.plane.print(subtext, .{}) catch {};
self.plane.cursor_move_yx(9, 8) catch return false;
} else {
self.set_style(style_title);
self.plane.cursor_move_yx(1, 4) catch return false;
_ = self.plane.print(title, .{}) catch return false;
self.set_style(style_subtext);
self.plane.cursor_move_yx(3, 6) catch return false;
_ = self.plane.print(subtext, .{}) catch {};
self.plane.cursor_move_yx(5, 8) catch return false;
}
if (self.plane.dim_x() > 48 and self.plane.dim_y() > 12)
self.render_hints(style_subtext, style_text, style_keybind);
return true;
}
fn render_hints(self: *Self, style_base: Widget.Theme.Style, style_text: Widget.Theme.Style, style_keybind: Widget.Theme.Style) void {
const hint_text: [:0]const u8 =
\\Help ······················· :F1 / C-?
\\Open file ·················· :C-o
\\Open recent file ··········· :C-e / C-r
\\Show/Run commands ·········· :C-p / C-S-p
\\Open config file ··········· :F6
\\Quit/Close ················· :C-q, C-w
\\
;
const left: c_int = @intCast(self.plane.cursor_x());
var pos: usize = 0;
while (std.mem.indexOfScalarPos(u8, hint_text, pos, '\n')) |next| {
const line = hint_text[pos..next];
const sep = std.mem.indexOfScalar(u8, line, ':') orelse line.len;
self.set_style(style_base);
self.set_style(style_text);
_ = self.plane.print("{s}", .{line[0..sep]}) catch {};
self.set_style(style_keybind);
_ = self.plane.print("{s}", .{line[sep + 1 ..]}) catch {};
self.plane.cursor_move_rel(1, 0) catch {};
self.plane.cursor_move_yx(-1, left) catch {};
pos = next + 1;
}
}
fn set_style(self: *Self, style: Widget.Theme.Style) void {
tui.set_style(&self.plane, style);
}
pub fn handle_resize(self: *Self, pos: Widget.Box) void {
self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return;
self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return;
if (self.fire) |*fire| {
fire.deinit();
self.fire = Fire.init(self.a, self.plane, pos) catch unreachable;
}
}
const Commands = command.Collection(cmds);
const cmds = struct {
pub const Target = Self;
const Ctx = command.Context;
pub fn home_sheeran(self: *Self, _: Ctx) tp.result {
self.fire = if (self.fire) |*fire| ret: {
fire.deinit();
break :ret null;
} else Fire.init(self.a, self.plane, Widget.Box.from(self.plane)) catch |e| return tp.exit_error(e);
}
};
const Fire = struct {
const px = "";
allocator: std.mem.Allocator,
plane: nc.Plane,
prng: std.rand.DefaultPrng,
//scope cache - spread fire
spread_px: u8 = 0,
spread_rnd_idx: u8 = 0,
spread_dst: u16 = 0,
FIRE_H: u16,
FIRE_W: u16,
FIRE_SZ: u16,
FIRE_LAST_ROW: u16,
screen_buf: []u8,
const MAX_COLOR = 256;
const LAST_COLOR = MAX_COLOR - 1;
fn init(a: std.mem.Allocator, plane: nc.Plane, pos: Widget.Box) !Fire {
const FIRE_H = @as(u16, @intCast(pos.h)) * 2;
const FIRE_W = @as(u16, @intCast(pos.w));
var self: Fire = .{
.allocator = a,
.plane = plane,
.prng = std.rand.DefaultPrng.init(blk: {
var seed: u64 = undefined;
try std.os.getrandom(std.mem.asBytes(&seed));
break :blk seed;
}),
.FIRE_H = FIRE_H,
.FIRE_W = FIRE_W,
.FIRE_SZ = FIRE_H * FIRE_W,
.FIRE_LAST_ROW = (FIRE_H - 1) * FIRE_W,
.screen_buf = try a.alloc(u8, FIRE_H * FIRE_W),
};
var buf_idx: u16 = 0;
while (buf_idx < self.FIRE_SZ) : (buf_idx += 1) {
self.screen_buf[buf_idx] = fire_black;
}
// last row is white...white is "fire source"
buf_idx = 0;
while (buf_idx < self.FIRE_W) : (buf_idx += 1) {
self.screen_buf[self.FIRE_LAST_ROW + buf_idx] = fire_white;
}
return self;
}
fn deinit(self: *Fire) void {
self.allocator.free(self.screen_buf);
}
const fire_palette = [_]u8{ 0, 233, 234, 52, 53, 88, 89, 94, 95, 96, 130, 131, 132, 133, 172, 214, 215, 220, 220, 221, 3, 226, 227, 230, 195, 230 };
const fire_black: u8 = 0;
const fire_white: u8 = fire_palette.len - 1;
fn render(self: *Fire) !void {
var rand = self.prng.random();
//update fire buf
var doFire_x: u16 = 0;
while (doFire_x < self.FIRE_W) : (doFire_x += 1) {
var doFire_y: u16 = 0;
while (doFire_y < self.FIRE_H) : (doFire_y += 1) {
const doFire_idx: u16 = doFire_y * self.FIRE_W + doFire_x;
//spread fire
self.spread_px = self.screen_buf[doFire_idx];
//bounds checking
if ((self.spread_px == 0) and (doFire_idx >= self.FIRE_W)) {
self.screen_buf[doFire_idx - self.FIRE_W] = 0;
} else {
self.spread_rnd_idx = rand.intRangeAtMost(u8, 0, 3);
if (doFire_idx >= (self.spread_rnd_idx + 1)) {
self.spread_dst = doFire_idx - self.spread_rnd_idx + 1;
} else {
self.spread_dst = doFire_idx;
}
if (self.spread_dst >= self.FIRE_W) {
if (self.spread_px > (self.spread_rnd_idx & 1)) {
self.screen_buf[self.spread_dst - self.FIRE_W] = self.spread_px - (self.spread_rnd_idx & 1);
} else {
self.screen_buf[self.spread_dst - self.FIRE_W] = 0;
}
}
}
}
}
//scope cache - fire 2 screen buffer
var frame_x: u16 = 0;
var frame_y: u16 = 0;
// for each row
frame_y = 0;
while (frame_y < self.FIRE_H) : (frame_y += 2) { // 'paint' two rows at a time because of half height char
// for each col
frame_x = 0;
while (frame_x < self.FIRE_W) : (frame_x += 1) {
//each character rendered is actually to rows of 'pixels'
// - "hi" (current px row => fg char)
// - "low" (next row => bg color)
const px_hi = self.screen_buf[frame_y * self.FIRE_W + frame_x];
const px_lo = self.screen_buf[(frame_y + 1) * self.FIRE_W + frame_x];
try self.plane.set_fg_palindex(fire_palette[px_hi]);
try self.plane.set_bg_palindex(fire_palette[px_lo]);
_ = try self.plane.putstr(px);
}
self.plane.cursor_move_yx(-1, 0) catch {};
self.plane.cursor_move_rel(1, 0) catch {};
}
}
};

106
src/tui/inputview.zig Normal file
View file

@ -0,0 +1,106 @@
const eql = @import("std").mem.eql;
const fmt = @import("std").fmt;
const time = @import("std").time;
const Allocator = @import("std").mem.Allocator;
const Mutex = @import("std").Thread.Mutex;
const nc = @import("notcurses");
const tp = @import("thespian");
const tui = @import("tui.zig");
const Widget = @import("Widget.zig");
const EventHandler = @import("EventHandler.zig");
const A = nc.Align;
pub const name = "inputview";
parent: nc.Plane,
plane: nc.Plane,
lastbuf: [4096]u8 = undefined,
last: []u8 = "",
last_count: u64 = 0,
last_time: i64 = 0,
last_tdiff: i64 = 0,
const Self = @This();
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
try tui.current().input_listeners.add(EventHandler.bind(self, listen));
return Widget.to(self);
}
fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts_vscroll(@typeName(Self)), parent);
errdefer n.deinit();
return .{
.parent = parent,
.plane = n,
.last_time = time.microTimestamp(),
};
}
pub fn deinit(self: *Self, a: Allocator) void {
tui.current().input_listeners.remove_ptr(self);
self.plane.deinit();
a.destroy(self);
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
tui.set_base_style(&self.plane, " ", theme.panel);
return false;
}
fn output_tdiff(self: *Self, tdiff: i64) !void {
const msi = @divFloor(tdiff, time.us_per_ms);
if (msi == 0) {
const d: f64 = @floatFromInt(tdiff);
const ms = d / time.us_per_ms;
_ = try self.plane.print("{d:6.2}▎", .{ms});
} else {
const ms: u64 = @intCast(msi);
_ = try self.plane.print("{d:6}▎", .{ms});
}
}
fn output_new(self: *Self, json: []const u8) !void {
if (self.plane.cursor_x() != 0)
_ = try self.plane.putstr("\n");
const ts = time.microTimestamp();
const tdiff = ts - self.last_time;
self.last_count = 0;
self.last = self.lastbuf[0..json.len];
@memcpy(self.last, json);
try self.output_tdiff(tdiff);
_ = try self.plane.print("{s}", .{json});
self.last_time = ts;
self.last_tdiff = tdiff;
}
fn output_repeat(self: *Self, json: []const u8) !void {
if (self.plane.cursor_x() != 0)
try self.plane.cursor_move_yx(-1, 0);
self.last_count += 1;
try self.output_tdiff(self.last_tdiff);
_ = try self.plane.print("{s} ({})", .{ json, self.last_count });
}
fn output(self: *Self, json: []const u8) !void {
return if (!eql(u8, json, self.last))
self.output_new(json)
else
self.output_repeat(json);
}
pub fn listen(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
if (try m.match(.{ "M", tp.more })) return;
var buf: [4096]u8 = undefined;
const json = m.to_json(&buf) catch |e| return tp.exit_error(e);
self.output(json) catch |e| return tp.exit_error(e);
}
pub fn receive(_: *Self, _: tp.pid_ref, _: tp.message) error{Exit}!bool {
return false;
}

181
src/tui/inspector_view.zig Normal file
View file

@ -0,0 +1,181 @@
const eql = @import("std").mem.eql;
const fmt = @import("std").fmt;
const time = @import("std").time;
const Allocator = @import("std").mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const Buffer = @import("Buffer");
const color = @import("color");
const syntax = @import("syntax");
const tui = @import("tui.zig");
const Widget = @import("Widget.zig");
const EventHandler = @import("EventHandler.zig");
const mainview = @import("mainview.zig");
const ed = @import("editor.zig");
const A = nc.Align;
pub const name = @typeName(Self);
plane: nc.Plane,
editor: *ed.Editor,
need_render: bool = true,
need_clear: bool = false,
theme: ?*const Widget.Theme = null,
theme_name: []const u8 = "",
pos_cache: ed.PosToWidthCache,
const Self = @This();
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
if (tui.current().mainview.dynamic_cast(mainview)) |mv_| if (mv_.get_editor()) |editor| {
const self: *Self = try a.create(Self);
self.* = .{
.plane = try nc.Plane.init(&(Widget.Box{}).opts_vscroll(name), parent),
.editor = editor,
.pos_cache = try ed.PosToWidthCache.init(a),
};
try editor.handlers.add(EventHandler.bind(self, ed_receive));
return Widget.to(self);
};
return error.NotFound;
}
pub fn deinit(self: *Self, a: Allocator) void {
self.editor.handlers.remove_ptr(self);
tui.current().message_filters.remove_ptr(self);
self.plane.deinit();
a.destroy(self);
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
self.reset_style();
self.theme = theme;
if (self.theme_name.ptr != theme.name.ptr) {
self.theme_name = theme.name;
self.need_render = true;
}
if (self.need_render) {
self.need_render = false;
const cursor = self.editor.get_primary().cursor;
self.inspect_location(cursor.row, cursor.col);
}
return false;
}
pub fn handle_resize(self: *Self, pos: Widget.Box) void {
self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return;
self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return;
self.need_render = true;
}
fn ed_receive(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
var row: usize = 0;
var col: usize = 0;
if (try m.match(.{ "E", "pos", tp.any, tp.extract(&row), tp.extract(&col) }))
return self.inspect_location(row, col);
if (try m.match(.{ "E", "location", "modified", tp.extract(&row), tp.extract(&col), tp.more })) {
self.need_render = true;
return;
}
if (try m.match(.{ "E", "close" }))
return self.clear();
}
fn clear(self: *Self) void {
self.plane.erase();
self.plane.home();
}
fn inspect_location(self: *Self, row: usize, col: usize) void {
self.need_clear = true;
const syn = if (self.editor.syntax) |p| p else return;
syn.highlights_at_point(self, dump_highlight, .{ .row = @intCast(row), .column = @intCast(col) });
}
fn get_buffer_text(self: *Self, buf: []u8, sel: Buffer.Selection) ?[]const u8 {
const root = self.editor.get_current_root() orelse return null;
return root.get_range(sel, buf, null, null) catch return null;
}
fn dump_highlight(self: *Self, range: syntax.Range, scope: []const u8, id: u32, _: usize) error{Stop}!void {
const sel = self.pos_cache.range_to_selection(range, self.editor.get_current_root() orelse return) orelse return;
if (self.need_clear) {
self.need_clear = false;
self.clear();
}
if (self.editor.matches.items.len == 0) {
(self.editor.matches.addOne() catch return).* = ed.Match.from_selection(sel);
} else if (self.editor.matches.items.len == 1) {
self.editor.matches.items[0] = ed.Match.from_selection(sel);
}
var buf: [1024]u8 = undefined;
const text = self.get_buffer_text(&buf, sel) orelse "";
if (self.editor.style_lookup(self.theme, scope, id)) |token| {
if (text.len > 14) {
_ = self.plane.print("scope: {s} -> \"{s}...\" matched: {s}", .{
scope,
text[0..15],
Widget.scopes[token.id],
}) catch {};
} else {
_ = self.plane.print("scope: {s} -> \"{s}\" matched: {s}", .{
scope,
text,
Widget.scopes[token.id],
}) catch {};
}
self.show_color("fg", token.style.fg);
self.show_color("bg", token.style.bg);
self.show_font(token.style.fs);
_ = self.plane.print("\n", .{}) catch {};
return;
}
_ = self.plane.print("scope: {s} -> \"{s}\"\n", .{ scope, text }) catch return;
}
fn show_color(self: *Self, tag: []const u8, c_: ?Widget.Theme.Color) void {
const theme = self.theme orelse return;
if (c_) |c| {
_ = self.plane.print(" {s}:", .{tag}) catch return;
self.plane.set_bg_rgb(c) catch {};
self.plane.set_fg_rgb(color.max_contrast(c, theme.panel.fg orelse 0xFFFFFF, theme.panel.bg orelse 0x000000)) catch {};
_ = self.plane.print("#{x}", .{c}) catch return;
self.reset_style();
}
}
fn show_font(self: *Self, font: ?Widget.Theme.FontStyle) void {
if (font) |fs| switch (fs) {
.normal => {
self.plane.set_styles(nc.style.none);
_ = self.plane.print(" normal", .{}) catch return;
},
.bold => {
self.plane.set_styles(nc.style.bold);
_ = self.plane.print(" bold", .{}) catch return;
},
.italic => {
self.plane.set_styles(nc.style.italic);
_ = self.plane.print(" italic", .{}) catch return;
},
.underline => {
self.plane.set_styles(nc.style.underline);
_ = self.plane.print(" underline", .{}) catch return;
},
.strikethrough => {
self.plane.set_styles(nc.style.struck);
_ = self.plane.print(" strikethrough", .{}) catch return;
},
};
self.plane.set_styles(nc.style.none);
}
fn reset_style(self: *Self) void {
tui.set_base_style(&self.plane, " ", (self.theme orelse return).panel);
}

132
src/tui/logview.zig Normal file
View file

@ -0,0 +1,132 @@
const eql = @import("std").mem.eql;
const fmt = @import("std").fmt;
const time = @import("std").time;
const Allocator = @import("std").mem.Allocator;
const Mutex = @import("std").Thread.Mutex;
const nc = @import("notcurses");
const tp = @import("thespian");
const log = @import("log");
const tui = @import("tui.zig");
const Widget = @import("Widget.zig");
const MessageFilter = @import("MessageFilter.zig");
const escape = fmt.fmtSliceEscapeLower;
const A = nc.Align;
pub const name = @typeName(Self);
plane: nc.Plane,
lastbuf_src: [128]u8 = undefined,
lastbuf_msg: [log.max_log_message]u8 = undefined,
last_src: []u8 = "",
last_msg: []u8 = "",
last_count: u64 = 0,
last_time: i64 = 0,
last_tdiff: i64 = 0,
const Self = @This();
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = init(parent) catch |e| return tp.exit_error(e);
try tui.current().message_filters.add(MessageFilter.bind(self, log_receive));
try log.subscribe();
return Widget.to(self);
}
fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts_vscroll(name), parent);
errdefer n.deinit();
return .{
.plane = n,
.last_time = time.microTimestamp(),
};
}
pub fn deinit(self: *Self, a: Allocator) void {
log.unsubscribe() catch {};
tui.current().message_filters.remove_ptr(self);
self.plane.deinit();
a.destroy(self);
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
tui.set_base_style(&self.plane, " ", theme.panel);
return false;
}
pub fn log_receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "log", tp.more })) {
self.log_process(m) catch |e| return tp.exit_error(e);
return true;
}
return false;
}
pub fn log_process(self: *Self, m: tp.message) !void {
var src: []const u8 = undefined;
var context: []const u8 = undefined;
var msg: []const u8 = undefined;
if (try m.match(.{ "log", tp.extract(&src), tp.extract(&msg) })) {
try self.output(src, msg);
} else if (try m.match(.{ "log", "error", tp.extract(&src), tp.extract(&context), "->", tp.extract(&msg) })) {
try self.output_error(src, context, msg);
} else if (try m.match(.{ "log", tp.extract(&src), tp.more })) {
try self.output_json(src, m);
}
}
fn output_tdiff(self: *Self, tdiff: i64) !void {
const msi = @divFloor(tdiff, time.us_per_ms);
if (msi == 0) {
const d: f64 = @floatFromInt(tdiff);
const ms = d / time.us_per_ms;
_ = try self.plane.print("\n{d:6.2} ▏", .{ms});
} else {
const ms: u64 = @intCast(msi);
_ = try self.plane.print("\n{d:6} ▏", .{ms});
}
}
fn output_new(self: *Self, src: []const u8, msg: []const u8) !void {
const ts = time.microTimestamp();
const tdiff = ts - self.last_time;
self.last_count = 0;
self.last_src = self.lastbuf_src[0..src.len];
self.last_msg = self.lastbuf_msg[0..msg.len];
@memcpy(self.last_src, src);
@memcpy(self.last_msg, msg);
try self.output_tdiff(tdiff);
_ = try self.plane.print("{s}: {s}", .{ escape(src), escape(msg) });
self.last_time = ts;
self.last_tdiff = tdiff;
}
fn output_repeat(self: *Self, src: []const u8, msg: []const u8) !void {
_ = src;
self.last_count += 1;
try self.plane.cursor_move_rel(-1, 0);
try self.output_tdiff(self.last_tdiff);
_ = try self.plane.print("{s} ({})", .{ escape(msg), self.last_count });
}
fn output(self: *Self, src: []const u8, msg: []const u8) !void {
return if (eql(u8, msg, self.last_src) and eql(u8, msg, self.last_msg))
self.output_repeat(src, msg)
else
self.output_new(src, msg);
}
fn output_error(self: *Self, src: []const u8, context: []const u8, msg_: []const u8) !void {
var buf: [4096]u8 = undefined;
const msg = try fmt.bufPrint(&buf, "error in {s}: {s}", .{ context, msg_ });
try self.output(src, msg);
}
fn output_json(self: *Self, src: []const u8, m: tp.message) !void {
var buf: [4096]u8 = undefined;
const json = try m.to_json(&buf);
try self.output(src, json);
}

374
src/tui/mainview.zig Normal file
View file

@ -0,0 +1,374 @@
const std = @import("std");
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const root = @import("root");
const location_history = @import("location_history");
const tui = @import("tui.zig");
const command = @import("command.zig");
const Box = @import("Box.zig");
const EventHandler = @import("EventHandler.zig");
const Widget = @import("Widget.zig");
const WidgetList = @import("WidgetList.zig");
const WidgetStack = @import("WidgetStack.zig");
const ed = @import("editor.zig");
const home = @import("home.zig");
const Self = @This();
const Commands = command.Collection(cmds);
a: std.mem.Allocator,
plane: nc.Plane,
widgets: *WidgetList,
floating_views: WidgetStack,
commands: Commands = undefined,
statusbar: *Widget,
editor: ?*ed.Editor = null,
panels: ?*WidgetList = null,
last_match_text: ?[]const u8 = null,
logview_enabled: bool = false,
location_history: location_history,
const NavState = struct {
time: i64 = 0,
lines: usize = 0,
rows: usize = 0,
row: usize = 0,
col: usize = 0,
matches: usize = 0,
};
pub fn create(a: std.mem.Allocator, n: nc.Plane) !Widget {
const self = try a.create(Self);
self.* = .{
.a = a,
.plane = n,
.widgets = undefined,
.floating_views = WidgetStack.init(a),
.statusbar = undefined,
.location_history = try location_history.create(),
};
try self.commands.init(self);
const w = Widget.to(self);
const widgets = try WidgetList.createV(a, w, @typeName(Self), .dynamic);
self.widgets = widgets;
try widgets.add(try Widget.empty(a, n, .dynamic));
self.statusbar = try widgets.addP(try @import("status/statusbar.zig").create(a, w));
self.resize();
return w;
}
pub fn deinit(self: *Self, a: std.mem.Allocator) void {
self.close_all_panel_views();
self.commands.deinit();
self.widgets.deinit(a);
self.floating_views.deinit();
a.destroy(self);
}
pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{"write_restore_info"})) {
self.write_restore_info();
return true;
}
return if (try self.floating_views.send(from_, m)) true else self.widgets.send(from_, m);
}
pub fn update(self: *Self) void {
self.widgets.update();
self.floating_views.update();
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
var more = self.widgets.render(theme);
if (self.floating_views.render(theme))
more = true;
return more;
}
pub fn resize(self: *Self) void {
self.handle_resize(Box.from(self.plane));
}
pub fn handle_resize(self: *Self, pos: Box) void {
self.widgets.resize(pos);
self.floating_views.resize(pos);
}
pub fn box(self: *const Self) Box {
return Box.from(self.plane);
}
fn toggle_panel_view(self: *Self, view: anytype, enable_only: bool) error{Exit}!bool {
var enabled = true;
if (self.panels) |panels| {
if (panels.get(@typeName(view))) |w| {
if (!enable_only) {
panels.remove(w.*);
if (panels.empty()) {
self.widgets.remove(panels.widget());
self.panels = null;
}
enabled = false;
}
} else {
panels.add(view.create(self.a, self.widgets.plane) catch |e| return tp.exit_error(e)) catch |e| return tp.exit_error(e);
}
} else {
const panels = WidgetList.createH(self.a, self.widgets.widget(), "panel", .{ .static = self.box().h / 5 }) catch |e| return tp.exit_error(e);
self.widgets.add(panels.widget()) catch |e| return tp.exit_error(e);
panels.add(view.create(self.a, self.widgets.plane) catch |e| return tp.exit_error(e)) catch |e| return tp.exit_error(e);
self.panels = panels;
}
self.resize();
return enabled;
}
fn close_all_panel_views(self: *Self) void {
if (self.panels) |panels| {
self.widgets.remove(panels.widget());
self.panels = null;
}
self.resize();
}
fn toggle_view(self: *Self, view: anytype) tp.result {
if (self.widgets.get(@typeName(view))) |w| {
self.widgets.remove(w.*);
} else {
self.widgets.add(view.create(self.a, self.plane) catch |e| return tp.exit_error(e)) catch |e| return tp.exit_error(e);
}
self.resize();
}
const cmds = struct {
pub const Target = Self;
const Ctx = command.Context;
pub fn quit(self: *Self, _: Ctx) tp.result {
if (self.editor) |editor| if (editor.is_dirty())
return tp.exit("unsaved changes");
try tp.self_pid().send("quit");
}
pub fn quit_without_saving(_: *Self, _: Ctx) tp.result {
try tp.self_pid().send("quit");
}
pub fn navigate(self: *Self, ctx: Ctx) tp.result {
const frame = tracy.initZone(@src(), .{ .name = "navigate" });
defer frame.deinit();
var file: ?[]const u8 = null;
var file_name: []const u8 = undefined;
var line: ?i64 = null;
var column: ?i64 = null;
var obj = std.json.ObjectMap.init(self.a);
defer obj.deinit();
if (ctx.args.match(tp.extract(&obj)) catch false) {
if (obj.get("line")) |v| switch (v) {
.integer => |line_| line = line_,
else => return tp.exit_error(error.InvalidArgument),
};
if (obj.get("column")) |v| switch (v) {
.integer => |column_| column = column_,
else => return tp.exit_error(error.InvalidArgument),
};
if (obj.get("file")) |v| switch (v) {
.string => |file_| file = file_,
else => return tp.exit_error(error.InvalidArgument),
};
} else if (ctx.args.match(tp.extract(&file_name)) catch false) {
file = file_name;
} else return tp.exit_error(error.InvalidArgument);
if (file) |f| {
try self.create_editor();
try command.executeName("open_file", command.fmt(.{f}));
if (line) |l| {
try command.executeName("goto_line", command.fmt(.{l}));
}
if (column) |col| {
try command.executeName("goto_column", command.fmt(.{col}));
}
try command.executeName("scroll_view_center", .{});
tui.need_render();
}
}
pub fn open_help(self: *Self, _: Ctx) tp.result {
try self.create_editor();
try command.executeName("open_scratch_buffer", command.fmt(.{ "help.md", @embedFile("help.md") }));
tui.need_render();
}
pub fn open_config(_: *Self, _: Ctx) tp.result {
const file_name = root.get_config_file_name() catch |e| return tp.exit_error(e);
try tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_name } });
}
pub fn restore_session(self: *Self, _: Ctx) tp.result {
try self.create_editor();
self.read_restore_info() catch |e| return tp.exit_error(e);
tui.need_render();
}
pub fn toggle_logview(self: *Self, _: Ctx) tp.result {
self.logview_enabled = try self.toggle_panel_view(@import("logview.zig"), false);
}
pub fn show_logview(self: *Self, _: Ctx) tp.result {
self.logview_enabled = try self.toggle_panel_view(@import("logview.zig"), true);
}
pub fn toggle_inputview(self: *Self, _: Ctx) tp.result {
_ = try self.toggle_panel_view(@import("inputview.zig"), false);
}
pub fn toggle_inspector_view(self: *Self, _: Ctx) tp.result {
_ = try self.toggle_panel_view(@import("inspector_view.zig"), false);
}
pub fn show_inspector_view(self: *Self, _: Ctx) tp.result {
_ = try self.toggle_panel_view(@import("inspector_view.zig"), true);
}
pub fn jump_back(self: *Self, _: Ctx) tp.result {
try self.location_history.back(location_jump);
}
pub fn jump_forward(self: *Self, _: Ctx) tp.result {
try self.location_history.forward(location_jump);
}
pub fn show_home(self: *Self, _: Ctx) tp.result {
return self.create_home();
}
};
pub fn handle_editor_event(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
const editor = if (self.editor) |editor_| editor_ else return;
var sel: ed.Selection = undefined;
if (try m.match(.{ "E", "location", tp.more }))
return self.location_update(m);
if (try m.match(.{ "E", "close" })) {
self.editor = null;
self.show_home_async();
return;
}
if (try m.match(.{ "E", "sel", tp.more })) {
if (try m.match(.{ tp.any, tp.any, "none" }))
return self.clear_auto_find(editor);
if (try m.match(.{ tp.any, tp.any, tp.extract(&sel.begin.row), tp.extract(&sel.begin.col), tp.extract(&sel.end.row), tp.extract(&sel.end.col) })) {
sel.normalize();
if (sel.end.row - sel.begin.row > ed.max_match_lines)
return self.clear_auto_find(editor);
const text = editor.get_selection(sel, self.a) catch return self.clear_auto_find(editor);
if (text.len == 0)
return self.clear_auto_find(editor);
if (!self.is_last_match_text(text)) {
editor.find_in_buffer(text) catch return;
}
}
return;
}
}
pub fn location_update(self: *Self, m: tp.message) tp.result {
var row: usize = 0;
var col: usize = 0;
if (try m.match(.{ tp.any, tp.any, tp.any, tp.extract(&row), tp.extract(&col) }))
return self.location_history.add(.{ .row = row + 1, .col = col + 1 }, null);
var sel: location_history.Selection = .{};
if (try m.match(.{ tp.any, tp.any, tp.any, tp.extract(&row), tp.extract(&col), tp.extract(&sel.begin.row), tp.extract(&sel.begin.col), tp.extract(&sel.end.row), tp.extract(&sel.end.col) }))
return self.location_history.add(.{ .row = row + 1, .col = col + 1 }, sel);
}
fn location_jump(from: tp.pid_ref, cursor: location_history.Cursor, selection: ?location_history.Selection) void {
if (selection) |sel|
from.send(.{ "cmd", "goto", .{ cursor.row, cursor.col, sel.begin.row, sel.begin.col, sel.end.row, sel.end.col } }) catch return
else
from.send(.{ "cmd", "goto", .{ cursor.row, cursor.col } }) catch return;
}
fn clear_auto_find(self: *Self, editor: *ed.Editor) !void {
try editor.clear_matches();
self.store_last_match_text(null);
}
fn is_last_match_text(self: *Self, text: []const u8) bool {
const is = if (self.last_match_text) |old| std.mem.eql(u8, old, text) else false;
self.store_last_match_text(text);
return is;
}
fn store_last_match_text(self: *Self, text: ?[]const u8) void {
if (self.last_match_text) |old|
self.a.free(old);
self.last_match_text = text;
}
pub fn get_editor(self: *Self) ?*ed.Editor {
return self.editor;
}
pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool {
return if (self.floating_views.walk(walk_ctx, f)) true else self.widgets.walk(walk_ctx, f);
}
fn create_editor(self: *Self) tp.result {
command.executeName("enter_mode_default", .{}) catch {};
var editor_widget = ed.create(self.a, Widget.to(self)) catch |e| return tp.exit_error(e);
errdefer editor_widget.deinit(self.a);
if (editor_widget.get("editor")) |editor| {
editor.subscribe(EventHandler.to_unowned(self.statusbar)) catch unreachable;
editor.subscribe(EventHandler.bind(self, handle_editor_event)) catch unreachable;
self.editor = if (editor.dynamic_cast(ed.EditorWidget)) |p| &p.editor else null;
} else unreachable;
self.widgets.replace(0, editor_widget);
self.resize();
}
fn show_home_async(_: *Self) void {
tp.self_pid().send(.{ "cmd", "show_home" }) catch return;
}
fn create_home(self: *Self) tp.result {
if (self.editor) |_| return;
var home_widget = home.create(self.a, Widget.to(self)) catch |e| return tp.exit_error(e);
errdefer home_widget.deinit(self.a);
self.widgets.replace(0, home_widget);
self.resize();
}
fn write_restore_info(self: *Self) void {
if (self.editor) |editor| {
var sfa = std.heap.stackFallback(512, self.a);
const a = sfa.get();
var meta = std.ArrayList(u8).init(a);
editor.write_state(meta.writer()) catch return;
const file_name = root.get_restore_file_name() catch return;
var file = std.fs.createFileAbsolute(file_name, .{ .truncate = true }) catch return;
defer file.close();
file.writeAll(meta.items) catch return;
}
}
fn read_restore_info(self: *Self) !void {
if (self.editor) |editor| {
const file_name = try root.get_restore_file_name();
const file = try std.fs.cwd().openFile(file_name, .{ .mode = .read_only });
defer file.close();
const stat = try file.stat();
var buf = try self.a.alloc(u8, stat.size);
defer self.a.free(buf);
const size = try file.readAll(buf);
try editor.extract_state(buf[0..size]);
}
}

44
src/tui/message_box.zig Normal file
View file

@ -0,0 +1,44 @@
const Allocator = @import("std").mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const Widget = @import("Widget.zig");
const tui = @import("tui.zig");
pub const name = @typeName(Self);
const Self = @This();
plane: nc.Plane,
const y_pos = 10;
const y_pos_hidden = -15;
const x_pos = 10;
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
return Widget.to(self);
}
pub fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts_vscroll(name), parent);
errdefer n.deinit();
return .{
.plane = n,
};
}
pub fn deinit(self: *Self, a: Allocator) void {
self.plane.deinit();
a.destroy(self);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
_ = self;
_ = m;
return false;
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
tui.set_base_style(&self.plane, " ", theme.sidebar);
return false;
}

286
src/tui/mode/input/flow.zig Normal file
View file

@ -0,0 +1,286 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const root = @import("root");
const tui = @import("../../tui.zig");
const command = @import("../../command.zig");
const EventHandler = @import("../../EventHandler.zig");
const Allocator = @import("std").mem.Allocator;
const ArrayList = @import("std").ArrayList;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
const input_buffer_size = 1024;
a: Allocator,
input: ArrayList(u8),
last_cmd: []const u8 = "",
leader: ?struct { keypress: u32, modifiers: u32 } = null,
pub fn create(a: Allocator) !tui.Mode {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.input = try ArrayList(u8).initCapacity(a, input_buffer_size),
};
return .{
.handler = EventHandler.to_owned(self),
.name = root.application_logo ++ root.application_name,
};
}
pub fn deinit(self: *Self) void {
self.input.deinit();
self.a.destroy(self);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var egc: u32 = undefined;
var modifiers: u32 = undefined;
var text: []const u8 = undefined;
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, egc, modifiers);
} else if (try m.match(.{"F"})) {
try self.flush_input();
} else if (try m.match(.{ "system_clipboard", tp.extract(&text) })) {
try self.flush_input();
try self.insert_bytes(text);
try self.flush_input();
}
return false;
}
pub fn add_keybind() void {}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
return switch (evtype) {
nc.event_type.PRESS => self.mapPress(keypress, egc, modifiers),
nc.event_type.REPEAT => self.mapPress(keypress, egc, modifiers),
nc.event_type.RELEASE => self.mapRelease(keypress, egc, modifiers),
else => {},
};
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
if (self.leader) |_| return self.mapFollower(keynormal, egc, modifiers);
return switch (modifiers) {
mod.CTRL => switch (keynormal) {
'J' => self.cmd("toggle_logview", .{}),
'Z' => self.cmd("undo", .{}),
'Y' => self.cmd("redo", .{}),
'Q' => self.cmd("quit", .{}),
'O' => self.cmd("enter_open_file_mode", .{}),
'W' => self.cmd("close_file", .{}),
'S' => self.cmd("save_file", .{}),
'L' => self.cmd_cycle3("scroll_view_center", "scroll_view_top", "scroll_view_bottom", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'B' => self.cmd("enter_move_to_char_mode", command.fmt(.{false})),
'T' => self.cmd("enter_move_to_char_mode", command.fmt(.{true})),
'X' => self.cmd("cut", .{}),
'C' => self.cmd("copy", .{}),
'V' => self.cmd("system_paste", .{}),
'U' => self.cmd("pop_cursor", .{}),
'K' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'F' => self.cmd("enter_find_mode", .{}),
'G' => self.cmd("enter_goto_mode", .{}),
'D' => self.cmd("add_cursor_next_match", .{}),
'A' => self.cmd("select_all", .{}),
'I' => self.insert_bytes("\t"),
'/' => self.cmd("toggle_comment", .{}),
key.ENTER => self.cmd("insert_line_after", .{}),
key.SPACE => self.cmd("selections_reverse", .{}),
key.END => self.cmd("move_buffer_end", .{}),
key.HOME => self.cmd("move_buffer_begin", .{}),
key.UP => self.cmd("move_scroll_up", .{}),
key.DOWN => self.cmd("move_scroll_down", .{}),
key.PGUP => self.cmd("move_scroll_page_up", .{}),
key.PGDOWN => self.cmd("move_scroll_page_down", .{}),
key.LEFT => self.cmd("move_word_left", .{}),
key.RIGHT => self.cmd("move_word_right", .{}),
key.BACKSPACE => self.cmd("delete_word_left", .{}),
key.DEL => self.cmd("delete_word_right", .{}),
else => {},
},
mod.CTRL | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_down", .{}),
'Z' => self.cmd("redo", .{}),
'Q' => self.cmd("quit_without_saving", .{}),
'R' => self.cmd("restart", .{}),
'F' => self.cmd("enter_find_in_files_mode", .{}),
'L' => self.cmd_async("toggle_logview"),
'I' => self.cmd_async("toggle_inputview"),
'/' => self.cmd("log_widgets", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.END => self.cmd("select_buffer_end", .{}),
key.HOME => self.cmd("select_buffer_begin", .{}),
key.UP => self.cmd("select_scroll_up", .{}),
key.DOWN => self.cmd("select_scroll_down", .{}),
key.LEFT => self.cmd("select_word_left", .{}),
key.RIGHT => self.cmd("select_word_right", .{}),
else => {},
},
mod.ALT => switch (keynormal) {
'J' => self.cmd("join_next_line", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'L' => self.cmd("toggle_logview", .{}),
'I' => self.cmd("toggle_inputview", .{}),
'B' => self.cmd("move_word_left", .{}),
'F' => self.cmd("move_word_right", .{}),
'S' => self.cmd("filter", command.fmt(.{"sort"})),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("jump_back", .{}),
key.RIGHT => self.cmd("jump_forward", .{}),
key.UP => self.cmd("pull_up", .{}),
key.DOWN => self.cmd("pull_down", .{}),
key.ENTER => self.cmd("insert_line", .{}),
else => {},
},
mod.ALT | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_up", .{}),
// 'B' => self.cmd("select_word_left", .{}),
// 'F' => self.cmd("select_word_right", .{}),
'F' => self.cmd("filter", command.fmt(.{ "zig", "fmt", "--stdin" })),
'S' => self.cmd("filter", command.fmt(.{ "sort", "-u" })),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("move_scroll_left", .{}),
key.RIGHT => self.cmd("move_scroll_right", .{}),
key.UP => self.cmd("add_cursor_up", .{}),
key.DOWN => self.cmd("add_cursor_down", .{}),
else => {},
},
mod.SHIFT => switch (keypress) {
key.F03 => self.cmd("goto_prev_match", .{}),
key.LEFT => self.cmd("select_left", .{}),
key.RIGHT => self.cmd("select_right", .{}),
key.UP => self.cmd("select_up", .{}),
key.DOWN => self.cmd("select_down", .{}),
key.HOME => self.cmd("smart_select_begin", .{}),
key.END => self.cmd("select_end", .{}),
key.PGUP => self.cmd("select_page_up", .{}),
key.PGDOWN => self.cmd("select_page_down", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
key.TAB => self.cmd("unindent", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
0 => switch (keypress) {
key.F02 => self.cmd("toggle_input_mode", .{}),
key.F03 => self.cmd("goto_next_match", .{}),
key.F15 => self.cmd("goto_prev_match", .{}), // S-F3
key.F05 => self.cmd("toggle_inspector_view", .{}), // C-F5
key.F06 => self.cmd("dump_current_line_tree", .{}),
key.F07 => self.cmd("dump_current_line", .{}),
key.F09 => self.cmd("theme_prev", .{}),
key.F10 => self.cmd("theme_next", .{}),
key.F11 => self.cmd("toggle_logview", .{}),
key.F12 => self.cmd("toggle_inputview", .{}),
key.F34 => self.cmd("toggle_whitespace", .{}), // C-F10
key.ESC => self.cmd("cancel", .{}),
key.ENTER => self.cmd("smart_insert_line", .{}),
key.DEL => self.cmd("delete_forward", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
key.LEFT => self.cmd("move_left", .{}),
key.RIGHT => self.cmd("move_right", .{}),
key.UP => self.cmd("move_up", .{}),
key.DOWN => self.cmd("move_down", .{}),
key.HOME => self.cmd("smart_move_begin", .{}),
key.END => self.cmd("move_end", .{}),
key.PGUP => self.cmd("move_page_up", .{}),
key.PGDOWN => self.cmd("move_page_down", .{}),
key.LCTRL, key.RCTRL => self.cmd("enable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("enable_fast_scroll", .{}),
key.TAB => self.cmd("indent", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
else => {},
};
}
fn mapFollower(self: *Self, keypress: u32, _: u32, modifiers: u32) tp.result {
defer self.leader = null;
const ldr = if (self.leader) |leader| leader else return;
return switch (ldr.modifiers) {
mod.CTRL => switch (ldr.keypress) {
'K' => switch (modifiers) {
mod.CTRL => switch (keypress) {
'U' => self.cmd("delete_to_begin", .{}),
'K' => self.cmd("delete_to_end", .{}),
'D' => self.cmd("move_cursor_next_match", .{}),
else => {},
},
else => {},
},
else => {},
},
else => {},
};
}
fn mapRelease(self: *Self, keypress: u32, _: u32, _: u32) tp.result {
return switch (keypress) {
key.LCTRL, key.RCTRL => self.cmd("disable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("disable_fast_scroll", .{}),
else => {},
};
}
fn insert_code_point(self: *Self, c: u32) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
var buf: [6]u8 = undefined;
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, &buf) catch |e| return tp.exit_error(e);
self.input.appendSlice(buf[0..bytes]) catch |e| return tp.exit_error(e);
}
fn insert_bytes(self: *Self, bytes: []const u8) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
self.input.appendSlice(bytes) catch |e| return tp.exit_error(e);
}
var insert_chars_id: ?command.ID = null;
fn flush_input(self: *Self) tp.result {
if (self.input.items.len > 0) {
defer self.input.clearRetainingCapacity();
const id = insert_chars_id orelse command.get_id_cache("insert_chars", &insert_chars_id) orelse {
return tp.exit_error(error.InputTargetNotFound);
};
try command.execute(id, command.fmt(.{self.input.items}));
self.last_cmd = "insert_chars";
}
}
fn cmd(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
try self.flush_input();
self.last_cmd = name_;
try command.executeName(name_, ctx);
}
fn cmd_cycle3(self: *Self, name1: []const u8, name2: []const u8, name3: []const u8, ctx: command.Context) tp.result {
return if (eql(u8, self.last_cmd, name2))
self.cmd(name3, ctx)
else if (eql(u8, self.last_cmd, name1))
self.cmd(name2, ctx)
else
self.cmd(name1, ctx);
}
fn cmd_async(self: *Self, name_: []const u8) tp.result {
self.last_cmd = name_;
return tp.self_pid().send(.{ "cmd", name_ });
}

103
src/tui/mode/input/home.zig Normal file
View file

@ -0,0 +1,103 @@
const std = @import("std");
const nc = @import("notcurses");
const tp = @import("thespian");
const root = @import("root");
const tui = @import("../../tui.zig");
const command = @import("../../command.zig");
const EventHandler = @import("../../EventHandler.zig");
const Self = @This();
a: std.mem.Allocator,
f: usize = 0,
pub fn create(a: std.mem.Allocator) !tui.Mode {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
};
return .{
.handler = EventHandler.to_owned(self),
.name = root.application_logo ++ root.application_name,
};
}
pub fn deinit(self: *Self) void {
self.a.destroy(self);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var modifiers: u32 = undefined;
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.any, tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, modifiers);
}
return false;
}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, modifiers: u32) tp.result {
return switch (evtype) {
nc.event_type.PRESS => self.mapPress(keypress, modifiers),
else => {},
};
}
fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
return switch (modifiers) {
nc.mod.CTRL => switch (keynormal) {
'F' => self.sheeran(),
'J' => self.cmd("toggle_logview", .{}),
'Q' => self.cmd("quit", .{}),
'W' => self.cmd("quit", .{}),
'O' => self.cmd("enter_open_file_mode", .{}),
'/' => self.cmd("open_help", .{}),
else => {},
},
nc.mod.CTRL | nc.mod.SHIFT => switch (keynormal) {
'Q' => self.cmd("quit_without_saving", .{}),
'R' => self.cmd("restart", .{}),
'F' => self.cmd("enter_find_in_files_mode", .{}),
'L' => self.cmd_async("toggle_logview"),
'I' => self.cmd_async("toggle_inputview"),
'/' => self.cmd("open_help", .{}),
else => {},
},
nc.mod.ALT => switch (keynormal) {
'L' => self.cmd("toggle_logview", .{}),
'I' => self.cmd("toggle_inputview", .{}),
nc.key.LEFT => self.cmd("jump_back", .{}),
nc.key.RIGHT => self.cmd("jump_forward", .{}),
else => {},
},
0 => switch (keypress) {
nc.key.F01 => self.cmd("open_help", .{}),
nc.key.F06 => self.cmd("open_config", .{}),
nc.key.F09 => self.cmd("theme_prev", .{}),
nc.key.F10 => self.cmd("theme_next", .{}),
nc.key.F11 => self.cmd("toggle_logview", .{}),
nc.key.F12 => self.cmd("toggle_inputview", .{}),
else => {},
},
else => {},
};
}
fn cmd(_: *Self, name_: []const u8, ctx: command.Context) tp.result {
try command.executeName(name_, ctx);
}
fn cmd_async(_: *Self, name_: []const u8) tp.result {
return tp.self_pid().send(.{ "cmd", name_ });
}
fn sheeran(self: *Self) void {
self.f += 1;
if (self.f >= 5) {
self.f = 0;
self.cmd("home_sheeran", .{}) catch {};
}
}

View file

@ -0,0 +1,284 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const root = @import("root");
const tui = @import("../../../tui.zig");
const command = @import("../../../command.zig");
const EventHandler = @import("../../../EventHandler.zig");
const Allocator = @import("std").mem.Allocator;
const ArrayList = @import("std").ArrayList;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
const input_buffer_size = 1024;
a: Allocator,
input: ArrayList(u8),
last_cmd: []const u8 = "",
leader: ?struct { keypress: u32, modifiers: u32 } = null,
pub fn create(a: Allocator) !tui.Mode {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.input = try ArrayList(u8).initCapacity(a, input_buffer_size),
};
return .{
.handler = EventHandler.to_owned(self),
.name = root.application_logo ++ "INSERT",
};
}
pub fn deinit(self: *Self) void {
self.input.deinit();
self.a.destroy(self);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var egc: u32 = undefined;
var modifiers: u32 = undefined;
var text: []const u8 = undefined;
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, egc, modifiers);
} else if (try m.match(.{"F"})) {
try self.flush_input();
} else if (try m.match(.{ "system_clipboard", tp.extract(&text) })) {
try self.flush_input();
try self.insert_bytes(text);
try self.flush_input();
}
return false;
}
pub fn add_keybind() void {}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
return switch (evtype) {
nc.event_type.PRESS => self.mapPress(keypress, egc, modifiers),
nc.event_type.REPEAT => self.mapPress(keypress, egc, modifiers),
nc.event_type.RELEASE => self.mapRelease(keypress, egc, modifiers),
else => {},
};
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
if (self.leader) |_| return self.mapFollower(keynormal, egc, modifiers);
return switch (modifiers) {
mod.CTRL => switch (keynormal) {
'J' => self.cmd("toggle_logview", .{}),
'Z' => self.cmd("undo", .{}),
'Y' => self.cmd("redo", .{}),
'Q' => self.cmd("quit", .{}),
'W' => self.cmd("close_file", .{}),
'S' => self.cmd("save_file", .{}),
'L' => self.cmd_cycle3("scroll_view_center", "scroll_view_top", "scroll_view_bottom", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'B' => self.cmd("enter_move_to_char_mode", command.fmt(.{false})),
'T' => self.cmd("enter_move_to_char_mode", command.fmt(.{true})),
'X' => self.cmd("cut", .{}),
'C' => self.cmd("enter_mode", command.fmt(.{"vim/normal"})),
'V' => self.cmd("system_paste", .{}),
'U' => self.cmd("pop_cursor", .{}),
'K' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'F' => self.cmd("enter_find_mode", .{}),
'G' => self.cmd("enter_goto_mode", .{}),
'O' => self.cmd("run_ls", .{}),
'D' => self.cmd("add_cursor_next_match", .{}),
'A' => self.cmd("select_all", .{}),
'I' => self.insert_bytes("\t"),
'/' => self.cmd("toggle_comment", .{}),
key.ENTER => self.cmd("insert_line_after", .{}),
key.SPACE => self.cmd("selections_reverse", .{}),
key.END => self.cmd("move_buffer_end", .{}),
key.HOME => self.cmd("move_buffer_begin", .{}),
key.UP => self.cmd("move_scroll_up", .{}),
key.DOWN => self.cmd("move_scroll_down", .{}),
key.PGUP => self.cmd("move_scroll_page_up", .{}),
key.PGDOWN => self.cmd("move_scroll_page_down", .{}),
key.LEFT => self.cmd("move_word_left", .{}),
key.RIGHT => self.cmd("move_word_right", .{}),
key.BACKSPACE => self.cmd("delete_word_left", .{}),
key.DEL => self.cmd("delete_word_right", .{}),
else => {},
},
mod.CTRL | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_down", .{}),
'Z' => self.cmd("redo", .{}),
'Q' => self.cmd("quit_without_saving", .{}),
'R' => self.cmd("restart", .{}),
'F' => self.cmd("enter_find_in_files_mode", .{}),
'L' => self.cmd_async("toggle_logview"),
'I' => self.cmd_async("toggle_inputview"),
'/' => self.cmd("log_widgets", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.END => self.cmd("select_buffer_end", .{}),
key.HOME => self.cmd("select_buffer_begin", .{}),
key.UP => self.cmd("select_scroll_up", .{}),
key.DOWN => self.cmd("select_scroll_down", .{}),
key.LEFT => self.cmd("select_word_left", .{}),
key.RIGHT => self.cmd("select_word_right", .{}),
else => {},
},
mod.ALT => switch (keynormal) {
'J' => self.cmd("join_next_line", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'L' => self.cmd("toggle_logview", .{}),
'I' => self.cmd("toggle_inputview", .{}),
'B' => self.cmd("move_word_left", .{}),
'F' => self.cmd("move_word_right", .{}),
'S' => self.cmd("filter", command.fmt(.{"sort"})),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("jump_back", .{}),
key.RIGHT => self.cmd("jump_forward", .{}),
key.UP => self.cmd("pull_up", .{}),
key.DOWN => self.cmd("pull_down", .{}),
key.ENTER => self.cmd("insert_line", .{}),
else => {},
},
mod.ALT | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_up", .{}),
'F' => self.cmd("filter", command.fmt(.{ "zig", "fmt", "--stdin" })),
'S' => self.cmd("filter", command.fmt(.{ "sort", "-u" })),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("move_scroll_left", .{}),
key.RIGHT => self.cmd("move_scroll_right", .{}),
key.UP => self.cmd("add_cursor_up", .{}),
key.DOWN => self.cmd("add_cursor_down", .{}),
else => {},
},
mod.SHIFT => switch (keypress) {
key.F03 => self.cmd("goto_prev_match", .{}),
key.LEFT => self.cmd("select_left", .{}),
key.RIGHT => self.cmd("select_right", .{}),
key.UP => self.cmd("select_up", .{}),
key.DOWN => self.cmd("select_down", .{}),
key.HOME => self.cmd("smart_select_begin", .{}),
key.END => self.cmd("select_end", .{}),
key.PGUP => self.cmd("select_page_up", .{}),
key.PGDOWN => self.cmd("select_page_down", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
key.TAB => self.cmd("unindent", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
0 => switch (keypress) {
key.F02 => self.cmd("toggle_input_mode", .{}),
key.F03 => self.cmd("goto_next_match", .{}),
key.F15 => self.cmd("goto_prev_match", .{}), // S-F3
key.F05 => self.cmd("toggle_inspector_view", .{}), // C-F5
key.F06 => self.cmd("dump_current_line_tree", .{}),
key.F07 => self.cmd("dump_current_line", .{}),
key.F09 => self.cmd("theme_prev", .{}),
key.F10 => self.cmd("theme_next", .{}),
key.F11 => self.cmd("toggle_logview", .{}),
key.F12 => self.cmd("toggle_inputview", .{}),
key.F34 => self.cmd("toggle_whitespace", .{}), // C-F10
key.ESC => self.cmd("enter_mode", command.fmt(.{"vim/normal"})),
key.ENTER => self.cmd("smart_insert_line", .{}),
key.DEL => self.cmd("delete_forward", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
key.LEFT => self.cmd("move_left", .{}),
key.RIGHT => self.cmd("move_right", .{}),
key.UP => self.cmd("move_up", .{}),
key.DOWN => self.cmd("move_down", .{}),
key.HOME => self.cmd("smart_move_begin", .{}),
key.END => self.cmd("move_end", .{}),
key.PGUP => self.cmd("move_page_up", .{}),
key.PGDOWN => self.cmd("move_page_down", .{}),
key.LCTRL, key.RCTRL => self.cmd("enable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("enable_fast_scroll", .{}),
key.TAB => self.cmd("indent", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
else => {},
};
}
fn mapFollower(self: *Self, keypress: u32, _: u32, modifiers: u32) tp.result {
defer self.leader = null;
const ldr = if (self.leader) |leader| leader else return;
return switch (ldr.modifiers) {
mod.CTRL => switch (ldr.keypress) {
'K' => switch (modifiers) {
mod.CTRL => switch (keypress) {
'U' => self.cmd("delete_to_begin", .{}),
'K' => self.cmd("delete_to_end", .{}),
'D' => self.cmd("move_cursor_next_match", .{}),
else => {},
},
else => {},
},
else => {},
},
else => {},
};
}
fn mapRelease(self: *Self, keypress: u32, _: u32, _: u32) tp.result {
return switch (keypress) {
key.LCTRL, key.RCTRL => self.cmd("disable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("disable_fast_scroll", .{}),
else => {},
};
}
fn insert_code_point(self: *Self, c: u32) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
var buf: [6]u8 = undefined;
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, &buf) catch |e| return tp.exit_error(e);
self.input.appendSlice(buf[0..bytes]) catch |e| return tp.exit_error(e);
}
fn insert_bytes(self: *Self, bytes: []const u8) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
self.input.appendSlice(bytes) catch |e| return tp.exit_error(e);
}
var insert_chars_id: ?command.ID = null;
fn flush_input(self: *Self) tp.result {
if (self.input.items.len > 0) {
defer self.input.clearRetainingCapacity();
const id = insert_chars_id orelse command.get_id_cache("insert_chars", &insert_chars_id) orelse {
return tp.exit_error(error.InputTargetNotFound);
};
try command.execute(id, command.fmt(.{self.input.items}));
self.last_cmd = "insert_chars";
}
}
fn cmd(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
try self.flush_input();
self.last_cmd = name_;
try command.executeName(name_, ctx);
}
fn cmd_cycle3(self: *Self, name1: []const u8, name2: []const u8, name3: []const u8, ctx: command.Context) tp.result {
return if (eql(u8, self.last_cmd, name2))
self.cmd(name3, ctx)
else if (eql(u8, self.last_cmd, name1))
self.cmd(name2, ctx)
else
self.cmd(name1, ctx);
}
fn cmd_async(self: *Self, name_: []const u8) tp.result {
self.last_cmd = name_;
return tp.self_pid().send(.{ "cmd", name_ });
}

View file

@ -0,0 +1,497 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const root = @import("root");
const tui = @import("../../../tui.zig");
const command = @import("../../../command.zig");
const EventHandler = @import("../../../EventHandler.zig");
const Allocator = @import("std").mem.Allocator;
const ArrayList = @import("std").ArrayList;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
const input_buffer_size = 1024;
a: Allocator,
input: ArrayList(u8),
last_cmd: []const u8 = "",
leader: ?struct { keypress: u32, modifiers: u32 } = null,
count: usize = 0,
pub fn create(a: Allocator) !tui.Mode {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.input = try ArrayList(u8).initCapacity(a, input_buffer_size),
};
return .{
.handler = EventHandler.to_owned(self),
.name = root.application_logo ++ "NORMAL",
};
}
pub fn deinit(self: *Self) void {
self.input.deinit();
self.a.destroy(self);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var egc: u32 = undefined;
var modifiers: u32 = undefined;
var text: []const u8 = undefined;
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, egc, modifiers);
} else if (try m.match(.{"F"})) {
try self.flush_input();
} else if (try m.match(.{ "system_clipboard", tp.extract(&text) })) {
try self.flush_input();
try self.insert_bytes(text);
try self.flush_input();
}
return false;
}
pub fn add_keybind() void {}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
return switch (evtype) {
nc.event_type.PRESS => self.mapPress(keypress, egc, modifiers),
nc.event_type.REPEAT => self.mapPress(keypress, egc, modifiers),
nc.event_type.RELEASE => self.mapRelease(keypress, egc, modifiers),
else => {},
};
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
if (self.leader) |_| return self.mapFollower(keynormal, egc, modifiers);
return switch (modifiers) {
mod.CTRL => switch (keynormal) {
'R' => self.cmd("redo", .{}),
'O' => self.cmd("jump_back", .{}),
'I' => self.cmd("jump_forward", .{}),
'J' => self.cmd("toggle_logview", .{}),
'Z' => self.cmd("undo", .{}),
'Y' => self.cmd("redo", .{}),
'Q' => self.cmd("quit", .{}),
'W' => self.cmd("close_file", .{}),
'S' => self.cmd("save_file", .{}),
'L' => self.cmd_cycle3("scroll_view_center", "scroll_view_top", "scroll_view_bottom", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'B' => self.cmd("enter_move_to_char_mode", command.fmt(.{false})),
'T' => self.cmd("enter_move_to_char_mode", command.fmt(.{true})),
'X' => self.cmd("cut", .{}),
'C' => self.cmd("copy", .{}),
'V' => self.cmd("system_paste", .{}),
'U' => self.cmd("pop_cursor", .{}),
'K' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'F' => self.cmd("enter_find_mode", .{}),
'G' => self.cmd("enter_goto_mode", .{}),
'D' => self.cmd("add_cursor_next_match", .{}),
'A' => self.cmd("select_all", .{}),
'/' => self.cmd("toggle_comment", .{}),
key.ENTER => self.cmd("insert_line_after", .{}),
key.SPACE => self.cmd("selections_reverse", .{}),
key.END => self.cmd("move_buffer_end", .{}),
key.HOME => self.cmd("move_buffer_begin", .{}),
key.UP => self.cmd("move_scroll_up", .{}),
key.DOWN => self.cmd("move_scroll_down", .{}),
key.PGUP => self.cmd("move_scroll_page_up", .{}),
key.PGDOWN => self.cmd("move_scroll_page_down", .{}),
key.LEFT => self.cmd("move_word_left", .{}),
key.RIGHT => self.cmd("move_word_right", .{}),
key.BACKSPACE => self.cmd("delete_word_left", .{}),
key.DEL => self.cmd("delete_word_right", .{}),
else => {},
},
mod.CTRL | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_down", .{}),
'Z' => self.cmd("redo", .{}),
'Q' => self.cmd("quit_without_saving", .{}),
'R' => self.cmd("restart", .{}),
'F' => self.cmd("enter_find_in_files_mode", .{}),
'L' => self.cmd_async("toggle_logview"),
'I' => self.cmd_async("toggle_inputview"),
'/' => self.cmd("log_widgets", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.END => self.cmd("select_buffer_end", .{}),
key.HOME => self.cmd("select_buffer_begin", .{}),
key.UP => self.cmd("select_scroll_up", .{}),
key.DOWN => self.cmd("select_scroll_down", .{}),
key.LEFT => self.cmd("select_word_left", .{}),
key.RIGHT => self.cmd("select_word_right", .{}),
else => {},
},
mod.ALT => switch (keynormal) {
'J' => self.cmd("join_next_line", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'L' => self.cmd("toggle_logview", .{}),
'I' => self.cmd("toggle_inputview", .{}),
'B' => self.cmd("move_word_left", .{}),
'F' => self.cmd("move_word_right", .{}),
'S' => self.cmd("filter", command.fmt(.{"sort"})),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("jump_back", .{}),
key.RIGHT => self.cmd("jump_forward", .{}),
key.UP => self.cmd("pull_up", .{}),
key.DOWN => self.cmd("pull_down", .{}),
key.ENTER => self.cmd("insert_line", .{}),
else => {},
},
mod.ALT | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_up", .{}),
// 'B' => self.cmd("select_word_left", .{}),
// 'F' => self.cmd("select_word_right", .{}),
'F' => self.cmd("filter", command.fmt(.{ "zig", "fmt", "--stdin" })),
'S' => self.cmd("filter", command.fmt(.{ "sort", "-u" })),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("move_scroll_left", .{}),
key.RIGHT => self.cmd("move_scroll_right", .{}),
key.UP => self.cmd("add_cursor_up", .{}),
key.DOWN => self.cmd("add_cursor_down", .{}),
else => {},
},
mod.SHIFT => switch (keypress) {
key.F03 => self.cmd("goto_prev_match", .{}),
key.LEFT => self.cmd("select_left", .{}),
key.RIGHT => self.cmd("select_right", .{}),
key.UP => self.cmd("select_up", .{}),
key.DOWN => self.cmd("select_down", .{}),
key.HOME => self.cmd("smart_select_begin", .{}),
key.END => self.cmd("select_end", .{}),
key.PGUP => self.cmd("select_page_up", .{}),
key.PGDOWN => self.cmd("select_page_down", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
key.TAB => self.cmd("unindent", .{}),
'N' => self.cmd("goto_prev_match", .{}),
'A' => self.seq(.{ "move_end", "enter_mode" }, command.fmt(.{"vim/insert"})),
'4' => self.cmd("move_end", .{}),
'G' => if (self.count == 0)
self.cmd("move_buffer_end", .{})
else {
const count = self.count;
try self.cmd("move_buffer_begin", .{});
self.count = count - 1;
if (self.count > 0)
try self.cmd_count("move_down", .{});
},
'O' => self.seq(.{ "insert_line_before", "enter_mode" }, command.fmt(.{"vim/insert"})),
else => {},
},
0 => switch (keypress) {
key.F02 => self.cmd("toggle_input_mode", .{}),
key.F03 => self.cmd("goto_next_match", .{}),
key.F15 => self.cmd("goto_prev_match", .{}), // S-F3
key.F05 => self.cmd("toggle_inspector_view", .{}), // C-F5
key.F06 => self.cmd("dump_current_line_tree", .{}),
key.F07 => self.cmd("dump_current_line", .{}),
key.F09 => self.cmd("theme_prev", .{}),
key.F10 => self.cmd("theme_next", .{}),
key.F11 => self.cmd("toggle_logview", .{}),
key.F12 => self.cmd("toggle_inputview", .{}),
key.F34 => self.cmd("toggle_whitespace", .{}), // C-F10
key.ESC => self.cmd("cancel", .{}),
key.ENTER => self.cmd("smart_insert_line", .{}),
key.DEL => self.cmd("delete_forward", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
'i' => self.cmd("enter_mode", command.fmt(.{"vim/insert"})),
'a' => self.seq(.{ "move_right", "enter_mode" }, command.fmt(.{"vim/insert"})),
'v' => self.cmd("enter_mode", command.fmt(.{"vim/visual"})),
'/' => self.cmd("enter_find_mode", .{}),
'n' => self.cmd("goto_next_match", .{}),
'h' => self.cmd_count("move_left", .{}),
'j' => self.cmd_count("move_down", .{}),
'k' => self.cmd_count("move_up", .{}),
'l' => self.cmd_count("move_right", .{}),
' ' => self.cmd_count("move_right", .{}),
'b' => self.cmd_count("move_word_left", .{}),
'w' => self.cmd_count("move_word_right_vim", .{}),
'e' => self.cmd_count("move_word_right", .{}),
'$' => self.cmd_count("move_end", .{}),
'0' => self.cmd_count("move_begin", .{}),
'1' => self.add_count(1),
'2' => self.add_count(2),
'3' => self.add_count(3),
'4' => self.add_count(4),
'5' => self.add_count(5),
'6' => self.add_count(6),
'7' => self.add_count(7),
'8' => self.add_count(8),
'9' => self.add_count(9),
'x' => self.cmd_count("delete_forward", .{}),
'u' => self.cmd("undo", .{}),
'd' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'r' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'c' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'z' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'g' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'y' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'p' => self.cmd("paste", .{}),
'o' => self.seq(.{ "insert_line_after", "enter_mode" }, command.fmt(.{"vim/insert"})),
key.LEFT => self.cmd("move_left", .{}),
key.RIGHT => self.cmd("move_right", .{}),
key.UP => self.cmd("move_up", .{}),
key.DOWN => self.cmd("move_down", .{}),
key.HOME => self.cmd("smart_move_begin", .{}),
key.END => self.cmd("move_end", .{}),
key.PGUP => self.cmd("move_page_up", .{}),
key.PGDOWN => self.cmd("move_page_down", .{}),
key.LCTRL, key.RCTRL => self.cmd("enable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("enable_fast_scroll", .{}),
key.TAB => self.cmd("indent", .{}),
else => {},
},
else => {},
};
}
fn mapFollower(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
if (keypress == key.LCTRL or
keypress == key.RCTRL or
keypress == key.LALT or
keypress == key.RALT or
keypress == key.LSHIFT or
keypress == key.RSHIFT or
keypress == key.LSUPER or
keypress == key.RSUPER) return;
switch (modifiers) {
0 => switch (keypress) {
'1' => {
self.add_count(1);
return;
},
'2' => {
self.add_count(2);
return;
},
'3' => {
self.add_count(3);
return;
},
'4' => {
self.add_count(4);
return;
},
'5' => {
self.add_count(5);
return;
},
'6' => {
self.add_count(6);
return;
},
'7' => {
self.add_count(7);
return;
},
'8' => {
self.add_count(8);
return;
},
'9' => {
self.add_count(9);
return;
},
else => {},
},
else => {},
}
defer self.leader = null;
const ldr = if (self.leader) |leader| leader else return;
return switch (ldr.modifiers) {
mod.CTRL => switch (ldr.keypress) {
'K' => switch (modifiers) {
mod.CTRL => switch (keypress) {
'U' => self.cmd("delete_to_begin", .{}),
'K' => self.cmd("delete_to_end", .{}),
'D' => self.cmd("move_cursor_next_match", .{}),
else => {},
},
else => {},
},
else => {},
},
0 => switch (ldr.keypress) {
'D', 'C' => {
try switch (modifiers) {
mod.SHIFT => switch (keypress) {
'4' => self.cmd("delete_to_end", .{}),
else => {},
},
0 => switch (keypress) {
'D' => self.seq_count(.{ "move_begin", "select_end", "select_right", "cut" }, .{}),
'W' => self.seq_count(.{ "select_word_right", "select_word_right", "select_word_left", "cut" }, .{}),
'E' => self.seq_count(.{ "select_word_right", "cut" }, .{}),
else => {},
},
else => switch (egc) {
'$' => self.cmd("delete_to_end", .{}),
else => {},
},
};
if (ldr.keypress == 'C')
try self.cmd("enter_mode", command.fmt(.{"vim/insert"}));
},
'R' => switch (modifiers) {
mod.SHIFT, 0 => if (!key.synthesized_p(keypress)) {
var count = self.count;
try self.cmd_count("delete_forward", .{});
while (count > 0) : (count -= 1)
try self.insert_code_point(egc);
},
else => {},
},
'Z' => switch (modifiers) {
0 => switch (keypress) {
'Z' => self.cmd_cycle3("scroll_view_center", "scroll_view_top", "scroll_view_bottom", .{}),
else => {},
},
else => {},
},
'G' => switch (modifiers) {
0 => switch (keypress) {
'G' => self.cmd("move_buffer_begin", .{}),
else => {},
},
else => {},
},
'Y' => {
try switch (modifiers) {
mod.SHIFT => switch (keypress) {
'4' => self.seq(.{ "select_to_end", "copy" }, .{}),
else => {},
},
0 => switch (keypress) {
'Y' => self.seq_count(.{ "move_begin", "select_end", "select_right", "copy" }, .{}),
'W' => self.seq_count(.{ "select_word_right", "select_word_right", "select_word_left", "copy" }, .{}),
'E' => self.seq_count(.{ "select_word_right", "copy" }, .{}),
else => {},
},
else => switch (egc) {
'$' => self.seq(.{ "select_to_end", "copy" }, .{}),
else => {},
},
};
if (ldr.keypress == 'C')
try self.cmd("enter_mode", command.fmt(.{"vim/insert"}));
},
else => {},
},
else => {},
};
}
fn mapRelease(self: *Self, keypress: u32, _: u32, _: u32) tp.result {
return switch (keypress) {
key.LCTRL, key.RCTRL => self.cmd("disable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("disable_fast_scroll", .{}),
else => {},
};
}
fn add_count(self: *Self, value: usize) void {
if (self.count > 0) self.count *= 10;
self.count += value;
}
fn insert_code_point(self: *Self, c: u32) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
var buf: [6]u8 = undefined;
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, &buf) catch |e| return tp.exit_error(e);
self.input.appendSlice(buf[0..bytes]) catch |e| return tp.exit_error(e);
}
fn insert_bytes(self: *Self, bytes: []const u8) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
self.input.appendSlice(bytes) catch |e| return tp.exit_error(e);
}
var insert_chars_id: ?command.ID = null;
fn flush_input(self: *Self) tp.result {
if (self.input.items.len > 0) {
defer self.input.clearRetainingCapacity();
const id = insert_chars_id orelse command.get_id_cache("insert_chars", &insert_chars_id) orelse {
return tp.exit_error(error.InputTargetNotFound);
};
try command.execute(id, command.fmt(.{self.input.items}));
self.last_cmd = "insert_chars";
}
}
fn cmd(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
self.count = 0;
try self.flush_input();
self.last_cmd = name_;
try command.executeName(name_, ctx);
}
fn cmd_count(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
var count = if (self.count == 0) 1 else self.count;
self.count = 0;
try self.flush_input();
self.last_cmd = name_;
while (count > 0) : (count -= 1)
try command.executeName(name_, ctx);
}
fn cmd_cycle3(self: *Self, name1: []const u8, name2: []const u8, name3: []const u8, ctx: command.Context) tp.result {
return if (eql(u8, self.last_cmd, name2))
self.cmd(name3, ctx)
else if (eql(u8, self.last_cmd, name1))
self.cmd(name2, ctx)
else
self.cmd(name1, ctx);
}
fn cmd_async(self: *Self, name_: []const u8) tp.result {
self.last_cmd = name_;
return tp.self_pid().send(.{ "cmd", name_ });
}
fn seq(self: *Self, cmds: anytype, ctx: command.Context) tp.result {
const cmds_type_info = @typeInfo(@TypeOf(cmds));
if (cmds_type_info != .Struct) @compileError("expected tuple argument");
const fields_info = cmds_type_info.Struct.fields;
inline for (fields_info) |field_info|
try self.cmd(@field(cmds, field_info.name), ctx);
}
fn seq_count(self: *Self, cmds: anytype, ctx: command.Context) tp.result {
var count = if (self.count == 0) 1 else self.count;
self.count = 0;
const cmds_type_info = @typeInfo(@TypeOf(cmds));
if (cmds_type_info != .Struct) @compileError("expected tuple argument");
const fields_info = cmds_type_info.Struct.fields;
while (count > 0) : (count -= 1)
inline for (fields_info) |field_info|
try self.cmd(@field(cmds, field_info.name), ctx);
}

View file

@ -0,0 +1,472 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const root = @import("root");
const tui = @import("../../../tui.zig");
const command = @import("../../../command.zig");
const EventHandler = @import("../../../EventHandler.zig");
const Allocator = @import("std").mem.Allocator;
const ArrayList = @import("std").ArrayList;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
const input_buffer_size = 1024;
a: Allocator,
input: ArrayList(u8),
last_cmd: []const u8 = "",
leader: ?struct { keypress: u32, modifiers: u32 } = null,
count: usize = 0,
pub fn create(a: Allocator) !tui.Mode {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.input = try ArrayList(u8).initCapacity(a, input_buffer_size),
};
return .{
.handler = EventHandler.to_owned(self),
.name = root.application_logo ++ "VISUAL",
};
}
pub fn deinit(self: *Self) void {
self.input.deinit();
self.a.destroy(self);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var egc: u32 = undefined;
var modifiers: u32 = undefined;
var text: []const u8 = undefined;
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, egc, modifiers);
} else if (try m.match(.{"F"})) {
try self.flush_input();
} else if (try m.match(.{ "system_clipboard", tp.extract(&text) })) {
try self.flush_input();
try self.insert_bytes(text);
try self.flush_input();
}
return false;
}
pub fn add_keybind() void {}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
return switch (evtype) {
nc.event_type.PRESS => self.mapPress(keypress, egc, modifiers),
nc.event_type.REPEAT => self.mapPress(keypress, egc, modifiers),
nc.event_type.RELEASE => self.mapRelease(keypress, egc, modifiers),
else => {},
};
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
if (self.leader) |_| return self.mapFollower(keynormal, egc, modifiers);
return switch (modifiers) {
mod.CTRL => switch (keynormal) {
'R' => self.cmd("redo", .{}),
'O' => self.cmd("jump_back", .{}),
'I' => self.cmd("jump_forward", .{}),
'J' => self.cmd("toggle_logview", .{}),
'Z' => self.cmd("undo", .{}),
'Y' => self.cmd("redo", .{}),
'Q' => self.cmd("quit", .{}),
'W' => self.cmd("close_file", .{}),
'S' => self.cmd("save_file", .{}),
'L' => self.cmd_cycle3("scroll_view_center", "scroll_view_top", "scroll_view_bottom", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'B' => self.cmd("enter_move_to_char_mode", command.fmt(.{false})),
'T' => self.cmd("enter_move_to_char_mode", command.fmt(.{true})),
'X' => self.cmd("cut", .{}),
'C' => self.cmd("copy", .{}),
'V' => self.cmd("system_paste", .{}),
'U' => self.cmd("pop_cursor", .{}),
'K' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'F' => self.cmd("enter_find_mode", .{}),
'G' => self.cmd("enter_goto_mode", .{}),
'A' => self.cmd("select_all", .{}),
'/' => self.cmd("toggle_comment", .{}),
key.ENTER => self.cmd("insert_line_after", .{}),
key.SPACE => self.cmd("selections_reverse", .{}),
key.END => self.cmd("select_buffer_end", .{}),
key.HOME => self.cmd("select_buffer_begin", .{}),
key.UP => self.cmd("select_scroll_up", .{}),
key.DOWN => self.cmd("select_scroll_down", .{}),
key.PGUP => self.cmd("select_scroll_page_up", .{}),
key.PGDOWN => self.cmd("select_scroll_page_down", .{}),
key.LEFT => self.cmd("select_word_left", .{}),
key.RIGHT => self.cmd("select_word_right", .{}),
key.BACKSPACE => self.cmd("delete_word_left", .{}),
key.DEL => self.cmd("delete_word_right", .{}),
else => {},
},
mod.CTRL | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_down", .{}),
'Z' => self.cmd("redo", .{}),
'Q' => self.cmd("quit_without_saving", .{}),
'R' => self.cmd("restart", .{}),
'F' => self.cmd("enter_find_in_files_mode", .{}),
'L' => self.cmd_async("toggle_logview"),
'I' => self.cmd_async("toggle_inputview"),
'/' => self.cmd("log_widgets", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.END => self.cmd("select_buffer_end", .{}),
key.HOME => self.cmd("select_buffer_begin", .{}),
key.UP => self.cmd("select_scroll_up", .{}),
key.DOWN => self.cmd("select_scroll_down", .{}),
key.LEFT => self.cmd("select_word_left", .{}),
key.RIGHT => self.cmd("select_word_right", .{}),
else => {},
},
mod.ALT => switch (keynormal) {
'J' => self.cmd("join_next_line", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'L' => self.cmd("toggle_logview", .{}),
'I' => self.cmd("toggle_inputview", .{}),
'B' => self.cmd("select_word_left", .{}),
'F' => self.cmd("select_word_right", .{}),
'S' => self.cmd("filter", command.fmt(.{"sort"})),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("jump_back", .{}),
key.RIGHT => self.cmd("jump_forward", .{}),
key.UP => self.cmd("pull_up", .{}),
key.DOWN => self.cmd("pull_down", .{}),
key.ENTER => self.cmd("insert_line", .{}),
else => {},
},
mod.ALT | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_up", .{}),
'F' => self.cmd("filter", command.fmt(.{ "zig", "fmt", "--stdin" })),
'S' => self.cmd("filter", command.fmt(.{ "sort", "-u" })),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("move_scroll_left", .{}),
key.RIGHT => self.cmd("move_scroll_right", .{}),
else => {},
},
mod.SHIFT => switch (keypress) {
key.F03 => self.cmd("goto_prev_match", .{}),
key.LEFT => self.cmd("select_left", .{}),
key.RIGHT => self.cmd("select_right", .{}),
key.UP => self.cmd("select_up", .{}),
key.DOWN => self.cmd("select_down", .{}),
key.HOME => self.cmd("smart_select_begin", .{}),
key.END => self.cmd("select_end", .{}),
key.PGUP => self.cmd("select_page_up", .{}),
key.PGDOWN => self.cmd("select_page_down", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
key.TAB => self.cmd("unindent", .{}),
'N' => self.cmd("goto_prev_match", .{}),
'A' => self.seq(.{ "move_end", "enter_mode" }, command.fmt(.{"vim/insert"})),
'4' => self.cmd("select_end", .{}),
'G' => if (self.count == 0)
self.cmd("move_buffer_end", .{})
else {
const count = self.count;
try self.cmd("move_buffer_begin", .{});
self.count = count - 1;
if (self.count > 0)
try self.cmd_count("move_down", .{});
},
'O' => self.seq(.{ "insert_line_before", "enter_mode" }, command.fmt(.{"vim/insert"})),
else => {},
},
0 => switch (keypress) {
key.F02 => self.cmd("toggle_input_mode", .{}),
key.F03 => self.cmd("goto_next_match", .{}),
key.F15 => self.cmd("goto_prev_match", .{}), // S-F3
key.F05 => self.cmd("toggle_inspector_view", .{}), // C-F5
key.F06 => self.cmd("dump_current_line_tree", .{}),
key.F07 => self.cmd("dump_current_line", .{}),
key.F09 => self.cmd("theme_prev", .{}),
key.F10 => self.cmd("theme_next", .{}),
key.F11 => self.cmd("toggle_logview", .{}),
key.F12 => self.cmd("toggle_inputview", .{}),
key.F34 => self.cmd("toggle_whitespace", .{}), // C-F10
key.ESC => self.seq(.{ "cancel", "enter_mode" }, command.fmt(.{"vim/normal"})),
key.ENTER => self.cmd("smart_insert_line", .{}),
key.DEL => self.cmd("delete_forward", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
'i' => self.cmd("enter_mode", command.fmt(.{"vim/insert"})),
'a' => self.seq(.{ "move_right", "enter_mode" }, command.fmt(.{"vim/insert"})),
'v' => self.cmd("enter_mode", command.fmt(.{"vim/visual"})),
'/' => self.cmd("enter_find_mode", .{}),
'n' => self.cmd("goto_next_match", .{}),
'h' => self.cmd_count("select_left", .{}),
'j' => self.cmd_count("select_down", .{}),
'k' => self.cmd_count("select_up", .{}),
'l' => self.cmd_count("select_right", .{}),
' ' => self.cmd_count("select_right", .{}),
'b' => self.cmd_count("select_word_left", .{}),
'w' => self.cmd_count("select_word_right_vim", .{}),
'e' => self.cmd_count("select_word_right", .{}),
'$' => self.cmd_count("select_end", .{}),
'0' => self.cmd_count("select_begin", .{}),
'1' => self.add_count(1),
'2' => self.add_count(2),
'3' => self.add_count(3),
'4' => self.add_count(4),
'5' => self.add_count(5),
'6' => self.add_count(6),
'7' => self.add_count(7),
'8' => self.add_count(8),
'9' => self.add_count(9),
'u' => self.cmd("undo", .{}),
'd' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'r' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'c' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'z' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'g' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'x' => self.cmd("cut", .{}),
'y' => self.cmd("copy", .{}),
'p' => self.cmd("paste", .{}),
'o' => self.seq(.{ "insert_line_after", "enter_mode" }, command.fmt(.{"vim/insert"})),
key.LEFT => self.cmd("select_left", .{}),
key.RIGHT => self.cmd("select_right", .{}),
key.UP => self.cmd("select_up", .{}),
key.DOWN => self.cmd("select_down", .{}),
key.HOME => self.cmd("smart_select_begin", .{}),
key.END => self.cmd("select_end", .{}),
key.PGUP => self.cmd("select_page_up", .{}),
key.PGDOWN => self.cmd("select_page_down", .{}),
key.LCTRL, key.RCTRL => self.cmd("enable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("enable_fast_scroll", .{}),
key.TAB => self.cmd("indent", .{}),
else => {},
},
else => {},
};
}
fn mapFollower(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
if (keypress == key.LCTRL or
keypress == key.RCTRL or
keypress == key.LALT or
keypress == key.RALT or
keypress == key.LSHIFT or
keypress == key.RSHIFT or
keypress == key.LSUPER or
keypress == key.RSUPER) return;
switch (modifiers) {
0 => switch (keypress) {
'1' => {
self.add_count(1);
return;
},
'2' => {
self.add_count(2);
return;
},
'3' => {
self.add_count(3);
return;
},
'4' => {
self.add_count(4);
return;
},
'5' => {
self.add_count(5);
return;
},
'6' => {
self.add_count(6);
return;
},
'7' => {
self.add_count(7);
return;
},
'8' => {
self.add_count(8);
return;
},
'9' => {
self.add_count(9);
return;
},
else => {},
},
else => {},
}
defer self.leader = null;
const ldr = if (self.leader) |leader| leader else return;
return switch (ldr.modifiers) {
mod.CTRL => switch (ldr.keypress) {
'K' => switch (modifiers) {
mod.CTRL => switch (keypress) {
'U' => self.cmd("delete_to_begin", .{}),
'K' => self.cmd("delete_to_end", .{}),
'D' => self.cmd("move_cursor_next_match", .{}),
else => {},
},
else => {},
},
else => {},
},
0 => switch (ldr.keypress) {
'D', 'C' => {
try switch (modifiers) {
mod.SHIFT => switch (keypress) {
'4' => self.cmd("delete_to_end", .{}),
else => {},
},
0 => switch (keypress) {
'D' => self.seq_count(.{ "move_begin", "select_end", "select_right", "cut" }, .{}),
'W' => self.seq_count(.{ "select_word_right", "select_word_right", "select_word_left", "cut" }, .{}),
'E' => self.seq_count(.{ "select_word_right", "cut" }, .{}),
else => {},
},
else => switch (egc) {
'$' => self.cmd("delete_to_end", .{}),
else => {},
},
};
if (ldr.keypress == 'C')
try self.cmd("enter_mode", command.fmt(.{"vim/insert"}));
},
'R' => switch (modifiers) {
mod.SHIFT, 0 => if (!key.synthesized_p(keypress)) {
var count = self.count;
try self.cmd_count("delete_forward", .{});
while (count > 0) : (count -= 1)
try self.insert_code_point(egc);
},
else => {},
},
'Z' => switch (modifiers) {
0 => switch (keypress) {
'Z' => self.cmd_cycle3("scroll_view_center", "scroll_view_top", "scroll_view_bottom", .{}),
else => {},
},
else => {},
},
'G' => switch (modifiers) {
0 => switch (keypress) {
'G' => self.cmd("move_buffer_begin", .{}),
else => {},
},
else => {},
},
else => {},
},
else => {},
};
}
fn mapRelease(self: *Self, keypress: u32, _: u32, _: u32) tp.result {
return switch (keypress) {
key.LCTRL, key.RCTRL => self.cmd("disable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("disable_fast_scroll", .{}),
else => {},
};
}
fn add_count(self: *Self, value: usize) void {
if (self.count > 0) self.count *= 10;
self.count += value;
}
fn insert_code_point(self: *Self, c: u32) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
var buf: [6]u8 = undefined;
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, &buf) catch |e| return tp.exit_error(e);
self.input.appendSlice(buf[0..bytes]) catch |e| return tp.exit_error(e);
}
fn insert_bytes(self: *Self, bytes: []const u8) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
self.input.appendSlice(bytes) catch |e| return tp.exit_error(e);
}
var insert_chars_id: ?command.ID = null;
fn flush_input(self: *Self) tp.result {
if (self.input.items.len > 0) {
defer self.input.clearRetainingCapacity();
const id = insert_chars_id orelse command.get_id_cache("insert_chars", &insert_chars_id) orelse {
return tp.exit_error(error.InputTargetNotFound);
};
try command.execute(id, command.fmt(.{self.input.items}));
self.last_cmd = "insert_chars";
}
}
fn cmd(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
self.count = 0;
try self.flush_input();
self.last_cmd = name_;
try command.executeName(name_, ctx);
}
fn cmd_count(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
var count = if (self.count == 0) 1 else self.count;
self.count = 0;
try self.flush_input();
self.last_cmd = name_;
while (count > 0) : (count -= 1)
try command.executeName(name_, ctx);
}
fn cmd_cycle3(self: *Self, name1: []const u8, name2: []const u8, name3: []const u8, ctx: command.Context) tp.result {
return if (eql(u8, self.last_cmd, name2))
self.cmd(name3, ctx)
else if (eql(u8, self.last_cmd, name1))
self.cmd(name2, ctx)
else
self.cmd(name1, ctx);
}
fn cmd_async(self: *Self, name_: []const u8) tp.result {
self.last_cmd = name_;
return tp.self_pid().send(.{ "cmd", name_ });
}
fn seq(self: *Self, cmds: anytype, ctx: command.Context) tp.result {
const cmds_type_info = @typeInfo(@TypeOf(cmds));
if (cmds_type_info != .Struct) @compileError("expected tuple argument");
const fields_info = cmds_type_info.Struct.fields;
inline for (fields_info) |field_info|
try self.cmd(@field(cmds, field_info.name), ctx);
}
fn seq_count(self: *Self, cmds: anytype, ctx: command.Context) tp.result {
var count = if (self.count == 0) 1 else self.count;
self.count = 0;
const cmds_type_info = @typeInfo(@TypeOf(cmds));
if (cmds_type_info != .Struct) @compileError("expected tuple argument");
const fields_info = cmds_type_info.Struct.fields;
while (count > 0) : (count -= 1)
inline for (fields_info) |field_info|
try self.cmd(@field(cmds, field_info.name), ctx);
}

231
src/tui/mode/mini/find.zig Normal file
View file

@ -0,0 +1,231 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const tui = @import("../../tui.zig");
const mainview = @import("../../mainview.zig");
const command = @import("../../command.zig");
const EventHandler = @import("../../EventHandler.zig");
const ed = @import("../../editor.zig");
const Allocator = @import("std").mem.Allocator;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
a: Allocator,
buf: [1024]u8 = undefined,
input: []u8 = "",
last_buf: [1024]u8 = undefined,
last_input: []u8 = "",
start_view: ed.View,
start_cursor: ed.Cursor,
editor: *ed.Editor,
history_pos: ?usize = null,
pub fn create(a: Allocator, _: command.Context) !*Self {
if (tui.current().mainview.dynamic_cast(mainview)) |mv_| if (mv_.get_editor()) |editor| {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.start_view = editor.view,
.start_cursor = editor.get_primary().cursor,
.editor = editor,
};
if (editor.get_primary().selection) |sel| ret: {
const text = editor.get_selection(sel, self.a) catch break :ret;
defer self.a.free(text);
@memcpy(self.buf[0..text.len], text);
self.input = self.buf[0..text.len];
}
return self;
};
return error.NotFound;
}
pub fn deinit(self: *Self) void {
self.a.destroy(self);
}
pub fn handler(self: *Self) EventHandler {
return EventHandler.to_owned(self);
}
pub fn name(_: *Self) []const u8 {
return "find";
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var egc: u32 = undefined;
var modifiers: u32 = undefined;
defer {
if (tui.current().mini_mode) |*mini_mode| {
mini_mode.text = self.input;
mini_mode.cursor = self.input.len;
}
}
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, egc, modifiers);
} else if (try m.match(.{"F"})) {
self.flush_input() catch |e| return e;
}
return false;
}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
switch (evtype) {
nc.event_type.PRESS => try self.mapPress(keypress, egc, modifiers),
nc.event_type.REPEAT => try self.mapPress(keypress, egc, modifiers),
nc.event_type.RELEASE => try self.mapRelease(keypress, egc, modifiers),
else => {},
}
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
return switch (modifiers) {
mod.CTRL => switch (keynormal) {
'Q' => self.cmd("quit", .{}),
'V' => self.cmd("system_paste", .{}),
'U' => self.input = "",
'G' => self.cancel(),
'C' => self.cancel(),
'L' => self.cmd("scroll_view_center", .{}),
'F' => self.cmd("goto_next_match", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'I' => self.insert_bytes("\t"),
key.SPACE => self.cancel(),
key.ENTER => self.insert_bytes("\n"),
key.BACKSPACE => self.input = "",
else => {},
},
mod.ALT => switch (keynormal) {
'V' => self.cmd("system_paste", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
else => {},
},
mod.ALT | mod.SHIFT => switch (keynormal) {
'V' => self.cmd("system_paste", .{}),
else => {},
},
mod.SHIFT => switch (keypress) {
key.ENTER => self.cmd("goto_prev_match", .{}),
key.F03 => self.cmd("goto_prev_match", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
0 => switch (keypress) {
key.UP => self.find_history_prev(),
key.DOWN => self.find_history_next(),
key.F03 => self.cmd("goto_next_match", .{}),
key.F15 => self.cmd("goto_prev_match", .{}),
key.F09 => self.cmd("theme_prev", .{}),
key.F10 => self.cmd("theme_next", .{}),
key.ESC => self.cancel(),
key.ENTER => self.confirm(),
key.BACKSPACE => if (self.input.len > 0) {
self.input = self.input[0 .. self.input.len - 1];
},
key.LCTRL, key.RCTRL => self.cmd("enable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("enable_fast_scroll", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
else => {},
};
}
fn mapRelease(self: *Self, keypress: u32, _: u32, _: u32) tp.result {
return switch (keypress) {
key.LCTRL, key.RCTRL => self.cmd("disable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("disable_fast_scroll", .{}),
else => {},
};
}
fn insert_code_point(self: *Self, c: u32) tp.result {
if (self.input.len + 16 > self.buf.len)
try self.flush_input();
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, self.buf[self.input.len..]) catch |e| return tp.exit_error(e);
self.input = self.buf[0 .. self.input.len + bytes];
}
fn insert_bytes(self: *Self, bytes: []const u8) tp.result {
if (self.input.len + 16 > self.buf.len)
try self.flush_input();
const newlen = self.input.len + bytes.len;
@memcpy(self.buf[self.input.len..newlen], bytes);
self.input = self.buf[0..newlen];
}
var find_cmd_id: ?command.ID = null;
fn flush_input(self: *Self) tp.result {
if (self.input.len > 0) {
if (eql(u8, self.input, self.last_input))
return;
@memcpy(self.last_buf[0..self.input.len], self.input);
self.last_input = self.last_buf[0..self.input.len];
self.editor.find_operation = .goto_next_match;
self.editor.get_primary().cursor = self.start_cursor;
try self.editor.find_in_buffer(self.input);
}
}
fn cmd(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
self.flush_input() catch {};
return command.executeName(name_, ctx);
}
fn confirm(self: *Self) void {
self.editor.push_find_history(self.input);
self.cmd("exit_mini_mode", .{}) catch {};
}
fn cancel(self: *Self) void {
self.editor.get_primary().cursor = self.start_cursor;
self.editor.scroll_to(self.start_view.row);
command.executeName("exit_mini_mode", .{}) catch {};
}
fn find_history_prev(self: *Self) void {
if (self.editor.find_history) |*history| {
if (self.history_pos) |pos| {
if (pos > 0) self.history_pos = pos - 1;
} else {
self.history_pos = history.items.len - 1;
if (self.input.len > 0)
self.editor.push_find_history(self.editor.a.dupe(u8, self.input) catch return);
if (eql(u8, history.items[self.history_pos.?], self.input) and self.history_pos.? > 0)
self.history_pos = self.history_pos.? - 1;
}
self.load_history(self.history_pos.?);
}
}
fn find_history_next(self: *Self) void {
if (self.editor.find_history) |*history| if (self.history_pos) |pos| {
if (pos < history.items.len - 1) {
self.history_pos = pos + 1;
self.load_history(self.history_pos.?);
}
};
}
fn load_history(self: *Self, pos: usize) void {
if (self.editor.find_history) |*history| {
const new = history.items[pos];
@memcpy(self.buf[0..new.len], new);
self.input = self.buf[0..new.len];
}
}

View file

@ -0,0 +1,190 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const tui = @import("../../tui.zig");
const mainview = @import("../../mainview.zig");
const command = @import("../../command.zig");
const EventHandler = @import("../../EventHandler.zig");
const ed = @import("../../editor.zig");
const Allocator = @import("std").mem.Allocator;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
a: Allocator,
buf: [1024]u8 = undefined,
input: []u8 = "",
last_buf: [1024]u8 = undefined,
last_input: []u8 = "",
start_view: ed.View,
start_cursor: ed.Cursor,
editor: *ed.Editor,
pub fn create(a: Allocator, _: command.Context) !*Self {
const self: *Self = try a.create(Self);
if (tui.current().mainview.dynamic_cast(mainview)) |mv_| if (mv_.get_editor()) |editor| {
self.* = .{
.a = a,
.start_view = editor.view,
.start_cursor = editor.get_primary().cursor,
.editor = editor,
};
if (editor.get_primary().selection) |sel| ret: {
const text = editor.get_selection(sel, self.a) catch break :ret;
defer self.a.free(text);
@memcpy(self.buf[0..text.len], text);
self.input = self.buf[0..text.len];
}
return self;
};
return error.NotFound;
}
pub fn deinit(self: *Self) void {
self.a.destroy(self);
}
pub fn handler(self: *Self) EventHandler {
return EventHandler.to_owned(self);
}
pub fn name(_: *Self) []const u8 {
return "find in files";
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var egc: u32 = undefined;
var modifiers: u32 = undefined;
defer {
if (tui.current().mini_mode) |*mini_mode| {
mini_mode.text = self.input;
mini_mode.cursor = self.input.len;
}
}
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, egc, modifiers);
} else if (try m.match(.{"F"})) {
self.flush_input() catch |e| return e;
}
return false;
}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
switch (evtype) {
nc.event_type.PRESS => try self.mapPress(keypress, egc, modifiers),
nc.event_type.REPEAT => try self.mapPress(keypress, egc, modifiers),
nc.event_type.RELEASE => try self.mapRelease(keypress, egc, modifiers),
else => {},
}
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
return switch (modifiers) {
mod.CTRL => switch (keynormal) {
'Q' => self.cmd("quit", .{}),
'V' => self.cmd("system_paste", .{}),
'U' => self.input = "",
'G' => self.cancel(),
'C' => self.cancel(),
'L' => self.cmd("scroll_view_center", .{}),
'F' => self.cmd("goto_next_match", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'I' => self.insert_bytes("\t"),
key.SPACE => self.cancel(),
key.ENTER => self.insert_bytes("\n"),
key.BACKSPACE => self.input = "",
else => {},
},
mod.ALT => switch (keynormal) {
'V' => self.cmd("system_paste", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
else => {},
},
mod.ALT | mod.SHIFT => switch (keynormal) {
'V' => self.cmd("system_paste", .{}),
else => {},
},
mod.SHIFT => switch (keypress) {
key.ENTER => self.cmd("goto_prev_match", .{}),
key.F03 => self.cmd("goto_prev_match", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
0 => switch (keypress) {
key.F03 => self.cmd("goto_next_match", .{}),
key.F15 => self.cmd("goto_prev_match", .{}),
key.F09 => self.cmd("theme_prev", .{}),
key.F10 => self.cmd("theme_next", .{}),
key.ESC => self.cancel(),
key.ENTER => self.cmd("exit_mini_mode", .{}),
key.BACKSPACE => if (self.input.len > 0) {
self.input = self.input[0 .. self.input.len - 1];
},
key.LCTRL, key.RCTRL => self.cmd("enable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("enable_fast_scroll", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
else => {},
};
}
fn mapRelease(self: *Self, keypress: u32, _: u32, _: u32) tp.result {
return switch (keypress) {
key.LCTRL, key.RCTRL => self.cmd("disable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("disable_fast_scroll", .{}),
else => {},
};
}
fn insert_code_point(self: *Self, c: u32) tp.result {
if (self.input.len + 16 > self.buf.len)
try self.flush_input();
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, self.buf[self.input.len..]) catch |e| return tp.exit_error(e);
self.input = self.buf[0 .. self.input.len + bytes];
}
fn insert_bytes(self: *Self, bytes: []const u8) tp.result {
if (self.input.len + 16 > self.buf.len)
try self.flush_input();
const newlen = self.input.len + bytes.len;
@memcpy(self.buf[self.input.len..newlen], bytes);
self.input = self.buf[0..newlen];
}
var find_cmd_id: ?command.ID = null;
fn flush_input(self: *Self) tp.result {
if (self.input.len > 0) {
if (eql(u8, self.input, self.last_input))
return;
@memcpy(self.last_buf[0..self.input.len], self.input);
self.last_input = self.last_buf[0..self.input.len];
command.executeName("show_logview", .{}) catch {};
try self.editor.find_in_files(self.input);
}
}
fn cmd(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
self.flush_input() catch {};
return command.executeName(name_, ctx);
}
fn cancel(self: *Self) void {
self.editor.get_primary().cursor = self.start_cursor;
self.editor.scroll_to(self.start_view.row);
command.executeName("exit_mini_mode", .{}) catch {};
}

116
src/tui/mode/mini/goto.zig Normal file
View file

@ -0,0 +1,116 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const tui = @import("../../tui.zig");
const mainview = @import("../../mainview.zig");
const command = @import("../../command.zig");
const EventHandler = @import("../../EventHandler.zig");
const Allocator = @import("std").mem.Allocator;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const fmt = @import("std").fmt;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
a: Allocator,
buf: [30]u8 = undefined,
input: ?usize = null,
start: usize,
pub fn create(a: Allocator, _: command.Context) !*Self {
const self: *Self = try a.create(Self);
if (tui.current().mainview.dynamic_cast(mainview)) |mv_| if (mv_.get_editor()) |editor| {
self.* = .{
.a = a,
.start = editor.get_primary().cursor.row,
};
return self;
};
return error.NotFound;
}
pub fn deinit(self: *Self) void {
self.a.destroy(self);
}
pub fn handler(self: *Self) EventHandler {
return EventHandler.to_owned(self);
}
pub fn name(_: *Self) []const u8 {
return "goto";
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var modifiers: u32 = undefined;
defer {
if (tui.current().mini_mode) |*mini_mode| {
mini_mode.text = if (self.input) |linenum|
(fmt.bufPrint(&self.buf, "{d}", .{linenum}) catch "")
else
"";
mini_mode.cursor = mini_mode.text.len;
}
}
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.any, tp.string, tp.extract(&modifiers) }))
try self.mapEvent(evtype, keypress, modifiers);
return false;
}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, modifiers: u32) tp.result {
switch (evtype) {
nc.event_type.PRESS => try self.mapPress(keypress, modifiers),
nc.event_type.REPEAT => try self.mapPress(keypress, modifiers),
else => {},
}
}
fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
return switch (modifiers) {
mod.CTRL => switch (keynormal) {
'Q' => command.executeName("quit", .{}),
'U' => self.input = null,
'G' => self.cancel(),
'C' => self.cancel(),
'L' => command.executeName("scroll_view_center", .{}),
key.SPACE => self.cancel(),
else => {},
},
0 => switch (keypress) {
key.ESC => self.cancel(),
key.ENTER => command.executeName("exit_mini_mode", .{}),
key.BACKSPACE => if (self.input) |linenum| {
const newval = if (linenum < 10) 0 else linenum / 10;
self.input = if (newval == 0) null else newval;
self.goto();
},
'0' => {
if (self.input) |linenum| self.input = linenum * 10;
self.goto();
},
'1'...'9' => {
const digit: usize = @intCast(keypress - '0');
self.input = if (self.input) |x| x * 10 + digit else digit;
self.goto();
},
else => {},
},
else => {},
};
}
fn goto(self: *Self) void {
command.executeName("goto_line", command.fmt(.{self.input orelse self.start})) catch {};
}
fn cancel(self: *Self) void {
self.input = null;
self.goto();
command.executeName("exit_mini_mode", .{}) catch {};
}

View file

@ -0,0 +1,122 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const tui = @import("../../tui.zig");
const mainview = @import("../../mainview.zig");
const command = @import("../../command.zig");
const EventHandler = @import("../../EventHandler.zig");
const Allocator = @import("std").mem.Allocator;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const fmt = @import("std").fmt;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
a: Allocator,
key: [6]u8 = undefined,
direction: Direction,
operation: Operation,
const Direction = enum {
left,
right,
};
const Operation = enum {
move,
select,
};
pub fn create(a: Allocator, ctx: command.Context) !*Self {
var right: bool = true;
const select = if (tui.current().mainview.dynamic_cast(mainview)) |mv| if (mv.get_editor()) |editor| if (editor.get_primary().selection) |_| true else false else false else false;
_ = ctx.args.match(.{tp.extract(&right)}) catch return error.NotFound;
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.direction = if (right) .right else .left,
.operation = if (select) .select else .move,
};
return self;
}
pub fn deinit(self: *Self) void {
self.a.destroy(self);
}
pub fn handler(self: *Self) EventHandler {
return EventHandler.to_owned(self);
}
pub fn name(self: *Self) []const u8 {
return switch (self.operation) {
.move => switch (self.direction) {
.left => "move left to char",
.right => "move right to char",
},
.select => switch (self.direction) {
.left => "select left to char",
.right => "select right to char",
},
};
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var modifiers: u32 = undefined;
var egc: u32 = undefined;
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) }))
try self.mapEvent(evtype, keypress, egc, modifiers);
return false;
}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
switch (evtype) {
nc.event_type.PRESS => try self.mapPress(keypress, egc, modifiers),
else => {},
}
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
switch (keypress) {
key.LSUPER, key.RSUPER => return,
key.LSHIFT, key.RSHIFT => return,
key.LCTRL, key.RCTRL => return,
key.LALT, key.RALT => return,
else => {},
}
return switch (modifiers) {
mod.SHIFT => if (!key.synthesized_p(keypress)) self.execute_operation(egc) else self.cancel(),
0 => switch (keypress) {
key.ESC => self.cancel(),
key.ENTER => self.cancel(),
else => if (!key.synthesized_p(keypress)) self.execute_operation(egc) else self.cancel(),
},
else => self.cancel(),
};
}
fn execute_operation(self: *Self, c: u32) void {
const cmd = switch (self.direction) {
.left => switch (self.operation) {
.move => "move_to_char_left",
.select => "select_to_char_left",
},
.right => switch (self.operation) {
.move => "move_to_char_right",
.select => "select_to_char_right",
},
};
var buf: [6]u8 = undefined;
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, &buf) catch return;
command.executeName(cmd, command.fmt(.{buf[0..bytes]})) catch {};
command.executeName("exit_mini_mode", .{}) catch {};
}
fn cancel(_: *Self) void {
command.executeName("exit_mini_mode", .{}) catch {};
}

View file

@ -0,0 +1,144 @@
const std = @import("std");
const nc = @import("notcurses");
const tp = @import("thespian");
const tui = @import("../../tui.zig");
const mainview = @import("../../mainview.zig");
const command = @import("../../command.zig");
const EventHandler = @import("../../EventHandler.zig");
const Self = @This();
a: std.mem.Allocator,
file_path: std.ArrayList(u8),
pub fn create(a: std.mem.Allocator, _: command.Context) !*Self {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.file_path = std.ArrayList(u8).init(a),
};
if (tui.current().mainview.dynamic_cast(mainview)) |mv_| if (mv_.get_editor()) |editor| {
if (editor.is_dirty()) return tp.exit("unsaved changes");
if (editor.file_path) |old_path|
if (std.mem.lastIndexOf(u8, old_path, "/")) |pos|
try self.file_path.appendSlice(old_path[0 .. pos + 1]);
if (editor.get_primary().selection) |sel| ret: {
const text = editor.get_selection(sel, self.a) catch break :ret;
defer self.a.free(text);
if (!(text.len > 2 and std.mem.eql(u8, text[0..2], "..")))
self.file_path.clearRetainingCapacity();
try self.file_path.appendSlice(text);
}
};
return self;
}
pub fn deinit(self: *Self) void {
self.file_path.deinit();
self.a.destroy(self);
}
pub fn handler(self: *Self) EventHandler {
return EventHandler.to_owned(self);
}
pub fn name(_: *Self) []const u8 {
return "open file";
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var egc: u32 = undefined;
var modifiers: u32 = undefined;
defer {
if (tui.current().mini_mode) |*mini_mode| {
mini_mode.text = self.file_path.items;
mini_mode.cursor = self.file_path.items.len;
}
}
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, egc, modifiers);
}
return false;
}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
switch (evtype) {
nc.event_type.PRESS => try self.mapPress(keypress, egc, modifiers),
nc.event_type.REPEAT => try self.mapPress(keypress, egc, modifiers),
nc.event_type.RELEASE => try self.mapRelease(keypress, egc, modifiers),
else => {},
}
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
return switch (modifiers) {
nc.mod.CTRL => switch (keynormal) {
'Q' => self.cmd("quit", .{}),
'V' => self.cmd("system_paste", .{}),
'U' => self.file_path.clearRetainingCapacity(),
'G' => self.cancel(),
'C' => self.cancel(),
'L' => self.cmd("scroll_view_center", .{}),
'I' => self.insert_bytes("\t"),
nc.key.SPACE => self.cancel(),
nc.key.BACKSPACE => self.file_path.clearRetainingCapacity(),
else => {},
},
nc.mod.ALT => switch (keynormal) {
'V' => self.cmd("system_paste", .{}),
else => {},
},
nc.mod.ALT | nc.mod.SHIFT => switch (keynormal) {
'V' => self.cmd("system_paste", .{}),
else => {},
},
nc.mod.SHIFT => switch (keypress) {
else => if (!nc.key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
0 => switch (keypress) {
nc.key.ESC => self.cancel(),
nc.key.ENTER => self.navigate(),
nc.key.BACKSPACE => if (self.file_path.items.len > 0) {
self.file_path.shrinkRetainingCapacity(self.file_path.items.len - 1);
},
else => if (!nc.key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
else => {},
};
}
fn mapRelease(_: *Self, _: u32, _: u32, _: u32) tp.result {}
fn insert_code_point(self: *Self, c: u32) tp.result {
var buf: [32]u8 = undefined;
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, &buf) catch |e| return tp.exit_error(e);
self.file_path.appendSlice(buf[0..bytes]) catch |e| return tp.exit_error(e);
}
fn insert_bytes(self: *Self, bytes: []const u8) tp.result {
self.file_path.appendSlice(bytes) catch |e| return tp.exit_error(e);
}
fn cmd(_: *Self, name_: []const u8, ctx: command.Context) tp.result {
return command.executeName(name_, ctx);
}
fn cancel(_: *Self) void {
command.executeName("exit_mini_mode", .{}) catch {};
}
fn navigate(self: *Self) void {
if (self.file_path.items.len > 0)
tp.self_pid().send(.{ "cmd", "navigate", .{ .file = self.file_path.items } }) catch {};
command.executeName("exit_mini_mode", .{}) catch {};
}

171
src/tui/scrollbar_v.zig Normal file
View file

@ -0,0 +1,171 @@
const Allocator = @import("std").mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const Widget = @import("Widget.zig");
const EventHandler = @import("EventHandler.zig");
const tui = @import("tui.zig");
plane: nc.Plane,
pos_scrn: u32 = 0,
view_scrn: u32 = 8,
size_scrn: u32 = 8,
pos_virt: u32 = 0,
view_virt: u32 = 1,
size_virt: u32 = 1,
max_ypx: i32 = 8,
parent: Widget,
hover: bool = false,
active: bool = false,
const Self = @This();
pub fn create(a: Allocator, parent: Widget, event_source: Widget) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
try event_source.subscribe(EventHandler.bind(self, handle_event));
return self.widget();
}
fn init(parent: Widget) !Self {
return .{
.plane = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent.plane.*),
.parent = parent,
};
}
pub fn widget(self: *Self) Widget {
return Widget.to(self);
}
pub fn deinit(self: *Self, a: Allocator) void {
self.plane.deinit();
a.destroy(self);
}
pub fn layout(_: *Self) Widget.Layout {
return .{ .static = 1 };
}
pub fn handle_event(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
var size: u32 = 0;
var view: u32 = 0;
var pos: u32 = 0;
if (try m.match(.{ "E", "view", tp.extract(&size), tp.extract(&view), tp.extract(&pos) }))
self.set(size, view, pos);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var y: i32 = undefined;
var ypx: i32 = undefined;
if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) })) {
self.active = true;
self.move_to(y, ypx);
return true;
} else if (try m.match(.{ "B", nc.event_type.RELEASE, tp.more })) {
self.active = false;
return true;
} else if (try m.match(.{ "D", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) })) {
self.active = true;
self.move_to(y, ypx);
return true;
} else if (try m.match(.{ "B", nc.event_type.RELEASE, tp.more })) {
self.active = false;
return true;
} else if (try m.match(.{ "H", tp.extract(&self.hover) })) {
self.active = false;
return true;
}
return false;
}
fn move_to(self: *Self, y_: i32, ypx_: i32) void {
self.max_ypx = @max(self.max_ypx, ypx_);
const max_ypx: f64 = @floatFromInt(self.max_ypx);
const y: f64 = @floatFromInt(y_);
const ypx: f64 = @floatFromInt(ypx_);
const plane_y: f64 = @floatFromInt(self.plane.abs_y());
const size_scrn: f64 = @floatFromInt(self.size_scrn);
const view_scrn: f64 = @floatFromInt(self.view_scrn);
const ratio = max_ypx / eighths_c;
const pos_scrn: f64 = ((y - plane_y) * eighths_c) + (ypx / ratio) - (view_scrn / 2);
const max_pos_scrn = size_scrn - view_scrn;
const pos_scrn_clamped = @min(@max(0, pos_scrn), max_pos_scrn);
const pos_virt = self.pos_scrn_to_virt(@intFromFloat(pos_scrn_clamped));
self.set(self.size_virt, self.view_virt, pos_virt);
_ = self.parent.msg(.{ "scroll_to", pos_virt }) catch {};
}
fn pos_scrn_to_virt(self: Self, pos_scrn_: u32) u32 {
const size_virt: f64 = @floatFromInt(self.size_virt);
const size_scrn: f64 = @floatFromInt(self.plane.dim_y() * eighths_c);
const pos_scrn: f64 = @floatFromInt(pos_scrn_);
const ratio = size_virt / size_scrn;
return @intFromFloat(pos_scrn * ratio);
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
const frame = tracy.initZone(@src(), .{ .name = "scrollbar_v render" });
defer frame.deinit();
tui.set_base_style(&self.plane, " ", if (self.active) theme.scrollbar_active else if (self.hover) theme.scrollbar_hover else theme.scrollbar);
self.plane.erase();
smooth_bar_at(self.plane, @intCast(self.pos_scrn), @intCast(self.view_scrn)) catch {};
return false;
}
pub fn set(self: *Self, size_virt_: u32, view_virt_: u32, pos_virt_: u32) void {
self.pos_virt = pos_virt_;
self.view_virt = view_virt_;
self.size_virt = size_virt_;
var size_virt: f64 = @floatFromInt(size_virt_);
var view_virt: f64 = @floatFromInt(view_virt_);
const pos_virt: f64 = @floatFromInt(pos_virt_);
const size_scrn: f64 = @floatFromInt(self.plane.dim_y() * eighths_c);
if (size_virt == 0) size_virt = 1;
if (view_virt_ == 0) view_virt = 1;
if (view_virt > size_virt) view_virt = size_virt;
const ratio = size_virt / size_scrn;
self.pos_scrn = @intFromFloat(pos_virt / ratio);
self.view_scrn = @intFromFloat(view_virt / ratio);
self.size_scrn = @intFromFloat(size_scrn);
}
const eighths_b = [_][]const u8{ "", "", "", "", "", "", "", "" };
const eighths_t = [_][]const u8{ " ", "", "🮂", "🮃", "", "🮄", "🮅", "🮆" };
const eighths_c: i32 = @intCast(eighths_b.len);
fn smooth_bar_at(plane: nc.Plane, pos_: i32, size_: i32) !void {
const height: i32 = @intCast(plane.dim_y());
var size = @max(size_, 8);
const pos: i32 = @min(height * eighths_c - size, pos_);
var pos_y = @as(c_int, @intCast(@divFloor(pos, eighths_c)));
const blk = @mod(pos, eighths_c);
const b = eighths_b[@intCast(blk)];
plane.erase();
plane.cursor_move_yx(pos_y, 0) catch return;
_ = try plane.putstr(@ptrCast(b));
size -= @as(u16, @intCast(eighths_c)) - @as(u16, @intCast(blk));
while (size >= 8) {
pos_y += 1;
size -= 8;
plane.cursor_move_yx(pos_y, 0) catch return;
_ = try plane.putstr(@ptrCast(eighths_b[0]));
}
if (size > 0) {
pos_y += 1;
plane.cursor_move_yx(pos_y, 0) catch return;
const t = eighths_t[size];
_ = try plane.putstr(@ptrCast(t));
}
}

View file

@ -0,0 +1,218 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const root = @import("root");
const Widget = @import("../Widget.zig");
const command = @import("../command.zig");
const tui = @import("../tui.zig");
a: Allocator,
parent: nc.Plane,
plane: nc.Plane,
name: []const u8,
name_buf: [512]u8 = undefined,
title: []const u8 = "",
title_buf: [512]u8 = undefined,
file_type: []const u8,
file_type_buf: [64]u8 = undefined,
file_icon: [:0]const u8 = "",
file_icon_buf: [6]u8 = undefined,
file_color: u24 = 0,
line: usize,
lines: usize,
column: usize,
file_exists: bool,
file_dirty: bool = false,
detailed: bool = false,
const Self = @This();
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(a, parent);
self.show_cwd();
return Widget.to(self);
}
fn init(a: Allocator, parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent);
errdefer n.deinit();
return .{
.a = a,
.parent = parent,
.plane = n,
.name = "",
.file_type = "",
.lines = 0,
.line = 0,
.column = 0,
.file_exists = true,
};
}
pub fn deinit(self: *Self, a: Allocator) void {
self.plane.deinit();
a.destroy(self);
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
const frame = tracy.initZone(@src(), .{ .name = @typeName(@This()) ++ " render" });
defer frame.deinit();
tui.set_base_style(&self.plane, " ", theme.statusbar);
self.plane.erase();
self.plane.home();
if (tui.current().mini_mode) |_|
self.render_mini_mode(theme)
else if (self.detailed)
self.render_detailed(theme)
else
self.render_normal(theme);
self.render_terminal_title();
return false;
}
fn render_mini_mode(self: *Self, theme: *const Widget.Theme) void {
self.plane.off_styles(nc.style.italic);
const mini_mode = if (tui.current().mini_mode) |m| m else return;
_ = self.plane.print(" {s}", .{mini_mode.text}) catch {};
if (mini_mode.cursor) |cursor| {
const pos: c_int = @intCast(cursor);
self.plane.cursor_move_yx(0, pos + 1) catch return;
var cell = self.plane.cell_init();
_ = self.plane.at_cursor_cell(&cell) catch return;
tui.set_cell_style(&cell, theme.editor_cursor);
_ = self.plane.putc(&cell) catch {};
}
return;
}
// 󰆓 Content save
// 󰽂 Content save alert
// 󰳻 Content save edit
// 󰘛 Content save settings
// 󱙃 Content save off
// 󱣪 Content save check
// 󱑛 Content save cog
// 󰆔 Content save all
fn render_normal(self: *Self, theme: *const Widget.Theme) void {
self.plane.on_styles(nc.style.italic);
_ = self.plane.putstr(" ") catch {};
if (self.file_icon.len > 0) {
self.render_file_icon(theme);
_ = self.plane.print(" ", .{}) catch {};
}
_ = self.plane.putstr(if (!self.file_exists) "󰽂 " else if (self.file_dirty) "󰆓 " else "") catch {};
_ = self.plane.print("{s}", .{self.name}) catch {};
return;
}
fn render_detailed(self: *Self, theme: *const Widget.Theme) void {
self.plane.on_styles(nc.style.italic);
_ = self.plane.putstr(" ") catch {};
if (self.file_icon.len > 0) {
self.render_file_icon(theme);
_ = self.plane.print(" ", .{}) catch {};
}
_ = self.plane.putstr(if (!self.file_exists) "󰽂" else if (self.file_dirty) "󰆓" else "󱣪") catch {};
_ = self.plane.print(" {s}:{d}:{d}", .{ self.name, self.line + 1, self.column + 1 }) catch {};
_ = self.plane.print(" of {d} lines", .{self.lines}) catch {};
if (self.file_type.len > 0)
_ = self.plane.print(" ({s})", .{self.file_type}) catch {};
return;
}
fn render_terminal_title(self: *Self) void {
const file_name = if (std.mem.lastIndexOfScalar(u8, self.name, '/')) |pos|
self.name[pos + 1 ..]
else if (self.name.len == 0)
root.application_name
else
self.name;
var new_title_buf: [512]u8 = undefined;
const new_title = std.fmt.bufPrint(&new_title_buf, "{s}{s}", .{ if (!self.file_exists) "" else if (self.file_dirty) "" else "", file_name }) catch return;
if (std.mem.eql(u8, self.title, new_title)) return;
@memcpy(self.title_buf[0..new_title.len], new_title);
self.title = self.title_buf[0..new_title.len];
tui.set_terminal_title(self.title);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var file_path: []const u8 = undefined;
var file_type: []const u8 = undefined;
var file_icon: []const u8 = undefined;
var file_dirty: bool = undefined;
if (try m.match(.{ "E", "pos", tp.extract(&self.lines), tp.extract(&self.line), tp.extract(&self.column) }))
return false;
if (try m.match(.{ "E", "dirty", tp.extract(&file_dirty) })) {
self.file_dirty = file_dirty;
} else if (try m.match(.{ "E", "save", tp.extract(&file_path) })) {
@memcpy(self.name_buf[0..file_path.len], file_path);
self.name = self.name_buf[0..file_path.len];
self.file_exists = true;
self.file_dirty = false;
self.abbrv_home();
} else if (try m.match(.{ "E", "open", tp.extract(&file_path), tp.extract(&self.file_exists), tp.extract(&file_type), tp.extract(&file_icon), tp.extract(&self.file_color) })) {
@memcpy(self.name_buf[0..file_path.len], file_path);
self.name = self.name_buf[0..file_path.len];
@memcpy(self.file_type_buf[0..file_type.len], file_type);
self.file_type = self.file_type_buf[0..file_type.len];
@memcpy(self.file_icon_buf[0..file_icon.len], file_icon);
self.file_icon_buf[file_icon.len] = 0;
self.file_icon = self.file_icon_buf[0..file_icon.len :0];
self.file_dirty = false;
self.abbrv_home();
} else if (try m.match(.{ "E", "close" })) {
self.name = "";
self.lines = 0;
self.line = 0;
self.column = 0;
self.file_exists = true;
self.show_cwd();
}
if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.any, tp.any, tp.any })) {
self.detailed = !self.detailed;
return true;
}
return false;
}
fn render_file_icon(self: *Self, _: *const Widget.Theme) void {
var cell = self.plane.cell_init();
_ = self.plane.at_cursor_cell(&cell) catch return;
if (self.file_color != 0x000001) {
nc.channels_set_fg_rgb(&cell.channels, self.file_color) catch {};
nc.channels_set_fg_alpha(&cell.channels, nc.ALPHA_OPAQUE) catch {};
}
_ = self.plane.cell_load(&cell, self.file_icon) catch {};
_ = self.plane.putc(&cell) catch {};
self.plane.cursor_move_rel(0, 1) catch {};
}
fn show_cwd(self: *Self) void {
self.file_icon = "";
self.file_color = 0x000001;
self.name = std.fs.cwd().realpath(".", &self.name_buf) catch "(none)";
self.abbrv_home();
}
fn abbrv_home(self: *Self) void {
if (std.fs.path.isAbsolute(self.name)) {
if (std.os.getenv("HOME")) |homedir| {
const homerelpath = std.fs.path.relative(self.a, homedir, self.name) catch return;
if (homerelpath.len == 0) {
self.name = "~";
} else if (homerelpath.len > 3 and std.mem.eql(u8, homerelpath[0..3], "../")) {
return;
} else {
self.name_buf[0] = '~';
self.name_buf[1] = '/';
@memcpy(self.name_buf[2 .. homerelpath.len + 2], homerelpath);
self.name = self.name_buf[0 .. homerelpath.len + 2];
}
}
}
}

211
src/tui/status/keystate.zig Normal file
View file

@ -0,0 +1,211 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const Widget = @import("../Widget.zig");
const command = @import("../command.zig");
const tui = @import("../tui.zig");
const EventHandler = @import("../EventHandler.zig");
const history = 8;
parent: nc.Plane,
plane: nc.Plane,
frame: u64 = 0,
idle_frame: u64 = 0,
key_active_frame: u64 = 0,
wipe_after_frames: i64 = 60,
hover: bool = false,
keys: [history]Key = [_]Key{.{}} ** history,
const Key = struct { id: u32 = 0, mod: u32 = 0 };
const Self = @This();
const idle_msg = "🐶";
pub const width = idle_msg.len + 20;
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
try tui.current().input_listeners.add(EventHandler.bind(self, listen));
return self.widget();
}
fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent);
errdefer n.deinit();
var frame_rate = tp.env.get().num("frame-rate");
if (frame_rate == 0) frame_rate = 60;
return .{
.parent = parent,
.plane = n,
.wipe_after_frames = @divTrunc(frame_rate, 2),
};
}
pub fn widget(self: *Self) Widget {
return Widget.to(self);
}
pub fn deinit(self: *Self, a: Allocator) void {
tui.current().input_listeners.remove_ptr(self);
self.plane.deinit();
a.destroy(self);
}
pub fn layout(_: *Self) Widget.Layout {
return .{ .static = width };
}
fn render_active(self: *Self) bool {
var c: usize = 0;
for (self.keys) |k| {
if (k.id == 0)
return true;
if (c > 0)
_ = self.plane.putstr(" ") catch {};
if (nc.isSuper(k.mod))
_ = self.plane.putstr("H-") catch {};
if (nc.isCtrl(k.mod))
_ = self.plane.putstr("C-") catch {};
if (nc.isShift(k.mod))
_ = self.plane.putstr("S-") catch {};
if (nc.isAlt(k.mod))
_ = self.plane.putstr("A-") catch {};
_ = self.plane.print("{s}", .{nc.key_id_string(k.id)}) catch {};
c += 1;
}
return true;
}
const idle_spinner = [_][]const u8{ "🞻", "", "🞼", "🞽", "🞾", "🞿", "🞾", "🞽", "🞼", "" };
fn render_idle(self: *Self) bool {
self.idle_frame += 1;
if (self.idle_frame > 180) {
return self.animate();
} else {
const i = @mod(self.idle_frame / 8, idle_spinner.len);
_ = self.plane.print_aligned(0, .center, "{s} {s} {s}", .{ idle_spinner[i], idle_msg, idle_spinner[i] }) catch {};
}
return true;
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
const frame = tracy.initZone(@src(), .{ .name = @typeName(@This()) ++ " render" });
defer frame.deinit();
tui.set_base_style(&self.plane, " ", if (self.hover) theme.statusbar_hover else theme.statusbar);
self.frame += 1;
if (self.frame - self.key_active_frame > self.wipe_after_frames)
self.unset_key_all();
self.plane.erase();
self.plane.home();
return if (self.keys[0].id > 0) self.render_active() else self.render_idle();
}
fn set_nkey(self: *Self, key: Key) void {
for (self.keys, 0..) |k, i| {
if (k.id == 0) {
self.keys[i].id = key.id;
self.keys[i].mod = key.mod;
return;
}
}
for (self.keys, 0.., 1..) |_, i, j| {
if (j < self.keys.len)
self.keys[i] = self.keys[j];
}
self.keys[self.keys.len - 1].id = key.id;
self.keys[self.keys.len - 1].mod = key.mod;
}
fn unset_nkey_(self: *Self, key: u32) void {
for (self.keys, 0..) |k, i| {
if (k.id == key) {
for (i..self.keys.len, (i + 1)..) |i_, j| {
if (j < self.keys.len)
self.keys[i_] = self.keys[j];
}
self.keys[self.keys.len - 1].id = 0;
return;
}
}
}
const upper_offset: u32 = 'a' - 'A';
fn unset_nkey(self: *Self, key: Key) void {
self.unset_nkey_(key.id);
if (key.id >= 'a' and key.id <= 'z')
self.unset_nkey_(key.id - upper_offset);
if (key.id >= 'A' and key.id <= 'Z')
self.unset_nkey_(key.id + upper_offset);
}
fn unset_key_all(self: *Self) void {
for (0..self.keys.len) |i| {
self.keys[i].id = 0;
self.keys[i].mod = 0;
}
}
fn set_key(self: *Self, key: Key, val: bool) void {
self.idle_frame = 0;
self.key_active_frame = self.frame;
(if (val) &set_nkey else &unset_nkey)(self, key);
}
pub fn listen(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
var key: u32 = 0;
var mod: u32 = 0;
if (try m.match(.{ "I", nc.event_type.PRESS, tp.extract(&key), tp.any, tp.any, tp.extract(&mod), tp.more })) {
self.set_key(.{ .id = key, .mod = mod }, true);
} else if (try m.match(.{ "I", nc.event_type.RELEASE, tp.extract(&key), tp.any, tp.any, tp.extract(&mod), tp.more })) {
self.set_key(.{ .id = key, .mod = mod }, false);
}
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.any, tp.any, tp.any })) {
command.executeName("toggle_inputview", .{}) catch {};
return true;
}
if (try m.match(.{ "H", tp.extract(&self.hover) })) {
tui.current().request_mouse_cursor_pointer(self.hover);
return true;
}
return false;
}
fn animate(self: *Self) bool {
const positions = eighths_c * (width - 1);
const frame = @mod(self.frame, positions * 2);
const pos = if (frame > eighths_c * (width - 1))
positions * 2 - frame
else
frame;
smooth_block_at(self.plane, pos);
return false;
// return pos != 0;
}
const eighths_l = [_][]const u8{ "", "", "", "", "", "", "", "" };
const eighths_r = [_][]const u8{ " ", "", "🮇", "🮈", "", "🮉", "🮊", "🮋" };
const eighths_c = eighths_l.len;
fn smooth_block_at(plane: nc.Plane, pos: u64) void {
const blk = @mod(pos, eighths_c) + 1;
const l = eighths_l[eighths_c - blk];
const r = eighths_r[eighths_c - blk];
plane.erase();
plane.cursor_move_yx(0, @as(c_int, @intCast(@divFloor(pos, eighths_c)))) catch return;
_ = plane.putstr(@ptrCast(r)) catch return;
_ = plane.putstr(@ptrCast(l)) catch return;
}

View file

@ -0,0 +1,71 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const Widget = @import("../Widget.zig");
const tui = @import("../tui.zig");
parent: nc.Plane,
plane: nc.Plane,
line: usize = 0,
lines: usize = 0,
column: usize = 0,
buf: [256]u8 = undefined,
rendered: [:0]const u8 = "",
const Self = @This();
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
return Widget.to(self);
}
fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent);
errdefer n.deinit();
return .{
.parent = parent,
.plane = n,
};
}
pub fn deinit(self: *Self, a: Allocator) void {
self.plane.deinit();
a.destroy(self);
}
pub fn layout(self: *Self) Widget.Layout {
return .{ .static = self.rendered.len };
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
tui.set_base_style(&self.plane, " ", theme.statusbar);
self.plane.erase();
self.plane.home();
_ = self.plane.putstr(self.rendered) catch {};
return false;
}
fn format(self: *Self) void {
var fbs = std.io.fixedBufferStream(&self.buf);
const writer = fbs.writer();
std.fmt.format(writer, " Ln {d}, Col {d} ", .{ self.line + 1, self.column + 1 }) catch {};
self.rendered = @ptrCast(fbs.getWritten());
self.buf[self.rendered.len] = 0;
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "E", "pos", tp.extract(&self.lines), tp.extract(&self.line), tp.extract(&self.column) })) {
self.format();
} else if (try m.match(.{ "E", "close" })) {
self.lines = 0;
self.line = 0;
self.column = 0;
self.rendered = "";
}
return false;
}

110
src/tui/status/minilog.zig Normal file
View file

@ -0,0 +1,110 @@
const std = @import("std");
const nc = @import("notcurses");
const tp = @import("thespian");
const log = @import("log");
const Widget = @import("../Widget.zig");
const MessageFilter = @import("../MessageFilter.zig");
const tui = @import("../tui.zig");
const mainview = @import("../mainview.zig");
parent: nc.Plane,
plane: nc.Plane,
msg: std.ArrayList(u8),
is_error: bool = false,
timer: ?tp.timeout = null,
const message_display_time_seconds = 2;
const error_display_time_seconds = 4;
const Self = @This();
pub fn create(a: std.mem.Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = .{
.parent = parent,
.plane = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent),
.msg = std.ArrayList(u8).init(a),
};
try tui.current().message_filters.add(MessageFilter.bind(self, log_receive));
try log.subscribe();
return Widget.to(self);
}
pub fn deinit(self: *Self, a: std.mem.Allocator) void {
self.cancel_timer();
self.msg.deinit();
log.unsubscribe() catch {};
tui.current().message_filters.remove_ptr(self);
self.plane.deinit();
a.destroy(self);
}
pub fn layout(self: *Self) Widget.Layout {
return .{ .static = if (self.msg.items.len > 0) self.msg.items.len + 2 else 1 };
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
tui.set_base_style(&self.plane, " ", if (self.msg.items.len > 0) theme.sidebar else theme.statusbar);
self.plane.erase();
self.plane.home();
if (self.is_error)
tui.set_base_style(&self.plane, " ", theme.editor_error);
_ = self.plane.print(" {s} ", .{self.msg.items}) catch return false;
return false;
}
pub fn log_receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "log", tp.more })) {
self.log_process(m) catch |e| return tp.exit_error(e);
if (tui.current().mainview.dynamic_cast(mainview)) |mv_| if (mv_.logview_enabled)
return false; // pass on log messages to logview
return true;
} else if (try m.match(.{ "minilog", "clear" })) {
self.is_error = false;
self.cancel_timer();
self.msg.clearRetainingCapacity();
Widget.need_render();
return true;
}
return false;
}
pub fn log_process(self: *Self, m: tp.message) !void {
var src: []const u8 = undefined;
var context: []const u8 = undefined;
var msg: []const u8 = undefined;
if (try m.match(.{ "log", tp.extract(&src), tp.extract(&msg) })) {
if (self.is_error) return;
self.reset_timer();
self.msg.clearRetainingCapacity();
try self.msg.appendSlice(msg);
Widget.need_render();
} else if (try m.match(.{ "log", "error", tp.extract(&src), tp.extract(&context), "->", tp.extract(&msg) })) {
self.is_error = true;
self.reset_timer();
self.msg.clearRetainingCapacity();
try self.msg.appendSlice(msg);
Widget.need_render();
} else if (try m.match(.{ "log", tp.extract(&src), tp.more })) {
self.is_error = true;
self.reset_timer();
self.msg.clearRetainingCapacity();
var s = std.json.writeStream(self.msg.writer(), .{});
var iter: []const u8 = m.buf;
try @import("cbor").JsonStream(@TypeOf(self.msg)).jsonWriteValue(&s, &iter);
Widget.need_render();
}
}
fn reset_timer(self: *Self) void {
self.cancel_timer();
const delay: u64 = std.time.ms_per_s * @as(u64, if (self.is_error) error_display_time_seconds else message_display_time_seconds);
self.timer = tp.timeout.init_ms(delay, tp.message.fmt(.{ "minilog", "clear" })) catch null;
}
fn cancel_timer(self: *Self) void {
if (self.timer) |*timer| {
timer.deinit();
self.timer = null;
}
}

View file

@ -0,0 +1,74 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const root = @import("root");
const Widget = @import("../Widget.zig");
const command = @import("../command.zig");
const ed = @import("../editor.zig");
const tui = @import("../tui.zig");
parent: nc.Plane,
plane: nc.Plane,
const Self = @This();
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
return Widget.to(self);
}
fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent);
errdefer n.deinit();
return .{
.parent = parent,
.plane = n,
};
}
pub fn deinit(self: *Self, a: Allocator) void {
self.plane.deinit();
a.destroy(self);
}
pub fn layout(_: *Self) Widget.Layout {
return .{ .static = if (is_mini_mode()) tui.get_mode().len + 5 else tui.get_mode().len - 1 };
}
fn is_mini_mode() bool {
return if (tui.current().mini_mode) |_| true else false;
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
if (is_mini_mode())
self.render_mode(theme)
else
self.render_logo(theme);
return false;
}
fn render_mode(self: *Self, theme: *const Widget.Theme) void {
tui.set_base_style(&self.plane, " ", theme.statusbar_hover);
self.plane.on_styles(nc.style.bold);
self.plane.erase();
self.plane.home();
var buf: [31:0]u8 = undefined;
_ = self.plane.putstr(std.fmt.bufPrintZ(&buf, " {s} ", .{tui.get_mode()}) catch return) catch {};
if (theme.statusbar_hover.bg) |bg| self.plane.set_fg_rgb(bg) catch {};
if (theme.statusbar.bg) |bg| self.plane.set_bg_rgb(bg) catch {};
_ = self.plane.putstr("") catch {};
}
fn render_logo(self: *Self, theme: *const Widget.Theme) void {
tui.set_base_style(&self.plane, " ", theme.statusbar_hover);
self.plane.on_styles(nc.style.bold);
self.plane.erase();
self.plane.home();
var buf: [31:0]u8 = undefined;
_ = self.plane.putstr(std.fmt.bufPrintZ(&buf, " {s} ", .{tui.get_mode()}) catch return) catch {};
}

102
src/tui/status/modstate.zig Normal file
View file

@ -0,0 +1,102 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const Widget = @import("../Widget.zig");
const command = @import("../command.zig");
const tui = @import("../tui.zig");
const EventHandler = @import("../EventHandler.zig");
parent: nc.Plane,
plane: nc.Plane,
ctrl: bool = false,
shift: bool = false,
alt: bool = false,
hover: bool = false,
const Self = @This();
pub const width = 5;
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
try tui.current().input_listeners.add(EventHandler.bind(self, listen));
return self.widget();
}
fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent);
errdefer n.deinit();
return .{
.parent = parent,
.plane = n,
};
}
pub fn widget(self: *Self) Widget {
return Widget.to(self);
}
pub fn deinit(self: *Self, a: Allocator) void {
tui.current().input_listeners.remove_ptr(self);
self.plane.deinit();
a.destroy(self);
}
pub fn layout(_: *Self) Widget.Layout {
return .{ .static = width };
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
const frame = tracy.initZone(@src(), .{ .name = @typeName(@This()) ++ " render" });
defer frame.deinit();
tui.set_base_style(&self.plane, " ", if (self.hover) theme.statusbar_hover else theme.statusbar);
self.plane.erase();
self.plane.home();
_ = self.plane.print("\u{2003}{s}{s}{s}\u{2003}", .{
mode(self.ctrl, "", "🅒"),
mode(self.shift, "", "🅢"),
mode(self.alt, "", "🅐"),
}) catch {};
return false;
}
inline fn mode(state: bool, off: [:0]const u8, on: [:0]const u8) [:0]const u8 {
return if (state) on else off;
}
fn render_modifier(self: *Self, state: bool, off: [:0]const u8, on: [:0]const u8) void {
_ = self.plane.putstr(if (state) on else off) catch {};
}
fn set_modifiers(self: *Self, key: u32, mods: u32) void {
const modifiers = switch (key) {
nc.key.LCTRL, nc.key.RCTRL => mods ^ nc.mod.CTRL,
nc.key.LSHIFT, nc.key.RSHIFT => mods ^ nc.mod.SHIFT,
nc.key.LALT, nc.key.RALT => mods ^ nc.mod.ALT,
else => mods,
};
self.ctrl = nc.isCtrl(modifiers);
self.shift = nc.isShift(modifiers);
self.alt = nc.isAlt(modifiers);
}
pub fn listen(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
var key: u32 = 0;
var mod: u32 = 0;
if (try m.match(.{ "I", tp.any, tp.extract(&key), tp.any, tp.any, tp.extract(&mod), tp.more }))
self.set_modifiers(key, mod);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.any, tp.any, tp.any })) {
command.executeName("toggle_inputview", .{}) catch {};
return true;
}
return try m.match(.{ "H", tp.extract(&self.hover) });
}

View file

@ -0,0 +1,105 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const Widget = @import("../Widget.zig");
const ed = @import("../editor.zig");
const tui = @import("../tui.zig");
parent: nc.Plane,
plane: nc.Plane,
matches: usize = 0,
cursels: usize = 0,
selection: ?ed.Selection = null,
buf: [256]u8 = undefined,
rendered: [:0]const u8 = "",
const Self = @This();
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
return Widget.to(self);
}
fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent);
errdefer n.deinit();
return .{
.parent = parent,
.plane = n,
};
}
pub fn deinit(self: *Self, a: Allocator) void {
self.plane.deinit();
a.destroy(self);
}
pub fn layout(self: *Self) Widget.Layout {
return .{ .static = self.rendered.len };
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
const frame = tracy.initZone(@src(), .{ .name = @typeName(@This()) ++ " render" });
defer frame.deinit();
tui.set_base_style(&self.plane, " ", theme.statusbar);
self.plane.erase();
self.plane.home();
_ = self.plane.putstr(self.rendered) catch {};
return false;
}
fn format(self: *Self) void {
var fbs = std.io.fixedBufferStream(&self.buf);
const writer = fbs.writer();
_ = writer.write(" ") catch {};
if (self.matches > 1) {
std.fmt.format(writer, "({d} matches)", .{self.matches}) catch {};
if (self.selection) |_|
_ = writer.write(" ") catch {};
}
if (self.cursels > 1) {
std.fmt.format(writer, "({d} cursels)", .{self.cursels}) catch {};
if (self.selection) |_|
_ = writer.write(" ") catch {};
}
if (self.selection) |sel_| {
var sel = sel_;
sel.normalize();
const lines = sel.end.row - sel.begin.row;
if (lines == 0) {
std.fmt.format(writer, "({d} selected)", .{sel.end.col - sel.begin.col}) catch {};
} else {
std.fmt.format(writer, "({d} lines selected)", .{if (sel.end.col == 0) lines else lines + 1}) catch {};
}
}
_ = writer.write(" ") catch {};
self.rendered = @ptrCast(fbs.getWritten());
self.buf[self.rendered.len] = 0;
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "E", "match", tp.extract(&self.matches) }))
self.format();
if (try m.match(.{ "E", "cursels", tp.extract(&self.cursels) }))
self.format();
if (try m.match(.{ "E", "close" })) {
self.matches = 0;
self.selection = null;
self.format();
} else if (try m.match(.{ "E", "sel", tp.more })) {
var sel: ed.Selection = undefined;
if (try m.match(.{ tp.any, tp.any, "none" })) {
self.matches = 0;
self.selection = null;
} else if (try m.match(.{ tp.any, tp.any, tp.extract(&sel.begin.row), tp.extract(&sel.begin.col), tp.extract(&sel.end.row), tp.extract(&sel.end.col) })) {
self.selection = sel;
}
self.format();
}
return false;
}

View file

@ -0,0 +1,23 @@
const std = @import("std");
const nc = @import("notcurses");
const Widget = @import("../Widget.zig");
const WidgetList = @import("../WidgetList.zig");
const tui = @import("../tui.zig");
parent: nc.Plane,
plane: nc.Plane,
const Self = @This();
pub fn create(a: std.mem.Allocator, parent: Widget) !Widget {
var w = try WidgetList.createH(a, parent, "statusbar", .{ .static = 1 });
if (tui.current().config.modestate_show) try w.add(try @import("modestate.zig").create(a, w.plane));
try w.add(try @import("filestate.zig").create(a, w.plane));
try w.add(try @import("minilog.zig").create(a, w.plane));
if (tui.current().config.selectionstate_show) try w.add(try @import("selectionstate.zig").create(a, w.plane));
try w.add(try @import("linenumstate.zig").create(a, w.plane));
if (tui.current().config.modstate_show) try w.add(try @import("modstate.zig").create(a, w.plane));
if (tui.current().config.keystate_show) try w.add(try @import("keystate.zig").create(a, w.plane));
return w.widget();
}

1021
src/tui/tui.zig Normal file

File diff suppressed because it is too large Load diff

39
src/unicode.zig Normal file
View file

@ -0,0 +1,39 @@
pub fn control_code_to_unicode(code: u8) [:0]const u8 {
return switch (code) {
'\x00' => "",
'\x01' => "",
'\x02' => "",
'\x03' => "",
'\x04' => "",
'\x05' => "",
'\x06' => "",
'\x07' => "",
'\x08' => "",
'\x09' => "",
'\x0A' => "",
'\x0B' => "",
'\x0C' => "",
'\x0D' => "",
'\x0E' => "",
'\x0F' => "",
'\x10' => "",
'\x11' => "",
'\x12' => "",
'\x13' => "",
'\x14' => "",
'\x15' => "",
'\x16' => "",
'\x17' => "",
'\x18' => "",
'\x19' => "",
'\x1A' => "",
'\x1B' => "",
'\x1C' => "",
'\x1D' => "",
'\x1E' => "",
'\x1F' => "",
'\x20' => "",
'\x7F' => "",
else => "",
};
}