├── .dockerignore ├── .gitignore ├── .gitmodules ├── Dockerfile.alpine ├── appveyor.yml ├── Dockerfile.debian ├── detect-platform.sh ├── deps └── inih.gyp ├── Makefile ├── .circleci └── config.yml ├── Readme.md └── src └── main.c /.dockerignore: -------------------------------------------------------------------------------- 1 | /out 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | /packed 3 | /nightscout-ps1 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/libuv"] 2 | path = deps/libuv 3 | url = git://github.com/libuv/libuv.git 4 | [submodule "deps/inih"] 5 | path = deps/inih 6 | url = git://github.com/benhoyt/inih.git 7 | [submodule "deps/gyp"] 8 | path = deps/gyp 9 | url = https://chromium.googlesource.com/external/gyp 10 | -------------------------------------------------------------------------------- /Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | # docker build -t nightscout-ps1-build:alpine -f Dockerfile.alpine . 2 | # docker run -it --rm -v $PWD:/nightscout-ps1 nightscout-ps1-build:alpine 3 | FROM alpine:edge 4 | 5 | RUN apk add --no-cache \ 6 | build-base \ 7 | python 8 | 9 | WORKDIR /nightscout-ps1 10 | 11 | CMD make build && cp -v ./out/Default/nightscout-ps1 ./packed/ && make clean 12 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # http://www.appveyor.com/docs/appveyor-yml 2 | 3 | environment: 4 | # Visual Studio Version 5 | MSVS_VERSION: 2013 6 | 7 | platform: 8 | - x64 9 | 10 | build: off 11 | 12 | install: 13 | - git submodule sync 14 | - git submodule update --init 15 | - python --version 16 | - echo install 17 | - pwd 18 | 19 | test_script: 20 | - echo test 21 | - pwd 22 | -------------------------------------------------------------------------------- /Dockerfile.debian: -------------------------------------------------------------------------------- 1 | # docker build -t nightscout-ps1-build:debian -f Dockerfile.debian . 2 | # docker run -it --rm -v $PWD:/nightscout-ps1 nightscout-ps1-build:debian 3 | FROM ubuntu 4 | 5 | RUN apt-get update && apt-get install -y \ 6 | build-essential \ 7 | python 8 | 9 | WORKDIR /nightscout-ps1 10 | 11 | CMD make build && cp -v ./out/Default/nightscout-ps1 ./packed/ && make clean 12 | -------------------------------------------------------------------------------- /detect-platform.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | platform="$(uname -s | tr '[:upper:]' '[:lower:]')" 3 | 4 | # check for MUSL 5 | if [ "${platform}" = "linux" ]; then 6 | if ldd /bin/sh | grep -i musl >/dev/null; then 7 | platform=alpine 8 | fi 9 | fi 10 | 11 | # mingw is Git-Bash 12 | if echo "${platform}" | grep -i mingw >/dev/null; then 13 | platform=win 14 | fi 15 | 16 | # map "darwin" to "macos" 17 | if [ "${platform}" = "darwin" ]; then 18 | platform=macos 19 | fi 20 | 21 | echo "${platform}" 22 | -------------------------------------------------------------------------------- /deps/inih.gyp: -------------------------------------------------------------------------------- 1 | # This file is used with the GYP meta build system. 2 | # http://code.google.com/p/gyp 3 | # To build try this: 4 | # svn co http://gyp.googlecode.com/svn/trunk gyp 5 | # ./gyp/gyp -f make --depth=. inih.gyp 6 | # make 7 | # ./out/Debug/test 8 | { 9 | 'targets': [ 10 | { 11 | 'target_name': 'inih', 12 | 'type': 'static_library', 13 | 'sources': [ 14 | 'inih/ini.c', 15 | ], 16 | 'include_dirs': [ 17 | 'inih', 18 | ], 19 | 'direct_dependent_settings': { 20 | 'include_dirs': [ 21 | 'inih', 22 | ], 23 | }, 24 | }, 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME?=nightscout-ps1 2 | PREFIX?=/usr/local 3 | OUTDIR?=out 4 | BIN?=$(OUTDIR)/Default/$(NAME) 5 | VERSION?=$(shell git describe --tags --always) 6 | PLATFORM?=$(shell ./detect-platform.sh) 7 | DISTDIR?=packed 8 | DISTBIN?=$(DISTDIR)/$(NAME)-v$(VERSION)-$(PLATFORM)-x64 9 | 10 | build: $(BIN) 11 | 12 | dist: $(DISTBIN) 13 | 14 | $(BIN): $(OUTDIR) src/* 15 | @$(MAKE) -C "$(OUTDIR)" 16 | @strip "$(BIN)" 17 | 18 | $(DISTBIN): $(BIN) 19 | @mkdir -p "$(DISTDIR)" 20 | @cp -v "$(BIN)" "$(DISTBIN)" 21 | 22 | $(OUTDIR): build.gyp 23 | @./deps/gyp/gyp \ 24 | --no-parallel \ 25 | -f make \ 26 | build.gyp \ 27 | --depth=. \ 28 | --generator-output=$(shell pwd)/$(OUTDIR) \ 29 | -Goutput_dir=$(shell pwd)/$(OUTDIR) \ 30 | -Dcomponent=static_library \ 31 | -Dlibrary=static_library \ 32 | -Duv_library=static_library 33 | 34 | install: build 35 | @cp -v "$(BIN)" "$(PREFIX)/bin/$(NAME)" 36 | 37 | clean: 38 | @rm -vrf $(OUTDIR) 39 | 40 | test: 41 | 42 | .PHONY: clean build dist install test 43 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | dependencies: 4 | pre: 5 | - go get github.com/tcnksm/ghr 6 | 7 | compile: 8 | override: 9 | - ant package 10 | 11 | jobs: 12 | 13 | build_alpine: 14 | docker: 15 | - image: alpine:3.5 16 | working_directory: /project 17 | 18 | steps: 19 | - run: apk add --no-cache build-base git python openssh-client 20 | - checkout 21 | - run: git submodule sync 22 | - run: git submodule update --init 23 | - run: make dist 24 | - store_artifacts: 25 | path: packed 26 | 27 | build_linux: 28 | docker: 29 | - image: debian:jessie 30 | working_directory: /project 31 | 32 | steps: 33 | - run: apt-get update && apt-get install -y build-essential git python 34 | - checkout 35 | - run: git submodule sync 36 | - run: git submodule update --init 37 | - run: make dist 38 | - store_artifacts: 39 | path: packed 40 | 41 | build_macos: 42 | macos: 43 | xcode: "8.3.3" 44 | 45 | steps: 46 | - checkout 47 | - run: git submodule sync 48 | - run: git submodule update --init 49 | - run: make dist 50 | - store_artifacts: 51 | path: packed 52 | 53 | workflows: 54 | version: 2 55 | build_all: 56 | jobs: 57 | - build_alpine 58 | - build_linux 59 | - build_macos 60 | 61 | deployment: 62 | release: 63 | tag: /(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)/ 64 | commands: 65 | - ghr -t $GITHUB_TOKEN -u $CIRCLE_PROJECT_USERNAME -r $CIRCLE_PROJECT_REPONAME packed/ 66 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # nightscout-ps1 2 | 3 | [![CircleCI](https://circleci.com/gh/TooTallNate/nightscout-ps1/tree/master.svg?style=svg)](https://circleci.com/gh/TooTallNate/nightscout-ps1/tree/master) 4 | [![AppVeyor](https://ci.appveyor.com/api/projects/status/1rp3biuu82e76fgq/branch/master?svg=true)](https://ci.appveyor.com/project/TooTallNate/nightscout-ps1/branch/master) 5 | 6 | 7 | 8 | Tiny C program that formats your latest Nightscout BGL reading for use in 9 | your terminal prompt (a.k.a $PS1). It's written in C to be as fast as possible, 10 | since it gets executed every time the prompt is rendered, so latency is critical. 11 | 12 | You will need to set up [`nightscout-ps1-daemon`][daemon] which is responsible for 13 | connecting to your Nightscout server and creating the INI file that this 14 | program reads. 15 | 16 | The trend and target high/low values are also provided, so that you may 17 | render arrows and colors as desired: 18 | 19 | 20 | 21 | ### Installation 22 | 23 | Preferred installation is by downloading a pre-compiled binary for your platform: 24 | 25 | * [GitHub Releases](https://github.com/TooTallNate/nightscout-ps1/releases) 26 | 27 | If there is no binary for your platform, or you would simply like to install 28 | from source, invoke the `make` command: 29 | 30 | ```bash 31 | # build the `nightscout-ps1` binary 32 | $ git submodule update --init 33 | $ make 34 | $ ./out/Default/nightscout-ps1 35 | 36 | # install to /usr/local by default, set PREFIX for a custom dir 37 | $ make install 38 | ``` 39 | 40 | ### Configure your `$PS1` 41 | 42 | Add this to your `.bashrc` file: 43 | 44 | ```bash 45 | export PS1="\$(nightscout-ps1) $ " 46 | ``` 47 | 48 | Be sure to add further customizations to your `$PS1` from there! 49 | 50 | [daemon]: https://github.com/TooTallNate/nightscout-ps1-daemon 51 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "uv.h" 12 | #include "ini.h" 13 | 14 | #define NO_COLOR L"\001\x1b[0m\002" 15 | #define BOLD L"\001\x1b[1m\002" 16 | #define INVERSE L"\001\x1b[7m\002" 17 | #define RED L"\001\x1b[31m\002" 18 | #define RED L"\001\x1b[31m\002" 19 | #define GREEN L"\001\x1b[32m\002" 20 | #define YELLOW L"\001\x1b[33m\002" 21 | #define BLUE L"\001\x1b[34m\002" 22 | #define MAGENTA L"\001\x1b[35m\002" 23 | #define CYAN L"\001\x1b[36m\002" 24 | 25 | #define STRIKE 0x0336 26 | #define MS_PER_MINUTE (1000 * 60) 27 | 28 | /** 29 | * Convert struct timeval to milliseconds. 30 | * 31 | * \param tv The time value value to convert. 32 | * 33 | * \return The number off milliseconds in \a tv. 34 | */ 35 | long unsigned tv2ms(const struct timeval *tv) { 36 | return tv->tv_sec * 1000 + (tv->tv_usec + 500)/ 1000; 37 | } 38 | 39 | /* https://en.wikipedia.org/wiki/Strikethrough#Unicode */ 40 | int strikethrough(const wint_t* str, wint_t* buf, size_t size) { 41 | int i; 42 | int written = 0; 43 | int len = wcslen(str); 44 | for (i = 0; i < len; i++) { 45 | buf[written + 0] = str[i]; 46 | buf[written + 1] = STRIKE; 47 | written += 2; 48 | } 49 | return written; 50 | } 51 | 52 | typedef struct 53 | { 54 | /* previous entry */ 55 | int previous_mgdl; 56 | long unsigned previous_mills; 57 | 58 | /* latest entry */ 59 | int mgdl; 60 | long unsigned mills; 61 | const char* direction; 62 | 63 | /* stale entry alarms */ 64 | int alarm_timeago_warn; 65 | int alarm_timeago_warn_mins; 66 | int alarm_timeago_urgent; 67 | int alarm_timeago_urgent_mins; 68 | 69 | /* thresholds */ 70 | int bg_high; 71 | int bg_target_top; 72 | int bg_target_bottom; 73 | int bg_low; 74 | } status; 75 | 76 | static int handler(void* user, const char* section, const char* name, 77 | const char* value) 78 | { 79 | status* pStatus = (status*)user; 80 | 81 | #define MATCH(s, n) strcmp(section, s) == 0 && strcmp(name, n) == 0 82 | #define PARSE_BOOL strcmp(value, "true") == 0 83 | /* previous entry */ 84 | if (MATCH("previous_entry", "mgdl")) { 85 | pStatus->previous_mgdl = atoi(value); 86 | } else if (MATCH("previous_entry", "mills")) { 87 | pStatus->previous_mills = atoll(value); 88 | 89 | /* latest entry */ 90 | } else if (MATCH("latest_entry", "mgdl")) { 91 | pStatus->mgdl = atoi(value); 92 | } else if (MATCH("latest_entry", "mills")) { 93 | pStatus->mills = atoll(value); 94 | } else if (MATCH("latest_entry", "direction")) { 95 | pStatus->direction = strdup(value); 96 | 97 | /* stale entry alarms */ 98 | } else if (MATCH("settings", "alarm_timeago_warn")) { 99 | pStatus->alarm_timeago_warn = PARSE_BOOL; 100 | } else if (MATCH("settings", "alarm_timeago_warn_mins")) { 101 | pStatus->alarm_timeago_warn_mins = atoi(value); 102 | } else if (MATCH("settings", "alarm_timeago_urgent")) { 103 | pStatus->alarm_timeago_urgent = PARSE_BOOL; 104 | } else if (MATCH("settings", "alarm_timeago_urgent_mins")) { 105 | pStatus->alarm_timeago_urgent_mins = atoi(value); 106 | 107 | /* thresholds */ 108 | } else if (MATCH("settings.thresholds", "bg_high")) { 109 | pStatus->bg_high = atoi(value); 110 | } else if (MATCH("settings.thresholds", "bg_target_top")) { 111 | pStatus->bg_target_top = atoi(value); 112 | } else if (MATCH("settings.thresholds", "bg_target_bottom")) { 113 | pStatus->bg_target_bottom = atoi(value); 114 | } else if (MATCH("settings.thresholds", "bg_low")) { 115 | pStatus->bg_low = atoi(value); 116 | 117 | } else { 118 | return 0; /* unknown section/name, error */ 119 | } 120 | 121 | return 1; 122 | } 123 | 124 | uv_loop_t *loop; 125 | uv_tty_t tty; 126 | int main(int argc, char* argv[]) { 127 | status s; 128 | struct timeval now; 129 | char *locale = setlocale(LC_ALL, ""); 130 | 131 | loop = uv_default_loop(); 132 | 133 | uv_tty_init(loop, &tty, 1, 0); 134 | uv_tty_set_mode(&tty, UV_TTY_MODE_NORMAL); 135 | 136 | int i = 0; 137 | wint_t buf[100] = { 0 }; 138 | 139 | char homedir[1024]; 140 | size_t size = sizeof(homedir); 141 | if (uv_os_homedir(homedir, &size)) { 142 | printf("Failed to read homedir\n"); 143 | return 1; 144 | } 145 | 146 | char latest_entry_path[1024]; 147 | snprintf(latest_entry_path, sizeof(latest_entry_path), "%s/%s", homedir, ".nightscout-latest-entry"); 148 | 149 | if (ini_parse(latest_entry_path, handler, &s) < 0) { 150 | printf("Can't load '%s'\n", latest_entry_path); 151 | return 1; 152 | } 153 | 154 | gettimeofday(&now, NULL); 155 | long unsigned ms_ago = tv2ms(&now) - s.mills; 156 | 157 | const wint_t* color; 158 | wint_t trend = L'?'; 159 | int strike = 0; 160 | int delta = s.mgdl - s.previous_mgdl; 161 | 162 | /* If the previous reading was more than 6 minutes ago (5 minutes is "normal", 163 | plus or minus some time to allow the reading to be uploaded */ 164 | long delta_is_stale = s.mills - s.previous_mills > (MS_PER_MINUTE * 6); 165 | 166 | /* The mg/dl and delta from previous reading are put in their own buffer 167 | initially because they may be re-written with strikethrough in the end */ 168 | wint_t mgdl_and_delta[12] = { 0 }; 169 | i += swprintf(mgdl_and_delta, sizeof(mgdl_and_delta), L"%d %+d", s.mgdl, delta); 170 | if (delta_is_stale) { 171 | mgdl_and_delta[i++] = L'*'; 172 | } 173 | 174 | /* Now calculate the color, trend, and whether or not to strikeout the values */ 175 | if (s.alarm_timeago_urgent && ms_ago > s.alarm_timeago_urgent_mins * MS_PER_MINUTE) { 176 | trend = L'↛'; 177 | strike = 1; 178 | color = INVERSE RED BOLD; 179 | } else if (s.alarm_timeago_warn && ms_ago > s.alarm_timeago_warn_mins * MS_PER_MINUTE) { 180 | trend = L'↛'; 181 | strike = 1; 182 | color = INVERSE YELLOW BOLD; 183 | } else { 184 | if (strcmp(s.direction, "DoubleUp") == 0) { 185 | trend = L'⇈'; 186 | } else if (strcmp(s.direction, "SingleUp") == 0) { 187 | trend = L'↑'; 188 | } else if (strcmp(s.direction, "FortyFiveUp") == 0) { 189 | trend = L'↗'; 190 | } else if (strcmp(s.direction, "Flat") == 0) { 191 | trend = L'→'; 192 | } else if (strcmp(s.direction, "FortyFiveDown") == 0) { 193 | trend = L'↘'; 194 | } else if (strcmp(s.direction, "SingleDown") == 0) { 195 | trend = L'↓'; 196 | } else if (strcmp(s.direction, "DoubleDown") == 0) { 197 | trend = L'⇊'; 198 | } else if (strcmp(s.direction, "NONE") == 0) { 199 | trend = L'⇼'; 200 | } 201 | 202 | if (s.mgdl > s.bg_high) { 203 | color = YELLOW BOLD; 204 | } else if (s.mgdl > s.bg_target_top) { 205 | color = YELLOW; 206 | } else if (s.mgdl < s.bg_low) { 207 | color = RED BOLD; 208 | } else if (s.mgdl < s.bg_target_bottom) { 209 | color = RED; 210 | } else { 211 | color = GREEN; 212 | } 213 | } 214 | 215 | /* fill buffer as wint_t */ 216 | i = 0; 217 | i += swprintf(buf + i, sizeof(buf) - (sizeof(wint_t) * i), L"%S", color); 218 | if (strike) { 219 | i += strikethrough(mgdl_and_delta, buf + i, sizeof(buf) - (sizeof(wint_t) * i)); 220 | } else { 221 | i += swprintf(buf + i, sizeof(buf) - (sizeof(wint_t) * i), L"%S", mgdl_and_delta); 222 | } 223 | i += swprintf(buf + i, sizeof(buf) - (sizeof(wint_t) * i), L" %C%S", trend, NO_COLOR); 224 | 225 | /* print buffer to stdout as UTF-8 through the libuv TTY machinery 226 | so that we get Windows normalization as well */ 227 | char output[1024] = { 0 }; 228 | snprintf(output, sizeof(output), "%S\n", buf); 229 | 230 | uv_write_t req; 231 | uv_buf_t buff; 232 | buff.base = output; 233 | buff.len = strlen(output); 234 | uv_write(&req, (uv_stream_t*) &tty, &buff, 1, NULL); 235 | uv_tty_reset_mode(); 236 | 237 | return uv_run(loop, UV_RUN_DEFAULT); 238 | } 239 | --------------------------------------------------------------------------------