sugarrush 2026.8.3

A terminal UI for viewing Nightscout CGM (blood glucose sensor) data
// The four alert thresholds as one band with four handles.
//
// They are not four settings. They are one band with two guard rails, and the
// relationship between them is the thing being edited — which four numbered
// stepper rows hide completely. It is also why crossing them is possible
// enough that `set_key` has to refuse it; here the handles simply cannot pass
// each other, so the illegal state is unreachable rather than rejected.
//
// The steppers stay underneath. Dragging is for seeing the shape; 0.1 mmol/L
// on a 380px panel is what a stepper is for.

import QtQuick

Item {
  id: root

  property real urgentLow: 3.5
  property real low: 3.9
  property real high: 10.0
  property real urgentHigh: 13.9
  // "mgdl" or anything else, which means mmol/L.
  property string units: "mmol"
  property var themeColors: null
  property color foreground: "white"

  // Emitted on release, not on every pixel: each one is a config write.
  signal changed(string role, real value)

  readonly property bool mgdl: units === "mgdl"
  readonly property real scaleMin: mgdl ? 40 : 2
  readonly property real scaleMax: mgdl ? 400 : 22
  readonly property real step: mgdl ? 1 : 0.1
  // Handles cannot close up entirely: two thresholds a tenth apart is not a
  // configuration anyone means, and it makes the band unreadable.
  readonly property real minGap: mgdl ? 5 : 0.3

  readonly property int handleWidth: 3
  readonly property int bandTop: 18
  readonly property int bandHeight: 16

  function themed(role, fallback) {
    return themeColors && themeColors[role] ? themeColors[role] : fallback
  }

  function xOf(value) {
    var t = (value - scaleMin) / Math.max(0.0001, scaleMax - scaleMin)
    return Math.max(0, Math.min(1, t)) * width
  }

  function valueOf(x) {
    var t = Math.max(0, Math.min(1, x / Math.max(1, width)))
    var raw = scaleMin + t * (scaleMax - scaleMin)
    return Math.round(raw / step) * step
  }

  // Which handle a press grabs, and the bounds it may move between. Order is
  // fixed: urgent low ≤ low ≤ high ≤ urgent high.
  function handles() {
    return [
      { role: "alerts.urgent_low", value: urgentLow, lower: scaleMin, upper: low - minGap },
      { role: "alerts.low", value: low, lower: urgentLow + minGap, upper: high - minGap },
      { role: "alerts.high", value: high, lower: low + minGap, upper: urgentHigh - minGap },
      { role: "alerts.urgent_high", value: urgentHigh, lower: high + minGap, upper: scaleMax }
    ]
  }

  property int dragging: -1

  implicitHeight: bandTop + bandHeight + 16

  onUrgentLowChanged: band.requestPaint()
  onLowChanged: band.requestPaint()
  onHighChanged: band.requestPaint()
  onUrgentHighChanged: band.requestPaint()
  onThemeColorsChanged: band.requestPaint()
  onWidthChanged: band.requestPaint()

  Canvas {
    id: band
    anchors.fill: parent

    onPaint: {
      var ctx = getContext("2d")
      ctx.reset()
      var y = root.bandTop
      var h = root.bandHeight

      // Five zones, in the colours the reading itself is drawn in, so the
      // band is a legend for the rest of the panel as much as a control.
      var zones = [
        { from: root.scaleMin, to: root.urgentLow, color: root.themed("urgent", "#cc241d"), alpha: 0.75 },
        { from: root.urgentLow, to: root.low, color: root.themed("low", "#d79921"), alpha: 0.7 },
        { from: root.low, to: root.high, color: root.themed("in_range", "#98971a"), alpha: 0.85 },
        { from: root.high, to: root.urgentHigh, color: root.themed("high", "#d79921"), alpha: 0.7 },
        { from: root.urgentHigh, to: root.scaleMax, color: root.themed("urgent", "#cc241d"), alpha: 0.75 }
      ]
      for (var z = 0; z < zones.length; z++) {
        var x0 = root.xOf(zones[z].from)
        var x1 = root.xOf(zones[z].to)
        if (x1 <= x0) continue
        ctx.globalAlpha = zones[z].alpha
        ctx.fillStyle = zones[z].color
        ctx.fillRect(x0, y, x1 - x0, h)
      }
      ctx.globalAlpha = 1

      // Handles, and the value each one is currently at.
      ctx.font = "9px monospace"
      ctx.textAlign = "center"
      var list = root.handles()
      for (var i = 0; i < list.length; i++) {
        var hx = root.xOf(list[i].value)
        ctx.fillStyle = root.foreground
        ctx.fillRect(hx - root.handleWidth / 2, y - 4, root.handleWidth, h + 8)
        ctx.fillStyle = Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b,
                                root.dragging === i ? 1 : 0.72)
        ctx.fillText(list[i].value.toFixed(root.mgdl ? 0 : 1),
                     Math.max(12, Math.min(root.width - 12, hx)), y - 8)
      }
    }
  }

  MouseArea {
    anchors.fill: parent
    // Fine control belongs to the steppers below; this is for the shape.
    preventStealing: true

    function nearest(x) {
      var list = root.handles()
      var best = 0
      var bestDistance = Infinity
      for (var i = 0; i < list.length; i++) {
        var d = Math.abs(root.xOf(list[i].value) - x)
        if (d < bestDistance) { bestDistance = d; best = i }
      }
      return best
    }

    function move(x) {
      var list = root.handles()
      var at = root.dragging
      if (at < 0) return
      var next = Math.max(list[at].lower, Math.min(list[at].upper, root.valueOf(x)))
      switch (at) {
      case 0: root.urgentLow = next; break
      case 1: root.low = next; break
      case 2: root.high = next; break
      case 3: root.urgentHigh = next; break
      }
    }

    onPressed: function (mouse) {
      root.dragging = nearest(mouse.x)
      move(mouse.x)
      band.requestPaint()
    }

    onPositionChanged: function (mouse) {
      if (root.dragging < 0) return
      move(mouse.x)
      band.requestPaint()
    }

    onReleased: {
      if (root.dragging < 0) return
      var list = root.handles()
      // One write, on release. Writing per pixel would be a config file
      // rewritten a hundred times to move one threshold.
      root.changed(list[root.dragging].role, list[root.dragging].value)
      root.dragging = -1
      band.requestPaint()
    }
  }
}