1- /* status.c — top status bar .
1+ /* status.c — Pulse Bar .
22 *
33 * Reads /proc/uptime for clock display (since this rootfs has no RTC),
4- * /proc/stat for CPU usage, and surfaces the agent prediction. Renders
5- * along the very top edge of the screen above the desktop. */
4+ * /proc/stat for CPU usage, and surfaces the agent prediction.
5+ *
6+ * v0.38-C: Pulse Bar 2.0 per ChatGPT 2026-05-10 directive — make the
7+ * top bar feel like a real status instrument, not a thin text strip.
8+ * - bar height 32 → ATOMIK_PULSE_BAR_H (40 px) for visual weight
9+ * - ATOMiK wordmark scaled 1 → 2 (the visual anchor on the left)
10+ * - DATA / MODE / PERSONALITY badges with colored locator dots
11+ * - Event-pulse mini-waveform driven by atomik_event_total deltas
12+ * - Mode is now controllable via 'M' (DEV → DEMO → INVESTOR → DEV)
13+ * - Right segment still: wallet · cpu · uptime · version */
614#include "atomik_os.h"
715#include <stdio.h>
816#include <string.h>
@@ -19,6 +27,20 @@ static unsigned long s_last_total = 0;
1927static unsigned long s_last_idle = 0 ;
2028static int s_cpu_pct = 0 ;
2129
30+ /* === v0.38-C: event-pulse mini-waveform state ===
31+ *
32+ * Sample atomik_event_total() at fixed cadence; render the last N
33+ * deltas as a thin sparkline inside the Pulse Bar. Visible signal of
34+ * "is the system alive and doing work right now." Real data — the
35+ * bus is the same event ring every other surface consumes. */
36+ #define PULSE_HISTORY_N 32
37+ #define PULSE_SAMPLE_MS 150
38+ static uint16_t s_pulse_hist [PULSE_HISTORY_N ];
39+ static uint8_t s_pulse_head ;
40+ static uint8_t s_pulse_count ;
41+ static unsigned long s_last_pulse_sample_ms ;
42+ static unsigned long s_last_pulse_total ;
43+
2244static void update_cpu (void ) {
2345 FILE * f = fopen ("/proc/stat" , "r" );
2446 if (!f ) return ;
@@ -51,6 +73,93 @@ static void format_uptime(char *out, size_t cap) {
5173 snprintf (out , cap , "uptime %02d:%02d:%02d" , h , m , s );
5274}
5375
76+ /* v0.38-C: poll the event bus at PULSE_SAMPLE_MS and push the delta
77+ * into the sparkline ring. Called every frame; self-rate-limits. No
78+ * heavy work — one atomik_event_total() call + a ring store. */
79+ void status_tick (void ) {
80+ unsigned long now = anim_now_ms ();
81+ if (now - s_last_pulse_sample_ms < PULSE_SAMPLE_MS ) return ;
82+ s_last_pulse_sample_ms = now ;
83+ unsigned long total = atomik_event_total ();
84+ unsigned long delta = total - s_last_pulse_total ;
85+ s_last_pulse_total = total ;
86+ /* Clamp so a burst doesn't blow out the auto-normalization. */
87+ if (delta > 0xFFFF ) delta = 0xFFFF ;
88+ s_pulse_hist [s_pulse_head ] = (uint16_t )delta ;
89+ s_pulse_head = (s_pulse_head + 1 ) % PULSE_HISTORY_N ;
90+ if (s_pulse_count < PULSE_HISTORY_N ) s_pulse_count ++ ;
91+ }
92+
93+ /* Thin Bresenham segment — same approach as fabric_history mini-waves. */
94+ static void pulse_line (int x0 , int y0 , int x1 , int y1 , pixel_t color ) {
95+ int dx = (x1 > x0 ) ? (x1 - x0 ) : (x0 - x1 );
96+ int dy = (y1 > y0 ) ? (y1 - y0 ) : (y0 - y1 );
97+ int sx = (x0 < x1 ) ? 1 : -1 ;
98+ int sy = (y0 < y1 ) ? 1 : -1 ;
99+ int err = dx - dy ;
100+ while (1 ) {
101+ draw_pixel (x0 , y0 , color );
102+ if (x0 == x1 && y0 == y1 ) break ;
103+ int e2 = err * 2 ;
104+ if (e2 > - dy ) { err -= dy ; x0 += sx ; }
105+ if (e2 < dx ) { err += dx ; y0 += sy ; }
106+ }
107+ }
108+
109+ static int draw_event_pulse (int x , int y , int w , int h ) {
110+ /* Track outline — dim 1-px hairline so the sparkline always sits
111+ * on a visible bed. */
112+ pixel_t bed = rgb (0x11 , 0x18 , 0x28 );
113+ draw_rect (x , y , w , h , bed );
114+ draw_rect (x , y + h - 1 , w , 1 , ATOMIK_DOCK_BORDER );
115+ if (s_pulse_count < 2 ) return w ;
116+ /* Auto-normalize: max-of-window so quiet periods don't compress. */
117+ uint16_t mx = 0 ;
118+ for (uint8_t i = 0 ; i < s_pulse_count ; i ++ )
119+ if (s_pulse_hist [i ] > mx ) mx = s_pulse_hist [i ];
120+ if (mx == 0 ) {
121+ /* Constant zero — draw a flat dim baseline. */
122+ draw_rect (x , y + h - 2 , w , 1 , ATOMIK_DOCK_BORDER );
123+ return w ;
124+ }
125+ /* Walk samples in chronological order. */
126+ int prev_px = -1 , prev_py = -1 ;
127+ for (uint8_t i = 0 ; i < s_pulse_count ; i ++ ) {
128+ int idx = (s_pulse_head - s_pulse_count + i + PULSE_HISTORY_N )
129+ % PULSE_HISTORY_N ;
130+ uint16_t v = s_pulse_hist [idx ];
131+ int px = x + (int )((long )i * (w - 1 ) / (PULSE_HISTORY_N - 1 ));
132+ int py = y + h - 2 -
133+ (int )((long )v * (h - 3 ) / mx );
134+ if (py < y + 1 ) py = y + 1 ;
135+ if (py > y + h - 2 ) py = y + h - 2 ;
136+ if (prev_px >= 0 )
137+ pulse_line (prev_px , prev_py , px , py , ATOMIK_SEM_HARDWARE );
138+ else
139+ draw_pixel (px , py , ATOMIK_SEM_HARDWARE );
140+ prev_px = px ;
141+ prev_py = py ;
142+ }
143+ return w ;
144+ }
145+
146+ static const char * mode_label (metric_mode_t m ) {
147+ switch (m ) {
148+ case METRIC_MODE_DEV : return "DEV" ;
149+ case METRIC_MODE_DEMO : return "DEMO" ;
150+ case METRIC_MODE_INVESTOR : return "INVESTOR" ;
151+ default : return "?" ;
152+ }
153+ }
154+ static pixel_t mode_color (metric_mode_t m ) {
155+ switch (m ) {
156+ case METRIC_MODE_DEV : return ATOMIK_SEM_WASTE ; /* amber — relaxed truth */
157+ case METRIC_MODE_DEMO : return ATOMIK_SEM_HARDWARE ; /* cyan — rehearsal */
158+ case METRIC_MODE_INVESTOR : return ATOMIK_SEM_SAVINGS ; /* green — strictest */
159+ default : return ATOMIK_FG_DIM ;
160+ }
161+ }
162+
54163void status_draw (void ) {
55164 update_cpu ();
56165
@@ -66,9 +175,10 @@ void status_draw(void) {
66175 * which appeared bottom-biased on monitors with less than 32 px
67176 * of overscan (text 8 px from the bottom edge of a visibly-larger
68177 * bar). User feedback 2026-05-07. */
69- const int bar_h = 32 ;
178+ const int bar_h = ATOMIK_PULSE_BAR_H ; /* v0.38-C: 40 px */
70179 const int bar_y = ATOMIK_SAFE_TOP ;
71180 const int ty = bar_y + (bar_h - text_height (1 )) / 2 ;
181+ const int ty_brand = bar_y + (bar_h - text_height (2 )) / 2 ;
72182
73183 /* Bar fills only the safe zone — wallpaper covers y=0..SAFE_TOP
74184 * (cropped/invisible on this monitor; visible as a thin strip on
@@ -98,8 +208,10 @@ void status_draw(void) {
98208 */
99209 const char * brand = "ATOMiK" ;
100210 int cur_x = ATOMIK_GRID_L ;
101- draw_text (cur_x , ty , brand , 1 , ATOMIK_SEM_HARDWARE );
102- cur_x += text_width (brand , 1 ) + ATOMIK_GRID_M ;
211+ /* v0.38-C: scale-2 brand — visual anchor on the left. Scale-2
212+ * text is 32 px tall, vertically centered inside the 40 px bar. */
213+ draw_text (cur_x , ty_brand , brand , 2 , ATOMIK_SEM_HARDWARE );
214+ cur_x += text_width (brand , 2 ) + ATOMIK_GRID_L ;
103215
104216 /* Vertical separator between sections — dim, 1 px wide, stops the
105217 * bar from feeling like one long undifferentiated text run. */
@@ -137,6 +249,28 @@ void status_draw(void) {
137249 cur_x += ATOMIK_GRID_M + 1 ;
138250 }
139251
252+ /* === MODE: <DEV/DEMO/INVESTOR> badge — v0.38-C ===
253+ *
254+ * Surfaces the metric_mode_t that gates which metric sources can
255+ * display. DEV (amber) shows everything including MOCK; DEMO
256+ * (cyan) hides pure MOCK; INVESTOR (green) shows only LIVE +
257+ * DERIVED + labeled SCENARIO. Cycle with 'M' key from the global
258+ * key router. */
259+ {
260+ metric_mode_t m = metric_mode ();
261+ char mode_badge [32 ];
262+ snprintf (mode_badge , sizeof mode_badge , "MODE: %s" , mode_label (m ));
263+ pixel_t mcol = mode_color (m );
264+ int dot_y4 = bar_y + (bar_h - ATOMIK_GRID_M ) / 2 ;
265+ draw_rect (cur_x , dot_y4 , ATOMIK_GRID_M , ATOMIK_GRID_M , mcol );
266+ cur_x += ATOMIK_GRID_M + ATOMIK_GRID_S ;
267+ draw_text (cur_x , ty , mode_badge , 1 , mcol );
268+ cur_x += text_width (mode_badge , 1 ) + ATOMIK_GRID_M ;
269+ draw_rect (cur_x , bar_y + ATOMIK_GRID_S ,
270+ 1 , bar_h - ATOMIK_GRID_S * 2 , ATOMIK_DOCK_BORDER );
271+ cur_x += ATOMIK_GRID_M + 1 ;
272+ }
273+
140274 /* === Active personality badge ===
141275 *
142276 * v0.34-B: surfaces the current Resource Fabric personality globally
@@ -217,6 +351,24 @@ void status_draw(void) {
217351 }
218352 }
219353
354+ /* === Event-pulse mini-waveform — v0.38-C ===
355+ *
356+ * Sparkline of atomik_event_total() deltas over the last ~5 s
357+ * (32 samples × 150 ms). Real data — the same event ring every
358+ * other surface consumes. Visual signal: "is the system alive
359+ * and producing events right now?". Cyan (HARDWARE) so it reads
360+ * as system telemetry, not application state. */
361+ {
362+ const char * pulse_label = "PULSE" ;
363+ draw_text (cur_x , ty , pulse_label , 1 , ATOMIK_FG_DIM );
364+ cur_x += text_width (pulse_label , 1 ) + ATOMIK_GRID_S ;
365+ int pulse_w = 96 ;
366+ int pulse_h = bar_h - ATOMIK_GRID_S * 2 ;
367+ int pulse_y = bar_y + ATOMIK_GRID_S ;
368+ draw_event_pulse (cur_x , pulse_y , pulse_w , pulse_h );
369+ cur_x += pulse_w + ATOMIK_GRID_M ;
370+ }
371+
220372 /* Separator before window strip + hints. */
221373 draw_rect (cur_x , bar_y + ATOMIK_GRID_S ,
222374 1 , bar_h - ATOMIK_GRID_S * 2 , ATOMIK_DOCK_BORDER );
0 commit comments