Initial public release
This commit is contained in:
parent
3c3f068914
commit
4ece4babad
63 changed files with 15101 additions and 0 deletions
40
src/tui/Box.zig
Normal file
40
src/tui/Box.zig
Normal 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
175
src/tui/EventHandler.zig
Normal 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
138
src/tui/MessageFilter.zig
Normal 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
273
src/tui/Widget.zig
Normal 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
223
src/tui/WidgetList.zig
Normal 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
94
src/tui/WidgetStack.zig
Normal 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
194
src/tui/command.zig
Normal 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
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
327
src/tui/editor_gutter.zig
Normal 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
185
src/tui/fonts.zig
Normal 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
263
src/tui/home.zig
Normal 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
106
src/tui/inputview.zig
Normal 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
181
src/tui/inspector_view.zig
Normal 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
132
src/tui/logview.zig
Normal 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
374
src/tui/mainview.zig
Normal 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
44
src/tui/message_box.zig
Normal 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
286
src/tui/mode/input/flow.zig
Normal 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
103
src/tui/mode/input/home.zig
Normal 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 {};
|
||||
}
|
||||
}
|
284
src/tui/mode/input/vim/insert.zig
Normal file
284
src/tui/mode/input/vim/insert.zig
Normal 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_ });
|
||||
}
|
497
src/tui/mode/input/vim/normal.zig
Normal file
497
src/tui/mode/input/vim/normal.zig
Normal 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);
|
||||
}
|
472
src/tui/mode/input/vim/visual.zig
Normal file
472
src/tui/mode/input/vim/visual.zig
Normal 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
231
src/tui/mode/mini/find.zig
Normal 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];
|
||||
}
|
||||
}
|
190
src/tui/mode/mini/find_in_files.zig
Normal file
190
src/tui/mode/mini/find_in_files.zig
Normal 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
116
src/tui/mode/mini/goto.zig
Normal 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 {};
|
||||
}
|
122
src/tui/mode/mini/move_to_char.zig
Normal file
122
src/tui/mode/mini/move_to_char.zig
Normal 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 {};
|
||||
}
|
144
src/tui/mode/mini/open_file.zig
Normal file
144
src/tui/mode/mini/open_file.zig
Normal 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
171
src/tui/scrollbar_v.zig
Normal 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));
|
||||
}
|
||||
}
|
218
src/tui/status/filestate.zig
Normal file
218
src/tui/status/filestate.zig
Normal 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
211
src/tui/status/keystate.zig
Normal 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;
|
||||
}
|
71
src/tui/status/linenumstate.zig
Normal file
71
src/tui/status/linenumstate.zig
Normal 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
110
src/tui/status/minilog.zig
Normal 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;
|
||||
}
|
||||
}
|
74
src/tui/status/modestate.zig
Normal file
74
src/tui/status/modestate.zig
Normal 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
102
src/tui/status/modstate.zig
Normal 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) });
|
||||
}
|
105
src/tui/status/selectionstate.zig
Normal file
105
src/tui/status/selectionstate.zig
Normal 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;
|
||||
}
|
23
src/tui/status/statusbar.zig
Normal file
23
src/tui/status/statusbar.zig
Normal 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
1021
src/tui/tui.zig
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue