diff options
| author | Elizabeth Hunt <me@liz.coffee> | 2025-10-05 16:42:02 -0700 |
|---|---|---|
| committer | Elizabeth Hunt <me@liz.coffee> | 2025-10-05 23:11:41 -0700 |
| commit | de43eb05d2e43ab31effce3dcca62ad91a556b26 (patch) | |
| tree | 47a62b61bfc97dda639dea70627ecf3005ba7b02 | |
| parent | 35add63ec4dce39710095f17abd86777de9e5b49 (diff) | |
| download | ansicolor-main.tar.gz ansicolor-main.zip | |
| -rw-r--r-- | .ci/bundle.js | 12 | ||||
| -rwxr-xr-x | .ci/ci.js | 3 | ||||
| -rw-r--r-- | .ci/ci.json | 4 | ||||
| -rw-r--r-- | .ci/ci.ts | 74 | ||||
| -rw-r--r-- | .ci/package-lock.json | 514 | ||||
| -rw-r--r-- | .ci/package.json | 17 | ||||
| -rw-r--r-- | .ci/tsconfig.json | 16 | ||||
| -rw-r--r-- | .dockerignore | 8 | ||||
| -rw-r--r-- | Dockerfile | 31 | ||||
| -rw-r--r-- | index.html | 1 | ||||
| -rw-r--r-- | package-lock.json | 11 | ||||
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | src/App.tsx | 62 | ||||
| -rw-r--r-- | src/components/LoadScreen.tsx | 164 | ||||
| -rw-r--r-- | src/components/SaveModal.tsx | 79 | ||||
| -rw-r--r-- | src/components/grid/Cell.tsx | 11 | ||||
| -rw-r--r-- | src/components/grid/GridComponent.tsx | 37 | ||||
| -rw-r--r-- | src/components/toolbar/ColorSwatch.tsx | 227 | ||||
| -rw-r--r-- | src/components/toolbar/Toolbar.tsx | 9 | ||||
| -rw-r--r-- | src/components/toolbar/ToolbarItem.tsx | 57 | ||||
| -rw-r--r-- | src/pages/Paint.tsx | 156 | ||||
| -rw-r--r-- | src/styles/styles.css | 107 | ||||
| -rw-r--r-- | src/utils/ansi.ts | 12 | ||||
| -rw-r--r-- | src/utils/grid.ts | 83 | ||||
| -rw-r--r-- | src/utils/storage.ts | 42 |
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;"] @@ -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)); +}; |
