├── .gitignore
├── LICENSE.md
├── README.md
├── Run.bat
├── TestNode.bat
├── TestPHP.bat
├── ansi.hxproj
├── haxelib.json
├── src
└── ANSI.hx
└── test
└── Test.hx
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | src/Version.hx
3 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2016 Miha Lunar
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ansi
2 | ====
3 |
4 | Haxe utility for working with ANSI escape sequences.
5 |
6 | Provides functions that return a String with the appropriate ANSI escape sequence. This is usually written to standard output for the hosting console to process.
7 |
8 | **Note**: If the console doesn't support the escape sequences (e.g. default Command Prompt on Windows), you're going to see garbage.
9 |
10 | Tested with the neko target and [ansicon](https://github.com/adoxa/ansicon).
11 |
12 |
13 | ## Examples
14 |
15 |
16 | ### Text Color
17 |
18 | ```haxe
19 | Sys.stdout().writeString(ANSI.set(ANSI.attr.Green) + "green text");
20 | ```
21 | or shortened by importing ANSI and with it the Attribute enum:
22 | ```haxe
23 | import ANSI;
24 | ...
25 | Sys.stdout().writeString(ANSI.set(Green) + "green text");
26 | ```
27 | 
28 |
29 |
30 |
31 | ```haxe
32 | Sys.stdout().writeString(
33 | ANSI.set(Green, Bold) + "vivid green text" +
34 | ANSI.set(DefaultForeground) + " normal text"
35 | );
36 | ```
37 | 
38 |
39 |
40 |
41 | ### Move and Delete
42 |
43 | ```haxe
44 | Sys.stdout().writeString("hello world" + ANSI.moveLeft(5) + ANSI.deleteChars(5) + "ansi");
45 | ```
46 | 
47 |
48 |
49 |
50 | ### Window Title
51 |
52 | ```haxe
53 | Sys.stdout().writeString(ANSI.title("Window title goes here"));
54 | ```
55 | 
56 |
57 |
58 |
59 | ### Particles!
60 |
61 | See [Test.hx](test/Test.hx).
62 |
63 | 
64 |
--------------------------------------------------------------------------------
/Run.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | cd bin
3 | neko ansi.n
4 | pause
5 |
--------------------------------------------------------------------------------
/TestNode.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | haxe -cp src -cp test -js bin/ansi.js -lib hxnodejs -dce full -main Test
3 | node bin/ansi.js
4 |
--------------------------------------------------------------------------------
/TestPHP.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | haxe -cp src -cp test -php bin/php/ -dce full -main Test
3 | php bin/php/index.php
--------------------------------------------------------------------------------
/ansi.hxproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/haxelib.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "ansi",
3 | "url" : "https://github.com/SmilyOrg/ansi",
4 | "license" : "MIT",
5 | "tags" : ["cross"],
6 | "description" : "Haxe utility for working with ANSI escape sequences",
7 | "version" : "1.0.0",
8 | "releasenote" : "Initial release on haxelib",
9 | "contributors" : ["Smily"],
10 | "dependencies" : {},
11 | "classPath" : "src"
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/ANSI.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import haxe.macro.Expr;
4 |
5 | using StringTools;
6 | using Lambda;
7 |
8 | enum Attribute {
9 | Off;
10 |
11 | Bold;
12 | Underline;
13 | Blink;
14 | ReverseVideo;
15 | Concealed;
16 |
17 | BoldOff;
18 | UnderlineOff;
19 | BlinkOff;
20 | NormalVideo;
21 | ConcealedOff;
22 |
23 | Black;
24 | Red;
25 | Green;
26 | Yellow;
27 | Blue;
28 | Magenta;
29 | Cyan;
30 | White;
31 | DefaultForeground;
32 |
33 | BlackBack;
34 | RedBack;
35 | GreenBack;
36 | YellowBack;
37 | BlueBack;
38 | MagentaBack;
39 | CyanBack;
40 | WhiteBack;
41 | DefaultBackground;
42 | }
43 |
44 | typedef SequenceParam = {
45 | index:Int,
46 | name:String
47 | }
48 |
49 | typedef Sequence = {
50 | var val:String;
51 | var doc:String;
52 | @:optional var params:Array;
53 | }
54 |
55 | #if !macro
56 | @:build(ANSI.build())
57 | #end
58 | class ANSI {
59 |
60 | public inline static var ESCAPE:String = "\x1B";
61 | public inline static var BELL:String = "\x07";
62 |
63 | public inline static var CSI:String = ESCAPE+"[";
64 |
65 | public static var attr = Attribute;
66 |
67 | private static var values:Map = [
68 | Off => 0,
69 |
70 | Bold => 1,
71 | Underline => 4,
72 | Blink => 5,
73 | ReverseVideo => 7,
74 | Concealed => 8,
75 |
76 | BoldOff => 22,
77 | UnderlineOff => 24,
78 | BlinkOff => 25,
79 | NormalVideo => 27,
80 | ConcealedOff => 28,
81 |
82 | Black => 30,
83 | Red => 31,
84 | Green => 32,
85 | Yellow => 33,
86 | Blue => 34,
87 | Magenta => 35,
88 | Cyan => 36,
89 | White => 37,
90 | DefaultForeground => 39,
91 |
92 | BlackBack => 40,
93 | RedBack => 41,
94 | GreenBack => 42,
95 | YellowBack => 43,
96 | BlueBack => 44,
97 | MagentaBack => 45,
98 | CyanBack => 46,
99 | WhiteBack => 47,
100 | DefaultBackground => 49
101 |
102 | ];
103 |
104 | public static var set:Dynamic = Reflect.makeVarArgs(aset);
105 | public static var available = detectSupport();
106 | public static var strip:Bool = false;
107 | public static var stripIfUnavailable:Bool = true;
108 |
109 | @:ansi
110 | public static function aset(attributes:Array):String {
111 | return CSI+[for (arg in attributes) {
112 | if (!Std.is(arg, Attribute)) throw "Set argument is not an Attribute: "+arg;
113 | values.get(arg);
114 | }].join(";")+"m";
115 | }
116 |
117 | private static function detectSupport() {
118 | #if (sys || nodejs)
119 | return if (Sys.systemName().toLowerCase().indexOf("window") == -1) {
120 | var result = -1;
121 | try {
122 | #if (nodejs && !macro)
123 | result = js.node.ChildProcess.spawnSync("tput", ["colors"]).error == null ? 0 : 125;
124 | #else
125 | var process = new sys.io.Process("tput", [ "colors" ]);
126 | result = process.exitCode ();
127 | process.close ();
128 | #end
129 | } catch (e:Dynamic) {};
130 | result == 0;
131 | } else {
132 | Sys.getEnv ("ANSICON") != null;
133 | }
134 | #else
135 | return false;
136 | #end
137 | }
138 |
139 | @:ansi
140 | public inline static function title(str:String):String {
141 | return ESCAPE+"]0;" + str + BELL;
142 | }
143 |
144 | /*
145 | public static function replaceLine():String {
146 | return eraseLine()+moveToColumn();
147 | }
148 |
149 | public static function replaceLastLine():String {
150 | return moveUp()+eraseLine()+moveToColumn();
151 | }
152 | */
153 |
154 | public static var sequences:Map = [
155 |
156 | "eraseDisplayToEnd" => { val: CSI+ "J", doc: "Erase from cursor to the end of display." },
157 | "eraseDisplayToCursor" => { val: CSI+"1J", doc: "Erase from the start of diplay to cursor (inclusive)." },
158 | "eraseDisplay" => { val: CSI+"2J", doc: "Erase display and move cursor to the top-left." },
159 |
160 | "eraseLineToEnd" => { val: CSI+ "K", doc: "Erase from cursor to the end of line." },
161 | "eraseLineToCursor" => { val: CSI+"1K", doc: "Erase from the start of line to cursor (inclusive)." },
162 | "eraseLine" => { val: CSI+"2K", doc: "Erase line." },
163 |
164 | "eraseChar" => { val: CSI+ "X", doc: "Erase one character." },
165 | "eraseChars" => { val: CSI+"#X", doc: "Erase # characters." },
166 |
167 | "insertLine" => { val: CSI+ "L", doc: "Insert one blank line." },
168 | "insertLines" => { val: CSI+"#L", doc: "Insert # blank lines." },
169 |
170 | "deleteLine" => { val: CSI+ "M", doc: "Delete one line." },
171 | "deleteLines" => { val: CSI+"#M", doc: "Delete # lines." },
172 |
173 | "deleteChar" => { val: CSI+ "P", doc: "Delete one character." },
174 | "deleteChars" => { val: CSI+"#P", doc: "Delete # characters." },
175 |
176 | "insertChar" => { val: CSI+ "@", doc: "Insert one blank character." },
177 | "insertChars" => { val: CSI+"#@", doc: "Insert # blank characters." },
178 |
179 | "moveUp" => { val: CSI+"#A", doc: "Move cursor up # lines." },
180 | "moveDown" => { val: CSI+"#B", doc: "Move cursor down # lines." },
181 | "moveRight" => { val: CSI+"#C", doc: "Move cursor right # characters." },
182 | "moveLeft" => { val: CSI+"#D", doc: "Move cursor left # characters." },
183 |
184 | "moveDownReset" => { val: CSI+"#E", doc: "Move cursor down # lines and to first column." },
185 | "moveUpReset" => { val: CSI+"#F", doc: "Move cursor up # lines and to first column." },
186 |
187 | "setX" => { val: CSI+"#G", doc: "Move cursor to column #." },
188 | "setY" => { val: CSI+"#d", doc: "Move cursor to line #." },
189 |
190 | "reset" => { val: CSI+ "H", doc: "Move cursor to top-left." },
191 | "resetY" => { val: CSI+"#H", doc: "Move cursor to line # and first column." },
192 |
193 | "setXY" => { val: CSI+"#;#H", doc: "Move cursor to line #, column #.", params: [{ index: 1, name: "column" }, { index: 0, name: "line" }] },
194 |
195 | "saveCursor" => { val: CSI+ "s", doc: "Save cursor position." },
196 | "loadCursor" => { val: CSI+ "u", doc: "Move cursor to saved position." },
197 |
198 | "showCursor" => { val: CSI+"?25h", doc: "Show cursor." },
199 | "hideCursor" => { val: CSI+"?25l", doc: "Hide cursor." },
200 |
201 | ];
202 |
203 | #if macro
204 | /**
205 | * Builds fields out of the sequences above.
206 | */
207 | macro public static function build():Array {
208 | var pos = haxe.macro.Context.currentPos();
209 | var fields = haxe.macro.Context.getBuildFields();
210 |
211 | addSequences(pos, fields);
212 | addStripConditionals(fields);
213 |
214 | return fields;
215 | }
216 |
217 | static private function addStripConditionals(fields:Array) {
218 | for (field in fields) {
219 | switch (field.kind) {
220 | case FFun(f) if (field.meta.exists(hasMeta)):
221 | var exprs:Array = [
222 | macro if (strip || (stripIfUnavailable && !available)) return "",
223 | f.expr
224 | ];
225 | f.expr = macro { $a{exprs} };
226 | default:
227 | }
228 | }
229 | }
230 |
231 | static private inline function hasMeta(meta:MetadataEntry) {
232 | return meta.name == ":ansi";
233 | }
234 |
235 | static private function addSequences(pos:Position, fields:Array) {
236 | var tint = TPath({ pack: [], name: "Int" });
237 | var tstr = TPath({ pack: [], name: "String" });
238 |
239 | for (seq in sequences.keys()) {
240 | var s = sequences[seq];
241 | var hasParams = s.val.indexOf("#") != -1;
242 | var doc = s.doc;
243 |
244 | var pieces = s.val.split("#");
245 | var args:Array;
246 | var expr:Expr;
247 | if (pieces.length == 1) {
248 | args = [];
249 | expr = macro return $v{pieces[0]};
250 | } else if (pieces.length == 2) {
251 | var arg = { name: "num", type: tint, opt: false, value: macro 1 };
252 | args = [arg];
253 | expr = macro return $v{pieces[0]} + $i{arg.name} + $v{pieces[1]};
254 | } else {
255 | if (s.params == null) throw "Sequence definition with multiple params missing required `params` field.";
256 | if (s.params.length < pieces.length-1) throw "Not enough params in sequence definition.";
257 |
258 | var pieceIndex = 0;
259 | args = [];
260 | expr = macro $v{pieces[pieceIndex++]};
261 | for (param in s.params) {
262 | var arg = { name: param.name, type: tint, opt: false, value: null };
263 | args.push(arg);
264 | expr = macro $expr + $i{s.params[param.index].name} + $v{pieces[pieceIndex++]};
265 | }
266 |
267 | expr = macro return $expr;
268 | };
269 |
270 | var index = doc.length;
271 | var arg = args.length-1;
272 | while ((index = doc.lastIndexOf("#", index-1)) != -1) {
273 | var argIndex = s.params == null ? arg : s.params[arg].index;
274 | doc = doc.substr(0, index) + "`" + args[argIndex].name + "`" + doc.substr(index+1);
275 | arg--;
276 | if (arg < 0) break;
277 | }
278 |
279 | var kind = FFun({ args: args, ret: tstr, expr: expr }); //FVar(tstr, macro $v{s.val});
280 |
281 | var names = seq.split("|");
282 | for (name in names) {
283 | fields.push({
284 | name: name,
285 | doc:
286 | '${doc}\n' +
287 | (names.length == 1 ? "" : "Aliases: "+[for (n in names) if (name != n) n].join(", ")) +
288 | 'ANSI sequence: ${s.val}',
289 | access: [APublic, AStatic, AInline],
290 | kind: kind,
291 | meta: [{ name: ":ansi", pos: pos }],
292 | pos: pos
293 | });
294 | }
295 | }
296 | }
297 | #end
298 |
299 | }
300 |
--------------------------------------------------------------------------------
/test/Test.hx:
--------------------------------------------------------------------------------
1 | import ANSI;
2 | import haxe.Timer;
3 |
4 | class Particle {
5 | public var x:Float;
6 | public var y:Float;
7 | public var vx:Float;
8 | public var vy:Float;
9 | public var attr:Array;
10 | public function new() {}
11 | }
12 |
13 | class Test {
14 |
15 | static private function println(s:String = "") {
16 | #if sys
17 | Sys.stdout().writeString(s+"\n");
18 | #else
19 | trace(s);
20 | #end
21 | }
22 |
23 | static function main() {
24 |
25 | //ANSI.strip = true;
26 |
27 | println("Available: "+ANSI.available);
28 | println("Strip: "+(ANSI.strip ? "always" : ANSI.stripIfUnavailable ? "if not available" : "never"));
29 |
30 | println(ANSI.set(ANSI.attr.Green) + "green text");
31 |
32 | println(ANSI.set(Green) + "green text");
33 |
34 | println(
35 | ANSI.set(Green, Bold) + "vivid green text" +
36 | ANSI.set(DefaultForeground) + " bold text" +
37 | ANSI.set(BoldOff)
38 | );
39 |
40 | println("hello world" + ANSI.moveLeft(5) + ANSI.deleteChars(5) + "ansi");
41 |
42 | println(ANSI.title("Window title goes here"));
43 |
44 | #if sys
45 | if (ANSI.available && !ANSI.strip) {
46 | Sys.sleep(2);
47 | testParticles();
48 | }
49 | #end
50 |
51 | }
52 |
53 | #if sys
54 | static private function testParticles() {
55 | var out = Sys.stdout();
56 | var pixelAspectRatio = 8/14;
57 | var w = 80;
58 | var h = Std.int(25/pixelAspectRatio);
59 | var cx = w/2;
60 | var cy = h/2;
61 | var particles = [];
62 | var colors = [Black, Red, Green, Yellow, Blue, Magenta, Cyan, White];
63 | for (i in 0...50) {
64 | var p = new Particle();
65 | p.x = Std.random(w);
66 | p.y = Std.random(h);
67 | p.vx = (Math.random()-0.5)*10;
68 | p.vy = (Math.random()-0.5)*10;
69 | p.attr = [Std.random(2) == 0 ? BoldOff : Bold, colors[Std.random(colors.length)]];
70 | particles.push(p);
71 | }
72 | var drag:Float = 0.99;
73 | var angular:Float = 0.3;
74 | var wander:Float = 1;
75 | var dt:Float = 1/60;
76 | var time:Float = 0;
77 | while (true) {
78 | for (p in particles) {
79 | var dx:Float;
80 | var dy:Float;
81 | var dl:Float;
82 |
83 | dx = cx-p.x;
84 | dy = cy-p.y;
85 | dl = Math.sqrt(dx*dx+dy*dy);
86 | dx /= dl; dy /= dl;
87 |
88 | p.vx += dx+dy*angular;
89 | p.vy += dy-dx*angular;
90 |
91 | dx = Math.random()-0.5;
92 | dy = Math.random()-0.5;
93 | dl = Math.sqrt(dx*dx+dy*dy);
94 | dx /= dl; dy /= dl;
95 |
96 | p.vx += dx*wander;
97 | p.vy += dy*wander;
98 |
99 | p.vx *= drag;
100 | p.vy *= drag;
101 | p.x += p.vx*dt;
102 | p.y += p.vy*dt;
103 | }
104 | var cmd = "";
105 | cmd += ANSI.eraseDisplay();
106 | cmd += ANSI.set(Off, Green);
107 | cmd += ANSI.set(Std.int(time/0.25) & 1 == 0 ? ReverseVideo : NormalVideo);
108 | var header = "!ANSI PARTICLES!";
109 | cmd += ANSI.moveRight(Math.round(cx-header.length/2+Math.sin(time*2)*(w-header.length)/2));
110 | cmd += header;
111 | cmd += ANSI.set(Off);
112 | for (p in particles) {
113 | if (p.x < 0 || p.x > w-1 || p.y < 0 || p.y > h-1) continue;
114 | cmd += ANSI.setXY(Math.round(p.x), Math.round(p.y*pixelAspectRatio));
115 | cmd += ANSI.aset(p.attr);
116 | cmd += "o";
117 | }
118 | out.writeString(cmd);
119 | time += dt;
120 | Sys.sleep(dt);
121 | }
122 | }
123 | #end
124 |
125 | }
--------------------------------------------------------------------------------