├── .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 | [](https://circleci.com/gh/TooTallNate/nightscout-ps1/tree/master)
4 | [](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 |
--------------------------------------------------------------------------------