const std = @import( "std" ); // const p = std.debug.print; const assert = std.debug.assert; const testing = std.testing; // # ansi controls & escapes // https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences // https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b // https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 // "\r" move cursor to beginning of line // "\x1b" is the escape code '^[' // "\033" is the escape code '^[' // TODO NO_COLOR // https://no-color.org/ // XXX nocolor.org // 403 Forbidden // https://ziglang.org/documentation/master/std/#std.posix.getenv // 3 options // - writer api - writer_xyz( writer, ... ) !void // - comptime fmt - fmt_xyz( comptime ... ) !*const [_:0]u8 // - runtime alloc - alloc_xyz( allocator, ... ) ![:0]u8 // TODO consider formatting numbers with leading zeros to avoid variable with comptime prints // zig test ansi-lib.zig --test-filter "rgb" test "zero-prefix decimal formats" { const writer = std.io.getStdOut().writer(); // // XXX the truecolor rgb style escape codes don't work... // // \033[38;2;;;m #Select RGB foreground color // // \033[48;2;;;m #Select RGB background color // pub fn writer_set_foreground( writer :anytype, r :u8, g :u8, b :u8 ) !void { // try writer.print( "\x1b[38;2;{d};{d};{d}m", .{ r, g, b } ); // } // pub fn writer_set_background( writer :anytype, r :u8, g :u8, b :u8 ) !void { // try writer.print( "\x1b[48;2;{d};{d};{d}m", .{ r, g, b } ); // } try writer.print( "\x1b[48;2;000;000;0m", .{} ); // background, black try writer.print( "\\x1b[48;2;000;000;0m // background truecolor black\n", .{} ); try writer.print( "\x1b[38;2;000;255;001m", .{} ); // foreground, truecolor, green try writer.print( "\\x1b[38;2;000;255;001m // foreground truecolor green\n", .{} ); try writer.print( "\x1b[0m", .{} ); try writer.print( "\\x1b[0m clear color\n", .{} ); try writer.print( "\x1b[48;2;00;00;000m", .{} ); // background, black try writer.print( "\\x1b[48;2;00;00;000m // background truecolor black\n", .{} ); try writer.print( "\x1b[38;2;0;255;1m", .{} ); // foreground, truecolor, green try writer.print( "\\x1b[38;2;0;255;1m // foreground truecolor green\n", .{} ); try writer.print( "\x1b[0m", .{} ); try writer.print( "\\x1b[0m clear color\n", .{} ); try writer.print( \\okay.. so for truecolor at least, \\left-padding the decimals with zeros seems to be valid, \\so the effort in dynamic comptime formatting.. is unnecessary \\.. dang; but that does mean the logic can be simplified \\ , .{} ); } // # clearing /// clear end of line pub const CLEAR_EOL = "\x1b[0K"; // "\x1b[K" /// clear beginning of line pub const CLEAR_BOL = "\x1b[1K"; /// clear line pub const CLEAR_LINE = "\x1b[2K"; /// clear end of screen pub const CLEAR_EOS = "\x1b[0J"; // "\x1b[J" /// clear beginning of screen pub const CLEAR_BOS = "\x1b[1J"; /// clear screen pub const CLEAR_SCREEN = "\x1b[2J"; // pub fn writer_clear_end_of_line ( writer :anytype ) void { try writer.print( "\x1b[K" , .{} ); } // cleareol EL0 Clear line from cursor right "^[[K" pub fn writer_clear_end_of_line ( writer :anytype ) void { try writer.print( "\x1b[0K", .{} ); } // cleareol EL0 Clear line from cursor right "^[[0K" pub fn writer_clear_begin_of_line ( writer :anytype ) void { try writer.print( "\x1b[1K", .{} ); } // clearbol EL1 Clear line from cursor left "^[[1K" pub fn writer_clear_line ( writer :anytype ) void { try writer.print( "\x1b[2K", .{} ); } // clearline EL2 Clear entire line "^[[2K" // pub fn writer_clear_end_of_screen ( writer :anytype ) void { try writer.print( "\x1b[J" , .{} ); } // cleareos ED0 Clear screen from cursor down "^[[J" pub fn writer_clear_end_of_screen ( writer :anytype ) void { try writer.print( "\x1b[0J", .{} ); } // cleareos ED0 Clear screen from cursor down "^[[0J" pub fn writer_clear_begin_of_screen ( writer :anytype ) void { try writer.print( "\x1b[1J", .{} ); } // clearbos ED1 Clear screen from cursor up "^[[1J" pub fn writer_clear_screen ( writer :anytype ) void { try writer.print( "\x1b[2J", .{} ); } // clearscreen ED2 Clear entire screen "^[[2J" // # cursor movement pub const CURSOR_ONE_UP = "\x1b[1A"; pub const CURSOR_ONE_DOWN = "\x1b[1B"; pub const CURSOR_ONE_RIGHT = "\x1b[1C"; pub const CURSOR_ONE_LEFT = "\x1b[1D"; // note: '1' can be changed to move more than one tile pub const Direction = enum( u8 ) { up = 'A', down = 'B', left = 'D', right = 'C', pub fn val( direction :Direction ) u8 { return @intFromEnum( direction ); } /// direction format string /// steps(int) then direction(A,B,C,D) pub const go_fmt = "\x1b[{d}{c}"; /// like `std.fmt.comptimePrint()` using `std.fmt.count()` /// https://ziglang.org/documentation/master/std/#std.fmt.comptimePrint pub inline fn go( comptime direction :Direction, comptime steps :u16 ) *const [std.fmt.count( go_fmt, .{ steps, @intFromEnum( direction ) } ):0]u8 { return std.fmt.comptimePrint( "\x1b[{d}{c}", .{ steps, direction.val() } ); // comptime { // // const len = "\x1b[65536A".len; // // var buffer :[_:0]u8 = [_]u8 { 0 } ** ( len + 1 ); // const len = std.fmt.count( go_fmt, .{ steps, @intFromEnum( direction ) } ); // // var buffer :[len+1:0]u8 = [_:0]u8 { 0 } ** ( len + 1 ); // var buffer :[len:0]u8 = [_:0]u8 { 0 } ** len; // // _ = std.fmt.bufPrint( buffer[ 0..buffer.len-1 ], "\x1b[{d}{c}", .{ steps, @intFromEnum( direction ) } ) catch unreachable; // _ = std.fmt.bufPrint( &buffer, "\x1b[{d}{c}", .{ steps, @intFromEnum( direction ) } ) catch unreachable; // const final = buffer; // return &final; // } } pub fn writer_go( writer :anytype, direction :Direction, distance :u16 ) !void { try writer.print( go_fmt, .{ distance, direction.val() } ); } // pub fn all_go( comptime direction :Direction, all :Allocator, comptime distance :u16 ) [:0]u8 { // return try bufPrintZ( go_fmt, .{ distance, direction.val() } ); // TODO // allocPrintZ, allocPrint, bufPrint, count ; std.fmt.* // } }; test "Direction.go()" { const writer = std.io.getStdOut().writer(); try writer.print( "cursor movement:\n", .{} ); try writer.print( "\n\n\n", .{} ); try writer.print( Direction.up .go( 3 ), .{} ); try writer.print( Direction.right .go( 2 ), .{} ); try writer.print( "^", .{} ); try writer.print( Direction.left .go( 2 ), .{} ); try writer.print( Direction.down .go( 1 ), .{} ); try writer.print( "<", .{} ); try writer.print( Direction.right .go( 1 ), .{} ); try writer.print( ">", .{} ); // try writer.print( Direction.left .go( 2 ), .{} ); // try writer.print( Direction.down .go( 1 ), .{} ); // try writer.print( "v", .{} ); // try writer.print( "\n", .{} ); try writer.print( "{s}{s}v\n", .{ comptime Direction.left.go( 2 ), comptime Direction.down.go( 1 ), } ); // ^ // < > // v } pub fn writer_move_cursor_up ( writer :anytype, distance :u16 ) !void { try writer_move_cursor( writer, .up , distance ); } pub fn writer_move_cursor_down ( writer :anytype, distance :u16 ) !void { try writer_move_cursor( writer, .down , distance ); } pub fn writer_move_cursor_right ( writer :anytype, distance :u16 ) !void { try writer_move_cursor( writer, .right , distance ); } pub fn writer_move_cursor_left ( writer :anytype, distance :u16 ) !void { try writer_move_cursor( writer, .left , distance ); } pub const fmt_distance_direction = "\x1b[{d}{c}"; pub fn writer_move_cursor( writer :anytype, direction :Direction, distance :u16 ) !void { try writer.print( fmt_distance_direction, .{ distance, @intFromEnum( direction ) } ); } // # colors pub const Palette = enum( u4 ) { dark_black = 0, light_black = 8, dark_red = 1, light_red = 9, dark_green = 2, light_green = 10, dark_orange = 3, light_orange = 11, dark_blue = 4, light_blue = 12, dark_purple = 5, light_purple = 13, dark_cyan = 6, light_cyan = 14, dark_white = 7, light_white = 15, fn val( palette :Palette ) u4 { return @intFromEnum( palette ); } }; pub const Color_Palette = enum( u3 ) { black = 0, // light_black = 8, red = 1, // light_red = 9, green = 2, // light_green = 10, orange = 3, // light_orange = 11, blue = 4, // light_blue = 12, purple = 5, // light_purple = 13, cyan = 6, // light_cyan = 14, white = 7, // light_white = 15, pub const Shade = enum( u4 ) { dark = 0, light = 8 }; pub fn shade( palette :Color_Palette, _shade :Shade ) u8 { return @as( u8, @intFromEnum( palette ) ) + @as( u8, @intFromEnum( _shade ) ); } }; pub const Ground = enum( u8 ) { foreground = 38, background = 48, fn val( ground :Ground ) u8 { return @intFromEnum( ground ); } }; pub const _fmt_palette_fg = "\x1b[38;5;{d}m"; pub const _fmt_palette_bg = "\x1b[48;5;{d}m"; pub const _fmt_palette_fg_bg = _fmt_palette_fg ++ _fmt_palette_bg; pub inline fn fmt_palette_fg_bg( comptime fg :u8, comptime bg :u8 ) *const [std.fmt.count( _fmt_palette_fg_bg, .{ fg, bg } ):0]u8 { comptime { const len = std.fmt.count( _fmt_palette_fg_bg, .{ fg, bg } ); var buffer :[len:0]u8 = [_:0]u8 { 0 } ** len; _ = std.fmt.bufPrint( &buffer, _fmt_palette_fg_bg, .{ fg, bg } ) catch unreachable; const final = buffer; return &final; } } test "palette colors" { const writer = std.io.getStdOut().writer(); try writer.print( "palette:\n", .{} ); try writer.print( "{s} black {s} = 0, {s}\n", .{ fmt_palette_fg_bg( Palette.light_black .val(), Palette.dark_black .val() ), fmt_palette_fg_bg( Palette.dark_black .val(), Palette.light_black .val() ), _fmt_clear_color } ); try writer.print( "{s} red {s} = 1, {s}\n", .{ fmt_palette_fg_bg( Palette.light_red .val(), Palette.dark_red .val() ), fmt_palette_fg_bg( Palette.dark_red .val(), Palette.light_red .val() ), _fmt_clear_color } ); try writer.print( "{s} green {s} = 2, {s}\n", .{ fmt_palette_fg_bg( Palette.light_green .val(), Palette.dark_green .val() ), fmt_palette_fg_bg( Palette.dark_green .val(), Palette.light_green .val() ), _fmt_clear_color } ); try writer.print( "{s} orange {s} = 3, {s}\n", .{ fmt_palette_fg_bg( Palette.light_orange .val(), Palette.dark_orange .val() ), fmt_palette_fg_bg( Palette.dark_orange .val(), Palette.light_orange .val() ), _fmt_clear_color } ); try writer.print( "{s} blue {s} = 4, {s}\n", .{ fmt_palette_fg_bg( Palette.light_blue .val(), Palette.dark_blue .val() ), fmt_palette_fg_bg( Palette.dark_blue .val(), Palette.light_blue .val() ), _fmt_clear_color } ); try writer.print( "{s} purple {s} = 5, {s}\n", .{ fmt_palette_fg_bg( Palette.light_purple .val(), Palette.dark_purple .val() ), fmt_palette_fg_bg( Palette.dark_purple .val(), Palette.light_purple .val() ), _fmt_clear_color } ); try writer.print( "{s} cyan {s} = 6, {s}\n", .{ fmt_palette_fg_bg( Palette.light_cyan .val(), Palette.dark_cyan .val() ), fmt_palette_fg_bg( Palette.dark_cyan .val(), Palette.light_cyan .val() ), _fmt_clear_color } ); try writer.print( "{s} white {s} = 7, {s}\n", .{ fmt_palette_fg_bg( Palette.light_white .val(), Palette.dark_white .val() ), fmt_palette_fg_bg( Palette.dark_white .val(), Palette.light_white .val() ), _fmt_clear_color } ); } pub fn color_rgb( r :u3, g :u3, b :u3 ) u8 { assert( 0 <= r and r < 6 ); assert( 0 <= g and g < 6 ); assert( 0 <= b and b < 6 ); return ( @as( u8, 16 ) + @as( u8, 1 )*b + @as( u8, 6 )*g + @as( u8, 36 )*r ); } pub const _fmt_g_rgb = "\x1b[{d};2;{d};{d};{d}m"; // .{ r, g, b } pub inline fn fmt_g_rgb( comptime ground :Ground, comptime r :u8, comptime g :u8, comptime b :u8 ) *const [std.fmt.count( _fmt_g_rgb, .{ Ground.background.val(), r, g, b } ):0]u8 { comptime { const len = std.fmt.count( _fmt_g_rgb, .{ Ground.background.val(), r, g, b } ); var buffer :[len:0]u8 = [_:0]u8 { 0 } ** len; _ = std.fmt.bufPrint( &buffer, _fmt_g_rgb, .{ ground.val(), r, g, b } ) catch unreachable; const final = buffer; return &final; } } test "rgb colors" { const writer = std.io.getStdOut().writer(); try writer.print( "rgb:\n", .{} ); // inline for( 0..6 ) |i| { // if( i % 3 != 1 ) { continue; } // inline for( 0..6 ) |j| { // if( j % 3 != 1 ) { continue; } // inline for( 0..6 ) |k| { // if( k % 3 != 1 ) { continue; } inline for( 0..6 ) |i| { if( i % 2 == 1 ) { continue; } inline for( 0..6 ) |j| { if( j % 2 == 1 ) { continue; } inline for( 0..6 ) |k| { if( k % 2 == 1 ) { continue; } // try writer.print( "{s}r={d},g={d},b={d}{s}\n", .{ comptime fmt_g_rgb( .background, i, j, k ), i, j, k, _fmt_clear_color } ); try writer.print( "{s} r={d},g={d},b={d} {s}and more! {s}\n", .{ comptime fmt_palette_fg_bg( color_rgb( 5-i, 5-j, 5-k ), color_rgb( i, j, k ) ), i, j, k, comptime fmt_palette_fg_bg( color_rgb( (i+3)%6, (j+3)%6, (k+3)%6 ), color_rgb( i, j, k ) ), _fmt_clear_color } ); } } } } test "monochrome colors" { const writer = std.io.getStdOut().writer(); try writer.print( "monochrome:\n", .{} ); inline for( 0..24 ) |i| { try writer.print( "{s} white = {d} {s}\n", .{ comptime fmt_palette_fg_bg( color_monochrome( if( i < 12 ) 23 else 0 ), color_monochrome( i ), ), i, _fmt_clear_color, } ); } } pub fn color_monochrome( white :u5 ) u8 { assert( 0 <= white and white < 24 ); return ( @as( u8, 232 ) + white ); } test "truecolor" { // demonstrate that truecolor is more fine-grained than palette const writer = std.io.getStdOut().writer(); try writer.print( "truecolor:\n", .{} ); const print_truecolor_and_palette = struct { fn print( w :anytype, comptime fg :u8, comptime r :u8, comptime g :u8, comptime b :u8 ) !void { try w.print( "{s}{s} r={d},g={d},b={d} {s} palette {s}\n", .{ comptime fmt_g_rgb( .foreground, fg,fg,fg ), comptime fmt_g_rgb( .background, r,g,b ), r,g,b, comptime fmt_palette_fg_bg( truecolor_to_rgb( fg,fg,fg ), truecolor_to_rgb( r,g,b ) ), _fmt_clear_color, } ); } }.print; // inline for( &[3][3]usize { .{1,0,0}, .{0,1,0}, .{0,0,1} } ) |ijk| {} inline for( &[3]struct{ usize, usize, usize } { .{1,0,0}, .{0,1,0}, .{0,0,1} } ) |ijk| { inline for( 0..16 ) |x| { const i = ijk.@"0" * x * 17; const j = ijk.@"1" * x * 17; const k = ijk.@"2" * x * 17; try print_truecolor_and_palette( writer, 255, i,j,k ); } inline for( 0..16 ) |x| { const i = @max( ijk.@"0" * 15 , x ) * @as( u8, 17 ); const j = @max( ijk.@"1" * 15 , x ) * @as( u8, 17 ); const k = @max( ijk.@"2" * 15 , x ) * @as( u8, 17 ); try print_truecolor_and_palette( writer, 0, i,j,k ); } } } const expectEqual = std.testing.expectEqual; test "color palette math" { // black, white, blue, red try expectEqual( 0, Color_Palette .black.shade( .dark ) ); try expectEqual( 15, Color_Palette .white.shade( .light ) ); try expectEqual( 4, Color_Palette .blue.shade( .dark ) ); try expectEqual( 9, Color_Palette .red.shade( .light ) ); // black, white, dull-blueish-green try expectEqual( 16, color_rgb( 0, 0, 0 ) ); try expectEqual( 231, color_rgb( 5, 5, 5 ) ); try expectEqual( 110, color_rgb( 2, 3, 4 ) ); // black, white, grey try expectEqual( 232, color_monochrome( 0 ) ); try expectEqual( 255, color_monochrome( 23 ) ); try expectEqual( 242, color_monochrome( 10 ) ); } // # printing colors // if( fg ) |c| p( "\x1b[38;5;{d}m", .{ c } ); // set foreground color // if( bg ) |c| p( "\x1b[48;5;{d}m", .{ c } ); // set background color // p( "{s}\x1b[0m", .{ text } ); // clear color formatting pub fn writer_print_color_text( writer :anytype, text :[]const u8, fg :?u8, bg :?u8 ) !void { if( fg ) |c| try writer_set_color( c, .foreground, writer ); if( bg ) |c| try writer_set_color( c, .background, writer ); try writer.print( "{s}", .{ text } ); try writer_clear_color( writer ); } pub fn writer_set_foreground_color( writer :anytype, color :u8 ) !void { try writer_set_color( writer, color, .foreground ); } pub fn writer_set_background_color( writer :anytype, color :u8 ) void { try writer_set_color( writer, color, .background ); } pub const _fmt_g_c = "\x1b[{d};5;{d}m"; pub fn writer_set_color( writer :anytype, color :u8, ground :Ground ) !void { try writer.print( _fmt_g_c, .{ ground.val(), color } ); } pub const _fmt_clear_color = "\x1b[0m"; pub fn writer_clear_color( writer :anytype ) !void { try writer.print( _fmt_clear_color, .{} ); } // ... TODO swap names of // writer_set_xxxxground_color() and // writer_set_xxxxground() // XXX the truecolor rgb style escape codes don't work... // \033[38;2;;;m #Select RGB foreground color // \033[48;2;;;m #Select RGB background color pub fn writer_set_foreground( writer :anytype, r :u8, g :u8, b :u8 ) !void { try writer.print( "\x1b[38;2;{d};{d};{d}m", .{ r, g, b } ); } pub fn writer_set_background( writer :anytype, r :u8, g :u8, b :u8 ) !void { try writer.print( "\x1b[48;2;{d};{d};{d}m", .{ r, g, b } ); } fn truecolor_to_rgb( r :u8, g :u8, b :u8 ) u8 { const six :u16 = 6; // easy cast to larger integer return color_rgb( @intCast( @divTrunc( r * six, 256 ) ), @intCast( @divTrunc( g * six, 256 ) ), @intCast( @divTrunc( b * six, 256 ) ), ); } test "truecolor rebeccapurple" { // rebeccapurple // #663399 const red = 0x66; const green = 0x33; const blue = 0x99; const writer = std.io.getStdOut().writer(); try writer.print( "rebeccapurple:\n", .{} ); // palette rgb try writer.print( "\x1b[48;5;{d}m palette style, 16 <= _ < 232 {s}\n", .{ truecolor_to_rgb( red, green, blue ), _fmt_clear_color, } ); // truecolor manual try writer.print( "\x1b[48;2;{d};{d};{d}m truecolor style, 0 <= _ < 16777216 {s}\n", .{ red, green, blue, _fmt_clear_color, } ); }