summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2025-10-05 16:42:02 -0700
committerElizabeth Hunt <me@liz.coffee>2025-10-05 23:11:41 -0700
commitde43eb05d2e43ab31effce3dcca62ad91a556b26 (patch)
tree47a62b61bfc97dda639dea70627ecf3005ba7b02
parent35add63ec4dce39710095f17abd86777de9e5b49 (diff)
downloadansicolor-de43eb05d2e43ab31effce3dcca62ad91a556b26.tar.gz
ansicolor-de43eb05d2e43ab31effce3dcca62ad91a556b26.zip
-rw-r--r--.ci/bundle.js12
-rwxr-xr-x.ci/ci.js3
-rw-r--r--.ci/ci.json4
-rw-r--r--.ci/ci.ts74
-rw-r--r--.ci/package-lock.json514
-rw-r--r--.ci/package.json17
-rw-r--r--.ci/tsconfig.json16
-rw-r--r--.dockerignore8
-rw-r--r--Dockerfile31
-rw-r--r--index.html1
-rw-r--r--package-lock.json11
-rw-r--r--package.json1
-rw-r--r--src/App.tsx62
-rw-r--r--src/components/LoadScreen.tsx164
-rw-r--r--src/components/SaveModal.tsx79
-rw-r--r--src/components/grid/Cell.tsx11
-rw-r--r--src/components/grid/GridComponent.tsx37
-rw-r--r--src/components/toolbar/ColorSwatch.tsx227
-rw-r--r--src/components/toolbar/Toolbar.tsx9
-rw-r--r--src/components/toolbar/ToolbarItem.tsx57
-rw-r--r--src/pages/Paint.tsx156
-rw-r--r--src/styles/styles.css107
-rw-r--r--src/utils/ansi.ts12
-rw-r--r--src/utils/grid.ts83
-rw-r--r--src/utils/storage.ts42
25 files changed, 1675 insertions, 63 deletions
diff --git a/.ci/bundle.js b/.ci/bundle.js
new file mode 100644
index 0000000..46a9ce4
--- /dev/null
+++ b/.ci/bundle.js
@@ -0,0 +1,12 @@
+import * as esbuild from 'esbuild';
+await esbuild
+ .build({
+ entryPoints: ['dist/ci.js'],
+ bundle: true,
+ minify: true,
+ platform: 'node',
+ outfile: 'ci.js',
+ logLevel: 'info',
+ sourcemap: false,
+ })
+ .catch(() => process.exit(1));
diff --git a/.ci/ci.js b/.ci/ci.js
new file mode 100755
index 0000000..6758688
--- /dev/null
+++ b/.ci/ci.js
@@ -0,0 +1,3 @@
+#!/usr/bin/env node
+var Hr=Object.create;var Mt=Object.defineProperty;var xr=Object.getOwnPropertyDescriptor;var Wr=Object.getOwnPropertyNames;var Fr=Object.getPrototypeOf,Jr=Object.prototype.hasOwnProperty;var s=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var Vr=(t,e,r,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of Wr(e))!Jr.call(t,n)&&n!==r&&Mt(t,n,{get:()=>e[n],enumerable:!(i=xr(e,n))||i.enumerable});return t};var Fe=(t,e,r)=>(r=t!=null?Hr(Fr(t)):{},Vr(e||!t||!t.__esModule?Mt(r,"default",{value:t,enumerable:!0}):r,t));var Tt=s(me=>{"use strict";Object.defineProperty(me,"__esModule",{value:!0});me.prependWith=void 0;var $r=(t,e)=>Array(t.length*2).fill(0).map((r,i)=>i%2===0).map((r,i)=>r?e:t[Math.floor(i/2)]);me.prependWith=$r});var St=s(F=>{"use strict";Object.defineProperty(F,"__esModule",{value:!0});F.isDebug=F.isProd=void 0;var wt=!0,Gr=wt&&(process.env.ENVIRONMENT??"").toLowerCase().includes("prod")?"production":"development",Yr=()=>Gr==="production";F.isProd=Yr;var Kr=!(0,F.isProd)()||wt&&["y","t"].some((process.env.DEBUG??"").toLowerCase().startsWith),zr=()=>Kr;F.isDebug=zr});var Et=s(be=>{"use strict";Object.defineProperty(be,"__esModule",{value:!0});be.memoize=void 0;var Qr=t=>{let e=new Map;return(...r)=>{let i=JSON.stringify(r);if(e.has(i))return e.get(i);let n=t(...r);return e.set(i,n),n}};be.memoize=Qr});var Rt=s(L=>{"use strict";var Zr=L&&L.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),Je=L&&L.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&Zr(e,t,r)};Object.defineProperty(L,"__esModule",{value:!0});Je(Tt(),L);Je(St(),L);Je(Et(),L)});var jt=s(Pt=>{"use strict";Object.defineProperty(Pt,"__esModule",{value:!0})});var Lt=s(_e=>{"use strict";Object.defineProperty(_e,"__esModule",{value:!0});_e.isObject=void 0;var Xr=t=>typeof t=="object"&&!Array.isArray(t)&&!!t;_e.isObject=Xr});var qt=s(ye=>{"use strict";Object.defineProperty(ye,"__esModule",{value:!0});ye.isTagged=void 0;var kr=Ve(),ei=(t,e)=>!!((0,kr.isObject)(t)&&"_tag"in t&&t._tag===e);ye.isTagged=ei});var At=s(Nt=>{"use strict";Object.defineProperty(Nt,"__esModule",{value:!0})});var Ct=s(m=>{"use strict";Object.defineProperty(m,"__esModule",{value:!0});m.Optional=m.IOptionalEmptyError=m.isOptional=m.IOptionalTag=void 0;var Ye=u();m.IOptionalTag="IOptional";var ti=t=>(0,Ye.isTagged)(t,m.IOptionalTag);m.isOptional=ti;var Oe=class extends Error{};m.IOptionalEmptyError=Oe;var Dt="O.Some",Ut="O.None",ie=t=>(0,Ye.isTagged)(t,Ut),It=t=>(0,Ye.isTagged)(t,Dt),$e=class{_tag;constructor(e=m.IOptionalTag){this._tag=e}},Ge=class t extends $e{self;constructor(e){super(),this.self=e}move(e){return this.map(()=>e)}orSome(e){return ie(this.self)?t.from(e()):this}get(){if(ie(this.self))throw new Oe("called get() on None optional");return this.self.value}filter(e){return ie(this.self)||!e(this.self.value)?t.none():t.some(this.self.value)}map(e){return ie(this.self)?t.none():t.from(e(this.self.value))}flatMap(e){return ie(this.self)?t.none():t.from(e(this.self.value)).orSome(()=>t.none()).get()}present(){return It(this.self)}*[Symbol.iterator](){It(this.self)&&(yield this.self.value)}static some(e){return new t({value:e,_tag:Dt})}static _none=new t({_tag:Ut});static none(){return this._none}static from(e){return e==null?t.none():t.some(e)}};m.Optional=Ge});var xt=s(c=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0});c.Either=c.isRight=c.isLeft=c.isEither=c.IEitherTag=void 0;var J=u();c.IEitherTag="IEither";var ri=t=>(0,J.isTagged)(t,c.IEitherTag);c.isEither=ri;var Bt="E.Left",ii=t=>(0,J.isTagged)(t,Bt);c.isLeft=ii;var Ht="E.Right",ni=t=>(0,J.isTagged)(t,Ht);c.isRight=ni;var Ke=class{_tag;constructor(e=c.IEitherTag){this._tag=e}},ze=class t extends Ke{self;constructor(e){super(),this.self=e}moveRight(e){return this.mapRight(()=>e)}mapBoth(e,r){return(0,c.isLeft)(this.self)?t.left(e(this.self.err)):t.right(r(this.self.ok))}mapRight(e){return(0,c.isRight)(this.self)?t.right(e(this.self.ok)):t.left(this.self.err)}mapLeft(e){return(0,c.isLeft)(this.self)?t.left(e(this.self.err)):t.right(this.self.ok)}flatMap(e){return(0,c.isRight)(this.self)?e(this.self.ok):t.left(this.self.err)}filter(e){return(0,c.isLeft)(this.self)?t.left(this.self.err):t.fromFailable(()=>this.right().filter(e).get())}async flatMapAsync(e){return(0,c.isLeft)(this.self)?Promise.resolve(t.left(this.self.err)):await e(this.self.ok).catch(r=>t.left(r))}fold(e,r){return(0,c.isLeft)(this.self)?e(this.self.err):r(this.self.ok)}left(){return(0,c.isLeft)(this.self)?J.Optional.from(this.self.err):J.Optional.none()}right(){return(0,c.isRight)(this.self)?J.Optional.from(this.self.ok):J.Optional.none()}joinRight(e,r){return this.flatMap(i=>e.mapRight(n=>r(n,i)))}joinRightAsync(e,r){return this.flatMapAsync(async i=>await(typeof e=="function"?e():e).then(a=>a.mapRight(o=>r(o,i))))}static left(e){return new t({err:e,_tag:Bt})}static right(e){return new t({ok:e,_tag:Ht})}static fromFailable(e){try{return t.right(e())}catch(r){return t.left(r)}}static async fromFailableAsync(e){return await(typeof e=="function"?e():e).then(r=>t.right(r)).catch(r=>t.left(r))}};c.Either=ze});var Wt=s(q=>{"use strict";var si=q&&q.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),Qe=q&&q.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&si(e,t,r)};Object.defineProperty(q,"__esModule",{value:!0});Qe(At(),q);Qe(Ct(),q);Qe(xt(),q)});var Ft=s(ee=>{"use strict";Object.defineProperty(ee,"__esModule",{value:!0});ee.ListZipper=ee.Cons=void 0;var V=u(),ne=class t{value;next;constructor(e,r=V.Optional.none()){this.value=e,this.next=r}before(e){return new t(this.value,e)}replace(e){return new t(e,this.next)}*[Symbol.iterator](){for(let e=V.Optional.some(this);e.present();e=e.flatMap(r=>r.next))yield e.get().value}static addOnto(e,r){return Array.from(e).reverse().reduce((i,n)=>V.Optional.from(new t(n,i)),r)}static from(e){return t.addOnto(e,V.Optional.none())}};ee.Cons=ne;var Ze=class t{reversedPathToHead;currentHead;constructor(e,r){this.reversedPathToHead=e,this.currentHead=r}read(){return this.currentHead.map(({value:e})=>e)}next(){return this.currentHead.map(e=>new t(V.Optional.some(e.before(this.reversedPathToHead)),e.next))}previous(){return this.reversedPathToHead.map(e=>new t(e.next,V.Optional.some(e.before(this.currentHead))))}prependChunk(e){return new t(ne.addOnto(Array.from(e).reverse(),this.reversedPathToHead),this.currentHead)}prepend(e){return this.prependChunk([e])}remove(){let e=this.currentHead.flatMap(r=>r.next);return new t(this.reversedPathToHead,e)}replace(e){let r=this.currentHead.map(i=>i.replace(e));return new t(this.reversedPathToHead,r)}*[Symbol.iterator](){let e=this;for(let r=e.previous();r.present();r=r.flatMap(i=>i.previous()))e=r.get();e.currentHead.present()&&(yield*e.currentHead.get())}collection(){return Array.from(this)}static from(e){return new t(V.Optional.none(),ne.from(e))}};ee.ListZipper=Ze});var Jt=s($=>{"use strict";var oi=$&&$.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),ai=$&&$.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&oi(e,t,r)};Object.defineProperty($,"__esModule",{value:!0});ai(Ft(),$)});var Ve=s(_=>{"use strict";var ci=_&&_.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),se=_&&_.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&ci(e,t,r)};Object.defineProperty(_,"__esModule",{value:!0});se(jt(),_);se(Lt(),_);se(qt(),_);se(Wt(),_);se(Jt(),_)});var Vt=s(Me=>{"use strict";Object.defineProperty(Me,"__esModule",{value:!0});Me.TraceableImpl=void 0;var Xe=class t{item;trace;constructor(e,r){this.item=e,this.trace=r}map(e){let r=e(this);return new t(r,this.trace)}coExtend(e){let r=e(this);return Array.from(r).map(i=>this.move(i))}flatMap(e){return e(this)}flatMapAsync(e){return new t(e(this).then(r=>r.get()),this.trace)}traceScope(e){return new t(this.get(),this.trace.traceScope(e(this)))}peek(e){return e(this),this}move(e){return this.map(()=>e)}bimap(e){let{item:r,trace:i}=e(this);return this.move(r).traceScope(()=>i)}get(){return this.item}};Me.TraceableImpl=Xe});var $t=s(Te=>{"use strict";Object.defineProperty(Te,"__esModule",{value:!0});Te.EmittableMetric=void 0;var ui=we(),ke=class{name;unit;constructor(e,r){this.name=e,this.unit=r}withValue(e){return{name:this.name,unit:this.unit,emissionTimestamp:Date.now(),value:e,_tag:ui.MetricValueTag}}};Te.EmittableMetric=ke});var Gt=s(te=>{"use strict";Object.defineProperty(te,"__esModule",{value:!0});te.ResultMetric=te.Metric=void 0;var oe=we(),et=class{_tag;constructor(e=oe.IMetricTag){this._tag=e}},Se=class t extends et{name;parent;count;time;static DELIM=".";constructor(e,r=void 0,i=new oe.EmittableMetric(t.join(e,"count"),oe.Unit.COUNT),n=new oe.EmittableMetric(t.join(e,"time"),oe.Unit.MILLISECONDS)){super(),this.name=e,this.parent=r,this.count=i,this.time=n}child(e){let r=t.join(this.name,e);return new t(r,this)}asResult(){return Ee.from(this)}static join(...e){return e.join(t.DELIM)}static fromName(e){return new t(e)}};te.Metric=Se;var Ee=class t extends Se{name;parent;failure;success;warn;constructor(e,r=void 0,i,n,a){super(e,r),this.name=e,this.parent=r,this.failure=i,this.success=n,this.warn=a}static from(e){let r=e.child("failure"),i=e.child("success"),n=e.child("warn");return new t(e.name,e.parent,r,i,n)}};te.ResultMetric=Ee});var Yt=s(re=>{"use strict";Object.defineProperty(re,"__esModule",{value:!0});re.MetricsTrace=re.isMetricsTraceSupplier=void 0;var G=u(),li=t=>(0,G.isMetricValue)(t)||(0,G.isIMetric)(t)||Array.isArray(t)&&t.every(e=>(0,G.isMetricValue)(e)||(0,G.isIMetric)(e));re.isMetricsTraceSupplier=li;var tt=class t{metricConsumer;activeTraces;completedTraces;constructor(e,r=new Map,i=new Set){this.metricConsumer=e,this.activeTraces=r,this.completedTraces=i}traceScope(e){let r=Date.now(),i=(Array.isArray(e)?e:[e]).filter(G.isIMetric),n=new Map(i.map(a=>[a,r]));return new t(this.metricConsumer,n)}trace(e){if(!e||typeof e=="string")return this;let r=Date.now(),i=Array.isArray(e)?e:[e],n=i.filter(G.isMetricValue),a=i.filter(G.isIMetric),o=a.filter(f=>!this.activeTraces.has(f)),d=a.filter(f=>this.activeTraces.has(f)&&!this.completedTraces.has(f)),v=d.flatMap(f=>[f.count.withValue(1),f.time.withValue(r-this.activeTraces.get(f))]),h=[...n,...v];h.length>0&&this.metricConsumer(h);let k=new Map([...this.activeTraces,...o.map(f=>[f,r])]),B=new Set([...this.completedTraces,...d]);return new t(this.metricConsumer,k,B)}};re.MetricsTrace=tt});var we=s(l=>{"use strict";var di=l&&l.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),rt=l&&l.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&di(e,t,r)};Object.defineProperty(l,"__esModule",{value:!0});l.isIMetric=l.IMetricTag=l.isMetricValue=l.MetricValueTag=l.Unit=void 0;var zt=u(),Kt;(function(t){t.COUNT="COUNT",t.MILLISECONDS="MILLISECONDS"})(Kt||(l.Unit=Kt={}));l.MetricValueTag="MetricValue";var fi=t=>(0,zt.isTagged)(t,l.MetricValueTag);l.isMetricValue=fi;l.IMetricTag="IMetric";var hi=t=>(0,zt.isTagged)(t,l.IMetricTag);l.isIMetric=hi;rt($t(),l);rt(Gt(),l);rt(Yt(),l)});var Qt=s(Re=>{"use strict";Object.defineProperty(Re,"__esModule",{value:!0});Re.ANSI=void 0;Re.ANSI={RESET:"\x1B[0m",BOLD:"\x1B[1m",DIM:"\x1B[2m",RED:"\x1B[31m",GREEN:"\x1B[32m",YELLOW:"\x1B[33m",BLUE:"\x1B[34m",MAGENTA:"\x1B[35m",CYAN:"\x1B[36m",WHITE:"\x1B[37m",BRIGHT_RED:"\x1B[91m",BRIGHT_YELLOW:"\x1B[93m",GRAY:"\x1B[90m"}});var Zt=s(N=>{"use strict";Object.defineProperty(N,"__esModule",{value:!0});N.isLogLevel=N.logLevelOrder=N.LogLevel=void 0;var Y;(function(t){t.UNKNOWN="UNKNOWN",t.INFO="INFO",t.WARN="WARN",t.DEBUG="DEBUG",t.ERROR="ERROR",t.SYS="SYS"})(Y||(N.LogLevel=Y={}));N.logLevelOrder=[Y.DEBUG,Y.INFO,Y.WARN,Y.ERROR,Y.SYS];var gi=t=>typeof t=="string"&&N.logLevelOrder.some(e=>e===t);N.isLogLevel=gi});var kt=s(Xt=>{"use strict";Object.defineProperty(Xt,"__esModule",{value:!0})});var er=s(Pe=>{"use strict";Object.defineProperty(Pe,"__esModule",{value:!0});Pe.PrettyJsonConsoleLogger=void 0;var g=je(),it=class{log(e,...r){let i=JSON.stringify({level:e,trace:r},null,4),n=`${this.getStyle(e)}${i}${g.ANSI.RESET}
+`;this.getStream(e)(n)}getStream(e){return e===g.LogLevel.ERROR?console.error:console.log}getStyle(e){switch(e){case g.LogLevel.UNKNOWN:case g.LogLevel.INFO:return`${g.ANSI.MAGENTA}`;case g.LogLevel.DEBUG:return`${g.ANSI.CYAN}`;case g.LogLevel.WARN:return`${g.ANSI.BRIGHT_YELLOW}`;case g.LogLevel.ERROR:return`${g.ANSI.BRIGHT_RED}`;case g.LogLevel.SYS:return`${g.ANSI.DIM}${g.ANSI.BLUE}`}}};Pe.PrettyJsonConsoleLogger=it});var rr=s(Le=>{"use strict";Object.defineProperty(Le,"__esModule",{value:!0});Le.LogTrace=void 0;var tr=u(),p=je(),nt=class t{logger;traces;defaultLevel;allowedLevels;constructor(e=new p.PrettyJsonConsoleLogger,r=[pi],i=p.LogLevel.INFO,n=mi){this.logger=e,this.traces=r,this.defaultLevel=i,this.allowedLevels=n}traceScope(e){return new t(this.logger,this.traces.concat(e),this.defaultLevel,this.allowedLevels)}trace(e){let{traces:r,level:i}=this.foldTraces(this.traces.concat(e));if(!this.allowedLevels().has(i))return;let n=i===p.LogLevel.UNKNOWN?this.defaultLevel:i;this.logger.log(n,...r)}foldTraces(e){let r=e.map(o=>typeof o=="function"?o():o),i=r.filter(o=>(0,p.isLogLevel)(o)).reduce((o,d)=>Math.max(p.logLevelOrder.indexOf(d),o),-1),n=p.logLevelOrder[i]??p.LogLevel.UNKNOWN,a=r.filter(o=>!(0,p.isLogLevel)(o)).map(o=>typeof o=="object"?`TracedException.Name = ${o.name}, TracedException.Message = ${o.message}, TracedException.Stack = ${o.stack}`:o);return{level:n,traces:a}}};Le.LogTrace=nt;var pi=()=>`TimeStamp = ${new Date().toISOString()}`,vi=(0,tr.memoize)(t=>new Set([p.LogLevel.UNKNOWN,...t?[p.LogLevel.DEBUG]:[],p.LogLevel.INFO,p.LogLevel.WARN,p.LogLevel.ERROR,p.LogLevel.SYS])),mi=()=>vi((0,tr.isDebug)())});var je=s(y=>{"use strict";var bi=y&&y.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),ae=y&&y.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&bi(e,t,r)};Object.defineProperty(y,"__esModule",{value:!0});ae(Qt(),y);ae(Zt(),y);ae(kt(),y);ae(er(),y);ae(rr(),y)});var nr=s(R=>{"use strict";Object.defineProperty(R,"__esModule",{value:!0});R.LogMetricTraceable=R.LogMetricTrace=R.EmbeddedMetricsTraceable=R.LogTraceable=void 0;var A=at(),ce=class t extends A.TraceableImpl{static LogTrace=new A.LogTrace;static of(e){return new t(e,t.LogTrace)}};R.LogTraceable=ce;var ir=t=>e=>{e.length!==0&&t.traceScope(A.LogLevel.SYS).trace(`Metrics = <metrics>${JSON.stringify(e)}</metrics>`)},st=class t extends A.TraceableImpl{static MetricsTrace=new A.MetricsTrace(ir(ce.LogTrace));static of(e,r=t.MetricsTrace){return new t(e,r)}};R.EmbeddedMetricsTraceable=st;var qe=class t{logTrace;metricsTrace;constructor(e,r){this.logTrace=e,this.metricsTrace=r}traceScope(e){return(0,A.isMetricsTraceSupplier)(e)?new t(this.logTrace,this.metricsTrace.traceScope(e)):new t(this.logTrace.traceScope(e),this.metricsTrace)}trace(e){return(0,A.isMetricsTraceSupplier)(e)?(this.metricsTrace.trace(e),this):(this.logTrace.trace(e),this)}};R.LogMetricTrace=qe;var ot=class t extends A.TraceableImpl{static ofLogTraceable(e){let r=new A.MetricsTrace(ir(e.trace));return new t(e.get(),new qe(e.trace,r))}static of(e){let r=ce.of(e);return t.ofLogTraceable(r)}};R.LogMetricTraceable=ot});var sr=s(Ne=>{"use strict";Object.defineProperty(Ne,"__esModule",{value:!0});Ne.TraceUtil=void 0;var ct=u(),ut=class t{static promiseify(e){return r=>r.flatMapAsync(async i=>i.move(await i.get()).map(e)).get()}static traceResultingEither(e,r=!1){return i=>(e&&i.trace.trace(i.get().fold(n=>r?e.warn:e.failure,n=>e.success)),i.traceScope(n=>n.get().fold(a=>r?ct.LogLevel.WARN:ct.LogLevel.ERROR,a=>ct.LogLevel.INFO)))}static withTrace(e){return r=>r.traceScope(()=>e)}static withMetricTrace(e){return t.withTrace(e)}static withFunctionTrace(e){return t.withTrace(`fn.${e.name}`)}static withClassTrace(e){return t.withTrace(`class.${e.constructor.name}`)}};Ne.TraceUtil=ut});var at=s(O=>{"use strict";var _i=O&&O.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),ue=O&&O.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&_i(e,t,r)};Object.defineProperty(O,"__esModule",{value:!0});ue(Vt(),O);ue(we(),O);ue(je(),O);ue(nr(),O);ue(sr(),O)});var or=s(M=>{"use strict";Object.defineProperty(M,"__esModule",{value:!0});M.getStdoutMany=M.getStdout=M.CmdMetric=void 0;var H=u(),yi=require("node:child_process");M.CmdMetric=H.Metric.fromName("Exec").asResult();var Oi=(t,e={streamTraceable:[]})=>t.flatMap(H.TraceUtil.withFunctionTrace(M.getStdout)).flatMap(r=>r.traceScope(()=>`Command = ${r.get()}`)).map(r=>{let i=r.get(),n=typeof i=="string"?i:i.join(" "),a=e.clearEnv?e.env:{...process.env,...e.env};return H.Either.fromFailableAsync(new Promise((o,d)=>{let v=(0,yi.exec)(n,{env:a}),h="";v.stdout?.on("data",B=>{let f=B.toString();h+=f,e.streamTraceable?.includes("stdout")&&r.trace.trace(f)});let k="";v.stderr?.on("data",B=>{let f=B.toString();h+=f,e.streamTraceable?.includes("stderr")&&r.trace.trace(f)}),v.on("exit",B=>{B===0?o({stdout:h,stderr:k}):d(new Error(`exited with non-zero code: ${B}. ${k}`))})}))}).map(H.TraceUtil.promiseify(r=>r.get().mapRight(({stderr:i,stdout:n})=>(i&&r.trace.traceScope(H.LogLevel.DEBUG).trace(`StdErr = ${i}`),n)))).peek(H.TraceUtil.promiseify(H.TraceUtil.traceResultingEither(M.CmdMetric))).get();M.getStdout=Oi;var Mi=(t,e={streamTraceable:[]})=>t.coExtend(r=>r.get()).reduce(async(r,i)=>(await r).joinRightAsync(()=>i.map(a=>(0,M.getStdout)(a,e)).get(),(a,o)=>o.concat(a)),Promise.resolve(H.Either.right([])));M.getStdoutMany=Mi});var ar=s(P=>{"use strict";Object.defineProperty(P,"__esModule",{value:!0});P.getRequiredEnvVars=P.getRequiredEnv=P.getEnv=void 0;var lt=u(),Ti=t=>lt.Optional.from(process.env[t]);P.getEnv=Ti;var wi=t=>lt.Either.fromFailable(()=>(0,P.getEnv)(t).get()).mapLeft(()=>new Error(`environment variable "${t}" is required D:`));P.getRequiredEnv=wi;var Si=t=>{let e=lt.Either.right({}),r=(i,n,a)=>({...i,[n]:a});return t.reduce((i,n)=>i.joinRight((0,P.getRequiredEnv)(n),(a,o)=>r(o,n,a)),e)};P.getRequiredEnvVars=Si});var ur=s(K=>{"use strict";Object.defineProperty(K,"__esModule",{value:!0});K.validateExecutionEntries=K.validateIdentifier=void 0;var cr=u(),Ei=t=>/^[a-zA-Z0-9_\-:. \/]+$/.test(t)&&!t.includes("..");K.validateIdentifier=Ei;var Ri=t=>{let e=Object.entries(t).filter(r=>!r.every(i=>typeof i=="string"&&(0,K.validateIdentifier)(i)));return e.length>0?cr.Either.left(e):cr.Either.right(t)};K.validateExecutionEntries=Ri});var lr=s(T=>{"use strict";Object.defineProperty(T,"__esModule",{value:!0});T.argv=T.getArg=T.isArgKey=void 0;var z=u(),Pi=t=>t.startsWith("--");T.isArgKey=Pi;var ji=(t,e,r)=>{let i=z.Optional.from(e.findIndex(n=>(0,T.isArgKey)(n)&&n.split("=")[0]===t)).filter(n=>n>=0&&n<e.length);return i.present()?i.flatMap(n=>z.Optional.from(e.at(n)).map(a=>a.includes("=")?a.split("=")[1]:e.at(n+1))).filter(n=>!(0,T.isArgKey)(n)).map(n=>r.present(n)).orSome(()=>r.unspecified).map(n=>z.Either.right(n)).get():z.Optional.from(r.absent).map(n=>z.Either.right(n)).orSome(()=>z.Either.left(new Error(`arg ${t} is not present in arguments list and does not have an 'absent' value`))).get()};T.getArg=ji;var Li=(t,e,r=process.argv.slice(2))=>{let i={present:o=>o},n=o=>{let d=e?.[o]??i;return(0,T.getArg)(o,r,d).mapRight(v=>[o,v])};return t.map(n).reduce((o,d)=>o.flatMap(v=>d.mapRight(([h,k])=>({...v,[h]:k}))),z.Either.right({})).mapRight(o=>o)};T.argv=Li});var dr=s(j=>{"use strict";Object.defineProperty(j,"__esModule",{value:!0});j.Signals=j.SigTermMetric=j.SigIntMetric=void 0;var I=u();j.SigIntMetric=I.Metric.fromName("SigInt").asResult();j.SigTermMetric=I.Metric.fromName("SigTerm").asResult();var dt=class{static async awaitClose(e){let r=I.Either.right(void 0);return new Promise(i=>{let n=d=>v=>e.flatMap(I.TraceUtil.withMetricTrace(d)).peek(h=>h.trace.trace("closing")).move(I.Optional.from(v).map(h=>I.Either.left(h)).orSome(()=>r).get()).flatMap(I.TraceUtil.traceResultingEither(d)).map(h=>i(h.get())).peek(h=>h.trace.trace("finished")).get(),a=n(j.SigIntMetric),o=n(j.SigTermMetric);process.on("SIGINT",()=>e.flatMap(I.TraceUtil.withTrace("SIGINT")).get().close(a)),process.on("SIGTERM",()=>e.flatMap(I.TraceUtil.withTrace("SIGTERM")).get().close(o))})}};j.Signals=dt});var fr=s(w=>{"use strict";var qi=w&&w.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),le=w&&w.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&qi(e,t,r)};Object.defineProperty(w,"__esModule",{value:!0});le(or(),w);le(ar(),w);le(ur(),w);le(lr(),w);le(dr(),w)});var gr=s(hr=>{"use strict";Object.defineProperty(hr,"__esModule",{value:!0})});var pr=s(Ae=>{"use strict";Object.defineProperty(Ae,"__esModule",{value:!0});Ae.HttpStatusCodes=void 0;Ae.HttpStatusCodes={100:"Continue",101:"Switching Protocols",102:"Processing (WebDAV)",200:"OK",201:"Created",202:"Accepted",203:"Non-Authoritative Information",204:"No Content",205:"Reset Content",206:"Partial Content",207:"Multi-Status (WebDAV)",208:"Already Reported (WebDAV)",226:"IM Used",300:"Multiple Choices",301:"Moved Permanently",302:"Found",303:"See Other",304:"Not Modified",305:"Use Proxy",306:"(Unused)",307:"Temporary Redirect",308:"Permanent Redirect (experimental)",400:"Bad Request",401:"Unauthorized",402:"Payment Required",403:"Forbidden",404:"Not Found",405:"Method Not Allowed",406:"Not Acceptable",407:"Proxy Authentication Required",408:"Request Timeout",409:"Conflict",410:"Gone",411:"Length Required",412:"Precondition Failed",413:"Request Entity Too Large",414:"Request-URI Too Long",415:"Unsupported Media Type",416:"Requested Range Not Satisfiable",417:"Expectation Failed",418:"I'm a teapot (RFC 2324)",420:"Enhance Your Calm (Twitter)",422:"Unprocessable Entity (WebDAV)",423:"Locked (WebDAV)",424:"Failed Dependency (WebDAV)",425:"Reserved for WebDAV",426:"Upgrade Required",428:"Precondition Required",429:"Too Many Requests",431:"Request Header Fields Too Large",444:"No Response (Nginx)",449:"Retry With (Microsoft)",450:"Blocked by Windows Parental Controls (Microsoft)",451:"Unavailable For Legal Reasons",499:"Client Closed Request (Nginx)",500:"Internal Server Error",501:"Not Implemented",502:"Bad Gateway",503:"Service Unavailable",504:"Gateway Timeout",505:"HTTP Version Not Supported",506:"Variant Also Negotiates (Experimental)",507:"Insufficient Storage (WebDAV)",508:"Loop Detected (WebDAV)",509:"Bandwidth Limit Exceeded (Apache)",510:"Not Extended",511:"Network Authentication Required",598:"Network read timeout error",599:"Network connect timeout error"}});var mr=s(vr=>{"use strict";Object.defineProperty(vr,"__esModule",{value:!0})});var br=s(D=>{"use strict";var Ni=D&&D.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),ft=D&&D.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&Ni(e,t,r)};Object.defineProperty(D,"__esModule",{value:!0});ft(gr(),D);ft(pr(),D);ft(mr(),D)});var _r=s(U=>{"use strict";Object.defineProperty(U,"__esModule",{value:!0});U.JsonResponse=U.PenguenoResponse=U.getResponseMetrics=void 0;var de=u(),Ai=(t,e)=>{let r={...t.getResponseHeaders(),...e};return r["Content-Type"]=(r["Content-Type"]??"text/plain")+"; charset=utf-8",r},Ii=[0,1,2,3,4,5].map(t=>de.Metric.fromName(`response.${t}xx`).asResult()),Di=(t,e)=>{let r=Math.floor(t/100);return Ii.flatMap((i,n)=>de.Optional.from(n).filter(a=>a===r).map(()=>[i.count.withValue(1)]).flatMap(a=>de.Optional.from(e).map(o=>a.concat(i.time.withValue(o))).orSome(()=>a)).orSome(()=>[i.count.withValue(0)]).get())};U.getResponseMetrics=Di;var Ie=class{_body;statusText;status;headers;constructor(e,r,i){this._body=r,this.headers=Ai(e.get(),i?.headers??{}),this.status=i.status,this.statusText=i.statusText??de.HttpStatusCodes[this.status],e.trace.trace((0,U.getResponseMetrics)(i.status,e.get().elapsedTimeMs()))}body(){return this._body}};U.PenguenoResponse=Ie;var ht=class extends Ie{constructor(e,r,i){let n={...i,headers:{...i.headers,"Content-Type":"application/json"}};if((0,de.isEither)(r)){super(e,JSON.stringify(r.fold(a=>({error:a,ok:void 0}),a=>({ok:a}))),n);return}super(e,JSON.stringify(Math.floor(n.status/100)>4?{error:r}:{ok:r}),n)}};U.JsonResponse=ht});var yr=s(Q=>{"use strict";var Ui=Q&&Q.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),Ci=Q&&Q.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&Ui(e,t,r)};Object.defineProperty(Q,"__esModule",{value:!0});Ci(_r(),Q)});var Mr=s(De=>{"use strict";Object.defineProperty(De,"__esModule",{value:!0});De.PenguenoRequest=void 0;var Or=["hewwo :D","hiya cutie","boop!","sending virtual hugs!","stay pawsitive"],Bi=()=>Or[Math.floor(Math.random()*Or.length)],gt=class t{req;id;at;constructor(e,r,i){this.req=e,this.id=r,this.at=i}elapsedTimeMs(e=()=>Date.now()){return e()-this.at.getTime()}getResponseHeaders(){let e=this.id,r=this.at.getTime(),i=Date.now(),n=this.elapsedTimeMs(()=>i),a=Bi();return Object.entries({RequestId:e,RequestReceivedUnix:r,RequestHandleUnix:i,DeltaUnix:n,Hai:a}).reduce((o,[d,v])=>({...o,[d]:v.toString()}),{})}static from(e){let r=crypto.randomUUID();return e.bimap(i=>{let n=i.get(),a=new URL(n.url),{pathname:o}=a,d=`RequestId = ${r}, Method = ${n.method}, Path = ${o}`;return{item:new t(n,r,new Date),trace:d}})}};De.PenguenoRequest=gt});var Tr=s(Z=>{"use strict";var Hi=Z&&Z.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),xi=Z&&Z.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&Hi(e,t,r)};Object.defineProperty(Z,"__esModule",{value:!0});xi(Mr(),Z)});var Er=s(x=>{"use strict";Object.defineProperty(x,"__esModule",{value:!0});x.HealthCheckActivityImpl=x.HealthCheckOutput=x.HealthCheckInput=void 0;var X=u(),pt;(function(t){t[t.CHECK=0]="CHECK"})(pt||(x.HealthCheckInput=pt={}));var wr;(function(t){t[t.YAASSSLAYQUEEN=0]="YAASSSLAYQUEEN"})(wr||(x.HealthCheckOutput=wr={}));var Sr=X.Metric.fromName("Health").asResult(),vt=class{check;constructor(e){this.check=e}checkHealth(e){return e.flatMap(X.TraceUtil.withFunctionTrace(this.checkHealth)).flatMap(X.TraceUtil.withMetricTrace(Sr)).flatMap(r=>r.move(pt.CHECK).map(i=>this.check(i))).peek(X.TraceUtil.promiseify(X.TraceUtil.traceResultingEither(Sr))).map(X.TraceUtil.promiseify(r=>{let{status:i,message:n}=r.get().fold(()=>({status:500,message:"err"}),()=>({status:200,message:"ok"}));return new X.JsonResponse(e,n,{status:i})})).get()}};x.HealthCheckActivityImpl=vt});var Pr=s(Ue=>{"use strict";Object.defineProperty(Ue,"__esModule",{value:!0});Ue.FourOhFourActivityImpl=void 0;var Wi=u(),Rr=["D: meow-t found! your api call ran away!","404-bidden! but like...in a cute way >:3 !",":< your data went on a paw-sible vacation!","uwu~ not found, but found our hearts instead!"],Fi=()=>Rr[Math.floor(Math.random()*Rr.length)],mt=class{fourOhFour(e){return e.move(new Wi.JsonResponse(e,Fi(),{status:404})).map(r=>Promise.resolve(r.get())).get()}};Ue.FourOhFourActivityImpl=mt});var Lr=s(W=>{"use strict";var Ji=W&&W.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),jr=W&&W.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&Ji(e,t,r)};Object.defineProperty(W,"__esModule",{value:!0});jr(Er(),W);jr(Pr(),W)});var qr=s(he=>{"use strict";Object.defineProperty(he,"__esModule",{value:!0});he.requireMethod=void 0;var fe=u(),Vi=t=>e=>e.flatMap(fe.TraceUtil.withFunctionTrace(he.requireMethod)).map(r=>{let{req:{method:i}}=r.get();if(!t.includes(i)){let n="that's not how you pet me (\u22DF\uFE4F\u22DE)~";return r.trace.traceScope(fe.LogLevel.WARN).trace(n),fe.Either.left(new fe.PenguenoError(n,405))}return fe.Either.right(i)}).get();he.requireMethod=Vi});var Ar=s(ge=>{"use strict";Object.defineProperty(ge,"__esModule",{value:!0});ge.jsonModel=void 0;var C=u(),Nr=C.Metric.fromName("JsonParse").asResult(),$i=t=>e=>e.flatMap(C.TraceUtil.withFunctionTrace(ge.jsonModel)).flatMap(C.TraceUtil.withMetricTrace(Nr)).map(r=>C.Either.fromFailableAsync(r.get().req.json()).then(i=>i.mapLeft(n=>(r.trace.traceScope(C.LogLevel.WARN).trace(n),new C.PenguenoError("seems to be invalid JSON (>//<) can you fix?",400))))).flatMapAsync(C.TraceUtil.promiseify(C.TraceUtil.traceResultingEither(Nr))).map(C.TraceUtil.promiseify(r=>r.get().mapRight(i=>r.move(i)).flatMap(t))).get();ge.jsonModel=$i});var Dr=s(b=>{"use strict";var Gi=b&&b.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),Ir=b&&b.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&Gi(e,t,r)};Object.defineProperty(b,"__esModule",{value:!0});b.PenguenoError=b.ErrorSource=void 0;var kn=u(),Ce;(function(t){t.USER="WARN",t.SYSTEM="ERROR"})(Ce||(b.ErrorSource=Ce={}));var bt=class extends Error{message;status;source;constructor(e,r){super(e),this.message=e,this.status=r,this.source=Math.floor(r/100)===4?Ce.USER:Ce.SYSTEM}};b.PenguenoError=bt;Ir(qr(),b);Ir(Ar(),b)});var Ur=s(S=>{"use strict";var Yi=S&&S.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),pe=S&&S.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&Yi(e,t,r)};Object.defineProperty(S,"__esModule",{value:!0});pe(br(),S);pe(yr(),S);pe(Tr(),S);pe(Lr(),S);pe(Dr(),S)});var u=s(E=>{"use strict";var Ki=E&&E.__createBinding||(Object.create?function(t,e,r,i){i===void 0&&(i=r);var n=Object.getOwnPropertyDescriptor(e,r);(!n||("get"in n?!e.__esModule:n.writable||n.configurable))&&(n={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,i,n)}:function(t,e,r,i){i===void 0&&(i=r),t[i]=e[r]}),ve=E&&E.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&Ki(e,t,r)};Object.defineProperty(E,"__esModule",{value:!0});ve(Rt(),E);ve(Ve(),E);ve(at(),E);ve(fr(),E);ve(Ur(),E)});var _t=Fe(u(),1),zi=["fetch_code","ci_pipeline","build_docker_image.js","ansible_playbook.js","checkout_ci.js","npm_publish.js"],Qi=t=>typeof t=="string"&&zi.includes(t),Cr=t=>!!((0,_t.isObject)(t)&&"arguments"in t&&(0,_t.isObject)(t.arguments)&&"type"in t&&Qi(t.type)&&t);var Ot=Fe(u(),1);var yt=class{stages=[];addStage(e){return this.stages.push(e),this}build(){return new He(this.stages)}},Be=class extends yt{remoteUrl;refname;constructor(e=process.env.remote,r=process.env.rev,i=process.env.refname){super(),this.remoteUrl=e,this.refname=i,this.addStage({parallelJobs:[{type:"fetch_code",arguments:{remoteUrl:e,checkout:r,path:this.getSourceDestination()}}]})}getSourceDestination(){return this.remoteUrl.replace(".git","").split("/").at(-1)??"src"}getBranch(){return this.refname.split("refs/heads/").at(1)}};var xe=Fe(u(),1);var He=class t{serialJobs;constructor(e){this.serialJobs=e}serialize(){return JSON.stringify({serialJobs:this.serialJobs})}static from(e){return xe.Either.fromFailable(()=>JSON.parse(e)).flatMap(r=>Br(r)?xe.Either.right(r):xe.Either.left(new Error("oh noes D: its a bad pipewine :(("))).mapRight(r=>new t(r.serialJobs))}};var Zi=t=>(0,Ot.isObject)(t)&&"parallelJobs"in t&&Array.isArray(t.parallelJobs)&&t.parallelJobs.every(e=>Cr(e)),Br=t=>(0,Ot.isObject)(t)&&"serialJobs"in t&&Array.isArray(t.serialJobs)&&t.serialJobs.every(e=>Zi(e));var We=exports&&exports.__assign||function(){return We=Object.assign||function(t){for(var e,r=1,i=arguments.length;r<i;r++){e=arguments[r];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n])}return t},We.apply(this,arguments)},Xi="oci.liz.coffee",ki="emprespresso",en="ansicolor",tn="ssh://src.liz.coffee:2222",rn=function(){var t=new Be,e=t.getBranch();if(!e)return t.build();var r={context:t.getSourceDestination(),registry:Xi,namespace:ki,imageTag:e},i={type:"build_docker_image.js",arguments:We(We({},r),{repository:en,buildTarget:"ansicolor",dockerfile:"Dockerfile"})};t.addStage({parallelJobs:[i]});var n=e==="release";if(!n)return t.build();var a={type:"fetch_code",arguments:{remoteUrl:"".concat(tn,"/infra"),checkout:"main",path:"infra"}},o={type:"ansible_playbook.js",arguments:{path:"infra",playbooks:"playbooks/ansicolor.yml"}};return[a,o].forEach(function(d){return t.addStage({parallelJobs:[d]})}),t.build()},nn=function(){var t=rn().serialize();process.stdout.write(t)};nn();
diff --git a/.ci/ci.json b/.ci/ci.json
new file mode 100644
index 0000000..bc3df7d
--- /dev/null
+++ b/.ci/ci.json
@@ -0,0 +1,4 @@
+{
+ "workflow": ".ci/ci.js"
+}
+
diff --git a/.ci/ci.ts b/.ci/ci.ts
new file mode 100644
index 0000000..d1208f7
--- /dev/null
+++ b/.ci/ci.ts
@@ -0,0 +1,74 @@
+#!/usr/bin/env node
+
+import {
+ AnsiblePlaybookJob,
+ BuildDockerImageJob,
+ DefaultGitHookPipelineBuilder,
+ FetchCodeJob,
+ Job,
+} from '@emprespresso/ci_model';
+import { join } from 'path';
+
+const REGISTRY = 'oci.liz.coffee';
+const NAMESPACE = 'emprespresso';
+const IMG = 'ansicolor';
+const REMOTE = 'ssh://src.liz.coffee:2222';
+
+const getPipeline = () => {
+ const gitHookPipeline = new DefaultGitHookPipelineBuilder();
+ const branch = gitHookPipeline.getBranch();
+ if (!branch) return gitHookPipeline.build();
+
+ const commonBuildArgs = {
+ context: gitHookPipeline.getSourceDestination(),
+ registry: REGISTRY,
+ namespace: NAMESPACE,
+ imageTag: branch,
+ };
+
+ const build: BuildDockerImageJob = {
+ type: 'build_docker_image.js',
+ arguments: {
+ ...commonBuildArgs,
+ repository: IMG,
+ buildTarget: 'ansicolor',
+ dockerfile: 'Dockerfile',
+ },
+ };
+ gitHookPipeline.addStage({
+ parallelJobs: [build],
+ });
+
+ const isRelease = branch === 'release';
+ if (!isRelease) {
+ return gitHookPipeline.build();
+ }
+
+ const fetchAnsibleCode: FetchCodeJob = {
+ type: 'fetch_code',
+ arguments: {
+ remoteUrl: `${REMOTE}/infra`,
+ checkout: 'main',
+ path: 'infra',
+ },
+ };
+ const thenDeploy: AnsiblePlaybookJob = {
+ type: 'ansible_playbook.js',
+ arguments: {
+ path: 'infra',
+ playbooks: 'playbooks/ansicolor.yml',
+ },
+ };
+ [fetchAnsibleCode, thenDeploy].forEach((deploymentStage) =>
+ gitHookPipeline.addStage({ parallelJobs: [deploymentStage] }),
+ );
+
+ return gitHookPipeline.build();
+};
+
+const main = () => {
+ const data = getPipeline().serialize();
+ process.stdout.write(data);
+};
+
+main();
diff --git a/.ci/package-lock.json b/.ci/package-lock.json
new file mode 100644
index 0000000..51f2db2
--- /dev/null
+++ b/.ci/package-lock.json
@@ -0,0 +1,514 @@
+{
+ "name": ".ci",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "@emprespresso/ci_model": "^0.1.0"
+ },
+ "devDependencies": {
+ "esbuild": "0.25.5",
+ "typescript": "^5.8.3"
+ }
+ },
+ "node_modules/@emprespresso/ci_model": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@emprespresso/ci_model/-/ci_model-0.1.0.tgz",
+ "integrity": "sha512-Ig1F61q+KiMUBAiAP/rp8a2+WM3RKuz1+qWunHljS2KP28du4kCidHH+8Z/I9RHEi/5gPBTppIL2ZmXlccsRPg==",
+ "dependencies": {
+ "@emprespresso/pengueno": "^0.0.6"
+ }
+ },
+ "node_modules/@emprespresso/pengueno": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@emprespresso/pengueno/-/pengueno-0.0.6.tgz",
+ "integrity": "sha512-QjyNXJPFp6OlOuk6cH/0yzdFznItofqhB1wF75k/Len5A0BsqvuE1QGU9aZ7AkujGkIpbv21Vm6K21/bmk0S2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.16.0",
+ "npm": ">=10.0.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
+ "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
+ "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
+ "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
+ "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
+ "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
+ "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
+ "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
+ "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
+ "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
+ "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
+ "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
+ "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
+ "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
+ "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
+ "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
+ "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
+ "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
+ "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
+ "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
+ "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
+ "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
+ "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
+ "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.5",
+ "@esbuild/android-arm": "0.25.5",
+ "@esbuild/android-arm64": "0.25.5",
+ "@esbuild/android-x64": "0.25.5",
+ "@esbuild/darwin-arm64": "0.25.5",
+ "@esbuild/darwin-x64": "0.25.5",
+ "@esbuild/freebsd-arm64": "0.25.5",
+ "@esbuild/freebsd-x64": "0.25.5",
+ "@esbuild/linux-arm": "0.25.5",
+ "@esbuild/linux-arm64": "0.25.5",
+ "@esbuild/linux-ia32": "0.25.5",
+ "@esbuild/linux-loong64": "0.25.5",
+ "@esbuild/linux-mips64el": "0.25.5",
+ "@esbuild/linux-ppc64": "0.25.5",
+ "@esbuild/linux-riscv64": "0.25.5",
+ "@esbuild/linux-s390x": "0.25.5",
+ "@esbuild/linux-x64": "0.25.5",
+ "@esbuild/netbsd-arm64": "0.25.5",
+ "@esbuild/netbsd-x64": "0.25.5",
+ "@esbuild/openbsd-arm64": "0.25.5",
+ "@esbuild/openbsd-x64": "0.25.5",
+ "@esbuild/sunos-x64": "0.25.5",
+ "@esbuild/win32-arm64": "0.25.5",
+ "@esbuild/win32-ia32": "0.25.5",
+ "@esbuild/win32-x64": "0.25.5"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ }
+ }
+}
diff --git a/.ci/package.json b/.ci/package.json
new file mode 100644
index 0000000..d4ef12a
--- /dev/null
+++ b/.ci/package.json
@@ -0,0 +1,17 @@
+{
+ "scripts": {
+ "build": "tsc && node bundle.js",
+ "clean": "rm -rf dist ci.cjs"
+ },
+ "dependencies": {
+ "@emprespresso/ci_model": "^0.1.0"
+ },
+ "devDependencies": {
+ "esbuild": "0.25.5",
+ "typescript": "^5.8.3"
+ },
+ "files": [
+ "dist/**/*",
+ "package.json"
+ ]
+}
diff --git a/.ci/tsconfig.json b/.ci/tsconfig.json
new file mode 100644
index 0000000..b157060
--- /dev/null
+++ b/.ci/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "include": ["**/*.ts"],
+ "compilerOptions": {
+ "module": "ESNext",
+ "outDir": "./dist",
+ "rootDir": "./",
+ "composite": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "noEmit": false,
+ "moduleResolution": "node"
+ },
+ "exclude": ["node_modules", "dist", "**/*.d.ts"],
+ "references": []
+}
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..7bb7f8d
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,8 @@
+node_modules
+dist
+.git
+.gitignore
+*.md
+.env
+.env.local
+.DS_Store
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..1d70736
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,31 @@
+# Build stage
+FROM node:20-alpine AS builder
+
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+RUN npm ci
+
+# Copy source files
+COPY . .
+
+# Build the application
+RUN npm run build
+
+# Production stage
+FROM nginx:alpine AS ansicolor
+
+# Copy built files from builder stage
+COPY --from=builder /app/dist /usr/share/nginx/html
+
+# Copy nginx configuration if needed (optional, nginx default config works for SPAs)
+# COPY nginx.conf /etc/nginx/nginx.conf
+
+# Expose port 80
+EXPOSE 80
+
+# Start nginx
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/index.html b/index.html
index da18db3..c6f1d10 100644
--- a/index.html
+++ b/index.html
@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🖌️</text></svg>">
<title>ansicolor</title>
</head>
<body>
diff --git a/package-lock.json b/package-lock.json
index bb8da91..5051961 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@emprespresso/pengueno": "^0.0.13",
"react": "^19.1.1",
+ "react-colorful": "^5.6.1",
"react-dom": "^19.1.1"
},
"devDependencies": {
@@ -2949,6 +2950,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-colorful": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
+ "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/react-dom": {
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
diff --git a/package.json b/package.json
index 367c06c..7e5f160 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"dependencies": {
"@emprespresso/pengueno": "^0.0.13",
"react": "^19.1.1",
+ "react-colorful": "^5.6.1",
"react-dom": "^19.1.1"
},
"devDependencies": {
diff --git a/src/App.tsx b/src/App.tsx
index c50d1fc..dd3d76d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -2,28 +2,54 @@ import { useEffect, useState } from 'react';
import { ChooseArt } from '@/pages/ChooseArt';
import { Paint } from '@/pages/Paint';
+import { LoadScreen } from '@/components/LoadScreen';
-import { gridFromAscii } from '@/utils/grid';
-
-const butterfly = `| |
-| ⠀⠀⠀⠀⊹ |
-| ⢶⢻⣑⣒⢤⡀⠀⢄⠀⠀⡠⠀⢀⡤⣆⣊⡿⡷ |
-| ⠀⠹⠹⣚⣣⠻⣦⡀⠀⠀⢀⣴⠟⣸⢓⢎⠏⠀ |
-| ⠀⠀⢡⣱⣖⣢⡾⢿⣾⣷⡿⢷⣖⣒⣎⡎⠀⠀ |
-| ⠀⠀⠀⣠⠓⢬⠅⡺⢻⡟⢗⠨⡥⠚⣄⠀⠀⠀ |
-| ⠀⠀⠀⣿⡆⠘⠆⢇⢸⡇⠸⠰⠃⢰⣿⠀⠀⠀ |
-| ⠀⠀⠀⠐⡻⣮⣬⠞⠈⠁⠳⣤⣴⢿⠂⠀⠀⠀ |
-| ⠀⠀⠀⡜⠀⠁⠉⠀⠀⠀⠀⠈⠈⠀⢣⠀⠀⠀ |
-| ⊹ |
-| |`;
+import { gridFromAscii, gridFromAnsi } from '@/utils/grid';
+import type { Grid } from '@/types/grid';
export const App: React.FC = () => {
- // const [chosenArt, setChosenArt] = useState<undefined | string>(undefined);
- const [chosenArt, setChosenArt] = useState<undefined | string>(butterfly);
+ const [route, setRoute] = useState(window.location.hash || '#home');
+ const [chosenArt, setChosenArt] = useState<null | string>(null);
+ const [loadedGrid, setLoadedGrid] = useState<Grid | null>(null);
+
+ useEffect(() => {
+ const handleHashChange = () => {
+ setRoute(window.location.hash || '#home');
+ };
+ window.addEventListener('hashchange', handleHashChange);
+ return () => window.removeEventListener('hashchange', handleHashChange);
+ }, []);
+
+ const handleLoad = (grid: Grid) => {
+ setLoadedGrid(grid);
+ window.location.hash = '#paint';
+ };
+
+ const handleNew = () => {
+ setChosenArt('');
+ window.location.hash = '#paint';
+ };
+
+ const handlePaste = (ansiText: string) => {
+ const importedGrid = gridFromAnsi(ansiText);
+ setLoadedGrid(importedGrid);
+ window.location.hash = '#paint';
+ };
+
+ const handleGoHome = () => {
+ setLoadedGrid(null);
+ setChosenArt(null);
+ window.location.hash = '#home';
+ };
- if (chosenArt !== undefined) {
- return <Paint grid={gridFromAscii(chosenArt)} />;
+ if (route === '#paint') {
+ if (loadedGrid !== null) {
+ return <Paint grid={loadedGrid} onGoHome={handleGoHome} />;
+ }
+ if (chosenArt !== null) {
+ return <Paint grid={gridFromAscii(chosenArt)} onGoHome={handleGoHome} />;
+ }
}
- return <ChooseArt artSubmissionCallback={setChosenArt} />;
+ return <LoadScreen onLoad={handleLoad} onNew={handleNew} onPaste={handlePaste} />;
};
diff --git a/src/components/LoadScreen.tsx b/src/components/LoadScreen.tsx
new file mode 100644
index 0000000..e51ff6a
--- /dev/null
+++ b/src/components/LoadScreen.tsx
@@ -0,0 +1,164 @@
+import React, { useState } from 'react';
+import type { Grid } from '@/types/grid';
+import { getSavedArt, deleteSavedArt, type SavedArt } from '@/utils/storage';
+import { GridComponent } from './grid/GridComponent';
+import { gridFromAscii } from '@/utils/grid';
+
+const demoArt = [
+ { name: '🎨 Butterfly', art: `| |
+| ⠀⠀⠀⠀⊹ |
+| ⢶⢻⣑⣒⢤⡀⠀⢄⠀⠀⡠⠀⢀⡤⣆⣊⡿⡷ |
+| ⠀⠹⠹⣚⣣⠻⣦⡀⠀⠀⢀⣴⠟⣸⢓⢎⠏⠀ |
+| ⠀⠀⢡⣱⣖⣢⡾⢿⣾⣷⡿⢷⣖⣒⣎⡎⠀⠀ |
+| ⠀⠀⠀⣠⠓⢬⠅⡺⢻⡟⢗⠨⡥⠚⣄⠀⠀⠀ |
+| ⠀⠀⠀⣿⡆⠘⠆⢇⢸⡇⠸⠰⠃⢰⣿⠀⠀⠀ |
+| ⠀⠀⠀⠐⡻⣮⣬⠞⠈⠁⠳⣤⣴⢿⠂⠀⠀⠀ |
+| ⠀⠀⠀⡜⠀⠁⠉⠀⠀⠀⠀⠈⠈⠀⢣⠀⠀⠀ |
+| ⊹ |
+| |` },
+ { name: '😊 Smiley', art: ` ████████
+ ██ ██
+ ██ ██ ██ ██
+██ ██
+██ ██ ██ ██
+██ ██
+ ██ ██████████ ██
+ ██ ██
+ ████████ ` },
+ { name: '🌟 Star', art: ` ★
+ ███
+ █████
+ ███████
+█████████
+ ███████
+ █████
+ ███
+ █ ` },
+];
+
+interface LoadScreenProps {
+ onLoad: (grid: Grid) => void;
+ onNew: () => void;
+ onPaste: (ansiText: string) => void;
+}
+
+export const LoadScreen: React.FC<LoadScreenProps> = ({ onLoad, onNew, onPaste }) => {
+ const [saves] = useState<SavedArt[]>(getSavedArt());
+ const [pasteText, setPasteText] = useState('');
+
+ const handleDemoLoad = (art: string) => {
+ const grid = gridFromAscii(art);
+ onLoad(grid);
+ };
+
+ const formatDate = (timestamp: number) => {
+ const date = new Date(timestamp);
+ return date.toLocaleString();
+ };
+
+ const handleDelete = (id: string, e: React.MouseEvent) => {
+ e.stopPropagation();
+ deleteSavedArt(id);
+ window.location.reload();
+ };
+
+ const handlePaste = () => {
+ if (pasteText.trim()) {
+ onPaste(pasteText.trim());
+ }
+ };
+
+ return (
+ <div style={{
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '1.5rem',
+ padding: '2rem',
+ width: '100%',
+ maxWidth: '900px',
+ margin: '0 auto',
+ }}>
+ <h2>ANSI Color Paint</h2>
+
+ <div>
+ <h3>Demo Templates</h3>
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
+ {demoArt.map((demo, idx) => (
+ <button
+ key={idx}
+ onClick={() => handleDemoLoad(demo.art)}
+ style={{ flex: '1 1 calc(33% - 0.5rem)', minWidth: '150px' }}
+ >
+ {demo.name}
+ </button>
+ ))}
+ </div>
+ </div>
+
+ <div>
+ <h3>Recent Saves</h3>
+ {saves.length > 0 ? (
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
+ {saves.map((save) => (
+ <div
+ key={save.id}
+ onClick={() => onLoad(save.grid)}
+ style={{
+ border: '2px solid var(--regular3)',
+ borderRadius: '4px',
+ padding: '1rem',
+ cursor: 'pointer',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ transition: 'border-color 0.2s',
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.borderColor = 'var(--regular6)';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.borderColor = 'var(--regular3)';
+ }}
+ >
+ <div>
+ <div style={{ fontWeight: 'bold', marginBottom: '0.25rem' }}>
+ {save.name}
+ </div>
+ <div style={{ fontSize: '0.9rem', opacity: 0.7 }}>
+ {formatDate(save.timestamp)}
+ </div>
+ </div>
+ <button
+ onClick={(e) => handleDelete(save.id, e)}
+ style={{
+ padding: '0.25rem 0.5rem',
+ fontSize: '0.9rem',
+ width: 'auto',
+ minWidth: 'fit-content',
+ }}
+ >
+ Delete
+ </button>
+ </div>
+ ))}
+ </div>
+ ) : (
+ <p style={{ opacity: 0.7 }}>No saves yet</p>
+ )}
+ </div>
+
+ <div>
+ <h3>Import from ANSI Text</h3>
+ <textarea
+ value={pasteText}
+ onChange={(e) => setPasteText(e.target.value)}
+ placeholder="Paste ANSI art here..."
+ style={{ width: '100%', minHeight: '100px', marginBottom: '0.5rem' }}
+ />
+ <button onClick={handlePaste} disabled={!pasteText.trim()}>
+ Import
+ </button>
+ </div>
+ </div>
+ );
+};
diff --git a/src/components/SaveModal.tsx b/src/components/SaveModal.tsx
new file mode 100644
index 0000000..1ed8ada
--- /dev/null
+++ b/src/components/SaveModal.tsx
@@ -0,0 +1,79 @@
+import React, { useState } from 'react';
+
+interface SaveModalProps {
+ ansiOutput: string;
+ onSave: (name: string) => void;
+ onClose: () => void;
+}
+
+export const SaveModal: React.FC<SaveModalProps> = ({ ansiOutput, onSave, onClose }) => {
+ const [name, setName] = useState('');
+ const [copied, setCopied] = useState(false);
+
+ const handleSave = () => {
+ if (name.trim()) {
+ onSave(name.trim());
+ onClose();
+ }
+ };
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(ansiOutput).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ });
+ };
+
+ return (
+ <div style={{
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ zIndex: 2000,
+ }}>
+ <div style={{
+ backgroundColor: 'var(--background)',
+ border: '3px solid var(--regular6)',
+ borderRadius: '4px',
+ padding: '1.5rem',
+ minWidth: '300px',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '1rem',
+ }}>
+ <h3>Save ANSI Art</h3>
+ <input
+ type="text"
+ placeholder="Enter a name..."
+ value={name}
+ onChange={(e) => setName(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && handleSave()}
+ autoFocus
+ style={{
+ padding: '0.5rem',
+ border: '2px solid var(--regular3)',
+ borderRadius: '4px',
+ backgroundColor: 'var(--background)',
+ color: 'var(--foreground)',
+ fontSize: '1rem',
+ }}
+ />
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
+ <button onClick={handleCopy} style={{ flex: 1 }}>
+ {copied ? 'Copied!' : 'Copy to Clipboard'}
+ </button>
+ <button onClick={handleSave} disabled={!name.trim()} style={{ flex: 1 }}>
+ Save
+ </button>
+ </div>
+ <button onClick={onClose}>Cancel</button>
+ </div>
+ </div>
+ );
+};
diff --git a/src/components/grid/Cell.tsx b/src/components/grid/Cell.tsx
index fe91da8..1a21164 100644
--- a/src/components/grid/Cell.tsx
+++ b/src/components/grid/Cell.tsx
@@ -4,13 +4,20 @@ import { getStyleForAnsiColor } from '@/utils/ansi';
interface CellProps {
cell: GridCell;
onClick?: () => void;
+ onMouseEnter?: () => void;
}
-export const Cell: React.FC<CellProps> = ({ cell, onClick }) => {
+export const Cell: React.FC<CellProps> = ({ cell, onClick, onMouseEnter }) => {
+ const handleMouseDown = (e: React.MouseEvent) => {
+ e.preventDefault(); // Prevent text selection
+ onClick?.();
+ };
+
return (
<span
className={`grid-cell ${onClick ? 'highlightable' : ''}`}
- onMouseDown={onClick}
+ onMouseDown={handleMouseDown}
+ onMouseEnter={onMouseEnter}
style={getStyleForAnsiColor(cell.color)}
>
{cell.char === ' ' ? '\u00A0' : cell.char}
diff --git a/src/components/grid/GridComponent.tsx b/src/components/grid/GridComponent.tsx
index 75d65aa..9ec528e 100644
--- a/src/components/grid/GridComponent.tsx
+++ b/src/components/grid/GridComponent.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState } from 'react';
import type { Grid, GridCell } from '@/types/grid';
import { Cell } from '@/components/grid/Cell';
@@ -6,21 +6,52 @@ import { Cell } from '@/components/grid/Cell';
interface GridProps {
grid: Grid;
onCellInteract?: (cell: GridCell) => void;
+ onDragStart?: () => void;
+ onDragEnd?: () => void;
}
export const GridComponent: React.FC<GridProps> = ({
grid,
onCellInteract,
+ onDragStart,
+ onDragEnd,
}) => {
+ const [isDragging, setIsDragging] = useState(false);
+
+ const handleMouseDown = (e: React.MouseEvent) => {
+ e.preventDefault(); // Prevent text selection
+ setIsDragging(true);
+ onDragStart?.();
+ };
+
+ const handleMouseUp = () => {
+ setIsDragging(false);
+ onDragEnd?.();
+ };
+
+ const handleMouseLeave = () => {
+ if (isDragging) {
+ setIsDragging(false);
+ onDragEnd?.();
+ }
+ };
+
return (
- <div className='grid'>
+ <div
+ className='grid'
+ onMouseDown={handleMouseDown}
+ onMouseUp={handleMouseUp}
+ onMouseLeave={handleMouseLeave}
+ style={{ userSelect: 'none' }}
+ >
{grid.map((row, i) => (
<div className='grid-row' key={i}>
{row.map((cell, j) => (
<Cell
key={j}
cell={cell}
- onClick={() => onCellInteract?.(cell)}
+ onClick={onCellInteract ? () => onCellInteract(cell) : undefined}
+ onMouseEnter={isDragging && onCellInteract ? () => onCellInteract(cell) : undefined}
/>
))}
</div>
diff --git a/src/components/toolbar/ColorSwatch.tsx b/src/components/toolbar/ColorSwatch.tsx
new file mode 100644
index 0000000..6f33483
--- /dev/null
+++ b/src/components/toolbar/ColorSwatch.tsx
@@ -0,0 +1,227 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import type { AnsiTermColor, Grid } from '@/types/grid';
+import { hexToAnsi } from '@/utils/ansi';
+import { gridFromAscii } from '@/utils/grid';
+import { HexColorPicker } from 'react-colorful';
+import { GridComponent } from '../grid/GridComponent';
+
+interface AnsiColorSwatchProps {
+ onSelect: (color: AnsiTermColor) => void;
+ defaultColors: AnsiTermColor[];
+ onClose?: () => void;
+}
+
+const STORAGE_KEY = 'ansicolor-history';
+
+export const ColorSwatch: React.FC<AnsiColorSwatchProps> = ({
+ onSelect,
+ defaultColors,
+ onClose,
+}) => {
+ const [foregroundColor, setForegroundColor] = useState<string | null>(null);
+ const [backgroundColor, setBackgroundColor] = useState<string | null>(null);
+
+ // Initialize history from localStorage or defaults
+ const [history, setHistory] = useState<AnsiTermColor[]>(() => {
+ const savedHistory = localStorage.getItem(STORAGE_KEY);
+ if (savedHistory) {
+ try {
+ return JSON.parse(savedHistory);
+ } catch (e) {
+ console.error(
+ 'Failed to parse color history from localStorage',
+ e,
+ );
+ }
+ }
+ return defaultColors;
+ });
+ useEffect(() => {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
+ }, [history]);
+
+ const [selectedHistory, setSelectedHistory] = useState<number>(0);
+
+ const handleHistorySelection = (selected: number) => {
+ const color = history.at(selected);
+ if (color === undefined) return;
+ setSelectedHistory(selected);
+
+ // Update the hex color pickers
+ if (color.foreground) {
+ const hex = `#${[color.foreground.r, color.foreground.g, color.foreground.b]
+ .map(c => Math.round(c * 51).toString(16).padStart(2, '0'))
+ .join('')}`;
+ setForegroundColor(hex);
+ } else {
+ setForegroundColor(null);
+ }
+
+ if (color.background) {
+ const hex = `#${[color.background.r, color.background.g, color.background.b]
+ .map(c => Math.round(c * 51).toString(16).padStart(2, '0'))
+ .join('')}`;
+ setBackgroundColor(hex);
+ } else {
+ setBackgroundColor(null);
+ }
+
+ onSelect(color);
+ };
+
+ const handleColorSelect = () => {
+ // Only add to history if it has at least one non-null color
+ const hasColor = ansiColor.foreground !== null || ansiColor.background !== null;
+
+ if (hasColor) {
+ // Add to history if not already present
+ const colorExists = history.some(
+ (c) =>
+ JSON.stringify(c.foreground) ===
+ JSON.stringify(ansiColor.foreground) &&
+ JSON.stringify(c.background) ===
+ JSON.stringify(ansiColor.background),
+ );
+ if (!colorExists) {
+ setHistory((prev) => [ansiColor, ...prev]);
+ }
+ }
+
+ onSelect(ansiColor);
+ };
+
+ const ansiColor = useMemo((): AnsiTermColor => {
+ const [fg, bg] = [foregroundColor, backgroundColor].map((h) =>
+ h ? hexToAnsi(h) : null,
+ );
+ return { foreground: fg, background: bg };
+ }, [foregroundColor, backgroundColor]);
+ const previewGrid = useMemo(
+ (): Grid => gridFromAscii(butterfly, ansiColor),
+ [ansiColor],
+ );
+
+ return (
+ <div>
+ <div style={{ marginBottom: '0.75rem' }}>
+ <div style={{ display: 'flex', gap: '0.35rem', flexWrap: 'wrap', maxWidth: '350px', justifyContent: 'center' }}>
+ {history.map((color, idx) => {
+ const hasForeground = color.foreground !== null;
+ const hasBackground = color.background !== null;
+
+ return (
+ <div
+ key={idx}
+ onClick={() => handleHistorySelection(idx)}
+ style={{
+ cursor: 'pointer',
+ border:
+ selectedHistory === idx
+ ? '2px solid var(--regular6)'
+ : '1px solid var(--regular3)',
+ borderRadius: '3px',
+ padding: '4px',
+ width: '28px',
+ height: '28px',
+ position: 'relative',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ fontSize: '16px',
+ lineHeight: 1,
+ overflow: 'hidden',
+ }}
+ >
+ {hasForeground && hasBackground ? (
+ <>
+ <div style={{
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '50%',
+ backgroundColor: `rgb(${color.foreground!.r * 51}, ${color.foreground!.g * 51}, ${color.foreground!.b * 51})`,
+ }} />
+ <div style={{
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ width: '100%',
+ height: '50%',
+ backgroundColor: `rgb(${color.background!.r * 51}, ${color.background!.g * 51}, ${color.background!.b * 51})`,
+ }} />
+ </>
+ ) : hasForeground ? (
+ <div style={{
+ width: '100%',
+ height: '100%',
+ backgroundColor: `rgb(${color.foreground!.r * 51}, ${color.foreground!.g * 51}, ${color.foreground!.b * 51})`,
+ }} />
+ ) : hasBackground ? (
+ <div style={{
+ width: '100%',
+ height: '100%',
+ backgroundColor: `rgb(${color.background!.r * 51}, ${color.background!.g * 51}, ${color.background!.b * 51})`,
+ }} />
+ ) : (
+ <div style={{
+ width: '100%',
+ height: '100%',
+ background: 'linear-gradient(135deg, transparent 45%, var(--regular3) 45%, var(--regular3) 55%, transparent 55%)',
+ }} />
+ )}
+ </div>
+ );
+ })}
+ </div>
+ </div>
+ <div
+ style={{
+ display: 'flex',
+ flexDirection: 'row',
+ marginBottom: '1rem',
+ }}
+ >
+ <div style={{ padding: '1rem' }}>
+ <HexColorPicker
+ color={foregroundColor ?? ''}
+ onChange={setForegroundColor}
+ />
+ <br />
+ <button onClick={() => setForegroundColor(null)}>
+ clear
+ </button>
+ </div>
+ <div style={{ padding: '1rem' }}>
+ <HexColorPicker
+ color={backgroundColor ?? ''}
+ onChange={setBackgroundColor}
+ />
+ <br />
+ <button onClick={() => setBackgroundColor(null)}>
+ clear
+ </button>
+ </div>
+ </div>
+ <button
+ style={{ marginBottom: '1rem' }}
+ onClick={handleColorSelect}
+ >
+ use
+ </button>
+ <GridComponent onCellInteract={undefined} grid={previewGrid} />
+ </div>
+ );
+};
+
+const butterfly = `| |
+| ⠀⠀⠀⠀⊹ |
+| ⢶⢻⣑⣒⢤⡀⠀⢄⠀⠀⡠⠀⢀⡤⣆⣊⡿⡷ |
+| ⠀⠹⠹⣚⣣⠻⣦⡀⠀⠀⢀⣴⠟⣸⢓⢎⠏⠀ |
+| ⠀⠀⢡⣱⣖⣢⡾⢿⣾⣷⡿⢷⣖⣒⣎⡎⠀⠀ |
+| ⠀⠀⠀⣠⠓⢬⠅⡺⢻⡟⢗⠨⡥⠚⣄⠀⠀⠀ |
+| ⠀⠀⠀⣿⡆⠘⠆⢇⢸⡇⠸⠰⠃⢰⣿⠀⠀⠀ |
+| ⠀⠀⠀⠐⡻⣮⣬⠞⠈⠁⠳⣤⣴⢿⠂⠀⠀⠀ |
+| ⠀⠀⠀⡜⠀⠁⠉⠀⠀⠀⠀⠈⠈⠀⢣⠀⠀⠀ |
+| ⊹ |
+| |`;
diff --git a/src/components/toolbar/Toolbar.tsx b/src/components/toolbar/Toolbar.tsx
new file mode 100644
index 0000000..b2217e0
--- /dev/null
+++ b/src/components/toolbar/Toolbar.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+
+export interface ToolbarProps {
+ children?: React.ReactNode;
+}
+
+export const Toolbar: React.FC<ToolbarProps> = ({ children }) => {
+ return <div className="toolbar">{children}</div>;
+};
diff --git a/src/components/toolbar/ToolbarItem.tsx b/src/components/toolbar/ToolbarItem.tsx
new file mode 100644
index 0000000..76bd615
--- /dev/null
+++ b/src/components/toolbar/ToolbarItem.tsx
@@ -0,0 +1,57 @@
+import React, { useState } from 'react';
+
+export interface ToolbarItemProps {
+ children: React.ReactNode;
+ renderContent?: (onClose: () => void) => React.ReactNode;
+ onClick?: () => void;
+ disabled?: boolean;
+}
+
+export const ToolbarItem: React.FC<ToolbarItemProps> = ({
+ children,
+ renderContent,
+ onClick,
+ disabled = false,
+}) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [isFading, setIsFading] = useState(false);
+
+ const handleClose = () => {
+ setIsFading(true);
+ setTimeout(() => {
+ setIsOpen(false);
+ setIsFading(false);
+ }, 200);
+ };
+
+ const handleToggle = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (disabled) return;
+
+ if (renderContent) {
+ if (isOpen) {
+ handleClose();
+ } else {
+ setIsOpen(true);
+ }
+ }
+
+ onClick?.();
+ };
+
+ return (
+ <div className={`toolbar-item ${disabled ? 'disabled' : ''}`} onClick={handleToggle}>
+ <div className='toolbar-item-content'>
+ {children}
+ </div>
+ {renderContent && isOpen && !disabled && (
+ <div
+ className={`toolbar-item-content-panel ${isFading ? 'fading' : ''}`}
+ onClick={(e) => e.stopPropagation()}
+ >
+ {renderContent(handleClose)}
+ </div>
+ )}
+ </div>
+ );
+};
diff --git a/src/pages/Paint.tsx b/src/pages/Paint.tsx
index 136a347..995bcc3 100644
--- a/src/pages/Paint.tsx
+++ b/src/pages/Paint.tsx
@@ -1,16 +1,34 @@
import { GridComponent } from '@/components/grid/GridComponent';
+import { ColorSwatch } from '@/components/toolbar/ColorSwatch';
+import { Toolbar } from '@/components/toolbar/Toolbar';
+import { ToolbarItem } from '@/components/toolbar/ToolbarItem';
+import { SaveModal } from '@/components/SaveModal';
import type { AnsiTermColor, Grid, GridCell } from '@/types/grid';
-import {
- type IZipper,
- ListZipper,
-} from '@emprespresso/pengueno';
-import { useCallback, useState } from 'react';
+import { gridToAnsi } from '@/utils/grid';
+import { saveArt } from '@/utils/storage';
+import { type IZipper, ListZipper } from '@emprespresso/pengueno';
+import { useCallback, useEffect, useState } from 'react';
+import { flushSync } from 'react-dom';
export interface ChooseArtProps {
grid: Grid;
+ onGoHome?: () => void;
}
-export const Paint: React.FC<ChooseArtProps> = ({ grid }) => {
+// Gruvbox theme colors converted to ANSI RGB values
+const defaultColors: AnsiTermColor[] = [
+ { foreground: { r: 5, g: 5, b: 5 }, background: null }, // bright7 #ebdbb2
+ { foreground: { r: 5, g: 1, b: 1 }, background: null }, // regular1 #cc241d
+ { foreground: { r: 4, g: 4, b: 1 }, background: null }, // regular2 #98971a
+ { foreground: { r: 5, g: 4, b: 1 }, background: null }, // regular3 #d79921
+ { foreground: { r: 2, g: 3, b: 4 }, background: null }, // regular4 #458588
+ { foreground: { r: 4, g: 2, b: 3 }, background: null }, // regular5 #b16286
+ { foreground: { r: 3, g: 4, b: 3 }, background: null }, // regular6 #689d6a
+ { foreground: { r: 4, g: 4, b: 3 }, background: null }, // regular7 #a89984
+];
+
+export const Paint: React.FC<ChooseArtProps> = ({ grid, onGoHome }) => {
+ const [showSaveModal, setShowSaveModal] = useState(false);
const [selectedColor, setSelectedColor] = useState<AnsiTermColor>({
foreground: { r: 5, g: 5, b: 5 },
background: null,
@@ -18,49 +36,113 @@ export const Paint: React.FC<ChooseArtProps> = ({ grid }) => {
const [history, setHistory] = useState<IZipper<Grid>>(
ListZipper.from([grid]),
);
+ const [isDragging, setIsDragging] = useState(false);
+ const [workingGrid, setWorkingGrid] = useState<Grid>(grid);
+
+ const handleDragStart = useCallback(() => {
+ setIsDragging(true);
+ // Don't reset workingGrid here - let it accumulate changes
+ }, []);
+
+ const handleDragEnd = useCallback(() => {
+ if (isDragging) {
+ // Commit the working grid to history as a single atomic operation
+ setHistory((currentHistory) => {
+ return currentHistory.prepend(workingGrid).previous().get();
+ });
+ setIsDragging(false);
+ }
+ }, [isDragging, workingGrid]);
const cellInteractionCallback = useCallback(
(cell: GridCell) => {
- setHistory((currentHistory) => {
- const currentGrid = currentHistory.read().get();
- const newGrid = currentGrid.map((row) => [...row]); // Deep copy for current state
- newGrid[cell.y][cell.x] = { ...cell, color: selectedColor };
- return currentHistory.prepend(newGrid).previous().get();
+ flushSync(() => {
+ setWorkingGrid((currentGrid) => {
+ const newGrid = currentGrid.map((row) => [...row]);
+ newGrid[cell.y][cell.x] = { ...cell, color: selectedColor };
+ return newGrid;
+ });
});
},
[selectedColor],
);
+ const handleSave = (name: string) => {
+ const currentGrid = history.read().get();
+ saveArt(name, currentGrid);
+ };
+
+ const currentGrid = workingGrid;
+ const ansiOutput = gridToAnsi(currentGrid);
+
return (
- <div>
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
+ {showSaveModal && (
+ <SaveModal
+ ansiOutput={ansiOutput}
+ onSave={handleSave}
+ onClose={() => setShowSaveModal(false)}
+ />
+ )}
+ <Toolbar>
+ <ToolbarItem
+ renderContent={(onClose) => (
+ <ColorSwatch
+ onSelect={setSelectedColor}
+ onClose={onClose}
+ defaultColors={defaultColors}
+ ></ColorSwatch>
+ )}
+ >
+ 🎨
+ </ToolbarItem>
+ <ToolbarItem
+ disabled={
+ !history
+ .previous()
+ .flatMap((it) => it.read())
+ .present()
+ }
+ onClick={() => {
+ setHistory((history) => {
+ const newHistory = history.previous().get();
+ setWorkingGrid(newHistory.read().get());
+ return newHistory;
+ });
+ }}
+ >
+ ↷
+ </ToolbarItem>
+ <ToolbarItem
+ disabled={
+ !history
+ .next()
+ .flatMap((it) => it.read())
+ .present()
+ }
+ onClick={() => {
+ setHistory((history) => {
+ const newHistory = history.next().get();
+ setWorkingGrid(newHistory.read().get());
+ return newHistory;
+ });
+ }}
+ >
+ ↶
+ </ToolbarItem>
+ <ToolbarItem onClick={() => setShowSaveModal(true)}>
+ 💾
+ </ToolbarItem>
+ <ToolbarItem onClick={onGoHome}>
+ 🏠
+ </ToolbarItem>
+ </Toolbar>
<GridComponent
- grid={history.read().get()}
+ grid={currentGrid}
onCellInteract={cellInteractionCallback}
+ onDragStart={handleDragStart}
+ onDragEnd={handleDragEnd}
/>
- <button
- disabled={
- !history
- .next()
- .flatMap((it) => it.read())
- .present()
- }
- onClick={() => setHistory((history) => history.next().get())}
- >
- Undo
- </button>
- <button
- disabled={
- !history
- .previous()
- .flatMap((it) => it.read())
- .present()
- }
- onClick={() =>
- setHistory((history) => history.previous().get())
- }
- >
- Redo
- </button>
</div>
);
};
diff --git a/src/styles/styles.css b/src/styles/styles.css
index 96b39ec..7a9e9ec 100644
--- a/src/styles/styles.css
+++ b/src/styles/styles.css
@@ -97,6 +97,14 @@ textarea {
border-radius: 4px;
background: none;
resize: vertical;
+ font-family: var(--font), var(--font-fallback);
+ font-size: 1.5rem;
+ line-height: 1;
+ white-space: pre;
+ font-feature-settings:
+ 'tnum',
+ 'kern' 0;
+ font-variant-numeric: tabular-nums;
}
*:focus {
border-color: var(--regular6);
@@ -141,7 +149,7 @@ button[disabled] {
.grid {
border: 3px solid var(--regular4);
padding: 1rem;
-
+ background-color: var(--background);
font-size: 1.5rem;
cursor: pointer;
display: flex;
@@ -174,7 +182,102 @@ button[disabled] {
white-space: nowrap;
}
-.grid-cell:hover {
+.highlightable:hover {
background-color: var(--background-body);
}
/* </grid> */
+
+/* <toolbar> */
+.toolbar {
+ display: flex;
+ gap: 0.5rem;
+ padding: 0.75rem;
+ background-color: var(--background);
+ border: 3px solid var(--regular4);
+ border-radius: 4px;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto;
+ width: fit-content;
+}
+
+.toolbar-item {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.5rem;
+ border: 2px solid var(--regular3);
+ border-radius: 4px;
+ background: none;
+ cursor: pointer;
+ transition: border-color 0.2s ease-in-out;
+ min-width: 2.5rem;
+ min-height: 2.5rem;
+}
+
+.toolbar-item:hover:not(.disabled) {
+ border-color: var(--regular6);
+}
+
+.toolbar-item.disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ border-color: var(--bright0);
+}
+
+.toolbar-item-content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.toolbar-item-content-panel {
+ position: absolute;
+ top: calc(100% + 0.5rem);
+ left: 50%;
+ transform: translateX(-50%);
+ padding: 0.5rem 0.75rem;
+ background-color: var(--background);
+ border: 2px solid var(--regular6);
+ border-radius: 4px;
+ z-index: 1000;
+ animation: fadeIn 0.2s ease-in-out;
+}
+
+.toolbar-item-content-panel.fading {
+ animation: fadeOut 0.2s ease-in-out;
+}
+
+.toolbar-item-content-panel::after {
+ content: '';
+ position: absolute;
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ border: 0.375rem solid transparent;
+ border-bottom-color: var(--regular6);
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateX(-50%) translateY(-0.25rem);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+}
+
+@keyframes fadeOut {
+ from {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateX(-50%) translateY(-0.25rem);
+ }
+}
+/* </toolbar> */
diff --git a/src/utils/ansi.ts b/src/utils/ansi.ts
index 551a365..e57c075 100644
--- a/src/utils/ansi.ts
+++ b/src/utils/ansi.ts
@@ -66,3 +66,15 @@ export const getStyleForAnsiColor = (color: AnsiTermColor): CSSProperties => {
color: foreground ? ansiRgbToHex(foreground) : undefined,
};
};
+
+export const parseAnsiColorCode = (code: number): AnsiRgb | null => {
+ // Only handle 216 color cube (16-231)
+ if (code < 16 || code > 231) return null;
+
+ const adjusted = code - 16;
+ const r = Math.floor(adjusted / 36) as AnsiColorVal;
+ const g = Math.floor((adjusted % 36) / 6) as AnsiColorVal;
+ const b = (adjusted % 6) as AnsiColorVal;
+
+ return { r, g, b };
+};
diff --git a/src/utils/grid.ts b/src/utils/grid.ts
index 71370da..1a2aa29 100644
--- a/src/utils/grid.ts
+++ b/src/utils/grid.ts
@@ -1,7 +1,88 @@
import type { AnsiTermColor, Grid } from '@/types/grid';
-import { getAnsiColorEscape, getAnsiEscapeCodeFromDiff } from './ansi';
+import { getAnsiColorEscape, getAnsiEscapeCodeFromDiff, parseAnsiColorCode } from './ansi';
const defaultColor: AnsiTermColor = { foreground: null, background: null };
+
+export const gridFromAnsi = (ansiText: string): Grid => {
+ const lines = ansiText.split('\n');
+ const grid: Grid = [];
+
+ for (let y = 0; y < lines.length; y++) {
+ const line = lines[y];
+ const row: Grid[0] = [];
+ let x = 0;
+ let currentColor: AnsiTermColor = { ...defaultColor };
+
+ // Regex to match ANSI escape codes
+ const ansiRegex = /\x1b\[([0-9;]+)m/g;
+ let lastIndex = 0;
+ let match;
+
+ while ((match = ansiRegex.exec(line)) !== null) {
+ // Add characters before this escape code
+ const text = line.slice(lastIndex, match.index);
+ for (const char of text) {
+ row.push({ char, color: { ...currentColor }, x: x++, y });
+ }
+
+ // Parse escape code
+ const codes = match[1].split(';').map(Number);
+ let i = 0;
+ while (i < codes.length) {
+ const code = codes[i];
+
+ if (code === 38 && codes[i + 1] === 5) {
+ // Foreground color: ESC[38;5;{code}m
+ const colorCode = codes[i + 2];
+ currentColor.foreground = parseAnsiColorCode(colorCode);
+ i += 3;
+ } else if (code === 48 && codes[i + 1] === 5) {
+ // Background color: ESC[48;5;{code}m
+ const colorCode = codes[i + 2];
+ currentColor.background = parseAnsiColorCode(colorCode);
+ i += 3;
+ } else if (code === 39) {
+ // Reset foreground
+ currentColor.foreground = null;
+ i++;
+ } else if (code === 49) {
+ // Reset background
+ currentColor.background = null;
+ i++;
+ } else {
+ i++;
+ }
+ }
+
+ lastIndex = ansiRegex.lastIndex;
+ }
+
+ // Add remaining characters
+ const remainingText = line.slice(lastIndex);
+ for (const char of remainingText) {
+ row.push({ char, color: { ...currentColor }, x: x++, y });
+ }
+
+ grid.push(row);
+ }
+
+ // Normalize grid width
+ const maxWidth = Math.max(...grid.map(row => row.length));
+ for (let y = 0; y < grid.length; y++) {
+ while (grid[y].length < maxWidth) {
+ const x = grid[y].length;
+ grid[y].push({
+ char: ' ',
+ color: { ...defaultColor },
+ x,
+ y,
+ });
+ }
+ }
+
+ return grid;
+};
+
export const gridFromAscii = (
ascii: string,
color: AnsiTermColor = defaultColor,
diff --git a/src/utils/storage.ts b/src/utils/storage.ts
new file mode 100644
index 0000000..ee862ae
--- /dev/null
+++ b/src/utils/storage.ts
@@ -0,0 +1,42 @@
+import type { Grid } from '@/types/grid';
+
+export interface SavedArt {
+ id: string;
+ name: string;
+ grid: Grid;
+ timestamp: number;
+}
+
+const STORAGE_KEY = 'ansicolor-saved-art';
+const MAX_SAVES = 8;
+
+export const saveArt = (name: string, grid: Grid): void => {
+ const saves = getSavedArt();
+ const newSave: SavedArt = {
+ id: Date.now().toString(),
+ name,
+ grid,
+ timestamp: Date.now(),
+ };
+
+ const updatedSaves = [newSave, ...saves].slice(0, MAX_SAVES);
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedSaves));
+};
+
+export const getSavedArt = (): SavedArt[] => {
+ const saved = localStorage.getItem(STORAGE_KEY);
+ if (!saved) return [];
+
+ try {
+ return JSON.parse(saved);
+ } catch (e) {
+ console.error('Failed to parse saved art', e);
+ return [];
+ }
+};
+
+export const deleteSavedArt = (id: string): void => {
+ const saves = getSavedArt();
+ const filtered = saves.filter(save => save.id !== id);
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered));
+};