diff --git a/README.md b/README.md
index 06f5438..3f53115 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,8 @@
-# Curse π―οΈ
-
-
-
-
-
+# Curse
Curse is a dead simple Terminal UI for running processes, configured through a single `curse.toml` file.
-
+
## Installation & Setup
diff --git a/bun.lock b/bun.lock
index d70fae6..cb422e0 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,11 +5,12 @@
"": {
"name": "curse",
"dependencies": {
+ "@types/react": "19.1.5",
"arktype": "^2.1.22",
"gunshi": "^0.26.3",
- "ink": "^6.6.0",
+ "ink": "6.6.0",
"minisearch": "^7.1.2",
- "react": "^19.1.1",
+ "react": "19.1.1",
"strip-ansi": "^7.1.2",
"toml": "^3.0.0",
},
@@ -25,7 +26,7 @@
},
},
"packages": {
- "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-mkOh+Wwawzuf5wa30bvc4nA+Qb6DIrGWgBhRR/Pw4T9nsgYait8izvXkNyU78D6Wcu3Z+KUdwCmLCxlWjEotYA=="],
+ "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="],
"@ark/schema": ["@ark/schema@0.49.0", "", { "dependencies": { "@ark/util": "0.49.0" } }, "sha512-GphZBLpW72iS0v4YkeUtV3YIno35Gimd7+ezbPO9GwEi9kzdUrPVjvf6aXSBAfHikaFc/9pqZOpv3pOXnC71tw=="],
@@ -111,9 +112,9 @@
"@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
- "@types/react": ["@types/react@19.1.14", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-ukd93VGzaNPMAUPy0gRDSC57UuQbnH9Kussp7HBjM06YFi9uZTFhOvMSO2OKqXm1rSgzOE+pVx1k1PYHGwlc8Q=="],
+ "@types/react": ["@types/react@19.1.5", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g=="],
- "ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="],
+ "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
@@ -133,23 +134,23 @@
"cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
- "cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
+ "cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="],
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
- "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
- "emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="],
+ "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
- "es-toolkit": ["es-toolkit@1.39.10", "", {}, "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w=="],
+ "es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
"escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
- "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
+ "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
"gunshi": ["gunshi@0.26.3", "", { "dependencies": { "args-tokens": "^0.20.1" } }, "sha512-x/CaxovzDA3KMkrjqow7M1dnsO1CxGMNabBaeGTGdVNtjOMN1G/eK7vjKeJ+zHdFIsWHmo47YQ2DQIESUut2+A=="],
@@ -191,7 +192,7 @@
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
- "string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
+ "string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="],
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
@@ -213,7 +214,11 @@
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
- "ink/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=="],
+ "cli-truncate/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
+
+ "ink/ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
+
+ "is-fullwidth-code-point/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
diff --git a/curse.png b/curse.png
new file mode 100644
index 0000000..faf537f
Binary files /dev/null and b/curse.png differ
diff --git a/package.json b/package.json
index 5d25ad8..6a5e57d 100644
--- a/package.json
+++ b/package.json
@@ -5,11 +5,12 @@
"type": "module",
"module": "src/index.ts",
"dependencies": {
+ "@types/react": "19.1.5",
"arktype": "^2.1.22",
"gunshi": "^0.26.3",
- "ink": "^6.6.0",
+ "ink": "6.6.0",
"minisearch": "^7.1.2",
- "react": "^19.1.1",
+ "react": "19.1.1",
"strip-ansi": "^7.1.2",
"toml": "^3.0.0"
},
diff --git a/src/lib/Colors.ts b/src/lib/Colors.ts
index 9f18374..eadacdc 100644
--- a/src/lib/Colors.ts
+++ b/src/lib/Colors.ts
@@ -1,11 +1,12 @@
export const Colors = {
- primary: "#8b5cf6", // violet
+ white: "#ffffff",
darkGray: "#4b5563",
mutedCyan: "#5f8a8b",
- indigo: "#6366f1",
- purple: "#a78bfa",
- green: "#10b981",
- brightGreen: "#34d399",
- brightPink: "#ec4899",
- brightOrange: "#f97316",
+ gray: "#6b7280",
+ lightGray: "#e5e7eb",
+ mutedGreen: "#6b9e7e",
+ softGreen: "#7dab8e",
+ silver: "#9ca3af",
+ amber: "#d4a054",
+ mutedRed: "#b07070",
};
diff --git a/src/ui/View.tsx b/src/ui/View.tsx
index 88f0e99..fd73c40 100644
--- a/src/ui/View.tsx
+++ b/src/ui/View.tsx
@@ -66,19 +66,17 @@ function View(props: { config: CurseConfig }) {
{displayMode === "normal" && (
-
- Curse π―
+
+ Curse
v{version}
- {status === ProgramStatus.Quitting && (
- Quitting...
- )}
+ {status === ProgramStatus.Quitting && Quitting...}
)}
{displayMode === "normal" && (
- Config:
+ Config:
{props.config.fileName}
)}
diff --git a/src/ui/components/AggregatedSummary.tsx b/src/ui/components/AggregatedSummary.tsx
index 04b2f5c..049e435 100644
--- a/src/ui/components/AggregatedSummary.tsx
+++ b/src/ui/components/AggregatedSummary.tsx
@@ -5,13 +5,13 @@ import { useRenderTick } from "../../hooks/useRenderTick";
import { Colors } from "../../lib/Colors";
const statusColorMap: Record = {
- running: Colors.green,
- starting: Colors.indigo,
+ running: Colors.mutedGreen,
+ starting: Colors.gray,
pending: Colors.darkGray,
- error: "red",
+ error: Colors.mutedRed,
success: Colors.darkGray,
- killing: Colors.brightOrange,
- killed: Colors.brightOrange,
+ killing: Colors.amber,
+ killed: Colors.amber,
};
export function AggregatedSummary(props: { numberPrefix: string; waitingForSecondG: boolean }) {
@@ -65,12 +65,12 @@ export function AggregatedSummary(props: { numberPrefix: string; waitingForSecon
)}
-
+
{" "}
{selectedDetail}{" "}
- {props.numberPrefix && [{props.numberPrefix}]}
- {props.waitingForSecondG && [g]}
+ {props.numberPrefix && [{props.numberPrefix}]}
+ {props.waitingForSecondG && [g]}
);
diff --git a/src/ui/components/LogTailPreview.tsx b/src/ui/components/LogTailPreview.tsx
index cf7667a..3385d77 100644
--- a/src/ui/components/LogTailPreview.tsx
+++ b/src/ui/components/LogTailPreview.tsx
@@ -46,7 +46,10 @@ export function LogTailPreview(props: { height: number }) {
) : (
logs.map((log: string, index: number) => (
-
+
{preprocessLog(log)}
diff --git a/src/ui/views/LogPage.tsx b/src/ui/views/LogPage.tsx
index 7051947..8082091 100644
--- a/src/ui/views/LogPage.tsx
+++ b/src/ui/views/LogPage.tsx
@@ -588,9 +588,9 @@ function LogTable(props: {
// Helper functions for text colors
const getTextColor = (isCursor: boolean, isSelected: boolean, log: string) => {
if (isCursor) return "white";
- if (isSelected) return Colors.purple;
- if (log.includes("stderr")) return "red";
- return Colors.purple;
+ if (isSelected) return Colors.lightGray;
+ if (log.includes("stderr")) return Colors.mutedRed;
+ return Colors.lightGray;
};
const getPartTextColor = (
@@ -599,7 +599,7 @@ function LogTable(props: {
isSelected: boolean,
log: string,
) => {
- if (isHighlight) return Colors.brightOrange;
+ if (isHighlight) return Colors.amber;
return getTextColor(isCursor, isSelected, log);
};
@@ -614,20 +614,18 @@ function LogTable(props: {
Autoscroll:
-
+
{autoScroll ? "on" : "off"}
- {positionLost && (
- (position lost, returned to tail)
- )}
- {numberPrefix && [{numberPrefix}]}
- {waitingForSecondG && [g]}
- {isSelectMode && [SELECT]}
- {isSearchMode && </{searchQuery}>}
+ {positionLost && (position lost, returned to tail)}
+ {numberPrefix && [{numberPrefix}]}
+ {waitingForSecondG && [g]}
+ {isSelectMode && [SELECT]}
+ {isSearchMode && </{searchQuery}>}
{!isSearchMode && appliedSearchQuery && (
- </{appliedSearchQuery}>
+ </{appliedSearchQuery}>
)}
- {showCopyIndicator && β {copyIndicatorText}}
+ {showCopyIndicator && β {copyIndicatorText}}
{logs.map((log, index) => {
@@ -637,7 +635,7 @@ function LogTable(props: {
// Determine background color: cursor takes priority, then selection
let backgroundColor;
if (isCursor) {
- backgroundColor = Colors.indigo; // Cursor color
+ backgroundColor = Colors.gray; // Cursor color
} else if (isSelected) {
backgroundColor = Colors.darkGray; // Charcoal for selection
}
@@ -800,23 +798,23 @@ export function LogPage() {
<>
- Logs
- (
-
+ Logs
+ (
+
{selectedProcess.name}
- )
- [
- tail
- ]
+ )
+ [
+ tail
+ ]
{isSearchMode && (
- Search:
+ Search:
{searchQuery}
- β
+ β
)}
diff --git a/src/ui/views/MainPage.tsx b/src/ui/views/MainPage.tsx
index 4280ffe..34a155a 100644
--- a/src/ui/views/MainPage.tsx
+++ b/src/ui/views/MainPage.tsx
@@ -33,12 +33,12 @@ function getReadinessDisplay(process: Process): {
const color =
char === "x" || char === "β"
- ? "red"
+ ? Colors.mutedRed
: char === "β"
- ? Colors.green
+ ? Colors.mutedGreen
: char === "-"
? Colors.darkGray
- : Colors.indigo;
+ : Colors.gray;
return { char, color };
}
@@ -76,8 +76,8 @@ function ProcessTable(props: {
READY
- {props.numberPrefix && [{props.numberPrefix}]}
- {props.waitingForSecondG && [g]}
+ {props.numberPrefix && [{props.numberPrefix}]}
+ {props.waitingForSecondG && [g]}
{processes.map((process: Process, index: number) => {
@@ -93,13 +93,13 @@ function ProcessTable(props: {
? Colors.darkGray
: isShutdownHookPending
? Colors.mutedCyan
- : Colors.indigo;
+ : Colors.gray;
return (
@@ -157,8 +157,8 @@ function ProcessTable(props: {
CPU
- {props.numberPrefix && [{props.numberPrefix}]}
- {props.waitingForSecondG && [g]}
+ {props.numberPrefix && [{props.numberPrefix}]}
+ {props.waitingForSecondG && [g]}
{processes.map((process: Process, index: number) => {
@@ -174,13 +174,13 @@ function ProcessTable(props: {
? Colors.darkGray
: isShutdownHookPending
? Colors.mutedCyan
- : Colors.indigo;
+ : Colors.gray;
return (
@@ -237,7 +237,7 @@ function ProcessTable(props: {
: isSuccess
? Colors.darkGray
: process.profile?.memoryUsageMB !== undefined
- ? Colors.indigo
+ ? Colors.gray
: Colors.darkGray
}
bold={isSelected}
@@ -255,7 +255,7 @@ function ProcessTable(props: {
: isSuccess
? Colors.darkGray
: process.profile?.cpuUsagePercent !== undefined
- ? Colors.indigo
+ ? Colors.gray
: Colors.darkGray
}
bold={isSelected}