diff --git a/src/ctlseqs.zig b/src/ctlseqs.zig new file mode 100644 index 0000000..4e55448 --- /dev/null +++ b/src/ctlseqs.zig @@ -0,0 +1,140 @@ +// Queries +pub const primary_device_attrs = "\x1b[c"; +pub const tertiary_device_attrs = "\x1b[=c"; +pub const device_status_report = "\x1b[5n"; +pub const xtversion = "\x1b[>0q"; +pub const decrqm_focus = "\x1b[?1004$p"; +pub const decrqm_sgr_pixels = "\x1b[?1016$p"; +pub const decrqm_sync = "\x1b[?2026$p"; +pub const decrqm_unicode = "\x1b[?2027$p"; +pub const decrqm_color_scheme = "\x1b[?2031$p"; +pub const csi_u_query = "\x1b[?u"; +pub const kitty_graphics_query = "\x1b_Gi=1,a=q\x1b\\"; +pub const sixel_geometry_query = "\x1b[?2;1;0S"; + +// mouse. We try for button motion and any motion. terminals will enable the +// last one we tried (any motion). This was added because zellij doesn't +// support any motion currently +// See: https://github.com/zellij-org/zellij/issues/1679 +pub const mouse_set = "\x1b[?1002;1003;1004;1006h"; +pub const mouse_set_pixels = "\x1b[?1002;1003;1004;1016h"; +pub const mouse_reset = "\x1b[?1002;1003;1004;1006;1016l"; + +// in-band window size reports +pub const in_band_resize_set = "\x1b[?2048h"; +pub const in_band_resize_reset = "\x1b[?2048l"; + +// sync +pub const sync_set = "\x1b[?2026h"; +pub const sync_reset = "\x1b[?2026l"; + +// unicode +pub const unicode_set = "\x1b[?2027h"; +pub const unicode_reset = "\x1b[?2027l"; + +// bracketed paste +pub const bp_set = "\x1b[?2004h"; +pub const bp_reset = "\x1b[?2004l"; + +// color scheme updates +pub const color_scheme_request = "\x1b[?996n"; +pub const color_scheme_set = "\x1b[?2031h"; +pub const color_scheme_reset = "\x1b[?2031l"; + +// Key encoding +pub const csi_u_push = "\x1b[>{d}u"; +pub const csi_u_pop = "\x1b[ |size| { - this.size = size; - }, else => {}, } this.events.clearRetainingCapacity(); @@ -46,7 +44,7 @@ pub fn Layout(comptime Event: type) type { pub fn content(this: *@This()) !*std.ArrayList(u8) { const widget_content = try this.widget.content(); this.c.clearRetainingCapacity(); - try this.c.appendSlice(widget_content.items); + try this.c.appendSlice(widget_content); return &this.c; } }; diff --git a/src/main.zig b/src/main.zig index 845d0c6..f950a39 100644 --- a/src/main.zig +++ b/src/main.zig @@ -28,10 +28,12 @@ pub fn main() !void { var app: App = .{}; var renderer: App.Renderer = .{}; - var rawText = App.Widget.RawText.init(allocator); + const file = try std.fs.cwd().openFile("./src/main.zig", .{}); + var rawText = App.Widget.RawText.init(allocator, file); const widget = App.Widget.createFrom(&rawText); var layout = App.Layout.Pane.init(allocator, widget); defer layout.deinit(); // deinitializes the contained widget + file.close(); try app.start(); defer app.stop() catch unreachable; @@ -75,13 +77,6 @@ pub fn main() !void { } // TODO: I could use the ascii codes in vaxis // - see https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b - // how would I draw? - // use array for screen contents? <-> support partial re-draws - // support widget type drawing similar to the already existing widgets - // determine the corresponding capabilities of the terminal? - // support layouts - // - contents of corresponding locations - // resize event } test { diff --git a/src/style.zig b/src/style.zig index a6d5e09..fb46c9d 100644 --- a/src/style.zig +++ b/src/style.zig @@ -4,19 +4,281 @@ //! //! Stylings however also include highlighting for specific terminal capabilities. //! For example url highlighting. -const std = @import("std"); -// TODO: implement helper functions for the following stylings: -// - bold -// - italic -// - underline -// - curly line -// - strike through -// - reverse -// - blink -// - color: -// - foreground -// - background +// taken from https://github.com/rockorager/libvaxis/blob/main/src/Cell.zig (MIT-License) +// with slight modifications +const std = @import("std"); +const ctlseqs = @import("ctlseqs.zig"); + +pub const Underline = enum { + off, + single, + double, + curly, + dotted, + dashed, +}; + +pub const Color = union(enum) { + default, + index: u8, + rgb: [3]u8, + + pub fn eql(a: @This(), b: @This()) bool { + switch (a) { + .default => return b == .default, + .index => |a_idx| { + switch (b) { + .index => |b_idx| return a_idx == b_idx, + else => return false, + } + }, + .rgb => |a_rgb| { + switch (b) { + .rgb => |b_rgb| return a_rgb[0] == b_rgb[0] and + a_rgb[1] == b_rgb[1] and + a_rgb[2] == b_rgb[2], + else => return false, + } + }, + } + } + + pub fn rgbFromUint(val: u24) Color { + const r_bits = val & 0b11111111_00000000_00000000; + const g_bits = val & 0b00000000_11111111_00000000; + const b_bits = val & 0b00000000_00000000_11111111; + const rgb = [_]u8{ + @truncate(r_bits >> 16), + @truncate(g_bits >> 8), + @truncate(b_bits), + }; + return .{ .rgb = rgb }; + } + + /// parse an XParseColor-style rgb specification into an rgb Color. The spec + /// is of the form: rgb:rrrr/gggg/bbbb. Generally, the high two bits will always + /// be the same as the low two bits. + pub fn rgbFromSpec(spec: []const u8) !Color { + var iter = std.mem.splitScalar(u8, spec, ':'); + const prefix = iter.next() orelse return error.InvalidColorSpec; + if (!std.mem.eql(u8, "rgb", prefix)) return error.InvalidColorSpec; + + const spec_str = iter.next() orelse return error.InvalidColorSpec; + + var spec_iter = std.mem.splitScalar(u8, spec_str, '/'); + + const r_raw = spec_iter.next() orelse return error.InvalidColorSpec; + if (r_raw.len != 4) return error.InvalidColorSpec; + + const g_raw = spec_iter.next() orelse return error.InvalidColorSpec; + if (g_raw.len != 4) return error.InvalidColorSpec; + + const b_raw = spec_iter.next() orelse return error.InvalidColorSpec; + if (b_raw.len != 4) return error.InvalidColorSpec; + + const r = try std.fmt.parseUnsigned(u8, r_raw[2..], 16); + const g = try std.fmt.parseUnsigned(u8, g_raw[2..], 16); + const b = try std.fmt.parseUnsigned(u8, b_raw[2..], 16); + + return .{ + .rgb = [_]u8{ r, g, b }, + }; + } + + test "rgbFromSpec" { + const spec = "rgb:aaaa/bbbb/cccc"; + const actual = try rgbFromSpec(spec); + switch (actual) { + .rgb => |rgb| { + try std.testing.expectEqual(0xAA, rgb[0]); + try std.testing.expectEqual(0xBB, rgb[1]); + try std.testing.expectEqual(0xCC, rgb[2]); + }, + else => try std.testing.expect(false), + } + } +}; + +fg: Color = .default, +bg: Color = .default, +ul: Color = .default, +ul_style: Underline = .off, + +bold: bool = false, +dim: bool = false, +italic: bool = false, +blink: bool = false, +reverse: bool = false, +invisible: bool = false, +strikethrough: bool = false, + +fn start(this: @This(), writer: anytype) !void { + // foreground + switch (this.fg) { + .default => try std.fmt.format(writer, ctlseqs.fg_reset, .{}), + .index => |idx| { + switch (idx) { + 0...7 => { + try std.fmt.format(writer, ctlseqs.fg_base, .{idx}); + }, + 8...15 => { + try std.fmt.format(writer, ctlseqs.fg_bright, .{idx - 8}); + }, + else => { + try std.fmt.format(writer, ctlseqs.fg_indexed, .{idx}); + }, + } + }, + .rgb => |rgb| { + try std.fmt.format(writer, ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }); + }, + } + // background + switch (this.bg) { + .default => try std.fmt.format(writer, ctlseqs.bg_reset, .{}), + .index => |idx| { + switch (idx) { + 0...7 => { + try std.fmt.format(writer, ctlseqs.bg_base, .{idx}); + }, + 8...15 => { + try std.fmt.format(writer, ctlseqs.bg_bright, .{idx}); + }, + else => { + try std.fmt.format(writer, ctlseqs.bg_indexed, .{idx}); + }, + } + }, + .rgb => |rgb| { + try std.fmt.format(writer, ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }); + }, + } + // underline color + switch (this.ul) { + .default => try std.fmt.format(writer, ctlseqs.ul_reset, .{}), + .index => |idx| { + try std.fmt.format(writer, ctlseqs.ul_indexed, .{idx}); + }, + .rgb => |rgb| { + try std.fmt.format(writer, ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }); + }, + } + // underline style + switch (this.ul_style) { + .off => try std.fmt.format(writer, ctlseqs.ul_off, .{}), + .single => try std.fmt.format(writer, ctlseqs.ul_single, .{}), + .double => try std.fmt.format(writer, ctlseqs.ul_double, .{}), + .curly => try std.fmt.format(writer, ctlseqs.ul_curly, .{}), + .dotted => try std.fmt.format(writer, ctlseqs.ul_dotted, .{}), + .dashed => try std.fmt.format(writer, ctlseqs.ul_dashed, .{}), + } + // bold + switch (this.bold) { + true => try std.fmt.format(writer, ctlseqs.bold_set, .{}), + false => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), + } + // dim + switch (this.dim) { + true => try std.fmt.format(writer, ctlseqs.dim_set, .{}), + false => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), + } + // italic + switch (this.italic) { + true => try std.fmt.format(writer, ctlseqs.italic_set, .{}), + false => try std.fmt.format(writer, ctlseqs.italic_reset, .{}), + } + // blink + switch (this.blink) { + true => try std.fmt.format(writer, ctlseqs.blink_set, .{}), + false => try std.fmt.format(writer, ctlseqs.blink_reset, .{}), + } + // reverse + switch (this.reverse) { + true => try std.fmt.format(writer, ctlseqs.reverse_set, .{}), + false => try std.fmt.format(writer, ctlseqs.reverse_reset, .{}), + } + // invisible + switch (this.invisible) { + true => try std.fmt.format(writer, ctlseqs.invisible_set, .{}), + false => try std.fmt.format(writer, ctlseqs.invisible_reset, .{}), + } + // strikethrough + switch (this.strikethrough) { + true => try std.fmt.format(writer, ctlseqs.strikethrough_set, .{}), + false => try std.fmt.format(writer, ctlseqs.strikethrough_reset, .{}), + } +} + +fn end(this: @This(), writer: anytype) !void { + // foreground + switch (this.fg) { + .default => {}, + else => try std.fmt.format(writer, ctlseqs.fg_reset, .{}), + } + // background + switch (this.bg) { + .default => {}, + else => try std.fmt.format(writer, ctlseqs.bg_reset, .{}), + } + // underline color + switch (this.ul) { + .default => {}, + else => try std.fmt.format(writer, ctlseqs.ul_reset, .{}), + } + // underline style + switch (this.ul_style) { + .off => {}, + else => try std.fmt.format(writer, ctlseqs.ul_off, .{}), + } + // bold + switch (this.bold) { + true => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), + false => {}, + } + // dim + switch (this.dim) { + true => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), + false => {}, + } + // italic + switch (this.italic) { + true => try std.fmt.format(writer, ctlseqs.italic_reset, .{}), + false => {}, + } + // blink + switch (this.blink) { + true => try std.fmt.format(writer, ctlseqs.blink_reset, .{}), + false => {}, + } + // reverse + switch (this.reverse) { + true => try std.fmt.format(writer, ctlseqs.reverse_reset, .{}), + false => {}, + } + // invisible + switch (this.invisible) { + true => try std.fmt.format(writer, ctlseqs.invisible_reset, .{}), + false => {}, + } + // strikethrough + switch (this.strikethrough) { + true => try std.fmt.format(writer, ctlseqs.strikethrough_reset, .{}), + false => {}, + } +} + +pub fn format(this: @This(), writer: anytype, comptime content: []const u8, args: anytype) !void { + try this.start(writer); + try std.fmt.format(writer, content, args); + try this.end(writer); +} + +pub fn value(this: @This(), writer: anytype, content: []const u8) !void { + try this.start(writer); + _ = try writer.write(content); + try this.end(writer); +} // TODO: implement helper functions for terminal capabilities: // - links / url display (osc 8) diff --git a/src/widget.zig b/src/widget.zig index 7d6f29f..d99a1f3 100644 --- a/src/widget.zig +++ b/src/widget.zig @@ -1,7 +1,7 @@ //! Dynamic dispatch for widget implementations. //! Each widget should at last implement these functions: //! - handle(this: *@This(), event: Event) ?Event {} -//! - content(this: *@This()) *std.ArrayList(u8) {} +//! - content(this: *@This()) ![]u8 {} //! - deinit(this: *@This()) void {} //! //! Create a `Widget` using `createFrom(object: anytype)` and use them through @@ -10,7 +10,6 @@ //! //! Each `Widget` may cache its content and should if the contents will not //! change for a long time. -const std = @import("std"); const isTaggedUnion = @import("event.zig").isTaggedUnion; pub fn Widget(comptime Event: type) type { @@ -23,7 +22,7 @@ pub fn Widget(comptime Event: type) type { const VTable = struct { handle: *const fn (this: *WidgetType, event: Event) ?Event, - content: *const fn (this: *WidgetType) anyerror!*std.ArrayList(u8), + content: *const fn (this: *WidgetType) anyerror![]u8, deinit: *const fn (this: *WidgetType) void, }; @@ -36,7 +35,7 @@ pub fn Widget(comptime Event: type) type { } // Return the entire content of this `Widget`. - pub fn content(this: *WidgetType) !*std.ArrayList(u8) { + pub fn content(this: *WidgetType) ![]u8 { return try this.vtable.content(this); } @@ -58,7 +57,7 @@ pub fn Widget(comptime Event: type) type { }.handle, .content = struct { // Return the entire content of this `Widget`. - fn content(this: *WidgetType) !*std.ArrayList(u8) { + fn content(this: *WidgetType) ![]u8 { const widget: @TypeOf(object) = @ptrFromInt(this.object); return try widget.content(); } @@ -75,8 +74,5 @@ pub fn Widget(comptime Event: type) type { // TODO: import and export of `Widget` implementations (with corresponding intialization using `Event`) pub const RawText = @import("widget/RawText.zig").Widget(Event); - // pub const Header = @import("widget/Header.zig"); - // pub const ViewPort = @import("widget/ViewPort.zig"); - // pub const PopupMenu = @import("widget/PopupMenu.zig"); }; } diff --git a/src/widget/RawText.zig b/src/widget/RawText.zig index 7db46c2..7814b46 100644 --- a/src/widget/RawText.zig +++ b/src/widget/RawText.zig @@ -1,5 +1,6 @@ const std = @import("std"); const terminal = @import("../terminal.zig"); +const Style = @import("../style.zig"); const isTaggedUnion = @import("../event.zig").isTaggedUnion; const Error = @import("../event.zig").Error; @@ -11,15 +12,25 @@ pub fn Widget(comptime Event: type) type { } return struct { c: std.ArrayList(u8) = undefined, - key: Key = undefined, + line_index: std.ArrayList(usize) = undefined, + line: usize = 0, size: terminal.Size = undefined, - pub fn init(allocator: std.mem.Allocator) @This() { - return .{ .c = std.ArrayList(u8).init(allocator) }; + pub fn init(allocator: std.mem.Allocator, file: std.fs.File) @This() { + var c = std.ArrayList(u8).init(allocator); + var line_index = std.ArrayList(usize).init(allocator); + file.reader().readAllArrayList(&c, 4192) catch {}; + for (c.items, 0..) |item, i| { + if (item == '\n') { + line_index.append(i) catch {}; + } + } + return .{ .c = c, .line_index = line_index }; } pub fn deinit(this: *@This()) void { this.c.deinit(); + this.line_index.deinit(); this.* = undefined; } @@ -30,19 +41,43 @@ pub fn Widget(comptime Event: type) type { this.size = size; }, .key => |key| { - this.key = key; + if (key.matches(.{ .cp = 'g' })) { + // top + this.line = 0; + } + if (key.matches(.{ .cp = 'G' })) { + // bottom + this.line = this.line_index.items.len - 1; + } + if (key.matches(.{ .cp = 'j' })) { + // down + if (this.line < this.line_index.items.len - 1) { + this.line +|= 1; + } + } + if (key.matches(.{ .cp = 'k' })) { + // up + this.line -|= 1; + } }, else => {}, } return null; } - pub fn content(this: *@This()) !*std.ArrayList(u8) { - this.c.clearRetainingCapacity(); - const writer = this.c.writer(); - try std.fmt.format(writer, "The terminal has a reported size of [cols: {d}, rows: {d}]\n", .{ this.size.cols, this.size.rows }); - try std.fmt.format(writer, "User entered key: {any}\n", .{this.key}); - return &this.c; + pub fn content(this: *@This()) ![]u8 { + if (this.size.rows >= this.line_index.items.len) { + return this.c.items; + } else { + // more rows than we can display + const i = this.line_index.items[this.line]; + const e = this.size.rows + this.line; + if (e >= this.line_index.items.len) { + return this.c.items[i..]; + } + const x = this.line_index.items[e]; + return this.c.items[i..x]; + } } }; }