diff --git a/bun.lock b/bun.lock index 886b23e..a0035a0 100644 --- a/bun.lock +++ b/bun.lock @@ -4,15 +4,15 @@ "": { "name": "ccstatusline", "dependencies": { - "@types/react": "^19.1.9", "chalk": "^5.5.0", "ink": "^6.1.0", "ink-select-input": "^6.2.0", - "ink-text-input": "^6.0.0", "react": "^19.1.1", }, "devDependencies": { "@types/bun": "latest", + "@types/react": "^19.1.9", + "patch-package": "^8.0.0", }, "peerDependencies": { "typescript": "^5", @@ -28,18 +28,36 @@ "@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="], + "@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="], + "ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "chalk": ["chalk@5.5.0", "", {}, "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg=="], + "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], @@ -48,46 +66,136 @@ "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-toolkit": ["es-toolkit@1.39.8", "", {}, "sha512-A8QO9TfF+rltS8BXpdu8OS+rpGgEdnRhqIVxO/ZmNvnXBYgOdSsxukT55ELyP94gZIntWJ+Li9QRrT2u1Kitpg=="], "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-yarn-workspace-root": ["find-yarn-workspace-root@2.0.0", "", { "dependencies": { "micromatch": "^4.0.2" } }, "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ=="], + + "fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "ink": ["ink@6.1.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.22.0", "indent-string": "^5.0.0", "is-in-ci": "^1.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-YQ+lbMD79y3FBAJXXZnuRajLEgaMFp102361eY5NrBIEVCi9oFo7gNZU4z2LBWlcjZFiTt7jetlkIbKCCH4KJA=="], "ink-select-input": ["ink-select-input@6.2.0", "", { "dependencies": { "figures": "^6.1.0", "to-rotated": "^1.0.0" }, "peerDependencies": { "ink": ">=5.0.0", "react": ">=18.0.0" } }, "sha512-304fZXxkpYxJ9si5lxRCaX01GNlmPBgOZumXXRnPYbHW/iI31cgQynqk2tRypGLOF1cMIwPUzL2LSm6q4I5rQQ=="], - "ink-text-input": ["ink-text-input@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "type-fest": "^4.18.2" }, "peerDependencies": { "ink": ">=5", "react": ">=18" } }, "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw=="], + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], "is-in-ci": ["is-in-ci@1.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "json-stable-stringify": ["json-stable-stringify@1.3.0", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" } }, "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg=="], + + "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], + + "jsonify": ["jsonify@0.0.1", "", {}, "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="], + + "klaw-sync": ["klaw-sync@6.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11" } }, "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + "open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], + "patch-package": ["patch-package@8.0.0", "", { "dependencies": { "@yarnpkg/lockfile": "^1.1.0", "chalk": "^4.1.2", "ci-info": "^3.7.0", "cross-spawn": "^7.0.3", "find-yarn-workspace-root": "^2.0.0", "fs-extra": "^9.0.0", "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", "minimist": "^1.2.6", "open": "^7.4.2", "rimraf": "^2.6.3", "semver": "^7.5.3", "slash": "^2.0.0", "tmp": "^0.0.33", "yaml": "^2.2.2" }, "bin": { "patch-package": "index.js" } }, "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="], "react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="], @@ -96,12 +204,24 @@ "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], + "rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "slash": ["slash@2.0.0", "", {}, "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="], + "slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], @@ -110,6 +230,12 @@ "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "to-rotated": ["to-rotated@1.0.0", "", {}, "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -118,20 +244,32 @@ "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], "cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + "patch-package/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "react-reconciler/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="], + + "patch-package/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], } } diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 5a2a3d7..0000000 --- a/package-lock.json +++ /dev/null @@ -1,619 +0,0 @@ -{ - "name": "ccstatusline", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "ccstatusline", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@types/react": "^19.1.9", - "chalk": "^5.5.0", - "ink": "^6.1.0", - "ink-select-input": "^6.2.0", - "ink-text-input": "^6.0.0", - "react": "^19.1.1" - }, - "bin": { - "ccstatusline": "dist/ccstatusline.js" - }, - "devDependencies": { - "@types/bun": "latest" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "typescript": "^5" - } - }, - "node_modules/@alcalzone/ansi-tokenize": { - "version": "0.1.3", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=14.13.1" - } - }, - "node_modules/@types/bun": { - "version": "1.2.19", - "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.19.tgz", - "integrity": "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bun-types": "1.2.19" - } - }, - "node_modules/@types/node": { - "version": "24.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.10.0" - } - }, - "node_modules/@types/react": { - "version": "19.1.9", - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/ansi-escapes": { - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.1.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.1", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/auto-bind": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bun-types": { - "version": "1.2.19", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - }, - "peerDependencies": { - "@types/react": "^19" - } - }, - "node_modules/chalk": { - "version": "5.5.0", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cli-boxes": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-cursor": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "restore-cursor": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/slice-ansi": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/code-excerpt": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "convert-to-spaces": "^2.0.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/convert-to-spaces": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "10.4.0", - "license": "MIT" - }, - "node_modules/environment": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/es-toolkit": { - "version": "1.39.8", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/figures": { - "version": "6.1.0", - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.3.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/indent-string": { - "version": "5.0.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ink": { - "version": "6.1.0", - "license": "MIT", - "dependencies": { - "@alcalzone/ansi-tokenize": "^0.1.3", - "ansi-escapes": "^7.0.0", - "ansi-styles": "^6.2.1", - "auto-bind": "^5.0.1", - "chalk": "^5.3.0", - "cli-boxes": "^3.0.0", - "cli-cursor": "^4.0.0", - "cli-truncate": "^4.0.0", - "code-excerpt": "^4.0.0", - "es-toolkit": "^1.22.0", - "indent-string": "^5.0.0", - "is-in-ci": "^1.0.0", - "patch-console": "^2.0.0", - "react-reconciler": "^0.32.0", - "scheduler": "^0.23.0", - "signal-exit": "^3.0.7", - "slice-ansi": "^7.1.0", - "stack-utils": "^2.0.6", - "string-width": "^7.2.0", - "type-fest": "^4.27.0", - "widest-line": "^5.0.0", - "wrap-ansi": "^9.0.0", - "ws": "^8.18.0", - "yoga-layout": "~3.2.1" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@types/react": ">=19.0.0", - "react": ">=19.0.0", - "react-devtools-core": "^4.19.1" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react-devtools-core": { - "optional": true - } - } - }, - "node_modules/ink-select-input": { - "version": "6.2.0", - "license": "MIT", - "dependencies": { - "figures": "^6.1.0", - "to-rotated": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "ink": ">=5.0.0", - "react": ">=18.0.0" - } - }, - "node_modules/ink-text-input": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "type-fest": "^4.18.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "ink": ">=5", - "react": ">=18" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-in-ci": { - "version": "1.0.0", - "license": "MIT", - "bin": { - "is-in-ci": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/patch-console": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/react": { - "version": "19.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-reconciler": { - "version": "0.32.0", - "license": "MIT", - "dependencies": { - "scheduler": "^0.26.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "react": "^19.1.0" - } - }, - "node_modules/react-reconciler/node_modules/scheduler": { - "version": "0.26.0", - "license": "MIT" - }, - "node_modules/restore-cursor": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "license": "ISC" - }, - "node_modules/slice-ansi": { - "version": "7.1.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/to-rotated": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-fest": { - "version": "4.41.0", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.9.2", - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.10.0", - "dev": true, - "license": "MIT" - }, - "node_modules/widest-line": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wrap-ansi": { - "version": "9.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/ws": { - "version": "8.18.3", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yoga-layout": { - "version": "3.2.1", - "license": "MIT" - } - } -} diff --git a/package.json b/package.json index 38af959..a565af0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ccstatusline", - "version": "1.1.4", + "version": "1.1.5", "description": "A customizable status line formatter for Claude Code CLI", "module": "src/ccstatusline.ts", "type": "module", @@ -11,22 +11,23 @@ "dist/" ], "scripts": { - "start": "bun run src/ccstatusline.ts", - "build": "rm -rf dist/* && bun build src/ccstatusline.ts --target=node --outfile=dist/ccstatusline.js --packages=external --target-version=14", + "start": "bun run patch && bun run src/ccstatusline.ts", + "patch": "patch-package", + "build": "bun run patch && rm -rf dist/* && bun build src/ccstatusline.ts --target=node --outfile=dist/ccstatusline.js --target-version=14", "prepublishOnly": "bun run build" }, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "@types/react": "^19.1.9", + "patch-package": "^8.0.0" }, "peerDependencies": { "typescript": "^5" }, "dependencies": { - "@types/react": "^19.1.9", "chalk": "^5.5.0", "ink": "^6.1.0", "ink-select-input": "^6.2.0", - "ink-text-input": "^6.0.0", "react": "^19.1.1" }, "keywords": [ diff --git a/patches/ink+6.1.0.patch b/patches/ink+6.1.0.patch new file mode 100644 index 0000000..0b68204 --- /dev/null +++ b/patches/ink+6.1.0.patch @@ -0,0 +1,17 @@ +diff --git a/node_modules/ink/build/parse-keypress.js b/node_modules/ink/build/parse-keypress.js +index 1e13e81..e100611 100644 +--- a/node_modules/ink/build/parse-keypress.js ++++ b/node_modules/ink/build/parse-keypress.js +@@ -161,9 +161,9 @@ const parseKeypress = (s = '') => { + key.meta = s.charAt(0) === '\x1b'; + } + else if (s === '\x7f' || s === '\x1b\x7f') { +- // TODO(vadimdemedes): `enquirer` detects delete key as backspace, but I had to split them up to avoid breaking changes in Ink. Merge them back together in the next major version. +- // delete +- key.name = 'delete'; ++ // On macOS, \x7f is what the backspace key sends, not delete ++ // The actual forward delete sends escape sequences like \x1b[3~ ++ key.name = 'backspace'; + key.meta = s.charAt(0) === '\x1b'; + } + else if (s === '\x1b' || s === '\x1b\x1b') { diff --git a/src/ccstatusline.ts b/src/ccstatusline.ts index 8f17bac..f825ca2 100644 --- a/src/ccstatusline.ts +++ b/src/ccstatusline.ts @@ -1,73 +1,13 @@ #!/usr/bin/env node -import chalk from 'chalk'; -import { execSync } from 'child_process'; import { runTUI } from './tui.tsx'; -import { loadSettings, type StatusItem } from './config'; -import * as fs from 'fs'; -import { promisify } from 'util'; - -// Ensure fs.promises compatibility for older Node versions -const readFile = fs.promises?.readFile || promisify(fs.readFile); - -// Force chalk to use colors even when piped -chalk.level = 3; - -// Helper function to apply foreground, background colors and bold -function applyColors(text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string { - let result = text; - - // Standard color application - // Ignore 'dim' color - it causes issues with terminal rendering - if (foregroundColor && foregroundColor !== 'dim') { - const fgFunc = (chalk as any)[foregroundColor]; - if (fgFunc) { - result = fgFunc(result); - } - } - - if (backgroundColor && backgroundColor !== 'none') { - const bgFunc = (chalk as any)[backgroundColor]; - if (bgFunc) { - result = bgFunc(result); - } - } - - if (bold) { - result = chalk.bold(result); - } - - return result; -} - -interface StatusJSON { - session_id: string; - transcript_path: string; - cwd: string; - model: { - id: string; - display_name: string; - }; - workspace: { - current_dir: string; - project_dir: string; - }; - version?: string; -} - -interface TokenUsage { - input_tokens: number; - output_tokens: number; - cache_creation_input_tokens?: number; - cache_read_input_tokens?: number; -} - -interface TranscriptLine { - message?: { - usage?: TokenUsage; - }; - isSidechain?: boolean; - timestamp?: string; -} +import { loadSettings } from './utils/config'; +import { + type StatusJSON, + renderStatusLine, + getTokenMetrics, + getSessionDuration, + type RenderContext +} from './utils/renderer'; async function readStdin(): Promise { // Check if stdin is a TTY (terminal) - if it is, there's no piped data @@ -98,687 +38,11 @@ async function readStdin(): Promise { } } -// Helper to get default color for an item type -function getItemDefaultColor(type: string): string { - switch (type) { - case 'model': return 'cyan'; - case 'git-branch': return 'magenta'; - case 'git-changes': return 'yellow'; - case 'session-clock': return 'yellow'; - case 'version': return 'green'; - case 'tokens-input': return 'blue'; - case 'tokens-output': return 'white'; - case 'tokens-cached': return 'cyan'; - case 'tokens-total': return 'cyan'; - case 'context-length': return 'gray'; - case 'context-percentage': return 'blue'; - case 'context-percentage-usable': return 'green'; - case 'terminal-width': return 'gray'; - case 'custom-text': return 'white'; - case 'custom-command': return 'white'; - case 'separator': return 'gray'; - default: return 'white'; - } -} - -function getTerminalWidth(): number | null { - try { - // First try to get the tty of the parent process - const tty = execSync('ps -o tty= -p $(ps -o ppid= -p $$)', { - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'ignore'], - shell: '/bin/sh' - }).trim(); - - // Check if we got a valid tty (not ?? which means no tty) - if (tty && tty !== '??' && tty !== '?') { - // Now get the terminal size - const width = execSync( - `stty size < /dev/${tty} | awk '{print $2}'`, - { - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'ignore'], - shell: '/bin/sh' - } - ).trim(); - - const parsed = parseInt(width, 10); - if (!isNaN(parsed) && parsed > 0) { - return parsed; - } - } - } catch { - // Command failed, width detection not available - } - - // Fallback: try tput cols which might work in some environments - try { - const width = execSync('tput cols 2>/dev/null', { - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'ignore'] - }).trim(); - - const parsed = parseInt(width, 10); - if (!isNaN(parsed) && parsed > 0) { - return parsed; - } - } catch { - // tput also failed - } - - return null; -} - -function getGitBranch(): string | null { - try { - const branch = execSync('git branch --show-current 2>/dev/null', { - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'ignore'] - }).trim(); - return branch || null; - } catch { - return null; - } -} - -function getGitChanges(): { insertions: number; deletions: number } | null { - try { - let totalInsertions = 0; - let totalDeletions = 0; - - // Get unstaged changes - const unstagedStat = execSync('git diff --shortstat 2>/dev/null', { - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'ignore'] - }).trim(); - - // Get staged changes - const stagedStat = execSync('git diff --cached --shortstat 2>/dev/null', { - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'ignore'] - }).trim(); - - // Parse unstaged changes - if (unstagedStat) { - const insertMatch = unstagedStat.match(/(\d+) insertion/); - const deleteMatch = unstagedStat.match(/(\d+) deletion/); - totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0; - totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0; - } - - // Parse staged changes - if (stagedStat) { - const insertMatch = stagedStat.match(/(\d+) insertion/); - const deleteMatch = stagedStat.match(/(\d+) deletion/); - totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0; - totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0; - } - - // Always return the changes, even if they're zero - return { insertions: totalInsertions, deletions: totalDeletions }; - } catch { - return null; - } -} - -async function getSessionDuration(transcriptPath: string): Promise { - try { - if (!fs.existsSync(transcriptPath)) { - return null; - } - - const content = await readFile(transcriptPath, 'utf-8'); - const lines = content.trim().split('\n').filter(line => line.trim()); - - if (lines.length === 0) { - return null; - } - - let firstTimestamp: Date | null = null; - let lastTimestamp: Date | null = null; - - // Find first valid timestamp - for (const line of lines) { - try { - const data = JSON.parse(line); - if (data.timestamp) { - firstTimestamp = new Date(data.timestamp); - break; - } - } catch { - // Skip invalid lines - } - } - - // Find last valid timestamp (iterate backwards) - for (let i = lines.length - 1; i >= 0; i--) { - try { - const data = JSON.parse(lines[i]!); - if (data.timestamp) { - lastTimestamp = new Date(data.timestamp); - break; - } - } catch { - // Skip invalid lines - } - } - - if (!firstTimestamp || !lastTimestamp) { - return null; - } - - // Calculate duration in milliseconds - const durationMs = lastTimestamp.getTime() - firstTimestamp.getTime(); - - // Convert to minutes - const totalMinutes = Math.floor(durationMs / (1000 * 60)); - - if (totalMinutes < 1) { - return '<1m'; - } - - const hours = Math.floor(totalMinutes / 60); - const minutes = totalMinutes % 60; - - if (hours === 0) { - return `${minutes}m`; - } else if (minutes === 0) { - return `${hours}hr`; - } else { - return `${hours}hr ${minutes}m`; - } - } catch { - return null; - } -} - -async function getTokenMetrics(transcriptPath: string): Promise<{ - inputTokens: number; - outputTokens: number; - cachedTokens: number; - totalTokens: number; - contextLength: number; -}> { - try { - // Use Node.js-compatible file reading - if (!fs.existsSync(transcriptPath)) { - return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 }; - } - - const content = await readFile(transcriptPath, 'utf-8'); - const lines = content.trim().split('\n'); - - let inputTokens = 0; - let outputTokens = 0; - let cachedTokens = 0; - let contextLength = 0; - - // Parse each line and sum up token usage for totals - let mostRecentMainChainEntry: TranscriptLine | null = null; - let mostRecentTimestamp: Date | null = null; - - for (const line of lines) { - try { - const data: TranscriptLine = JSON.parse(line); - if (data.message?.usage) { - inputTokens += data.message.usage.input_tokens || 0; - outputTokens += data.message.usage.output_tokens || 0; - cachedTokens += data.message.usage.cache_read_input_tokens || 0; - cachedTokens += data.message.usage.cache_creation_input_tokens || 0; - - // Track the most recent entry with isSidechain: false (or undefined, which defaults to main chain) - if (data.isSidechain !== true && data.timestamp) { - const entryTime = new Date(data.timestamp); - if (!mostRecentTimestamp || entryTime > mostRecentTimestamp) { - mostRecentTimestamp = entryTime; - mostRecentMainChainEntry = data; - } - } - } - } catch { - // Skip invalid JSON lines - } - } - - // Calculate context length from the most recent main chain message - if (mostRecentMainChainEntry?.message?.usage) { - const usage = mostRecentMainChainEntry.message.usage; - contextLength = (usage.input_tokens || 0) + - (usage.cache_read_input_tokens || 0) + - (usage.cache_creation_input_tokens || 0); - } - - const totalTokens = inputTokens + outputTokens + cachedTokens; - - return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength }; - } catch { - return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 }; - } -} - -function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON, tokenMetrics: any, sessionDuration: string | null): string { - // Helper to apply colors with optional background and bold override - const applyColorsWithOverride = (text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string => { - // Override foreground color takes precedence over EVERYTHING, including passed foreground color - let fgColor = foregroundColor; - if (settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none') { - fgColor = settings.overrideForegroundColor; - } - - // Override background color takes precedence over EVERYTHING, including passed background color - let bgColor = backgroundColor; - if (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') { - bgColor = settings.overrideBackgroundColor; - } - - const shouldBold = settings.globalBold || bold; - return applyColors(text, fgColor, bgColor, shouldBold); - }; - const detectedWidth = getTerminalWidth(); - // Calculate terminal width based on flex mode settings - let terminalWidth: number | null = null; - if (detectedWidth) { - const flexMode = settings.flexMode || 'full-minus-40'; - - if (flexMode === 'full') { - // Use full width minus 4 for terminal padding - terminalWidth = detectedWidth - 4; - } else if (flexMode === 'full-minus-40') { - // Always subtract 40 for auto-compact message - terminalWidth = detectedWidth - 40; - } else if (flexMode === 'full-until-compact') { - // Check context percentage to decide - const threshold = settings.compactThreshold || 60; - const contextPercentage = tokenMetrics ? - Math.min(100, (tokenMetrics.contextLength / 200000) * 100) : 0; - - if (contextPercentage >= threshold) { - // Context is high, leave space for auto-compact - terminalWidth = detectedWidth - 40; - } else { - // Context is low, use full width minus 4 for padding - terminalWidth = detectedWidth - 4; - } - } - } - const elements: { content: string, type: string, item?: StatusItem }[] = []; - let hasFlexSeparator = false; - - // Helper function to format token counts - const formatTokens = (count: number): string => { - if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`; - if (count >= 1000) return `${(count / 1000).toFixed(1)}k`; - return count.toString(); - }; - - // Build elements based on configured items - for (const item of items) { - switch (item.type) { - case 'model': - if (data.model) { - const text = item.rawValue ? data.model.display_name : `Model: ${data.model.display_name}`; - elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'model', item }); - } - break; - - case 'git-branch': - const branch = getGitBranch(); - if (branch) { - const text = item.rawValue ? branch : `⎇ ${branch}`; - elements.push({ content: applyColorsWithOverride(text, item.color || 'magenta', item.backgroundColor, item.bold), type: 'git-branch', item }); - } - break; - - case 'git-changes': - const changes = getGitChanges(); - if (changes !== null) { - // Compact format: (+42,-10) - const changeStr = `(+${changes.insertions},-${changes.deletions})`; - elements.push({ content: applyColorsWithOverride(changeStr, item.color || 'yellow', item.backgroundColor, item.bold), type: 'git-changes', item }); - } - break; - - case 'tokens-input': - if (tokenMetrics) { - const text = item.rawValue ? formatTokens(tokenMetrics.inputTokens) : `In: ${formatTokens(tokenMetrics.inputTokens)}`; - elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'tokens-input', item }); - } - break; - - case 'tokens-output': - if (tokenMetrics) { - const text = item.rawValue ? formatTokens(tokenMetrics.outputTokens) : `Out: ${formatTokens(tokenMetrics.outputTokens)}`; - elements.push({ content: applyColorsWithOverride(text, item.color || 'white', item.backgroundColor, item.bold), type: 'tokens-output', item }); - } - break; - - case 'tokens-cached': - if (tokenMetrics) { - const text = item.rawValue ? formatTokens(tokenMetrics.cachedTokens) : `Cached: ${formatTokens(tokenMetrics.cachedTokens)}`; - elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-cached', item }); - } - break; - - case 'tokens-total': - if (tokenMetrics) { - const text = item.rawValue ? formatTokens(tokenMetrics.totalTokens) : `Total: ${formatTokens(tokenMetrics.totalTokens)}`; - elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-total', item }); - } - break; - - case 'context-length': - if (tokenMetrics) { - const text = item.rawValue ? formatTokens(tokenMetrics.contextLength) : `Ctx: ${formatTokens(tokenMetrics.contextLength)}`; - elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'context-length', item }); - } - break; - - case 'context-percentage': - if (tokenMetrics) { - const percentage = Math.min(100, (tokenMetrics.contextLength / 200000) * 100); - const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx: ${percentage.toFixed(1)}%`; - elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'context-percentage', item }); - } - break; - - case 'context-percentage-usable': - if (tokenMetrics) { - // Calculate percentage out of 160,000 (80% of full context for auto-compact) - const percentage = Math.min(100, (tokenMetrics.contextLength / 160000) * 100); - const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx(u): ${percentage.toFixed(1)}%`; - elements.push({ content: applyColorsWithOverride(text, item.color || 'green', item.backgroundColor, item.bold), type: 'context-percentage-usable', item }); - } - break; - - case 'terminal-width': - const width = terminalWidth || getTerminalWidth(); - if (width) { - const text = item.rawValue ? `${width}` : `Term: ${width}`; - elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'terminal-width', item }); - } - break; - - case 'session-clock': - if (sessionDuration) { - const text = item.rawValue ? sessionDuration : `Session: ${sessionDuration}`; - elements.push({ content: applyColorsWithOverride(text, item.color || 'yellow', item.backgroundColor, item.bold), type: 'session-clock', item }); - } - break; - - case 'version': - const versionString = data.version || 'Unknown'; - const versionText = item.rawValue ? versionString : `Version: ${versionString}`; - elements.push({ content: applyColorsWithOverride(versionText, item.color || 'green', item.backgroundColor, item.bold), type: 'version', item }); - break; - - case 'separator': - // Only add separator if there are already elements and the last one isn't a separator - const lastElement = elements[elements.length - 1]; - if (elements.length > 0 && lastElement && lastElement.type !== 'separator') { - const sepChar = item.character || '|'; - // Handle special separator cases - let sepText; - if (sepChar === ',') { - sepText = `${sepChar} `; - } else if (sepChar === ' ') { - sepText = ' '; - } else { - sepText = ` ${sepChar} `; - } - // Use item color if specified, otherwise default to gray - const sepContent = applyColorsWithOverride(sepText, item.color || 'gray', item.backgroundColor, item.bold); - elements.push({ content: sepContent, type: 'separator', item }); - } - break; - - case 'flex-separator': - elements.push({ content: 'FLEX', type: 'flex-separator', item }); - hasFlexSeparator = true; - break; - - case 'custom-text': - const customText = item.customText || ''; - elements.push({ content: applyColorsWithOverride(customText, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-text', item }); - break; - - case 'custom-command': - if (item.commandPath) { - try { - // Execute the command with JSON input via stdin - const timeout = item.timeout || 1000; // Default to 1000ms if not specified - const output = execSync(item.commandPath, { - encoding: 'utf8', - input: JSON.stringify(data), - stdio: ['pipe', 'pipe', 'ignore'], - timeout: timeout - }).trim(); - - if (output) { - let finalOutput = output; - - // Handle max width truncation - if (item.maxWidth && item.maxWidth > 0) { - // Remove ANSI codes to measure actual length - const plainLength = output.replace(/\x1b\[[0-9;]*m/g, '').length; - if (plainLength > item.maxWidth) { - // Truncate while preserving ANSI codes - let truncated = ''; - let currentLength = 0; - let inAnsiCode = false; - let ansiBuffer = ''; - - for (let i = 0; i < output.length; i++) { - const char = output[i]; - if (char === '\x1b') { - inAnsiCode = true; - ansiBuffer = char; - } else if (inAnsiCode) { - ansiBuffer += char; - if (char === 'm') { - truncated += ansiBuffer; - inAnsiCode = false; - ansiBuffer = ''; - } - } else { - if (currentLength < item.maxWidth) { - truncated += char; - currentLength++; - } else { - break; - } - } - } - finalOutput = truncated; - } - } - - // Apply color if not preserving original colors - if (!item.preserveColors) { - // Strip existing ANSI codes and apply new color - const stripped = finalOutput.replace(/\x1b\[[0-9;]*m/g, ''); - elements.push({ content: applyColorsWithOverride(stripped, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-command', item }); - } else { - // Preserve original colors from command output - ignore any color property - elements.push({ content: finalOutput, type: 'custom-command', item }); - } - } - } catch { - // Command failed or timed out - silently skip - } - } - break; - } - } - - if (elements.length === 0) return ''; - - // Remove trailing separators - while (elements.length > 0 && elements[elements.length - 1]?.type === 'separator') { - elements.pop(); - } - - // Apply default padding and separators - const finalElements: string[] = []; - const padding = settings.defaultPadding || ''; - const defaultSep = settings.defaultSeparator || ''; - - elements.forEach((elem, index) => { - // Add default separator between any two items (but not before first item) - if (defaultSep && index > 0) { - // Check if we should inherit colors from the previous element - if (settings.inheritSeparatorColors && index > 0) { - const prevElem = elements[index - 1]; - if (prevElem && prevElem.item) { - // Apply the previous element's colors to the separator (already handles override) - // Use the item's color if set, otherwise get the default color for that item type - const itemColor = prevElem.item.color || getItemDefaultColor(prevElem.item.type); - const coloredSep = applyColorsWithOverride(defaultSep, itemColor, prevElem.item.backgroundColor, prevElem.item.bold); - finalElements.push(coloredSep); - } else { - finalElements.push(defaultSep); - } - } else if ((settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') || - (settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none')) { - // Apply override colors even when not inheriting colors - const coloredSep = applyColorsWithOverride(defaultSep, undefined, undefined); - finalElements.push(coloredSep); - } else { - finalElements.push(defaultSep); - } - } - - // Add element with padding (separators don't get padding) - if (elem.type === 'separator' || elem.type === 'flex-separator') { - finalElements.push(elem.content); - } else { - // Apply padding with colors (using overrides if set) - const hasColorOverride = (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') || - (settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none'); - - if (padding && (elem.item?.backgroundColor || hasColorOverride)) { - // Apply colors to padding - applyColorsWithOverride will handle the overrides - const paddedContent = applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor) + - elem.content + - applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor); - finalElements.push(paddedContent); - } else { - // No colors or no padding - finalElements.push(padding + elem.content + padding); - } - } - }); - - // Build the final status line - let statusLine = ''; - - if (hasFlexSeparator && terminalWidth) { - // Split elements by flex separators - const parts: string[][] = [[]]; - let currentPart = 0; - - for (let i = 0; i < finalElements.length; i++) { - const elem = finalElements[i]; - if (elem === 'FLEX' || (elements[i] && elements[i]?.type === 'flex-separator')) { - currentPart++; - parts[currentPart] = []; - } else { - parts[currentPart]?.push(elem!); - } - } - - // Calculate total length of all non-flex content - const partLengths = parts.map(part => { - const joined = part.join(''); - return joined.replace(/\x1b\[[0-9;]*m/g, '').length; - }); - const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0); - - // Calculate space to distribute among flex separators - const flexCount = parts.length - 1; // Number of flex separators - const totalSpace = Math.max(0, terminalWidth - totalContentLength); - const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0; - const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0; - - // Build the status line with distributed spacing - statusLine = ''; - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - if (part) { - statusLine += part.join(''); - } - if (i < parts.length - 1) { - // Add flex spacing - const spaces = spacePerFlex + (i < extraSpace ? 1 : 0); - statusLine += ' '.repeat(spaces); - } - } - } else { - // No flex separator OR no width detected - if (hasFlexSeparator && !terminalWidth) { - // Treat flex separators as normal separators when width detection fails - statusLine = finalElements.map(e => e === 'FLEX' ? chalk.gray(' | ') : e).join(''); - } else { - // Just join all elements normally - statusLine = finalElements.join(''); - } - } - - // Truncate if the line exceeds the terminal width - if (detectedWidth && detectedWidth > 0) { - // Remove ANSI escape codes to get actual length - const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length; - - if (plainLength > detectedWidth) { - // Need to truncate - preserve ANSI codes while truncating - let truncated = ''; - let currentLength = 0; - let inAnsiCode = false; - let ansiBuffer = ''; - const targetLength = detectedWidth - 7; // Truncate to width-7 for proper fit - - for (let i = 0; i < statusLine.length; i++) { - const char = statusLine[i]; - - if (char === '\x1b') { - inAnsiCode = true; - ansiBuffer = char; - } else if (inAnsiCode) { - ansiBuffer += char; - if (char === 'm') { - truncated += ansiBuffer; - inAnsiCode = false; - ansiBuffer = ''; - } - } else { - if (currentLength < targetLength) { - truncated += char; - currentLength++; - } else { - break; - } - } - } - - statusLine = truncated + '...'; - } - } - - return statusLine; -} - -async function renderStatusLine(data: StatusJSON) { +async function renderMultipleLines(data: StatusJSON) { const settings = await loadSettings(); // Get all lines to render (support both old items format and new lines format) - let lines: StatusItem[][] = []; - if (settings.lines) { - lines = settings.lines; - } else if (settings.items) { - // Legacy support for single line - lines = [settings.items]; - } else { - lines = [[]]; - } + let lines = settings.lines || (settings.items ? [settings.items] : [[]]); // Get token metrics if needed (check all lines) const hasTokenItems = lines.some(line => @@ -792,21 +56,28 @@ async function renderStatusLine(data: StatusJSON) { line.some(item => item.type === 'session-clock') ); - let tokenMetrics: any = null; + let tokenMetrics = null; if (hasTokenItems && data.transcript_path) { tokenMetrics = await getTokenMetrics(data.transcript_path); } - let sessionDuration: string | null = null; + let sessionDuration = null; if (hasSessionClock && data.transcript_path) { sessionDuration = await getSessionDuration(data.transcript_path); } + // Create render context + const context: RenderContext = { + data, + tokenMetrics, + sessionDuration, + isPreview: false + }; + // Render each line - for (let i = 0; i < lines.length; i++) { - const lineItems = lines[i]; + for (const lineItems of lines) { if (lineItems && lineItems.length > 0) { - const line = renderSingleLine(lineItems, settings, data, tokenMetrics, sessionDuration); + const line = renderStatusLine(lineItems, settings, context); // Replace all spaces with non-breaking spaces to prevent VSCode trimming const outputLine = line.replace(/ /g, '\u00A0'); console.log(outputLine); @@ -822,7 +93,7 @@ async function main() { if (input && input.trim() !== '') { try { const data: StatusJSON = JSON.parse(input); - await renderStatusLine(data); + await renderMultipleLines(data); } catch (error) { console.error('Error parsing JSON:', error); process.exit(1); diff --git a/src/tui.tsx b/src/tui.tsx index 661f541..b954311 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -3,10 +3,16 @@ import { render, Box, Text, useInput, useApp } from 'ink'; import SelectInput from 'ink-select-input'; import chalk from 'chalk'; import { execSync } from 'child_process'; -import { loadSettings, saveSettings, type Settings, type StatusItem, type StatusItemType, type FlexMode } from './config'; -import { isInstalled, installStatusLine, uninstallStatusLine, getExistingStatusLine } from './claude-settings'; +import { loadSettings, saveSettings, type Settings, type StatusItem, type StatusItemType, type FlexMode } from './utils/config'; +import { isInstalled, installStatusLine, uninstallStatusLine, getExistingStatusLine } from './utils/claude-settings'; import * as fs from 'fs'; import * as path from 'path'; +import { + renderStatusLine as renderLine, + getItemDefaultColor, + applyColors, + type RenderContext +} from './utils/renderer'; // Get package version function getPackageVersion(): string { @@ -20,27 +26,8 @@ function getPackageVersion(): string { } // Get default color for each item type (matching ccstatusline.ts defaults) -function getDefaultColor(type: string): string { - switch (type) { - case 'model': return 'cyan'; - case 'git-branch': return 'magenta'; - case 'git-changes': return 'yellow'; - case 'session-clock': return 'yellow'; - case 'version': return 'green'; - case 'tokens-input': return 'blue'; - case 'tokens-output': return 'white'; - case 'tokens-cached': return 'cyan'; - case 'tokens-total': return 'cyan'; - case 'context-length': return 'gray'; - case 'context-percentage': return 'blue'; - case 'context-percentage-usable': return 'green'; - case 'terminal-width': return 'gray'; - case 'custom-text': return 'white'; - case 'custom-command': return 'white'; - case 'separator': return 'gray'; - default: return 'white'; - } -} +// Re-export from shared renderer module +const getDefaultColor = getItemDefaultColor; // Check if terminal width detection is available function canDetectTerminalWidth(): boolean { @@ -92,337 +79,31 @@ interface StatusLinePreviewProps { settings?: Settings; } -// Helper function to apply foreground, background colors and bold -const applyColors = (text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string => { - let result = text; - - // Ignore 'dim' color - it causes issues with terminal rendering - if (foregroundColor && foregroundColor !== 'dim') { - const fgFunc = (chalk as any)[foregroundColor]; - if (fgFunc) { - result = fgFunc(result); - } - } - - if (backgroundColor && backgroundColor !== 'none') { - const bgFunc = (chalk as any)[backgroundColor]; - if (bgFunc) { - result = bgFunc(result); - } - } - - if (bold) { - result = chalk.bold(result); - } - - return result; -}; - const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetectionAvailable: boolean, settings?: Settings): string => { - // Helper to apply colors with optional background and foreground override - const applyItemColors = (text: string, item: StatusItem, defaultColor?: string): string => { - // Override foreground color takes precedence over EVERYTHING - let fgColor = item.color || defaultColor; - if (settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none') { - fgColor = settings.overrideForegroundColor; - } - - // Override background color takes precedence over EVERYTHING - let bgColor = item.backgroundColor; - if (settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') { - bgColor = settings.overrideBackgroundColor; - } - - const shouldBold = settings?.globalBold || item.bold; - return applyColors(text, fgColor, bgColor, shouldBold); + // Create render context for preview + const context: RenderContext = { + terminalWidth, + isPreview: true }; - // Calculate effective width based on flex mode settings - let effectiveWidth: number | null = null; - if (widthDetectionAvailable && terminalWidth) { - const flexMode = settings?.flexMode || 'full-minus-40'; - if (flexMode === 'full') { - effectiveWidth = terminalWidth - 2; // Subtract 2 for terminal padding in preview - } else if (flexMode === 'full-minus-40') { - effectiveWidth = terminalWidth - 40; - } else if (flexMode === 'full-until-compact') { - // For preview, always show full width - effectiveWidth = terminalWidth - 2; // Subtract 2 for terminal padding in preview - } - } - // Always ensure we have a width for truncation - const width = terminalWidth; // Always use terminal width for truncation - const flexWidth = effectiveWidth; // Use this for flex separator calculations - const rawElements: { content: string; type: string; item?: StatusItem }[] = []; - let hasFlexSeparator = false; - - items.forEach((item, index) => { - switch (item.type) { - case 'model': - const modelText = item.rawValue ? 'Claude' : 'Model: Claude'; - rawElements.push({ content: applyItemColors(modelText, item, 'cyan'), type: 'model', item }); - break; - case 'git-branch': - const branchText = '⎇ main'; - rawElements.push({ content: applyItemColors(branchText, item, 'magenta'), type: 'git-branch', item }); - break; - case 'git-changes': - const changesText = '(+42,-10)'; - rawElements.push({ content: applyItemColors(changesText, item, 'yellow'), type: 'git-changes', item }); - break; - case 'tokens-input': - const inputText = item.rawValue ? '15.2k' : 'In: 15.2k'; - rawElements.push({ content: applyItemColors(inputText, item, 'blue'), type: 'tokens-input', item }); - break; - case 'tokens-output': - const outputText = item.rawValue ? '3.4k' : 'Out: 3.4k'; - rawElements.push({ content: applyItemColors(outputText, item, 'white'), type: 'tokens-output', item }); - break; - case 'tokens-cached': - const cachedText = item.rawValue ? '12k' : 'Cached: 12k'; - rawElements.push({ content: applyItemColors(cachedText, item, 'cyan'), type: 'tokens-cached', item }); - break; - case 'tokens-total': - const totalText = item.rawValue ? '30.6k' : 'Total: 30.6k'; - rawElements.push({ content: applyItemColors(totalText, item, 'cyan'), type: 'tokens-total', item }); - break; - case 'context-length': - const ctxText = item.rawValue ? '18.6k' : 'Ctx: 18.6k'; - rawElements.push({ content: applyItemColors(ctxText, item, 'gray'), type: 'context-length', item }); - break; - case 'context-percentage': - const ctxPctText = item.rawValue ? '9.3%' : 'Ctx: 9.3%'; - rawElements.push({ content: applyItemColors(ctxPctText, item, 'blue'), type: 'context-percentage', item }); - break; - case 'context-percentage-usable': - const ctxUsableText = item.rawValue ? '11.6%' : 'Ctx(u): 11.6%'; - rawElements.push({ content: applyItemColors(ctxUsableText, item, 'green'), type: 'context-percentage-usable', item }); - break; - case 'session-clock': - const sessionText = item.rawValue ? '2hr 15m' : 'Session: 2hr 15m'; - rawElements.push({ content: applyItemColors(sessionText, item, 'yellow'), type: 'session-clock', item }); - break; - case 'version': - const versionText = item.rawValue ? '1.0.72' : 'Version: 1.0.72'; - rawElements.push({ content: applyItemColors(versionText, item, 'green'), type: 'version', item }); - break; - case 'terminal-width': - const detectedWidth = canDetectTerminalWidth() ? terminalWidth : '??'; - const termText = item.rawValue ? `${detectedWidth}` : `Term: ${detectedWidth}`; - rawElements.push({ content: applyItemColors(termText, item, 'gray'), type: 'terminal-width', item }); - break; - case 'separator': - const sepChar = item.character || '|'; - // Handle special separator cases - let sepContent; - if (sepChar === ',') { - sepContent = `${sepChar} `; - } else if (sepChar === ' ') { - sepContent = ' '; - } else { - sepContent = ` ${sepChar} `; - } - rawElements.push({ content: applyItemColors(sepContent, item, 'gray'), type: 'separator', item }); - break; - case 'flex-separator': - rawElements.push({ content: 'FLEX', type: 'flex-separator', item }); - hasFlexSeparator = true; - break; - case 'custom-text': - const customText = item.customText || ''; - rawElements.push({ content: applyItemColors(customText, item, 'white'), type: 'custom-text', item }); - break; - case 'custom-command': - const cmdText = item.commandPath ? `[cmd: ${item.commandPath.substring(0, 20)}${item.commandPath.length > 20 ? '...' : ''}]` : '[No command]'; - // Only apply color if not preserving colors - if (!item.preserveColors) { - rawElements.push({ content: applyItemColors(cmdText, item, 'white'), type: 'custom-command', item }); - } else { - // When preserving colors, just show the text without applying our colors - rawElements.push({ content: cmdText, type: 'custom-command', item }); - } - break; - } - }); - - // Apply default padding and separators - const elements: string[] = []; - const padding = settings?.defaultPadding || ''; - const defaultSep = settings?.defaultSeparator || ''; - - // Debug: Check if we have a default separator - // console.log('Default separator:', JSON.stringify(defaultSep), 'Length:', defaultSep.length); - - rawElements.forEach((elem, index) => { - // Add default separator between any two items (but not before first item) - if (defaultSep && index > 0) { - // Check if we should inherit colors from the previous element - if (settings?.inheritSeparatorColors && index > 0) { - const prevElem = rawElements[index - 1]; - if (prevElem && prevElem.item) { - // Apply the previous element's colors to the separator (with overrides) - const fgColor = settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none' - ? settings.overrideForegroundColor - : (prevElem.item.color || getDefaultColor(prevElem.item.type)); - const bgColor = settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none' - ? settings.overrideBackgroundColor - : prevElem.item.backgroundColor; - const shouldBold = settings?.globalBold || prevElem.item.bold; - const coloredSep = applyColors(defaultSep, fgColor, bgColor, shouldBold); - elements.push(coloredSep); - } else { - elements.push(defaultSep); - } - } else if (settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none' || - settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none') { - // Apply override colors even when not inheriting colors - const fgColor = settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none' - ? settings.overrideForegroundColor - : undefined; - const coloredSep = applyColors(defaultSep, fgColor, settings.overrideBackgroundColor, settings?.globalBold); - elements.push(coloredSep); - } else { - elements.push(defaultSep); - } - } - - // Add the element itself - if (elem.type === 'separator' || elem.type === 'flex-separator') { - elements.push(elem.content); - } else { - // Apply padding with the same colors as the item (or overrides) - const fgColor = settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none' - ? settings.overrideForegroundColor - : undefined; - const bgColor = settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none' - ? settings.overrideBackgroundColor - : elem.item?.backgroundColor; - - if (padding && (bgColor || fgColor)) { - // Apply colors to padding - const paddedContent = applyColors(padding, fgColor, bgColor, settings?.globalBold) + - elem.content + - applyColors(padding, fgColor, bgColor, settings?.globalBold); - elements.push(paddedContent); - } else { - // No colors or no padding - elements.push(padding + elem.content + padding); - } - } - }); - - // Build the status line with flex separator support - let statusLine = ''; - if (hasFlexSeparator && flexWidth) { - const parts: string[][] = [[]]; - let currentPart = 0; - - for (let i = 0; i < items.length; i++) { - const item = items[i]; - if (item && item.type === 'flex-separator') { - currentPart++; - parts[currentPart] = []; - } else { - const element = elements[i]; - if (element !== 'FLEX' && parts[currentPart]) { - parts[currentPart]!.push(element as string); - } - } - } - - // Calculate total length of all non-flex content - const partLengths = parts.map(part => { - const joined = part.join(''); - return joined.replace(/\x1b\[[0-9;]*m/g, '').length; - }); - const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0); - - // Calculate space to distribute among flex separators - const flexCount = parts.length - 1; // Number of flex separators - const totalSpace = Math.max(0, flexWidth - totalContentLength); - const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0; - const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0; - - // Build the status line with distributed spacing - statusLine = ''; - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - if (part) { - statusLine += part.join(''); - } - if (i < parts.length - 1) { - // Add flex spacing - const spaces = spacePerFlex + (i < extraSpace ? 1 : 0); - statusLine += ' '.repeat(spaces); - } - } - } else { - // No width detection available - treat flex separators as normal separators - statusLine = elements.map(e => e === 'FLEX' ? chalk.gray(' | ') : e).join(''); - } - - // Truncate if the line exceeds the maximum width - if (width && width > 0) { - // Remove ANSI escape codes to get actual length - const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length; - - if (plainLength > width) { - // Need to truncate - preserve ANSI codes while truncating - let truncated = ''; - let currentLength = 0; - let inAnsiCode = false; - let ansiBuffer = ''; - const targetLength = width - 5; // Reserve 3 chars for ellipsis + 2 for proper fit in preview - - for (let i = 0; i < statusLine.length; i++) { - const char = statusLine[i]; - - if (char === '\x1b') { - inAnsiCode = true; - ansiBuffer = char; - } else if (inAnsiCode) { - ansiBuffer += char; - if (char === 'm') { - truncated += ansiBuffer; - inAnsiCode = false; - ansiBuffer = ''; - } - } else { - if (currentLength < targetLength) { - truncated += char; - currentLength++; - } else { - break; - } - } - } - - statusLine = truncated + '...'; - } - } - - return statusLine; + return renderLine(items, settings || {}, context); }; + const StatusLinePreview: React.FC = ({ lines, terminalWidth, settings }) => { const widthDetectionAvailable = canDetectTerminalWidth(); - // Build the Claude Code input box - account for ink's padding - const boxWidth = Math.min(terminalWidth - 4, process.stdout.columns - 4 || 76); - const topLine = chalk.gray('╭' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╮'); - const middleLine = chalk.gray('│') + ' > ' + ' '.repeat(Math.max(0, boxWidth - 5)) + chalk.gray('│'); - const bottomLine = chalk.gray('╰' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╯'); - // Render each configured line - account for 2-space prefix in display - const availableWidth = boxWidth - 2; // Account for 2-space indent + // Render each configured line + // Pass the full terminal width - the renderer will handle preview adjustments const renderedLines = lines.map(lineItems => - lineItems.length > 0 ? renderSingleLine(lineItems, availableWidth, widthDetectionAvailable, settings) : '' + lineItems.length > 0 ? renderSingleLine(lineItems, terminalWidth, widthDetectionAvailable, settings) : '' ).filter(line => line !== ''); // Remove empty lines return ( - {topLine} - {middleLine} - {bottomLine} + + > + {renderedLines.map((line, index) => ( {line} ))} @@ -518,7 +199,7 @@ const MainMenu: React.FC = ({ onSelect, isClaudeInstalled, hasCha const items = [ { label: '📝 Edit Lines', value: 'lines' }, { label: '🎨 Configure Colors', value: 'colors' }, - { label: '⚡ Flex Options', value: 'flex' }, + { label: '📏 Terminal Width Options', value: 'terminalWidth' }, { label: '🔧 Global Options', value: 'globalOptions' }, { label: isClaudeInstalled ? '🗑️ Uninstall from Claude Code' : '📦 Install to Claude Code', value: 'install' }, ]; @@ -536,9 +217,9 @@ const MainMenu: React.FC = ({ onSelect, isClaudeInstalled, hasCha Main Menu - onSelect(item.value)} + onSelect(item.value)} initialIndex={Math.min(initialSelection, items.length - 1)} /> @@ -597,11 +278,15 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack, line } else if (key.ctrl && key.rightArrow) { // Move to end setTextCursorPos(textInput.length); - } else if (key.backspace || key.delete) { + } else if (key.backspace) { if (textCursorPos > 0) { setTextInput(textInput.slice(0, textCursorPos - 1) + textInput.slice(textCursorPos)); setTextCursorPos(textCursorPos - 1); } + } else if (key.delete) { + if (textCursorPos < textInput.length) { + setTextInput(textInput.slice(0, textCursorPos) + textInput.slice(textCursorPos + 1)); + } } else if (input && input.length === 1) { setTextInput(textInput.slice(0, textCursorPos) + input + textInput.slice(textCursorPos)); setTextCursorPos(textCursorPos + 1); @@ -634,11 +319,15 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack, line } else if (key.ctrl && key.rightArrow) { // Move to end setCommandCursorPos(commandInput.length); - } else if (key.backspace || key.delete) { + } else if (key.backspace) { if (commandCursorPos > 0) { setCommandInput(commandInput.slice(0, commandCursorPos - 1) + commandInput.slice(commandCursorPos)); setCommandCursorPos(commandCursorPos - 1); } + } else if (key.delete) { + if (commandCursorPos < commandInput.length) { + setCommandInput(commandInput.slice(0, commandCursorPos) + commandInput.slice(commandCursorPos + 1)); + } } else if (input) { setCommandInput(commandInput.slice(0, commandCursorPos) + input + commandInput.slice(commandCursorPos)); setCommandCursorPos(commandCursorPos + input.length); @@ -666,8 +355,10 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack, line // Cancel editing setEditingMaxWidth(false); setMaxWidthInput(''); - } else if (key.backspace || key.delete) { + } else if (key.backspace) { setMaxWidthInput(maxWidthInput.slice(0, -1)); + } else if (key.delete) { + // For simple number inputs, forward delete does nothing since there's no cursor position } else if (input && /\d/.test(input)) { setMaxWidthInput(maxWidthInput + input); } @@ -694,8 +385,10 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack, line // Cancel editing setEditingTimeout(false); setTimeoutInput(''); - } else if (key.backspace || key.delete) { + } else if (key.backspace) { setTimeoutInput(timeoutInput.slice(0, -1)); + } else if (key.delete) { + // For simple number inputs, forward delete does nothing since there's no cursor position } else if (input && /\d/.test(input)) { setTimeoutInput(timeoutInput + input); } @@ -864,7 +557,7 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack, line // Get the color for this item (use custom color if set, otherwise default) const colorName = item.color || getDefaultColor(item.type); const colorFunc = (chalk as any)[colorName] || chalk.white; - + switch (item.type) { case 'model': return colorFunc('Model'); @@ -1012,10 +705,16 @@ interface ColorMenuProps { } const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { - const colorableItems = items.filter(item => - ['model', 'git-branch', 'git-changes', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', 'context-percentage-usable', 'session-clock', 'terminal-width', 'version', 'custom-text', 'custom-command'].includes(item.type) && - !(item.type === 'custom-command' && item.preserveColors) // Exclude custom-command items with preserveColors - ); + const [showSeparators, setShowSeparators] = useState(false); + + const colorableItems = items.filter(item => { + // Include separators only if showSeparators is true + if (item.type === 'separator') { + return showSeparators; + } + return ['model', 'git-branch', 'git-changes', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', 'context-percentage-usable', 'session-clock', 'terminal-width', 'version', 'custom-text', 'custom-command'].includes(item.type) && + !(item.type === 'custom-command' && item.preserveColors); // Exclude custom-command items with preserveColors + }); const [highlightedItemId, setHighlightedItemId] = useState(colorableItems[0]?.id || null); const [editingBackground, setEditingBackground] = useState(false); @@ -1028,7 +727,7 @@ const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { onBack(); return; } - + // Normal keyboard handling when there are items if (key.escape) { if (editingBackground) { @@ -1036,6 +735,20 @@ const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { } else { onBack(); } + } else if (input === 's' || input === 'S') { + // Toggle show separators + setShowSeparators(!showSeparators); + // Reset highlight to first item if there are items + const newColorableItems = items.filter(item => { + if (item.type === 'separator') { + return !showSeparators; // Will be toggled + } + return ['model', 'git-branch', 'git-changes', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', 'context-percentage-usable', 'session-clock', 'terminal-width', 'version', 'custom-text', 'custom-command'].includes(item.type) && + !(item.type === 'custom-command' && item.preserveColors); + }); + if (newColorableItems.length > 0) { + setHighlightedItemId(newColorableItems[0]!.id); + } } else if (input === 'f' || input === 'F') { if (colorableItems.length > 0) { setEditingBackground(!editingBackground); @@ -1099,6 +812,7 @@ const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { case 'session-clock': return 'Session Clock'; case 'terminal-width': return 'Terminal Width'; case 'version': return 'Version'; + case 'separator': return `Separator (${item.character || '|'})`; case 'custom-text': return `Custom Text (${item.customText || 'Empty'})`; case 'custom-command': { const cmd = item.commandPath ? item.commandPath.substring(0, 20) + (item.commandPath.length > 20 ? '...' : '') : 'No command'; @@ -1167,25 +881,25 @@ const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { const colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'gray', 'redBright', 'greenBright', 'yellowBright', 'blueBright', 'magentaBright', 'cyanBright', 'whiteBright']; - + // Background colors list (includes 'none' as first option) // Note: chalk doesn't support bgDim, so we exclude it from background colors - const bgColors = ['none', 'bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta', - 'bgCyan', 'bgWhite', 'bgGray', 'bgRedBright', 'bgGreenBright', 'bgYellowBright', + const bgColors = ['none', 'bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta', + 'bgCyan', 'bgWhite', 'bgGray', 'bgRedBright', 'bgGreenBright', 'bgYellowBright', 'bgBlueBright', 'bgMagentaBright', 'bgCyanBright', 'bgWhiteBright']; // Get current color for highlighted item - const selectedItem = highlightedItemId && highlightedItemId !== 'back' - ? colorableItems.find(item => item.id === highlightedItemId) + const selectedItem = highlightedItemId && highlightedItemId !== 'back' + ? colorableItems.find(item => item.id === highlightedItemId) : null; - const currentColor = editingBackground + const currentColor = editingBackground ? (selectedItem?.backgroundColor || 'none') : (selectedItem ? (selectedItem.color || getDefaultColor(selectedItem.type)) : 'white'); - + const colorList = editingBackground ? bgColors : colors; const colorIndex = colorList.indexOf(currentColor); const colorNumber = colorIndex === -1 ? (editingBackground ? 1 : 8) : colorIndex + 1; - + let colorDisplay; if (editingBackground) { if (currentColor === 'none') { @@ -1203,6 +917,7 @@ const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { Configure Colors {editingBackground && chalk.yellow('[Background Mode]')} ↑↓ to select, Enter to cycle {editingBackground ? 'background' : 'foreground'}, (f) to toggle bg/fg, (b)old, (r)eset, ESC to go back + (s)how separators: {showSeparators ? chalk.green('ON') : chalk.gray('OFF')} {selectedItem && ( @@ -1221,21 +936,19 @@ const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { ⚠ VSCode Users: - If colors appear incorrect in the VSCode integrated terminal, the "Terminal › Integrated: Minimum Contrast Ratio" - (`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors. - You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors. + If colors appear incorrect in the VSCode integrated terminal, the "Terminal › Integrated: Minimum Contrast Ratio" (`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors. You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors. ); }; -interface FlexOptionsProps { +interface TerminalWidthOptionsProps { settings: Settings; onUpdate: (settings: Settings) => void; onBack: () => void; } -const FlexOptions: React.FC = ({ settings, onUpdate, onBack }) => { +const TerminalWidthOptions: React.FC = ({ settings, onUpdate, onBack }) => { const [selectedOption, setSelectedOption] = useState(settings.flexMode || 'full-minus-40'); const [compactThreshold, setCompactThreshold] = useState(settings.compactThreshold || 60); const [editingThreshold, setEditingThreshold] = useState(false); @@ -1267,9 +980,11 @@ const FlexOptions: React.FC = ({ settings, onUpdate, onBack }) setThresholdInput(String(compactThreshold)); setEditingThreshold(false); setValidationError(null); - } else if (key.backspace || key.delete) { + } else if (key.backspace) { setThresholdInput(thresholdInput.slice(0, -1)); setValidationError(null); + } else if (key.delete) { + // For simple number inputs, forward delete does nothing since there's no cursor position } else if (input && /\d/.test(input)) { const newValue = thresholdInput + input; if (newValue.length <= 2) { @@ -1292,7 +1007,7 @@ const FlexOptions: React.FC = ({ settings, onUpdate, onBack }) }, { value: 'full-minus-40' as FlexMode, - label: 'Full width minus 40', + label: 'Full width minus 40 (default)', description: 'Leaves a gap to the right of the status line to accommodate the auto-compact message. This prevents wrapping but may leave unused space. This limitation exists because we cannot detect when the message will appear.' }, { @@ -1330,8 +1045,9 @@ const FlexOptions: React.FC = ({ settings, onUpdate, onBack }) return ( - Flex Separator Width Options - Select how flex separators calculate available width + Terminal Width Options + These settings affect where long lines are truncated, and where right-alignment occurs when using flex separators + These settings are necessary because claude code does not currently provide an available width variable for the statusline and features like IDE integration, auto-compaction notices, and rate limit messages can all cause the statusline to wrap if we do not truncate it {editingThreshold ? ( @@ -1393,13 +1109,13 @@ const GlobalOptionsMenu: React.FC = ({ settings, onUpdat const [separatorInput, setSeparatorInput] = useState(settings.defaultSeparator || ''); const [inheritColors, setInheritColors] = useState(settings.inheritSeparatorColors || false); const [globalBold, setGlobalBold] = useState(settings.globalBold || false); - + // Background color override - const bgColors = ['none', 'bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta', - 'bgCyan', 'bgWhite', 'bgGray', 'bgRedBright', 'bgGreenBright', 'bgYellowBright', + const bgColors = ['none', 'bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta', + 'bgCyan', 'bgWhite', 'bgGray', 'bgRedBright', 'bgGreenBright', 'bgYellowBright', 'bgBlueBright', 'bgMagentaBright', 'bgCyanBright', 'bgWhiteBright']; const currentBgIndex = bgColors.indexOf(settings.overrideBackgroundColor || 'none'); - + // Foreground color override const fgColors = ['none', 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'gray', 'redBright', 'greenBright', 'yellowBright', 'blueBright', @@ -1418,8 +1134,10 @@ const GlobalOptionsMenu: React.FC = ({ settings, onUpdat } else if (key.escape) { setPaddingInput(settings.defaultPadding || ''); setEditingPadding(false); - } else if (key.backspace || key.delete) { + } else if (key.backspace) { setPaddingInput(paddingInput.slice(0, -1)); + } else if (key.delete) { + // For simple text inputs without cursor, forward delete does nothing } else if (input) { setPaddingInput(paddingInput + input); } @@ -1434,8 +1152,10 @@ const GlobalOptionsMenu: React.FC = ({ settings, onUpdat } else if (key.escape) { setSeparatorInput(settings.defaultSeparator || ''); setEditingSeparator(false); - } else if (key.backspace || key.delete) { + } else if (key.backspace) { setSeparatorInput(separatorInput.slice(0, -1)); + } else if (key.delete) { + // For simple text inputs without cursor, forward delete does nothing } else if (input) { setSeparatorInput(separatorInput + input); } @@ -1504,7 +1224,7 @@ const GlobalOptionsMenu: React.FC = ({ settings, onUpdat Global Options Configure automatic padding and separators between items - + {editingPadding ? ( @@ -1528,25 +1248,25 @@ const GlobalOptionsMenu: React.FC = ({ settings, onUpdat {settings.defaultPadding ? `"${settings.defaultPadding}"` : '(none)'} - Press (p) to edit - + Default Separator: {settings.defaultSeparator ? `"${settings.defaultSeparator}"` : '(none)'} - Press (s) to edit - + Inherit Colors: {inheritColors ? '✓ Enabled' : '✗ Disabled'} - Press (i) to toggle - + Global Bold: {globalBold ? '✓ Enabled' : '✗ Disabled'} - Press (o) to toggle - + Override BG Color: {(() => { @@ -1562,7 +1282,7 @@ const GlobalOptionsMenu: React.FC = ({ settings, onUpdat })()} - (b) cycle, (c) clear - + Override FG Color: {(() => { @@ -1577,11 +1297,11 @@ const GlobalOptionsMenu: React.FC = ({ settings, onUpdat })()} - (f) cycle, (g) clear - + Press ESC to go back - + Note: These settings are applied during rendering and don't add items to your widget list. @@ -1597,9 +1317,7 @@ const GlobalOptionsMenu: React.FC = ({ settings, onUpdat ⚠ VSCode Users: - If colors appear incorrect in the VSCode integrated terminal, the "Terminal › Integrated: Minimum Contrast Ratio" - (`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors. - You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors. + If colors appear incorrect in the VSCode integrated terminal, the "Terminal › Integrated: Minimum Contrast Ratio" (`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors. You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors. @@ -1613,7 +1331,7 @@ const App: React.FC = () => { const [settings, setSettings] = useState(null); const [originalSettings, setOriginalSettings] = useState(null); const [hasChanges, setHasChanges] = useState(false); - const [screen, setScreen] = useState<'main' | 'lines' | 'items' | 'colors' | 'flex' | 'globalOptions' | 'confirm'>('main'); + const [screen, setScreen] = useState<'main' | 'lines' | 'items' | 'colors' | 'terminalWidth' | 'globalOptions' | 'confirm'>('main'); const [selectedLine, setSelectedLine] = useState(0); const [menuSelections, setMenuSelections] = useState>({}); const [confirmDialog, setConfirmDialog] = useState<{ message: string; action: () => Promise } | null>(null); @@ -1709,8 +1427,8 @@ const App: React.FC = () => { case 'colors': setScreen('colors'); break; - case 'flex': - setScreen('flex'); + case 'terminalWidth': + setScreen('terminalWidth'); break; case 'globalOptions': setScreen('globalOptions'); @@ -1754,14 +1472,14 @@ const App: React.FC = () => { {screen === 'main' && ( - { // Only persist menu selection if not exiting if (value !== 'save' && value !== 'exit') { const menuMap: Record = { 'lines': 0, 'colors': 1, - 'flex': 2, + 'terminalWidth': 2, 'globalOptions': 3, 'install': 4 }; @@ -1769,7 +1487,7 @@ const App: React.FC = () => { } handleMainMenuSelect(value); }} - isClaudeInstalled={isClaudeInstalled} + isClaudeInstalled={isClaudeInstalled} hasChanges={hasChanges} initialSelection={menuSelections.main || 0} /> @@ -1831,14 +1549,14 @@ const App: React.FC = () => { }} /> )} - {screen === 'flex' && ( - { setSettings(updatedSettings); }} onBack={() => { - // Save that we came from 'flex' menu (index 2) + // Save that we came from 'terminalWidth' menu (index 2) setMenuSelections({ ...menuSelections, main: 2 }); setScreen('main'); }} diff --git a/src/claude-settings.ts b/src/utils/claude-settings.ts similarity index 100% rename from src/claude-settings.ts rename to src/utils/claude-settings.ts diff --git a/src/config.ts b/src/utils/config.ts similarity index 100% rename from src/config.ts rename to src/utils/config.ts diff --git a/src/utils/renderer.ts b/src/utils/renderer.ts new file mode 100644 index 0000000..44f5460 --- /dev/null +++ b/src/utils/renderer.ts @@ -0,0 +1,826 @@ +import chalk from 'chalk'; +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import { promisify } from 'util'; +import type { StatusItem } from './config'; + +// Ensure fs.promises compatibility for older Node versions +const readFile = fs.promises?.readFile || promisify(fs.readFile); + +// Force chalk to use colors even when piped +chalk.level = 3; + +export interface StatusJSON { + session_id: string; + transcript_path: string; + cwd: string; + model: { + id: string; + display_name: string; + }; + workspace: { + current_dir: string; + project_dir: string; + }; + version?: string; +} + +export interface TokenUsage { + input_tokens: number; + output_tokens: number; + cache_creation_input_tokens?: number; + cache_read_input_tokens?: number; +} + +export interface TranscriptLine { + message?: { + usage?: TokenUsage; + }; + isSidechain?: boolean; + timestamp?: string; +} + +export interface TokenMetrics { + inputTokens: number; + outputTokens: number; + cachedTokens: number; + totalTokens: number; + contextLength: number; +} + +export interface RenderContext { + data?: StatusJSON; + tokenMetrics?: TokenMetrics | null; + sessionDuration?: string | null; + gitBranch?: string | null; + gitChanges?: { insertions: number; deletions: number } | null; + terminalWidth?: number | null; + isPreview?: boolean; +} + +// Helper function to apply foreground, background colors and bold +export function applyColors(text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string { + let result = text; + + // Standard color application + // Ignore 'dim' color - it causes issues with terminal rendering + if (foregroundColor && foregroundColor !== 'dim') { + const fgFunc = (chalk as any)[foregroundColor]; + if (fgFunc) { + result = fgFunc(result); + } + } + + if (backgroundColor && backgroundColor !== 'none') { + const bgFunc = (chalk as any)[backgroundColor]; + if (bgFunc) { + result = bgFunc(result); + } + } + + if (bold) { + result = chalk.bold(result); + } + + return result; +} + +// Helper to get default color for an item type +export function getItemDefaultColor(type: string): string { + switch (type) { + case 'model': return 'cyan'; + case 'git-branch': return 'magenta'; + case 'git-changes': return 'yellow'; + case 'session-clock': return 'yellow'; + case 'version': return 'green'; + case 'tokens-input': return 'blue'; + case 'tokens-output': return 'white'; + case 'tokens-cached': return 'cyan'; + case 'tokens-total': return 'cyan'; + case 'context-length': return 'gray'; + case 'context-percentage': return 'blue'; + case 'context-percentage-usable': return 'green'; + case 'terminal-width': return 'gray'; + case 'custom-text': return 'white'; + case 'custom-command': return 'white'; + case 'separator': return 'gray'; + default: return 'white'; + } +} + +// Helper function to format token counts +export function formatTokens(count: number): string { + if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`; + if (count >= 1000) return `${(count / 1000).toFixed(1)}k`; + return count.toString(); +} + +export function getTerminalWidth(): number | null { + try { + // First try to get the tty of the parent process + const tty = execSync('ps -o tty= -p $(ps -o ppid= -p $$)', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'ignore'], + shell: '/bin/sh' + }).trim(); + + // Check if we got a valid tty (not ?? which means no tty) + if (tty && tty !== '??' && tty !== '?') { + // Now get the terminal size + const width = execSync( + `stty size < /dev/${tty} | awk '{print $2}'`, + { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'ignore'], + shell: '/bin/sh' + } + ).trim(); + + const parsed = parseInt(width, 10); + if (!isNaN(parsed) && parsed > 0) { + return parsed; + } + } + } catch { + // Command failed, width detection not available + } + + // Fallback: try tput cols which might work in some environments + try { + const width = execSync('tput cols 2>/dev/null', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'ignore'] + }).trim(); + + const parsed = parseInt(width, 10); + if (!isNaN(parsed) && parsed > 0) { + return parsed; + } + } catch { + // tput also failed + } + + return null; +} + +export function getGitBranch(): string | null { + try { + const branch = execSync('git branch --show-current 2>/dev/null', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'ignore'] + }).trim(); + return branch || null; + } catch { + return null; + } +} + +export function getGitChanges(): { insertions: number; deletions: number } | null { + try { + let totalInsertions = 0; + let totalDeletions = 0; + + // Get unstaged changes + const unstagedStat = execSync('git diff --shortstat 2>/dev/null', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'ignore'] + }).trim(); + + // Get staged changes + const stagedStat = execSync('git diff --cached --shortstat 2>/dev/null', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'ignore'] + }).trim(); + + // Parse unstaged changes + if (unstagedStat) { + const insertMatch = unstagedStat.match(/(\d+) insertion/); + const deleteMatch = unstagedStat.match(/(\d+) deletion/); + totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0; + totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0; + } + + // Parse staged changes + if (stagedStat) { + const insertMatch = stagedStat.match(/(\d+) insertion/); + const deleteMatch = stagedStat.match(/(\d+) deletion/); + totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0; + totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0; + } + + // Always return the changes, even if they're zero + return { insertions: totalInsertions, deletions: totalDeletions }; + } catch { + return null; + } +} + +export async function getSessionDuration(transcriptPath: string): Promise { + try { + if (!fs.existsSync(transcriptPath)) { + return null; + } + + const content = await readFile(transcriptPath, 'utf-8'); + const lines = content.trim().split('\n').filter(line => line.trim()); + + if (lines.length === 0) { + return null; + } + + let firstTimestamp: Date | null = null; + let lastTimestamp: Date | null = null; + + // Find first valid timestamp + for (const line of lines) { + try { + const data = JSON.parse(line); + if (data.timestamp) { + firstTimestamp = new Date(data.timestamp); + break; + } + } catch { + // Skip invalid lines + } + } + + // Find last valid timestamp (iterate backwards) + for (let i = lines.length - 1; i >= 0; i--) { + try { + const data = JSON.parse(lines[i]!); + if (data.timestamp) { + lastTimestamp = new Date(data.timestamp); + break; + } + } catch { + // Skip invalid lines + } + } + + if (!firstTimestamp || !lastTimestamp) { + return null; + } + + // Calculate duration in milliseconds + const durationMs = lastTimestamp.getTime() - firstTimestamp.getTime(); + + // Convert to minutes + const totalMinutes = Math.floor(durationMs / (1000 * 60)); + + if (totalMinutes < 1) { + return '<1m'; + } + + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + if (hours === 0) { + return `${minutes}m`; + } else if (minutes === 0) { + return `${hours}hr`; + } else { + return `${hours}hr ${minutes}m`; + } + } catch { + return null; + } +} + +export async function getTokenMetrics(transcriptPath: string): Promise { + try { + // Use Node.js-compatible file reading + if (!fs.existsSync(transcriptPath)) { + return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 }; + } + + const content = await readFile(transcriptPath, 'utf-8'); + const lines = content.trim().split('\n'); + + let inputTokens = 0; + let outputTokens = 0; + let cachedTokens = 0; + let contextLength = 0; + + // Parse each line and sum up token usage for totals + let mostRecentMainChainEntry: TranscriptLine | null = null; + let mostRecentTimestamp: Date | null = null; + + for (const line of lines) { + try { + const data: TranscriptLine = JSON.parse(line); + if (data.message?.usage) { + inputTokens += data.message.usage.input_tokens || 0; + outputTokens += data.message.usage.output_tokens || 0; + cachedTokens += data.message.usage.cache_read_input_tokens || 0; + cachedTokens += data.message.usage.cache_creation_input_tokens || 0; + + // Track the most recent entry with isSidechain: false (or undefined, which defaults to main chain) + if (data.isSidechain !== true && data.timestamp) { + const entryTime = new Date(data.timestamp); + if (!mostRecentTimestamp || entryTime > mostRecentTimestamp) { + mostRecentTimestamp = entryTime; + mostRecentMainChainEntry = data; + } + } + } + } catch { + // Skip invalid JSON lines + } + } + + // Calculate context length from the most recent main chain message + if (mostRecentMainChainEntry?.message?.usage) { + const usage = mostRecentMainChainEntry.message.usage; + contextLength = (usage.input_tokens || 0) + + (usage.cache_read_input_tokens || 0) + + (usage.cache_creation_input_tokens || 0); + } + + const totalTokens = inputTokens + outputTokens + cachedTokens; + + return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength }; + } catch { + return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 }; + } +} + +export function renderStatusLine( + items: StatusItem[], + settings: any, + context: RenderContext +): string { + // Helper to apply colors with optional background and bold override + const applyColorsWithOverride = (text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string => { + // Override foreground color takes precedence over EVERYTHING, including passed foreground color + let fgColor = foregroundColor; + if (settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none') { + fgColor = settings.overrideForegroundColor; + } + + // Override background color takes precedence over EVERYTHING, including passed background color + let bgColor = backgroundColor; + if (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') { + bgColor = settings.overrideBackgroundColor; + } + + const shouldBold = settings.globalBold || bold; + return applyColors(text, fgColor, bgColor, shouldBold); + }; + + const detectedWidth = context.terminalWidth || getTerminalWidth(); + + // Calculate terminal width based on flex mode settings + let terminalWidth: number | null = null; + if (detectedWidth) { + const flexMode = settings.flexMode || 'full-minus-40'; + + if (context.isPreview) { + // In preview mode, account for box borders and padding (6 chars total) + if (flexMode === 'full') { + terminalWidth = detectedWidth - 6; // Subtract 6 for box borders and padding in preview + } else if (flexMode === 'full-minus-40') { + terminalWidth = detectedWidth - 43; // -40 for auto-compact + 3 for preview + } else if (flexMode === 'full-until-compact') { + // For preview, always show full width minus preview padding + terminalWidth = detectedWidth - 6; + } + } else { + // In actual rendering mode + if (flexMode === 'full') { + // Use full width minus 4 for terminal padding + terminalWidth = detectedWidth - 4; + } else if (flexMode === 'full-minus-40') { + // Always subtract 41 for auto-compact message + terminalWidth = detectedWidth - 41; + } else if (flexMode === 'full-until-compact') { + // Check context percentage to decide + const threshold = settings.compactThreshold || 60; + const contextPercentage = context.tokenMetrics ? + Math.min(100, (context.tokenMetrics.contextLength / 200000) * 100) : 0; + + if (contextPercentage >= threshold) { + // Context is high, leave space for auto-compact + terminalWidth = detectedWidth - 40; + } else { + // Context is low, use full width minus 4 for padding + terminalWidth = detectedWidth - 4; + } + } + } + } + + const elements: { content: string, type: string, item?: StatusItem }[] = []; + let hasFlexSeparator = false; + + // Build elements based on configured items + for (const item of items) { + switch (item.type) { + case 'model': + if (context.isPreview) { + const modelText = item.rawValue ? 'Claude' : 'Model: Claude'; + elements.push({ content: applyColorsWithOverride(modelText, item.color || 'cyan', item.backgroundColor, item.bold), type: 'model', item }); + } else if (context.data?.model) { + const text = item.rawValue ? context.data.model.display_name : `Model: ${context.data.model.display_name}`; + elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'model', item }); + } + break; + + case 'git-branch': + if (context.isPreview) { + const branchText = item.rawValue ? 'main' : '⎇ main'; + elements.push({ content: applyColorsWithOverride(branchText, item.color || 'magenta', item.backgroundColor, item.bold), type: 'git-branch', item }); + } else { + const branch = context.gitBranch || getGitBranch(); + if (branch) { + const text = item.rawValue ? branch : `⎇ ${branch}`; + elements.push({ content: applyColorsWithOverride(text, item.color || 'magenta', item.backgroundColor, item.bold), type: 'git-branch', item }); + } + } + break; + + case 'git-changes': + if (context.isPreview) { + const changesText = '(+42,-10)'; + elements.push({ content: applyColorsWithOverride(changesText, item.color || 'yellow', item.backgroundColor, item.bold), type: 'git-changes', item }); + } else { + const changes = context.gitChanges || getGitChanges(); + if (changes !== null) { + const changeStr = `(+${changes.insertions},-${changes.deletions})`; + elements.push({ content: applyColorsWithOverride(changeStr, item.color || 'yellow', item.backgroundColor, item.bold), type: 'git-changes', item }); + } + } + break; + + case 'tokens-input': + if (context.isPreview) { + const inputText = item.rawValue ? '15.2k' : 'In: 15.2k'; + elements.push({ content: applyColorsWithOverride(inputText, item.color || 'blue', item.backgroundColor, item.bold), type: 'tokens-input', item }); + } else if (context.tokenMetrics) { + const text = item.rawValue ? formatTokens(context.tokenMetrics.inputTokens) : `In: ${formatTokens(context.tokenMetrics.inputTokens)}`; + elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'tokens-input', item }); + } + break; + + case 'tokens-output': + if (context.isPreview) { + const outputText = item.rawValue ? '3.4k' : 'Out: 3.4k'; + elements.push({ content: applyColorsWithOverride(outputText, item.color || 'white', item.backgroundColor, item.bold), type: 'tokens-output', item }); + } else if (context.tokenMetrics) { + const text = item.rawValue ? formatTokens(context.tokenMetrics.outputTokens) : `Out: ${formatTokens(context.tokenMetrics.outputTokens)}`; + elements.push({ content: applyColorsWithOverride(text, item.color || 'white', item.backgroundColor, item.bold), type: 'tokens-output', item }); + } + break; + + case 'tokens-cached': + if (context.isPreview) { + const cachedText = item.rawValue ? '12k' : 'Cached: 12k'; + elements.push({ content: applyColorsWithOverride(cachedText, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-cached', item }); + } else if (context.tokenMetrics) { + const text = item.rawValue ? formatTokens(context.tokenMetrics.cachedTokens) : `Cached: ${formatTokens(context.tokenMetrics.cachedTokens)}`; + elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-cached', item }); + } + break; + + case 'tokens-total': + if (context.isPreview) { + const totalText = item.rawValue ? '30.6k' : 'Total: 30.6k'; + elements.push({ content: applyColorsWithOverride(totalText, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-total', item }); + } else if (context.tokenMetrics) { + const text = item.rawValue ? formatTokens(context.tokenMetrics.totalTokens) : `Total: ${formatTokens(context.tokenMetrics.totalTokens)}`; + elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-total', item }); + } + break; + + case 'context-length': + if (context.isPreview) { + const ctxText = item.rawValue ? '18.6k' : 'Ctx: 18.6k'; + elements.push({ content: applyColorsWithOverride(ctxText, item.color || 'gray', item.backgroundColor, item.bold), type: 'context-length', item }); + } else if (context.tokenMetrics) { + const text = item.rawValue ? formatTokens(context.tokenMetrics.contextLength) : `Ctx: ${formatTokens(context.tokenMetrics.contextLength)}`; + elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'context-length', item }); + } + break; + + case 'context-percentage': + if (context.isPreview) { + const ctxPctText = item.rawValue ? '9.3%' : 'Ctx: 9.3%'; + elements.push({ content: applyColorsWithOverride(ctxPctText, item.color || 'blue', item.backgroundColor, item.bold), type: 'context-percentage', item }); + } else if (context.tokenMetrics) { + const percentage = Math.min(100, (context.tokenMetrics.contextLength / 200000) * 100); + const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx: ${percentage.toFixed(1)}%`; + elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'context-percentage', item }); + } + break; + + case 'context-percentage-usable': + if (context.isPreview) { + const ctxUsableText = item.rawValue ? '11.6%' : 'Ctx(u): 11.6%'; + elements.push({ content: applyColorsWithOverride(ctxUsableText, item.color || 'green', item.backgroundColor, item.bold), type: 'context-percentage-usable', item }); + } else if (context.tokenMetrics) { + // Calculate percentage out of 160,000 (80% of full context for auto-compact) + const percentage = Math.min(100, (context.tokenMetrics.contextLength / 160000) * 100); + const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx(u): ${percentage.toFixed(1)}%`; + elements.push({ content: applyColorsWithOverride(text, item.color || 'green', item.backgroundColor, item.bold), type: 'context-percentage-usable', item }); + } + break; + + case 'terminal-width': + const width = terminalWidth || getTerminalWidth(); + if (context.isPreview) { + const detectedWidth = width || '??'; + const termText = item.rawValue ? `${detectedWidth}` : `Term: ${detectedWidth}`; + elements.push({ content: applyColorsWithOverride(termText, item.color || 'gray', item.backgroundColor, item.bold), type: 'terminal-width', item }); + } else if (width) { + const text = item.rawValue ? `${width}` : `Term: ${width}`; + elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'terminal-width', item }); + } + break; + + case 'session-clock': + if (context.isPreview) { + const sessionText = item.rawValue ? '2hr 15m' : 'Session: 2hr 15m'; + elements.push({ content: applyColorsWithOverride(sessionText, item.color || 'yellow', item.backgroundColor, item.bold), type: 'session-clock', item }); + } else if (context.sessionDuration) { + const text = item.rawValue ? context.sessionDuration : `Session: ${context.sessionDuration}`; + elements.push({ content: applyColorsWithOverride(text, item.color || 'yellow', item.backgroundColor, item.bold), type: 'session-clock', item }); + } + break; + + case 'version': + if (context.isPreview) { + const versionText = item.rawValue ? '1.0.72' : 'Version: 1.0.72'; + elements.push({ content: applyColorsWithOverride(versionText, item.color || 'green', item.backgroundColor, item.bold), type: 'version', item }); + } else if (context.data?.version) { + const versionString = context.data.version || 'Unknown'; + const versionText = item.rawValue ? versionString : `Version: ${versionString}`; + elements.push({ content: applyColorsWithOverride(versionText, item.color || 'green', item.backgroundColor, item.bold), type: 'version', item }); + } + break; + + case 'separator': + // Always add separators - users should be able to add multiple if they want + const sepChar = item.character || '|'; + // Handle special separator cases + let sepText; + if (sepChar === ',') { + sepText = `${sepChar} `; + } else if (sepChar === ' ') { + sepText = ' '; + } else { + sepText = ` ${sepChar} `; + } + // Use item color if specified, otherwise default to gray + const sepContent = applyColorsWithOverride(sepText, item.color || 'gray', item.backgroundColor, item.bold); + elements.push({ content: sepContent, type: 'separator', item }); + break; + + case 'flex-separator': + elements.push({ content: 'FLEX', type: 'flex-separator', item }); + hasFlexSeparator = true; + break; + + case 'custom-text': + const customText = item.customText || ''; + elements.push({ content: applyColorsWithOverride(customText, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-text', item }); + break; + + case 'custom-command': + if (context.isPreview) { + const cmdText = item.commandPath ? `[cmd: ${item.commandPath.substring(0, 20)}${item.commandPath.length > 20 ? '...' : ''}]` : '[No command]'; + // Only apply color if not preserving colors + if (!item.preserveColors) { + elements.push({ content: applyColorsWithOverride(cmdText, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-command', item }); + } else { + // When preserving colors, just show the text without applying our colors + elements.push({ content: cmdText, type: 'custom-command', item }); + } + } else if (item.commandPath && context.data) { + try { + // Execute the command with JSON input via stdin + const timeout = item.timeout || 1000; // Default to 1000ms if not specified + const output = execSync(item.commandPath, { + encoding: 'utf8', + input: JSON.stringify(context.data), + stdio: ['pipe', 'pipe', 'ignore'], + timeout: timeout + }).trim(); + + if (output) { + let finalOutput = output; + + // Handle max width truncation + if (item.maxWidth && item.maxWidth > 0) { + // Remove ANSI codes to measure actual length + const plainLength = output.replace(/\x1b\[[0-9;]*m/g, '').length; + if (plainLength > item.maxWidth) { + // Truncate while preserving ANSI codes + let truncated = ''; + let currentLength = 0; + let inAnsiCode = false; + let ansiBuffer = ''; + + for (let i = 0; i < output.length; i++) { + const char = output[i]; + if (char === '\x1b') { + inAnsiCode = true; + ansiBuffer = char; + } else if (inAnsiCode) { + ansiBuffer += char; + if (char === 'm') { + truncated += ansiBuffer; + inAnsiCode = false; + ansiBuffer = ''; + } + } else { + if (currentLength < item.maxWidth) { + truncated += char; + currentLength++; + } else { + break; + } + } + } + finalOutput = truncated; + } + } + + // Apply color if not preserving original colors + if (!item.preserveColors) { + // Strip existing ANSI codes and apply new color + const stripped = finalOutput.replace(/\x1b\[[0-9;]*m/g, ''); + elements.push({ content: applyColorsWithOverride(stripped, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-command', item }); + } else { + // Preserve original colors from command output - ignore any color property + elements.push({ content: finalOutput, type: 'custom-command', item }); + } + } + } catch { + // Command failed or timed out - silently skip + } + } + break; + } + } + + if (elements.length === 0) return ''; + + // Remove trailing separators + while (elements.length > 0 && elements[elements.length - 1]?.type === 'separator') { + elements.pop(); + } + + // Apply default padding and separators + const finalElements: string[] = []; + const padding = settings.defaultPadding || ''; + const defaultSep = settings.defaultSeparator || ''; + + elements.forEach((elem, index) => { + // Add default separator between any two items (but not before first item, and not around flex separators) + const prevElem = index > 0 ? elements[index - 1] : null; + const shouldAddSeparator = defaultSep && index > 0 && + elem.type !== 'flex-separator' && + prevElem?.type !== 'flex-separator'; + + if (shouldAddSeparator) { + // Check if we should inherit colors from the previous element + if (settings.inheritSeparatorColors && index > 0) { + const prevElem = elements[index - 1]; + if (prevElem && prevElem.item) { + // Apply the previous element's colors to the separator (already handles override) + // Use the item's color if set, otherwise get the default color for that item type + const itemColor = prevElem.item.color || getItemDefaultColor(prevElem.item.type); + const coloredSep = applyColorsWithOverride(defaultSep, itemColor, prevElem.item.backgroundColor, prevElem.item.bold); + finalElements.push(coloredSep); + } else { + finalElements.push(defaultSep); + } + } else if ((settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') || + (settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none')) { + // Apply override colors even when not inheriting colors + const coloredSep = applyColorsWithOverride(defaultSep, undefined, undefined); + finalElements.push(coloredSep); + } else { + finalElements.push(defaultSep); + } + } + + // Add element with padding (separators don't get padding) + if (elem.type === 'separator' || elem.type === 'flex-separator') { + finalElements.push(elem.content); + } else { + // Apply padding with colors (using overrides if set) + const hasColorOverride = (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') || + (settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none'); + + if (padding && (elem.item?.backgroundColor || hasColorOverride)) { + // Apply colors to padding - applyColorsWithOverride will handle the overrides + const paddedContent = applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor) + + elem.content + + applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor); + finalElements.push(paddedContent); + } else { + // No colors or no padding + finalElements.push(padding + elem.content + padding); + } + } + }); + + // Build the final status line + let statusLine = ''; + + if (hasFlexSeparator && terminalWidth) { + // Split elements by flex separators + const parts: string[][] = [[]]; + let currentPart = 0; + + for (let i = 0; i < finalElements.length; i++) { + const elem = finalElements[i]; + if (elem === 'FLEX') { + currentPart++; + parts[currentPart] = []; + } else { + parts[currentPart]?.push(elem!); + } + } + + // Calculate total length of all non-flex content + const partLengths = parts.map(part => { + const joined = part.join(''); + return joined.replace(/\x1b\[[0-9;]*m/g, '').length; + }); + const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0); + + // Calculate space to distribute among flex separators + const flexCount = parts.length - 1; // Number of flex separators + const totalSpace = Math.max(0, terminalWidth - totalContentLength); + const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0; + const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0; + + // Build the status line with distributed spacing + statusLine = ''; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part) { + statusLine += part.join(''); + } + if (i < parts.length - 1) { + // Add flex spacing + const spaces = spacePerFlex + (i < extraSpace ? 1 : 0); + statusLine += ' '.repeat(spaces); + } + } + } else { + // No flex separator OR no width detected + if (hasFlexSeparator && !terminalWidth) { + // Treat flex separators as normal separators when width detection fails + statusLine = finalElements.map(e => e === 'FLEX' ? chalk.gray(' | ') : e).join(''); + } else { + // Just join all elements normally + statusLine = finalElements.join(''); + } + } + + // Truncate if the line exceeds the terminal width + // Use terminalWidth if available (already accounts for flex mode adjustments), otherwise use detectedWidth + const maxWidth = terminalWidth || detectedWidth; + if (maxWidth && maxWidth > 0) { + // Remove ANSI escape codes to get actual length + const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length; + + if (plainLength > maxWidth) { + // Need to truncate - preserve ANSI codes while truncating + let truncated = ''; + let currentLength = 0; + let inAnsiCode = false; + let ansiBuffer = ''; + const targetLength = context.isPreview ? maxWidth - 3 : maxWidth - 3; // Reserve 3 chars for ellipsis + + for (let i = 0; i < statusLine.length; i++) { + const char = statusLine[i]; + + if (char === '\x1b') { + inAnsiCode = true; + ansiBuffer = char; + } else if (inAnsiCode) { + ansiBuffer += char; + if (char === 'm') { + truncated += ansiBuffer; + inAnsiCode = false; + ansiBuffer = ''; + } + } else { + if (currentLength < targetLength) { + truncated += char; + currentLength++; + } else { + break; + } + } + } + + statusLine = truncated + '...'; + } + } + + return statusLine; +} \ No newline at end of file