diff --git a/README.md b/README.md index 264024c..ef50a0b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The 3.5" AX206 USB displays are cheap and widely available, but most of the soft This fork aims to fix that by providing ready-to-use themes ported from the [Turing Smart Screen](https://github.com/mathoudebine/turing-smart-screen-python) theme ecosystem and adapted to work with LCD4Linux's config system. In the process, new widgets were added: **Gauge** (circular arc rings) and **Sparkline** (line graph history); to support the variety of visual styles these themes use. The goal is to make it as easy as possible to plug in a display and have a polished system monitor running in minutes. -The best source for general LCD4Linux information is [The unofficial LCD4Linux Wiki](https://wiki.lcd4linux.tk/doku.php/start). +For more information visit the [LCD4Linux-ax206 Wiki](https://github.com/amd989/lcd4linux-ax206/wiki). ![Example](lcd4linux_example.png) diff --git a/TODO b/TODO deleted file mode 100644 index 39c9520..0000000 --- a/TODO +++ /dev/null @@ -1,5 +0,0 @@ -# $Id$ -# $URL$ - -Sorry, there is no TODO anymore. -Go to http://lcd4linux.bulix.org for all the documentation. diff --git a/themes/NAS/dpf_4baynas.conf b/themes/NAS/dpf_4baynas.conf index bfa5227..d5f22de 100644 --- a/themes/NAS/dpf_4baynas.conf +++ b/themes/NAS/dpf_4baynas.conf @@ -177,7 +177,7 @@ Widget Storage_Status { class 'Truetype' expression '• Healthy' font 'fonts/jetbrains-mono/JetBrainsMono-Regular.ttf' - fcolor '00aaff' + fcolor '296ca3' size 9 align 'R' width 100 @@ -190,7 +190,7 @@ Widget RAID_Label { class 'Truetype' expression 'SNAPRAID' font 'fonts/jetbrains-mono/JetBrainsMono-Bold.ttf' - fcolor '00aaff' + fcolor '215683' size 9 align 'L' width 120 @@ -219,7 +219,7 @@ Widget Main_Storage_Bar { length 280 width 5 direction 'E' - color '00aaff' + color '296ca3' background '333333' min 0 max 100 @@ -246,7 +246,7 @@ Widget Bay1_Temp { class 'Truetype' expression precision(exec('cat /sys/block/' . bay1_blk . '/device/hwmon/hwmon*/temp1_input 2>/dev/null || echo 32000', 5000) / 1000, 0) . '°C' font 'fonts/jetbrains-mono/JetBrainsMono-Bold.ttf' - fcolor '00aaff' + fcolor '296ca3' size 11 align 'C' width 50 @@ -288,7 +288,7 @@ Widget Bay2_Temp { class 'Truetype' expression precision(exec('cat /sys/block/' . bay2_blk . '/device/hwmon/hwmon*/temp1_input 2>/dev/null || echo 35000', 5000) / 1000, 0) . '°C' font 'fonts/jetbrains-mono/JetBrainsMono-Bold.ttf' - fcolor '00aaff' + fcolor '296ca3' size 11 align 'C' width 50 @@ -330,7 +330,7 @@ Widget Bay3_Temp { class 'Truetype' expression precision(exec('cat /sys/block/' . bay3_blk . '/device/hwmon/hwmon*/temp1_input 2>/dev/null || echo 38000', 5000) / 1000, 0) . '°C' font 'fonts/jetbrains-mono/JetBrainsMono-Bold.ttf' - fcolor '00aaff' + fcolor '296ca3' size 11 align 'C' width 50 @@ -372,7 +372,7 @@ Widget Bay4_Temp { class 'Truetype' expression precision(exec('cat /sys/block/' . bay4_blk . '/device/hwmon/hwmon*/temp1_input 2>/dev/null || echo 40000', 5000) / 1000, 0) . '°C' font 'fonts/jetbrains-mono/JetBrainsMono-Bold.ttf' - fcolor '00aaff' + fcolor '296ca3' size 11 align 'C' width 50 @@ -453,14 +453,39 @@ Widget IO_Unit { } -Widget IO_Sparkline { +# Reads (field 3) — Layout layer 1; fillunder = gradient under curve (see Layer 0/1) +Widget IO_Sparkline_Read { class 'Sparkline' - expression exec('f=/tmp/lcd_io_' . io_disk . '; set -- $(cat /sys/block/' . io_disk . '/stat); c=$(($3+$7)); if [ -f $f ] && [ $(($(date +%s)-$(stat -c %Y $f))) -lt 15 ]; then p=$(cat $f); else p=$c; fi; echo $c > $f; echo $((c-p))', 5000) + expression exec('f=/tmp/lcd_io_' . io_disk . '_r; set -- $(cat /sys/block/' . io_disk . '/stat); c=$3; if [ -f $f ] && [ $(($(date +%s)-$(stat -c %Y $f))) -lt 15 ]; then p=$(cat $f); else p=$c; fi; echo $c > $f; echo $((c-p))', 5000) + width 80 + length 139 + samples 60 + color '296ca3' + background '000000ff' + thickness 2 + smooth 1 + smoothsteps 72 + valueblur 5 + fillunder 1 + fillalpha 56 + stroke 'solid' + update tack +} + +# Writes (field 7) — Layout layer 0 (above reads); lighter tint so dashes read against the read line +Widget IO_Sparkline_Write { + class 'Sparkline' + expression exec('f=/tmp/lcd_io_' . io_disk . '_w; set -- $(cat /sys/block/' . io_disk . '/stat); c=$7; if [ -f $f ] && [ $(($(date +%s)-$(stat -c %Y $f))) -lt 15 ]; then p=$(cat $f); else p=$c; fi; echo $c > $f; echo $((c-p))', 5000) width 80 length 139 samples 60 color '00aaff' background '000000ff' + thickness 2 + smooth 1 + smoothsteps 72 + valueblur 5 + stroke 'dashed' update tack } @@ -498,7 +523,7 @@ Widget CPU_Val { class 'Truetype' expression precision(proc_stat::cpu('busy', 500), 0).'%' font 'fonts/jetbrains-mono/JetBrainsMono-Bold.ttf' - fcolor '00aaff' + fcolor '296ca3' size 8 align 'R' width 35 @@ -514,7 +539,7 @@ Widget CPU_Bar { length 125 width 4 direction 'E' - color '00aaff' + color '296ca3' background '333333' min 0 max 100 @@ -539,7 +564,7 @@ Widget MEM_Val { class 'Truetype' expression precision((meminfo('MemTotal') - meminfo('MemAvailable')) / meminfo('MemTotal') * 100, 0).'%' font 'fonts/jetbrains-mono/JetBrainsMono-Bold.ttf' - fcolor '00aaff' + fcolor '296ca3' size 8 align 'R' width 35 @@ -555,7 +580,7 @@ Widget MEM_Bar { length 125 width 4 direction 'E' - color '00aaff' + color '296ca3' background '333333' min 0 max 100 @@ -580,7 +605,7 @@ Widget TEMP_Val { class 'Truetype' expression precision(exec('cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || cat /sys/class/thermal/thermal_zone1/temp 2>/dev/null || echo 45000', 10000) / 1000, 0).'°C' font 'fonts/jetbrains-mono/JetBrainsMono-Bold.ttf' - fcolor '00aaff' + fcolor '296ca3' size 8 align 'R' width 35 @@ -596,7 +621,7 @@ Widget TEMP_Bar { length 125 width 4 direction 'E' - color '00aaff' + color '296ca3' background '333333' max 100 update 5000 @@ -607,6 +632,10 @@ Widget TEMP_Bar { # Layout # --------------------------------------------------------------- Layout layout_4baynas { + # Layer 0 is composited above Layer 1 (see drv_generic_graphic_blend): sparkline write on top of read + Layer 0 { + X388.Y14 'IO_Sparkline_Write' + } Layer 1 { X5.Y104 'Time' X10.Y215 'TimeAmPm' @@ -636,7 +665,7 @@ Layout layout_4baynas { X365.Y30 'IO_Read_Val' X365.Y77 'IO_Write_Val' X365.Y120 'IO_Unit' - X388.Y14 'IO_Sparkline' + X388.Y14 'IO_Sparkline_Read' X343.Y189 'System_Title' X369.Y178 'CPU_Label' X369.Y258 'CPU_Val' @@ -656,5 +685,17 @@ Layout layout_4baynas { Display 'dpf' +Layout 'layout_4baynas' + +Mirror 'VNC' -Layout 'layout_4baynas' \ No newline at end of file +Display VNC { + Driver 'VNC' + Font '6x8' + Port '5900' + Xres '320' + Yres '480' + Bpp '4' + Maxclients '2' + Osd_showtime '2000' +} \ No newline at end of file diff --git a/themes/OPNSense/dpf_opnsense.conf b/themes/OPNSense/dpf_opnsense.conf index 075cbd7..47fbf62 100644 --- a/themes/OPNSense/dpf_opnsense.conf +++ b/themes/OPNSense/dpf_opnsense.conf @@ -160,14 +160,40 @@ Widget Net_Unit { } -Widget Net_Sparkline { +# Download (Rx) — Layer 1; solid + fill under curve +Widget Net_Sparkline_Down { class 'Sparkline' - expression netdev(net_if, 'Rx_bytes', 500) + netdev(net_if, 'Tx_bytes', 500) + expression netdev(net_if, 'Rx_bytes', 500) width 60 length 290 samples 60 color 'f97316' background '000000ff' + thickness 2 + smooth 1 + smoothsteps 48 + valueblur 3 + fillunder 1 + fillalpha 56 + stroke solid + update tack +} + +# Upload (Tx) — Layer 0 (above download); dashed +Widget Net_Sparkline_Up { + class 'Sparkline' + expression netdev(net_if, 'Tx_bytes', 500) + width 60 + length 290 + samples 60 + color 'f97316' + background '000000ff' + thickness 2 + smooth 1 + smoothsteps 48 + valueblur 3 + fillunder 0 + stroke dashed update tack } @@ -488,10 +514,10 @@ Widget CONN_Label { Widget CONN_Val { class 'Truetype' # Active TCP connections - # Linux: ss -t state established (iproute2, not available on FreeBSD) # FreeBSD / OPNsense (pfctl): exec('pfctl -si 2>/dev/null | grep "current entries" | grep -oE "[0-9]+" | head -1', 5000) # FreeBSD / OPNsense (netstat): exec('netstat -an 2>/dev/null | grep ESTABLISHED | wc -l', 5000) - expression exec('netstat -an 2>/dev/null | grep ESTABLISHED | wc -l', 5000) + # Linux: ss -t state established + expression exec('ss -t state established 2>/dev/null | tail -n +2 | wc -l', 5000) font 'fonts/source-sans-3/SourceSans3-Bold.ttf' fcolor 'f97316' size 10 @@ -519,9 +545,9 @@ Widget BLOCK_Label { Widget BLOCK_Val { class 'Truetype' # Firewall block rules hit count (or tracked state count on Linux) - # Linux: cat /proc/sys/net/netfilter/nf_conntrack_count (conntrack, not available on FreeBSD) # FreeBSD / OPNsense: exec('pfctl -vsr 2>/dev/null | grep -c "^block"', 10000) - expression exec('pfctl -vsr 2>/dev/null | grep -c "^block"', 10000) + # Linux: cat /proc/sys/net/netfilter/nf_conntrack_count + expression exec('cat /proc/sys/net/netfilter/nf_conntrack_count 2>/dev/null || echo 0', 10000) font 'fonts/source-sans-3/SourceSans3-Bold.ttf' fcolor 'f59e0b' size 10 @@ -549,6 +575,9 @@ Widget FW_Sparkline { # Layout # --------------------------------------------------------------- Layout layout_opnsense { + Layer 0 { + X132.Y16 'Net_Sparkline_Up' + } Layer 1 { X5.Y110 'Time' X30.Y210 'TimeAmPm' @@ -558,7 +587,7 @@ Layout layout_opnsense { X112.Y15 'Net_Down_Val' X112.Y75 'Net_Up_Val' X112.Y128 'Net_Unit' - X132.Y16 'Net_Sparkline' + X132.Y16 'Net_Sparkline_Down' X214.Y37 'Devices_Title' X214.Y125 'Devices_Count' X277.Y27 'LAN_Count' diff --git a/widget_sparkline.c b/widget_sparkline.c index ed99e2d..e66b164 100644 --- a/widget_sparkline.c +++ b/widget_sparkline.c @@ -29,6 +29,8 @@ #include #include #include +#include +#include #ifdef HAVE_GD_GD_H #include @@ -92,6 +94,396 @@ static int getcolorint(PROPERTY *colorprop, void *Image) } +typedef struct { + double x, y; +} SparkPt; + + +static int spark_rgb_from_prop(PROPERTY *prop, unsigned char *r, unsigned char *g, unsigned char *b) +{ + char *colorstr; + char *e; + unsigned long l; + + colorstr = P2S(prop); + if (colorstr == NULL || strlen(colorstr) < 6) + return -1; + if (strlen(colorstr) == 8) { + l = strtoul(colorstr, &e, 16); + *r = (unsigned char)((l >> 24) & 0xff); + *g = (unsigned char)((l >> 16) & 0xff); + *b = (unsigned char)((l >> 8) & 0xff); + } else { + l = strtoul(colorstr, &e, 16); + *r = (unsigned char)((l >> 16) & 0xff); + *g = (unsigned char)((l >> 8) & 0xff); + *b = (unsigned char)(l & 0xff); + } + return 0; +} + + +/* Leftmost x on the polyline at height iy (GD coords: x = value axis, y = time). */ +static double spark_min_x_at_y(SparkPt *pts, int n, double iy) +{ + double xmin = 1e300; + int j; + int any = 0; + + for (j = 0; j < n - 1; j++) { + double x0 = pts[j].x, x1 = pts[j + 1].x; + double y0 = pts[j].y, y1 = pts[j + 1].y; + + if (fabs(y1 - y0) < 1e-9) { + if (fabs(iy - y0) < 0.5) { + double xa = x0 < x1 ? x0 : x1; + + if (xa < xmin) { + xmin = xa; + any = 1; + } + } + continue; + } + if ((iy >= y0 && iy <= y1) || (iy >= y1 && iy <= y0)) { + double x = x0 + (x1 - x0) * (iy - y0) / (y1 - y0); + + if (x < xmin) { + xmin = x; + any = 1; + } + } + } + return any ? xmin : -1.0; +} + + +/* Horizontal gradient: strong at the curve, transparent toward x = xsize-1 (min value). + * Interpolate the left boundary per scanline when the spline wiggles in y (some rows miss + * ray hits), avoiding vertical cracks from sparse scanline sampling. */ +static void spark_fill_gradient_under(gdImagePtr im, SparkPt *pts, int n, int xsize, int ysize, unsigned char r, + unsigned char g, unsigned char b, int alpha_curve) +{ + double *xcrow; + int iy, ix; + int xlast = xsize - 1; + int k; + + if (n < 2 || xsize < 2) + return; + + xcrow = malloc((size_t)ysize * sizeof(double)); + if (xcrow == NULL) + return; + + for (iy = 0; iy < ysize; iy++) + xcrow[iy] = spark_min_x_at_y(pts, n, (double)iy + 0.5); + + for (iy = 0; iy < ysize; iy++) { + if (xcrow[iy] >= 0.0) + continue; + k = iy - 1; + while (k >= 0 && xcrow[k] < 0.0) + k--; + if (k >= 0) { + int k2 = iy + 1; + + while (k2 < ysize && xcrow[k2] < 0.0) + k2++; + if (k2 < ysize) + xcrow[iy] = xcrow[k] + (xcrow[k2] - xcrow[k]) * (double)(iy - k) / (double)(k2 - k); + else + xcrow[iy] = xcrow[k]; + } else { + k = iy + 1; + while (k < ysize && xcrow[k] < 0.0) + k++; + if (k < ysize) + xcrow[iy] = xcrow[k]; + else + xcrow[iy] = -1.0; + } + } + + gdImageAlphaBlending(im, 1); + + for (iy = 0; iy < ysize; iy++) { + double xc = xcrow[iy]; + double span; + double frac; + int a; + + if (xc < 0.0) + continue; + if (xc > (double)xlast) + continue; + + span = (double)xlast - xc; + if (span < 1.0) + span = 1.0; + + for (ix = (int)floor(xc); ix <= xlast; ix++) { + if (ix < 0) + continue; + frac = ((double)ix - xc) / span; + if (frac < 0.0) + frac = 0.0; + if (frac > 1.0) + frac = 1.0; + /* GD alpha: 0 = opaque, 127 = transparent. Start ~44 at the curve so the fill + * stays softer than full-opacity; ramp to fully transparent at the far edge. */ + { + int amax = 127 - alpha_curve; + a = alpha_curve + (int)((double)amax * frac + 0.5); + } + if (a > 127) + a = 127; + gdImageSetPixel(im, ix, iy, gdTrueColorAlpha(r, g, b, a)); + } + } + + free(xcrow); +} + +#define SPARK_SMOOTH_CAP 256 + +#define SPARK_STROKE_SOLID 0 +#define SPARK_STROKE_DASHED 1 +#define SPARK_STROKE_DOTTED 2 + +static int sparkline_parse_stroke(const char *s) +{ + if (s == NULL) + return SPARK_STROKE_SOLID; + if (!strcasecmp(s, "dashed") || !strcasecmp(s, "dash")) + return SPARK_STROKE_DASHED; + if (!strcasecmp(s, "dotted") || !strcasecmp(s, "dot")) + return SPARK_STROKE_DOTTED; + return SPARK_STROKE_SOLID; +} + +/* Endpoints: reflect phantom points so adjacent spline pieces are C1 (no kinks at samples). */ +/* 3-tap binomial low-pass along the time series (reduces step / square-wave look). */ +static void spark_value_smooth(double *data, double *tmp, int n, int passes) +{ + int p, i; + + for (p = 0; p < passes; p++) { + if (n < 2) + return; + tmp[0] = data[0]; + tmp[n - 1] = data[n - 1]; + for (i = 1; i < n - 1; i++) + tmp[i] = 0.25 * data[i - 1] + 0.5 * data[i] + 0.25 * data[i + 1]; + memcpy(data, tmp, (size_t)n * sizeof(double)); + } +} + +static void spark_catmull_controls(const SparkPt *ctrl, int n, int seg, SparkPt *p0, SparkPt *p1, SparkPt *p2, + SparkPt *p3) +{ + *p1 = ctrl[seg]; + *p2 = ctrl[seg + 1]; + if (seg == 0) { + p0->x = 2.0 * ctrl[0].x - ctrl[1].x; + p0->y = 2.0 * ctrl[0].y - ctrl[1].y; + } else { + *p0 = ctrl[seg - 1]; + } + if (seg + 2 >= n) { + p3->x = 2.0 * ctrl[n - 1].x - ctrl[n - 2].x; + p3->y = 2.0 * ctrl[n - 1].y - ctrl[n - 2].y; + } else { + *p3 = ctrl[seg + 2]; + } +} + +/* Catmull-Rom segment from P1 to P2 with controls P0..P3 (uniform parameterization). */ +static void spark_catmull_rom(double t, SparkPt p0, SparkPt p1, SparkPt p2, SparkPt p3, SparkPt *out) +{ + double t2 = t * t; + double t3 = t2 * t; + + out->x = 0.5 * ((2.0 * p1.x) + + (-p0.x + p2.x) * t + + (2.0 * p0.x - 5.0 * p1.x + 4.0 * p2.x - p3.x) * t2 + + (-p0.x + 3.0 * p1.x - 3.0 * p2.x + p3.x) * t3); + out->y = 0.5 * ((2.0 * p1.y) + + (-p0.y + p2.y) * t + + (2.0 * p0.y - 5.0 * p1.y + 4.0 * p2.y - p3.y) * t2 + + (-p0.y + 3.0 * p1.y - 3.0 * p2.y + p3.y) * t3); +} + + +static int spark_build_full_polyline(SparkPt *ctrl, int count, int do_smooth, int steps, SparkPt *segbuf, + SparkPt *out, int out_max) +{ + int seg, j, fn; + + fn = 0; + for (seg = 0; seg < count - 1; seg++) { + int seg_n; + + if (!do_smooth) { + segbuf[0] = ctrl[seg]; + segbuf[1] = ctrl[seg + 1]; + seg_n = 2; + } else { + SparkPt p0, p1, p2, p3; + + spark_catmull_controls(ctrl, count, seg, &p0, &p1, &p2, &p3); + for (j = 0; j <= steps; j++) { + double t = (double)j / (double)steps; + + spark_catmull_rom(t, p0, p1, p2, p3, &segbuf[j]); + } + seg_n = steps + 1; + } + { + int st = (seg == 0) ? 0 : 1; + + for (j = st; j < seg_n; j++) { + if (fn >= out_max) + return fn; + out[fn++] = segbuf[j]; + } + } + } + return fn; +} + + +static void spark_draw_polyline_solid(gdImagePtr im, SparkPt *pts, int n, int color, int ox, int oy) +{ + int j; + + if (n < 2) + return; + for (j = 0; j < n - 1; j++) { + gdImageLine(im, + (int)(pts[j].x + 0.5) + ox, (int)(pts[j].y + 0.5) + oy, + (int)(pts[j + 1].x + 0.5) + ox, (int)(pts[j + 1].y + 0.5) + oy, + color); + } +} + +/* Walk the polyline, drawing only dash segments (on/off lengths in pixels). + * dash_rem / dash_drawing carry phase across consecutive calls so dashes are not + * restarted every sparkline sample segment (each ~1–3px — far shorter than one dash). + */ +static void spark_draw_polyline_dashed(gdImagePtr im, SparkPt *pts, int n, int color, int ox, int oy, + double dash_on, double dash_gap, + double *dash_rem, int *dash_drawing) +{ + int i; + int drawing; + double rem; + double cx, cy; + + if (n < 2) + return; + + i = 0; + cx = pts[0].x; + cy = pts[0].y; + rem = *dash_rem; + drawing = *dash_drawing; + + while (i < n - 1) { + double tx = pts[i + 1].x; + double ty = pts[i + 1].y; + double dx = tx - cx; + double dy = ty - cy; + double elen = sqrt(dx * dx + dy * dy); + + if (elen < 1e-9) { + i++; + cx = pts[i].x; + cy = pts[i].y; + continue; + } + + while (elen > 1e-9) { + double step = rem < elen ? rem : elen; + double nx = cx + (dx / elen) * step; + double ny = cy + (dy / elen) * step; + + if (drawing) { + gdImageLine(im, + (int)(cx + 0.5) + ox, (int)(cy + 0.5) + oy, + (int)(nx + 0.5) + ox, (int)(ny + 0.5) + oy, + color); + } + cx = nx; + cy = ny; + dx = tx - cx; + dy = ty - cy; + elen = sqrt(dx * dx + dy * dy); + rem -= step; + if (rem < 1e-9) { + drawing = !drawing; + rem = drawing ? dash_on : dash_gap; + } + } + i++; + cx = pts[i].x; + cy = pts[i].y; + } + + *dash_rem = rem; + *dash_drawing = drawing; +} + +static void spark_dash_lengths(int thickness, int stroke, double *dash_on, double *dash_gap) +{ + double scale = (thickness > 1) ? (0.65 * (double)thickness + 0.35) : 1.0; + + if (stroke == SPARK_STROKE_DOTTED) { + *dash_on = 3.0 * scale; + *dash_gap = 6.0 * scale; + } else { + /* Longer gaps than dashes so thick gdImageLine caps do not bridge gaps (looks solid). */ + *dash_on = 14.0 * scale; + *dash_gap = 14.0 * scale; + } +} + +static void spark_draw_polyline_styled(gdImagePtr im, SparkPt *pts, int n, int color, int thickness, + int stroke, int ox, int oy, double *dash_rem, int *dash_drawing) +{ + double dash_on, dash_gap; + double local_rem; + int local_draw; + int draw_thick; + + if (n < 2) + return; + + if (stroke == SPARK_STROKE_SOLID) { + gdImageSetThickness(im, thickness); + spark_draw_polyline_solid(im, pts, n, color, ox, oy); + } else { + /* Thick strokes merge dash gaps; draw dashes slightly thinner than the main solid line. */ + draw_thick = thickness; + if (thickness > 2) + draw_thick = thickness / 2; + if (draw_thick < 1) + draw_thick = 1; + gdImageSetThickness(im, draw_thick); + spark_dash_lengths(thickness, stroke, &dash_on, &dash_gap); + if (dash_rem == NULL) { + local_rem = dash_on; + local_draw = 1; + dash_rem = &local_rem; + dash_drawing = &local_draw; + } + spark_draw_polyline_dashed(im, pts, n, color, ox, oy, dash_on, dash_gap, dash_rem, dash_drawing); + } + + gdImageSetThickness(im, 1); +} + + static void widget_sparkline_render(const char *Name, WIDGET_SPARKLINE *Spark) { int x, y; @@ -134,12 +526,11 @@ static void widget_sparkline_render(const char *Name, WIDGET_SPARKLINE *Spark) } Spark->gdImage = gdImageCreateTrueColor(Spark->xsize, Spark->ysize); - gdImageSaveAlpha(Spark->gdImage, 1); - if (Spark->gdImage == NULL) { error("Warning: Sparkline %s: Create failed!", Name); return; } + gdImageSaveAlpha(Spark->gdImage, 1); gdImage = Spark->gdImage; @@ -158,59 +549,201 @@ static void widget_sparkline_render(const char *Name, WIDGET_SPARKLINE *Spark) int colorlow_gd = has_low ? getcolorint(&Spark->colorlow, gdImage) : colorline; int colorhigh_gd = has_high ? getcolorint(&Spark->colorhigh, gdImage) : colorline; - if (Spark->count >= 2) { - int oldest; - int px, py, cx, cy; - double pval; - - /* oldest sample index in ring buffer */ - if (Spark->count < Spark->nsamples) { - oldest = 0; - } else { - oldest = Spark->head % Spark->nsamples; + { + int thickness = (int)(P2N(&Spark->thickness) + 0.5); + int do_smooth = (int)(P2N(&Spark->smooth) + 0.5) != 0; + int steps = (int)(P2N(&Spark->smoothsteps) + 0.5); + int stroke = sparkline_parse_stroke(P2S(&Spark->stroke)); + /* Bare word like "dashed" without quotes is treated as a variable by + * the expression evaluator, resolving to "". Fall back to the raw + * expression string so unquoted enum values still work. */ + if (stroke == SPARK_STROKE_SOLID && Spark->stroke.expression != NULL) { + int fb = sparkline_parse_stroke(Spark->stroke.expression); + if (fb != SPARK_STROKE_SOLID) + stroke = fb; } + int blurpasses = (int)(P2N(&Spark->valueblur) + 0.5); + int do_fillunder = (int)(P2N(&Spark->fillunder) + 0.5) != 0; + int fillalpha = (int)(P2N(&Spark->fillalpha) + 0.5); + SparkPt *ctrl = NULL; + double *vbuf = NULL; + double *vtmp = NULL; + SparkPt segbuf[SPARK_SMOOTH_CAP + 1]; + int seg_n; + int oldest; + int seg; + + if (thickness < 1) + thickness = 1; + if (thickness > 32) + thickness = 32; + if (steps < 2) + steps = 2; + if (steps > SPARK_SMOOTH_CAP) + steps = SPARK_SMOOTH_CAP; + if (blurpasses < 0) + blurpasses = 0; + if (blurpasses > 16) + blurpasses = 16; + if (fillalpha < 0) + fillalpha = 0; + if (fillalpha > 127) + fillalpha = 127; + + if (Spark->count >= 2) { + ctrl = malloc(Spark->count * sizeof(SparkPt)); + vbuf = malloc(Spark->count * sizeof(double)); + if (ctrl == NULL || vbuf == NULL) { + error("Warning: Sparkline %s: malloc failed", Name); + free(ctrl); + free(vbuf); + } else { - /* In portrait mode, GD x-axis = screen vertical, GD y-axis = screen horizontal. - * xsize = width (short/vertical), ysize = length (long/horizontal). - * Time samples span ysize (horizontal on screen), values span xsize (vertical). */ + if (Spark->count < Spark->nsamples) { + oldest = 0; + } else { + oldest = Spark->head % Spark->nsamples; + } - for (i = 0; i < Spark->count; i++) { - int idx = (oldest + i) % Spark->nsamples; - double val = Spark->history[idx]; - double scaled; + for (i = 0; i < Spark->count; i++) { + int idx = (oldest + i) % Spark->nsamples; - /* map sample index to GD y-axis (horizontal on screen) */ - if (Spark->count > 1) { - cy = (i * (Spark->ysize - 1)) / (Spark->count - 1); - } else { - cy = 0; - } + vbuf[i] = Spark->history[idx]; + } + + if (blurpasses > 0) { + vtmp = malloc(Spark->count * sizeof(double)); + if (vtmp == NULL) { + error("Warning: Sparkline %s: malloc failed", Name); + } else { + spark_value_smooth(vbuf, vtmp, Spark->count, blurpasses); + } + } - /* map value to GD x-axis (vertical on screen, inverted: max at top) */ - scaled = (val - dmin) / (dmax - dmin); - if (scaled < 0.0) scaled = 0.0; - if (scaled > 1.0) scaled = 1.0; - cx = (int)((1.0 - scaled) * (Spark->xsize - 1) + 0.5); - - if (i > 0) { - /* pick color based on the segment's midpoint value */ - double midval = (pval + val) / 2.0; - int segcolor; - - if (has_high && midval > dhigh) { - segcolor = colorhigh_gd; - } else if (has_low && midval < dlow) { - segcolor = colorlow_gd; + /* In portrait mode, GD x-axis = screen vertical, GD y-axis = screen horizontal. + * xsize = width (short/vertical), ysize = length (long/horizontal). + * Time samples span ysize (horizontal on screen), values span xsize (vertical). + * Fractional ctrl[].y avoids stepped horizontal bands that read as “square waves”. */ + + for (i = 0; i < Spark->count; i++) { + double val = vbuf[i]; + double scaled; + + if (Spark->count > 1) { + ctrl[i].y = (double)i * (double)(Spark->ysize - 1) / (double)(Spark->count - 1); + } else { + ctrl[i].y = 0.0; + } + + scaled = (val - dmin) / (dmax - dmin); + if (scaled < 0.0) + scaled = 0.0; + if (scaled > 1.0) + scaled = 1.0; + ctrl[i].x = (1.0 - scaled) * (Spark->xsize - 1); + } + + if (do_fillunder) { + unsigned char ru, gu, bu; + + if (spark_rgb_from_prop(&Spark->color, &ru, &gu, &bu) == 0) { + int full_max = do_smooth ? (Spark->count - 1) * steps + 1 : Spark->count; + SparkPt *full = malloc((size_t)full_max * sizeof(SparkPt)); + + if (full != NULL) { + int fn = spark_build_full_polyline(ctrl, Spark->count, do_smooth, steps, segbuf, full, + full_max); + + if (fn >= 2) + spark_fill_gradient_under(gdImage, full, fn, Spark->xsize, Spark->ysize, ru, gu, bu, + fillalpha); + free(full); + } + } + } + + if (stroke == SPARK_STROKE_SOLID) { + for (seg = 0; seg < Spark->count - 1; seg++) { + double midval = (vbuf[seg] + vbuf[seg + 1]) / 2.0; + int segcolor; + + if (has_high && midval > dhigh) { + segcolor = colorhigh_gd; + } else if (has_low && midval < dlow) { + segcolor = colorlow_gd; + } else { + segcolor = colorline; + } + + if (!do_smooth) { + segbuf[0] = ctrl[seg]; + segbuf[1] = ctrl[seg + 1]; + seg_n = 2; + } else { + SparkPt p0, p1, p2, p3; + int j; + + spark_catmull_controls(ctrl, Spark->count, seg, &p0, &p1, &p2, &p3); + + for (j = 0; j <= steps; j++) { + double t = (double)j / (double)steps; + + spark_catmull_rom(t, p0, p1, p2, p3, &segbuf[j]); + } + seg_n = steps + 1; + } + + spark_draw_polyline_styled(gdImage, segbuf, seg_n, segcolor, thickness, stroke, 0, 0, + NULL, NULL); + } } else { - segcolor = colorline; + /* Dashed/dotted: phase must continue across all sample segments (each is ~1–3px). */ + double dash_on, dash_gap; + double drem; + int ddraw; + + spark_dash_lengths(thickness, stroke, &dash_on, &dash_gap); + drem = dash_on; + ddraw = 1; + for (seg = 0; seg < Spark->count - 1; seg++) { + double midval = (vbuf[seg] + vbuf[seg + 1]) / 2.0; + int segcolor; + + if (has_high && midval > dhigh) { + segcolor = colorhigh_gd; + } else if (has_low && midval < dlow) { + segcolor = colorlow_gd; + } else { + segcolor = colorline; + } + + if (!do_smooth) { + segbuf[0] = ctrl[seg]; + segbuf[1] = ctrl[seg + 1]; + seg_n = 2; + } else { + SparkPt p0, p1, p2, p3; + int j; + + spark_catmull_controls(ctrl, Spark->count, seg, &p0, &p1, &p2, &p3); + + for (j = 0; j <= steps; j++) { + double t = (double)j / (double)steps; + + spark_catmull_rom(t, p0, p1, p2, p3, &segbuf[j]); + } + seg_n = steps + 1; + } + + spark_draw_polyline_styled(gdImage, segbuf, seg_n, segcolor, thickness, stroke, + 0, 0, &drem, &ddraw); + } } - gdImageLine(gdImage, px, py, cx, cy, segcolor); + free(vtmp); + free(vbuf); + free(ctrl); } - - px = cx; - py = cy; - pval = val; } } @@ -272,6 +805,13 @@ static void widget_sparkline_update(void *Self) property_eval(&Spark->colorhigh); property_eval(&Spark->valuehigh); property_eval(&Spark->background); + property_eval(&Spark->thickness); + property_eval(&Spark->smooth); + property_eval(&Spark->smoothsteps); + property_eval(&Spark->stroke); + property_eval(&Spark->valueblur); + property_eval(&Spark->fillunder); + property_eval(&Spark->fillalpha); property_eval(&Spark->update); property_eval(&Spark->reload); property_eval(&Spark->visible); @@ -367,6 +907,13 @@ int widget_sparkline_init(WIDGET *Self) property_load(section, "valuehigh", NULL, &Spark->valuehigh); property_load(section, "background", "000000", &Spark->background); property_load(section, "samples", NULL, &Spark->samples); + property_load(section, "thickness", "1", &Spark->thickness); + property_load(section, "smooth", "0", &Spark->smooth); + property_load(section, "smoothsteps", "24", &Spark->smoothsteps); + property_load(section, "stroke", "solid", &Spark->stroke); + property_load(section, "valueblur", "0", &Spark->valueblur); + property_load(section, "fillunder", "0", &Spark->fillunder); + property_load(section, "fillalpha", "44", &Spark->fillalpha); /* sanity checks */ if (!property_valid(&Spark->value)) { @@ -451,6 +998,13 @@ int widget_sparkline_quit(WIDGET *Self) property_free(&Spark->valuehigh); property_free(&Spark->background); property_free(&Spark->samples); + property_free(&Spark->thickness); + property_free(&Spark->smooth); + property_free(&Spark->smoothsteps); + property_free(&Spark->stroke); + property_free(&Spark->valueblur); + property_free(&Spark->fillunder); + property_free(&Spark->fillalpha); free(Self->data); Self->data = NULL; diff --git a/widget_sparkline.h b/widget_sparkline.h index eff796b..31748b4 100644 --- a/widget_sparkline.h +++ b/widget_sparkline.h @@ -53,6 +53,13 @@ typedef struct WIDGET_SPARKLINE { PROPERTY valuehigh; /* threshold above which to use colorhigh */ PROPERTY background; /* background color */ PROPERTY samples; /* optional max ring-buffer size (default: length) */ + PROPERTY thickness; /* line width in pixels (default 1) */ + PROPERTY smooth; /* 1 = Catmull-Rom smooth curve, 0 = straight segments */ + PROPERTY smoothsteps; /* subdivisions per segment when smooth (default 12) */ + PROPERTY stroke; /* solid | dashed | dotted */ + PROPERTY valueblur; /* binomial smooth passes on values (0=off); softens "square" I/O */ + PROPERTY fillunder; /* 1 = gradient fill from line color toward transparent toward min value */ + PROPERTY fillalpha; /* GD alpha at curve for fillunder (0..127, lower=stronger, default 44) */ double *history; /* ring buffer of sampled values */ int nsamples; /* size of ring buffer (<= length unless samples set larger) */ int head; /* next write index */