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

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