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.
@@ -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">