drexler 0.2.16 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/package.json +1 -1
- package/src/commands.ts +3 -0
- package/src/index.ts +1 -0
- package/src/ui/App.tsx +80 -57
- package/src/ui/MascotIntro.tsx +385 -0
- package/src/ui/PetPanel.tsx +296 -388
package/src/ui/MascotIntro.tsx
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
2
2
|
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
3
|
+
import {
|
|
4
|
+
formatTenure,
|
|
5
|
+
getPetMood,
|
|
6
|
+
getPetRank,
|
|
7
|
+
petTenureMs,
|
|
8
|
+
rankLabel,
|
|
9
|
+
type PetActivity,
|
|
10
|
+
type PetStats,
|
|
11
|
+
} from "../pet/petState.ts";
|
|
3
12
|
import { STARTUP_TIPS } from "../startupTips.ts";
|
|
4
13
|
import {
|
|
5
14
|
MascotFrame,
|
|
@@ -7,6 +16,14 @@ import {
|
|
|
7
16
|
type MascotState,
|
|
8
17
|
} from "./MascotFrame.tsx";
|
|
9
18
|
import { displayWidth, fitDisplayText } from "./graphemes.ts";
|
|
19
|
+
import {
|
|
20
|
+
COMPACT_PET_PANEL_MIN_WIDTH,
|
|
21
|
+
CompactPetPanel,
|
|
22
|
+
getPetStatusMessage,
|
|
23
|
+
PetScene,
|
|
24
|
+
PET_SCENE_WIDTH,
|
|
25
|
+
type Environment,
|
|
26
|
+
} from "./PetPanel.tsx";
|
|
10
27
|
import { useTheme } from "./ThemeContext.tsx";
|
|
11
28
|
|
|
12
29
|
interface IntroFrame extends MascotState {
|
|
@@ -152,6 +169,13 @@ const MAX_MOOD_PANEL_WIDTH = 44;
|
|
|
152
169
|
const RIGHT_COLUMN_INSET = 1;
|
|
153
170
|
const RIGHT_COLUMN_PAD_RIGHT = 1;
|
|
154
171
|
const LEFT_PANEL_MIN_COPY = 24;
|
|
172
|
+
const PET_STATS_MIN_WIDTH = 24;
|
|
173
|
+
const PET_STATS_MAX_WIDTH = 58;
|
|
174
|
+
const PET_SPLIT_DIVIDER_HEIGHT = 15;
|
|
175
|
+
const PET_SPLIT_DIVIDER_ROWS: number[] = Array.from(
|
|
176
|
+
{ length: PET_SPLIT_DIVIDER_HEIGHT },
|
|
177
|
+
(_, i) => i,
|
|
178
|
+
);
|
|
155
179
|
|
|
156
180
|
export type MascotLayoutMode = "tiny" | "compact" | "stacked" | "split";
|
|
157
181
|
|
|
@@ -257,6 +281,11 @@ interface MascotDashboardProps {
|
|
|
257
281
|
greeting: string;
|
|
258
282
|
width: number;
|
|
259
283
|
mood?: string;
|
|
284
|
+
mode?: "normal" | "pet";
|
|
285
|
+
petStats?: PetStats;
|
|
286
|
+
petActivity?: PetActivity;
|
|
287
|
+
petEnv?: Environment;
|
|
288
|
+
petPaused?: boolean;
|
|
260
289
|
bootProgress?: number;
|
|
261
290
|
state?: MascotState;
|
|
262
291
|
bar?: string;
|
|
@@ -780,10 +809,354 @@ function MoodReadout({
|
|
|
780
809
|
);
|
|
781
810
|
}
|
|
782
811
|
|
|
812
|
+
function padDisplayText(input: string, width: number): string {
|
|
813
|
+
const safeWidth = Math.max(1, width);
|
|
814
|
+
const fitted = fitDisplayText(input, safeWidth);
|
|
815
|
+
return `${fitted}${" ".repeat(Math.max(0, safeWidth - displayWidth(fitted)))}`;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function PetSceneReadout({
|
|
819
|
+
stats,
|
|
820
|
+
activity,
|
|
821
|
+
env,
|
|
822
|
+
isPaused,
|
|
823
|
+
width,
|
|
824
|
+
}: {
|
|
825
|
+
stats: PetStats;
|
|
826
|
+
activity: PetActivity;
|
|
827
|
+
env: Environment;
|
|
828
|
+
isPaused: boolean;
|
|
829
|
+
width: number;
|
|
830
|
+
}) {
|
|
831
|
+
const t = useTheme();
|
|
832
|
+
const safeWidth = Math.max(1, Math.floor(width));
|
|
833
|
+
|
|
834
|
+
if (safeWidth < PET_SCENE_WIDTH) {
|
|
835
|
+
return (
|
|
836
|
+
<CompactPetPanel
|
|
837
|
+
stats={stats}
|
|
838
|
+
activity={activity}
|
|
839
|
+
env={env}
|
|
840
|
+
isPaused={isPaused}
|
|
841
|
+
width={safeWidth}
|
|
842
|
+
/>
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return (
|
|
847
|
+
<Box flexDirection="column" width={safeWidth} alignItems="center">
|
|
848
|
+
<Text bold color={t.primaryLight}>
|
|
849
|
+
{fitDisplayText("Drexler Pet Desk [office]", safeWidth)}
|
|
850
|
+
</Text>
|
|
851
|
+
<PetScene
|
|
852
|
+
stats={stats}
|
|
853
|
+
activity={activity}
|
|
854
|
+
env={env}
|
|
855
|
+
isPaused={isPaused}
|
|
856
|
+
width={safeWidth}
|
|
857
|
+
/>
|
|
858
|
+
</Box>
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function PetStatsBodyLine({
|
|
863
|
+
text,
|
|
864
|
+
width,
|
|
865
|
+
color,
|
|
866
|
+
}: {
|
|
867
|
+
text: string;
|
|
868
|
+
width: number;
|
|
869
|
+
color: string;
|
|
870
|
+
}) {
|
|
871
|
+
const t = useTheme();
|
|
872
|
+
const innerWidth = Math.max(1, width - 4);
|
|
873
|
+
const content = padDisplayText(text, innerWidth);
|
|
874
|
+
return (
|
|
875
|
+
<Text>
|
|
876
|
+
<Text color={t.primary}>│ </Text>
|
|
877
|
+
<Text color={color}>{content}</Text>
|
|
878
|
+
<Text color={t.primary}> │</Text>
|
|
879
|
+
</Text>
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function PetDashboardStatBar({
|
|
884
|
+
label,
|
|
885
|
+
value,
|
|
886
|
+
width,
|
|
887
|
+
}: {
|
|
888
|
+
label: string;
|
|
889
|
+
value: number;
|
|
890
|
+
width: number;
|
|
891
|
+
}) {
|
|
892
|
+
const t = useTheme();
|
|
893
|
+
const innerWidth = Math.max(1, width - 4);
|
|
894
|
+
const pct = `${Math.round(value).toString().padStart(3)}%`;
|
|
895
|
+
const labelText = padDisplayText(label, Math.min(7, innerWidth));
|
|
896
|
+
const prefixWidth = displayWidth(labelText);
|
|
897
|
+
const barWidth = Math.max(
|
|
898
|
+
1,
|
|
899
|
+
innerWidth - prefixWidth - displayWidth(pct) - 2,
|
|
900
|
+
);
|
|
901
|
+
const bounded = Math.max(0, Math.min(100, value));
|
|
902
|
+
const filled = Math.max(
|
|
903
|
+
0,
|
|
904
|
+
Math.min(barWidth, Math.round((bounded / 100) * barWidth)),
|
|
905
|
+
);
|
|
906
|
+
const empty = Math.max(0, barWidth - filled);
|
|
907
|
+
const bar = `${"█".repeat(filled)}${"░".repeat(empty)}`;
|
|
908
|
+
const used = prefixWidth + 1 + displayWidth(bar) + 1 + displayWidth(pct);
|
|
909
|
+
const isLow = value < 25;
|
|
910
|
+
const barColor = isLow
|
|
911
|
+
? t.warning
|
|
912
|
+
: label === "deals"
|
|
913
|
+
? t.primaryDim
|
|
914
|
+
: t.primaryLight;
|
|
915
|
+
|
|
916
|
+
if (innerWidth < 14) {
|
|
917
|
+
return (
|
|
918
|
+
<PetStatsBodyLine
|
|
919
|
+
text={`${label} ${pct}`}
|
|
920
|
+
width={width}
|
|
921
|
+
color={isLow ? t.warning : t.text}
|
|
922
|
+
/>
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return (
|
|
927
|
+
<Text>
|
|
928
|
+
<Text color={t.primary}>│ </Text>
|
|
929
|
+
<Text color={t.dim}>{labelText} </Text>
|
|
930
|
+
<Text color={barColor}>{bar}</Text>
|
|
931
|
+
<Text color={isLow ? t.warning : t.dim}> {pct}</Text>
|
|
932
|
+
<Text color={t.primary}>
|
|
933
|
+
{" ".repeat(Math.max(0, innerWidth - used))} │
|
|
934
|
+
</Text>
|
|
935
|
+
</Text>
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function PetStatsReadout({
|
|
940
|
+
stats,
|
|
941
|
+
activity,
|
|
942
|
+
width,
|
|
943
|
+
}: {
|
|
944
|
+
stats: PetStats;
|
|
945
|
+
activity: PetActivity;
|
|
946
|
+
width: number;
|
|
947
|
+
}) {
|
|
948
|
+
const t = useTheme();
|
|
949
|
+
const panelWidth = Math.max(
|
|
950
|
+
1,
|
|
951
|
+
Math.min(PET_STATS_MAX_WIDTH, Math.floor(width)),
|
|
952
|
+
);
|
|
953
|
+
const innerWidth = Math.max(1, panelWidth - 4);
|
|
954
|
+
const mood = getPetMood(stats);
|
|
955
|
+
const rank = rankLabel(getPetRank(stats));
|
|
956
|
+
const name = stats.name ?? "Drexler";
|
|
957
|
+
const activityLabel = activity === "idle" ? "idle" : activity;
|
|
958
|
+
const title = "Pet Stats";
|
|
959
|
+
const topPrefix = "╭─ ";
|
|
960
|
+
const topSuffix = " ";
|
|
961
|
+
const topRule = "─".repeat(
|
|
962
|
+
Math.max(
|
|
963
|
+
0,
|
|
964
|
+
panelWidth -
|
|
965
|
+
displayWidth(topPrefix) -
|
|
966
|
+
displayWidth(title) -
|
|
967
|
+
displayWidth(topSuffix) -
|
|
968
|
+
displayWidth("╮"),
|
|
969
|
+
),
|
|
970
|
+
);
|
|
971
|
+
const memo = `memo ${getPetStatusMessage(stats, 0)}`;
|
|
972
|
+
|
|
973
|
+
if (panelWidth < PET_STATS_MIN_WIDTH) {
|
|
974
|
+
return (
|
|
975
|
+
<Box flexDirection="column" width={panelWidth}>
|
|
976
|
+
<Text bold color={t.primaryLight}>
|
|
977
|
+
{fitDisplayText("Pet Stats", panelWidth)}
|
|
978
|
+
</Text>
|
|
979
|
+
<Text color={t.text}>
|
|
980
|
+
{fitDisplayText(`${name} / ${rank}`, panelWidth)}
|
|
981
|
+
</Text>
|
|
982
|
+
<Text color={t.dim}>
|
|
983
|
+
{fitDisplayText(`${mood} / ${activityLabel}`, panelWidth)}
|
|
984
|
+
</Text>
|
|
985
|
+
</Box>
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return (
|
|
990
|
+
<Box flexDirection="column" width={panelWidth}>
|
|
991
|
+
<Text color={t.primary}>
|
|
992
|
+
{topPrefix}
|
|
993
|
+
<Text bold color={t.primaryLight}>{title}</Text>
|
|
994
|
+
{topSuffix}
|
|
995
|
+
{topRule}
|
|
996
|
+
╮
|
|
997
|
+
</Text>
|
|
998
|
+
<PetStatsBodyLine
|
|
999
|
+
text={`name ${name}`}
|
|
1000
|
+
width={panelWidth}
|
|
1001
|
+
color={t.text}
|
|
1002
|
+
/>
|
|
1003
|
+
<PetStatsBodyLine
|
|
1004
|
+
text={`rank ${rank} · mood ${mood}`}
|
|
1005
|
+
width={panelWidth}
|
|
1006
|
+
color={t.primaryLight}
|
|
1007
|
+
/>
|
|
1008
|
+
<PetStatsBodyLine
|
|
1009
|
+
text={`activity ${activityLabel} · office`}
|
|
1010
|
+
width={panelWidth}
|
|
1011
|
+
color={t.dim}
|
|
1012
|
+
/>
|
|
1013
|
+
<PetStatsBodyLine
|
|
1014
|
+
text={`tenure ${formatTenure(petTenureMs(stats))}`}
|
|
1015
|
+
width={panelWidth}
|
|
1016
|
+
color={t.dim}
|
|
1017
|
+
/>
|
|
1018
|
+
<PetStatsBodyLine
|
|
1019
|
+
text={"─".repeat(innerWidth)}
|
|
1020
|
+
width={panelWidth}
|
|
1021
|
+
color={t.primaryDim}
|
|
1022
|
+
/>
|
|
1023
|
+
<PetDashboardStatBar
|
|
1024
|
+
label="happy"
|
|
1025
|
+
value={stats.happiness}
|
|
1026
|
+
width={panelWidth}
|
|
1027
|
+
/>
|
|
1028
|
+
<PetDashboardStatBar
|
|
1029
|
+
label="hunger"
|
|
1030
|
+
value={stats.hunger}
|
|
1031
|
+
width={panelWidth}
|
|
1032
|
+
/>
|
|
1033
|
+
<PetDashboardStatBar
|
|
1034
|
+
label="energy"
|
|
1035
|
+
value={stats.energy}
|
|
1036
|
+
width={panelWidth}
|
|
1037
|
+
/>
|
|
1038
|
+
<PetDashboardStatBar
|
|
1039
|
+
label="deals"
|
|
1040
|
+
value={stats.deals}
|
|
1041
|
+
width={panelWidth}
|
|
1042
|
+
/>
|
|
1043
|
+
<PetStatsBodyLine
|
|
1044
|
+
text={memo}
|
|
1045
|
+
width={panelWidth}
|
|
1046
|
+
color={t.dim}
|
|
1047
|
+
/>
|
|
1048
|
+
<Text color={t.primary}>{titledPanelBottom(panelWidth)}</Text>
|
|
1049
|
+
</Box>
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function PetDashboard({
|
|
1054
|
+
layout,
|
|
1055
|
+
stats,
|
|
1056
|
+
activity,
|
|
1057
|
+
env,
|
|
1058
|
+
isPaused,
|
|
1059
|
+
}: {
|
|
1060
|
+
layout: MascotLayout;
|
|
1061
|
+
stats: PetStats;
|
|
1062
|
+
activity: PetActivity;
|
|
1063
|
+
env: Environment;
|
|
1064
|
+
isPaused: boolean;
|
|
1065
|
+
}) {
|
|
1066
|
+
const t = useTheme();
|
|
1067
|
+
const sideBySide = layout.mode === "split";
|
|
1068
|
+
|
|
1069
|
+
if (layout.mode === "tiny" || layout.mode === "compact") {
|
|
1070
|
+
return (
|
|
1071
|
+
<Box
|
|
1072
|
+
marginLeft={layout.leftPanel.inset}
|
|
1073
|
+
width={layout.available}
|
|
1074
|
+
flexDirection="column"
|
|
1075
|
+
>
|
|
1076
|
+
<CompactPetPanel
|
|
1077
|
+
stats={stats}
|
|
1078
|
+
activity={activity}
|
|
1079
|
+
env={env}
|
|
1080
|
+
isPaused={isPaused}
|
|
1081
|
+
width={Math.max(1, layout.available)}
|
|
1082
|
+
/>
|
|
1083
|
+
</Box>
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
return (
|
|
1088
|
+
<Box width={layout.available}>
|
|
1089
|
+
<Box
|
|
1090
|
+
width={layout.available}
|
|
1091
|
+
borderStyle="round"
|
|
1092
|
+
borderColor={t.primary}
|
|
1093
|
+
paddingX={1}
|
|
1094
|
+
flexDirection={sideBySide ? "row" : "column"}
|
|
1095
|
+
alignItems={sideBySide ? "flex-start" : "center"}
|
|
1096
|
+
>
|
|
1097
|
+
<Box
|
|
1098
|
+
flexDirection="column"
|
|
1099
|
+
width={layout.leftPanel.width}
|
|
1100
|
+
alignItems="center"
|
|
1101
|
+
>
|
|
1102
|
+
<PetSceneReadout
|
|
1103
|
+
stats={stats}
|
|
1104
|
+
activity={activity}
|
|
1105
|
+
env={env}
|
|
1106
|
+
isPaused={isPaused}
|
|
1107
|
+
width={layout.leftPanel.width}
|
|
1108
|
+
/>
|
|
1109
|
+
</Box>
|
|
1110
|
+
{sideBySide ? (
|
|
1111
|
+
<>
|
|
1112
|
+
<Box
|
|
1113
|
+
flexDirection="column"
|
|
1114
|
+
width={SPLIT_DIVIDER_WIDTH}
|
|
1115
|
+
flexShrink={0}
|
|
1116
|
+
>
|
|
1117
|
+
{PET_SPLIT_DIVIDER_ROWS.map((idx) => (
|
|
1118
|
+
<Text key={idx} color={t.primaryDim}>
|
|
1119
|
+
{" │ "}
|
|
1120
|
+
</Text>
|
|
1121
|
+
))}
|
|
1122
|
+
</Box>
|
|
1123
|
+
<Box
|
|
1124
|
+
flexDirection="column"
|
|
1125
|
+
width={layout.rightColumn.width}
|
|
1126
|
+
paddingRight={RIGHT_COLUMN_PAD_RIGHT}
|
|
1127
|
+
>
|
|
1128
|
+
<Box marginLeft={layout.dealDesk.inset}>
|
|
1129
|
+
<PetStatsReadout
|
|
1130
|
+
stats={stats}
|
|
1131
|
+
activity={activity}
|
|
1132
|
+
width={Math.min(PET_STATS_MAX_WIDTH, layout.dealDesk.width)}
|
|
1133
|
+
/>
|
|
1134
|
+
</Box>
|
|
1135
|
+
</Box>
|
|
1136
|
+
</>
|
|
1137
|
+
) : (
|
|
1138
|
+
<Box marginTop={1} width={layout.tips.width} alignItems="center">
|
|
1139
|
+
<PetStatsReadout
|
|
1140
|
+
stats={stats}
|
|
1141
|
+
activity={activity}
|
|
1142
|
+
width={Math.max(COMPACT_PET_PANEL_MIN_WIDTH, layout.tips.width)}
|
|
1143
|
+
/>
|
|
1144
|
+
</Box>
|
|
1145
|
+
)}
|
|
1146
|
+
</Box>
|
|
1147
|
+
</Box>
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
783
1151
|
export function MascotDashboard({
|
|
784
1152
|
greeting,
|
|
785
1153
|
width,
|
|
786
1154
|
mood,
|
|
1155
|
+
mode = "normal",
|
|
1156
|
+
petStats,
|
|
1157
|
+
petActivity = "idle",
|
|
1158
|
+
petEnv = "office",
|
|
1159
|
+
petPaused = false,
|
|
787
1160
|
bootProgress = 1,
|
|
788
1161
|
state = INTRO_FRAMES[INTRO_FRAMES.length - 1]!,
|
|
789
1162
|
bar = introBootBar(INTRO_FRAMES.length - 1, INTRO_FRAMES.length),
|
|
@@ -799,6 +1172,18 @@ export function MascotDashboard({
|
|
|
799
1172
|
? fixedDisplayRows(greeting, layout.copy.width, 2)
|
|
800
1173
|
: [];
|
|
801
1174
|
|
|
1175
|
+
if (mode === "pet" && petStats) {
|
|
1176
|
+
return (
|
|
1177
|
+
<PetDashboard
|
|
1178
|
+
layout={layout}
|
|
1179
|
+
stats={petStats}
|
|
1180
|
+
activity={petActivity}
|
|
1181
|
+
env={petEnv}
|
|
1182
|
+
isPaused={petPaused}
|
|
1183
|
+
/>
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
802
1187
|
if (layout.mode === "tiny") {
|
|
803
1188
|
return (
|
|
804
1189
|
<Box width={layout.available} flexDirection="column">
|