summaryrefslogtreecommitdiffabout
authorMichael Krelin <hacker@klever.net>2008-04-05 11:17:33 (UTC)
committer Michael Krelin <hacker@klever.net>2008-04-05 11:17:33 (UTC)
commit04fb190243442e83349f129b523ab747e58100bf (patch) (side-by-side diff)
treeddc28357fbe78b07fd3a5e0aa8088130bf305829
downloadnapkin-04fb190243442e83349f129b523ab747e58100bf.zip
napkin-04fb190243442e83349f129b523ab747e58100bf.tar.gz
napkin-04fb190243442e83349f129b523ab747e58100bf.tar.bz2
Initial commit into public repository0.0
Signed-off-by: Michael Krelin <hacker@klever.net>
Diffstat (more/less context) (ignore whitespace changes)
-rw-r--r--.gitignore14
-rw-r--r--AUTHORS8
-rw-r--r--COPYING19
-rw-r--r--ChangeLog0
-rw-r--r--Makefile.am15
-rw-r--r--NEWS.xml6
-rw-r--r--NEWS.xsl24
-rw-r--r--README15
-rw-r--r--autogen.sh7
-rw-r--r--configure.ac66
-rw-r--r--include/Makefile.am4
-rw-r--r--include/napkin/exception.h36
-rw-r--r--include/napkin/st/decode.h15
-rw-r--r--include/napkin/st/download.h20
-rw-r--r--include/napkin/types.h46
-rw-r--r--include/napkin/util.h14
-rw-r--r--lib/.gitignore11
-rw-r--r--lib/Makefile.am11
-rw-r--r--lib/hypnodata.cc92
-rw-r--r--lib/st-decode.cc121
-rw-r--r--lib/st-download.cc70
-rw-r--r--lib/util.cc13
-rw-r--r--src/.gitignore13
-rw-r--r--src/Makefile.am32
-rw-r--r--src/db.cc101
-rw-r--r--src/db.h28
-rw-r--r--src/dialogs.cc17
-rw-r--r--src/dialogs.h25
-rw-r--r--src/napkin.cc367
-rw-r--r--src/schema.sql9
-rw-r--r--src/sleep_history.cc158
-rw-r--r--src/sleep_history.h113
-rw-r--r--src/sleep_timeline.cc100
-rw-r--r--src/sleep_timeline.h29
-rw-r--r--src/sqlite.h103
-rw-r--r--src/widgets.cc63
-rw-r--r--src/widgets.h33
-rw-r--r--test/.gitignore4
-rw-r--r--test/Makefile.am7
-rw-r--r--test/sleeptracker-decode.cc49
40 files changed, 1878 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ed613b6
--- a/dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+/INSTALL
+Makefile
+Makefile.in
+/aclocal.m4
+/autom4te.cache
+/aux.d
+/config.h
+/config.h.in
+/config.log
+/config.status
+/configure
+/libtool
+/stamp-h1
+/NEWS
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..3a5571d
--- a/dev/null
+++ b/AUTHORS
@@ -0,0 +1,8 @@
+Klever dissected:
+ Michael 'hacker' Krelin <hacker@klever.net>
+ Leonid Ivanov <kamel@klever.net>
+
+The credits also go to the unknown hacker called Joe for dissecting
+sleeptracker data -
+http://www.sleeptracker-tec.de/tools/phorum/read.php?2,19
+I hope I find the way to contact him and give him proper credit.
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..31595a2
--- a/dev/null
+++ b/COPYING
@@ -0,0 +1,19 @@
+Copyright (c) 2008 Klever Group (http://www.klever.net/)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 0000000..e69de29
--- a/dev/null
+++ b/ChangeLog
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 0000000..c3bc31f
--- a/dev/null
+++ b/Makefile.am
@@ -0,0 +1,15 @@
+SUBDIRS=include lib src test
+EXTRA_DIST = NEWS NEWS.xml NEWS.xsl
+
+all-local: NEWS
+
+NEWS: NEWS.xsl NEWS.xml
+ ${XSLTPROC} -o $@ NEWS.xsl NEWS.xml
+
+ISSUEFILES = $$(find ${top_srcdir} -type f '(' \
+ -name '*.cc' -or -name '*.h' -or -name '*.sql' \
+ ')' ) \
+ ${top_srcdir}/configure.ac
+issues: todo fixme xxx
+todo fixme xxx:
+ @grep --color=auto -in '$@:' ${ISSUEFILES} || true
diff --git a/NEWS.xml b/NEWS.xml
new file mode 100644
index 0000000..33907ee
--- a/dev/null
+++ b/NEWS.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="us-ascii"?>
+<news>
+ <version version="0.0" date="April 5th, 2008">
+ <ni>Initial release</ni>
+ </version>
+</news>
diff --git a/NEWS.xsl b/NEWS.xsl
new file mode 100644
index 0000000..7c71307
--- a/dev/null
+++ b/NEWS.xsl
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="us-ascii"?>
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ >
+ <xsl:output
+ method="text"
+ encoding="us-ascii"
+ media-type="text/plain" />
+
+ <xsl:template match="news">
+ <xsl:apply-templates/>
+ </xsl:template>
+ <xsl:template match="version">
+ <xsl:value-of select="concat(@version,' (',@date,')&#xA;')"/>
+ <xsl:apply-templates/>
+ </xsl:template>
+ <xsl:template match="ni">
+ <xsl:text> - </xsl:text>
+ <xsl:apply-templates mode="text"/>
+ <xsl:text>&#xA;</xsl:text>
+ </xsl:template>
+ <xsl:template match="*|text()"/>
+
+</xsl:stylesheet>
diff --git a/README b/README
new file mode 100644
index 0000000..f353884
--- a/dev/null
+++ b/README
@@ -0,0 +1,15 @@
+
+Napkin expects you to have sleeptracker port at /dev/sleeptracker
+
+On linux you'd need to have 'ftdi_sio' kernel module loaded (and usb support,
+of course) and something along these lines in your udev rules:
+
+BUS=="usb", SYSFS{product}=="FT232R USB UART", SYSFS{serial}=="********", SYSFS{manufacturer}=="FTDI", KERNEL=="ttyUSB*", SYMLINK="sleeptracker", MODE="660", GROUP="usb"
+
+Note, that the I've wiped out the serial number, so that you don't try to use
+mine. Perhaps it will differ, although I have only one device.
+
+You can also set the other port name using SLEEPTRACKER_PORT environment
+variable, like
+
+env SLEEPTRACKER_PORT=/dev/ttyUSB0 napkin
diff --git a/autogen.sh b/autogen.sh
new file mode 100644
index 0000000..c3313be
--- a/dev/null
+++ b/autogen.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+test -d aux.d || mkdir aux.d
+ aclocal \
+&& autoheader \
+&& automake -a \
+&& autoconf \
+&& ./configure "$@"
diff --git a/configure.ac b/configure.ac
new file mode 100644
index 0000000..3ca0b4e
--- a/dev/null
+++ b/configure.ac
@@ -0,0 +1,66 @@
+AC_INIT([napkin], [0.0], [napkin-bugs@klever.net])
+AC_CONFIG_AUX_DIR([aux.d])
+AC_CONFIG_SRCDIR([src/napkin.cc])
+AC_CONFIG_HEADERS([config.h])
+AM_INIT_AUTOMAKE([dist-bzip2])
+
+AC_PROG_INSTALL
+AC_PROG_CXX
+AC_PROG_CC
+AC_PROG_LIBTOOL
+PKG_PROG_PKG_CONFIG
+
+AC_HEADER_STDC
+
+AC_PATH_PROG([XSLTPROC],[xsltproc],[true])
+
+PKG_CHECK_MODULES([MODULES],[gtkmm-2.4 sqlite3],,[
+ AC_MSG_ERROR([not all dependencies could be satisfied])
+])
+
+AC_MSG_CHECKING([whether to enable debugging code])
+ndebug=true
+AC_ARG_ENABLE([debug],
+ AC_HELP_STRING([--enable-debug],[enable debugging/development code]),
+ [ test "$enableval" = "no" || ndebug=false ]
+)
+if $ndebug ; then
+ AC_MSG_RESULT([no])
+ CPPFLAGS="${CPPFLAGS}-DNDEBUG"
+else
+ AC_MSG_RESULT([yes])
+fi
+
+nitpick=false
+AC_MSG_CHECKING([whether to enable compiler nitpicking])
+AC_ARG_ENABLE([nitpicking],
+ AC_HELP_STRING([--enable-nitpicking],[make compiler somewhat overly fastidious about the code it deals with]),
+ [ test "$enableval" = "no" || nitpick=true ]
+)
+if $nitpick ; then
+ AC_MSG_RESULT([yes])
+ CPP_NITPICK="-pedantic -Wall -Wextra -Wundef -Wshadow \
+ -Wunsafe-loop-optimizations -Wconversion -Wmissing-format-attribute \
+ -Wredundant-decls -ansi"
+ # -Wlogical-op -Wmissing-noreturn
+ C_NITPICK="$CPP_NITPICK"
+ CXX_NITPICK="$C_NITPICK"
+
+ CPPFLAGS="$CPPFLAGS $CPP_NITPICK"
+ CFLAGS="$CFLAGS $C_NITPICK"
+ CXXFLAGS="$CXXFLAGS $CXX_NITPICK"
+else
+ AC_MSG_RESULT([no])
+fi
+
+
+AC_CONFIG_FILES([
+ Makefile
+ include/Makefile
+ lib/Makefile
+ src/Makefile
+ test/Makefile
+])
+AC_OUTPUT
+
+dnl vim:set ft=m4 sw=1:
diff --git a/include/Makefile.am b/include/Makefile.am
new file mode 100644
index 0000000..f37e4d7
--- a/dev/null
+++ b/include/Makefile.am
@@ -0,0 +1,4 @@
+include_HEADERS = $(addprefix napkin/,\
+ exception.h types.h util.h \
+ st/decode.h st/download.h \
+ )
diff --git a/include/napkin/exception.h b/include/napkin/exception.h
new file mode 100644
index 0000000..b317886
--- a/dev/null
+++ b/include/napkin/exception.h
@@ -0,0 +1,36 @@
+#ifndef __NAPKIN_EXCEPTION_H
+#define __NAPKIN_EXCEPTION_H
+
+#include <stdexcept>
+#include <string>
+
+#define NAPKIN_E_SUBCLASS(derived,base) \
+ class derived : public base { \
+ public: \
+ explicit derived(const string& w) \
+ : base(w) { } \
+ }
+
+namespace napkin {
+ using std::string;
+
+ class exception : public std::runtime_error {
+ public:
+ explicit exception(const string& w)
+ : std::runtime_error(w) { }
+ ~exception() throw() { }
+ };
+
+ NAPKIN_E_SUBCLASS(exception_sleeptracker,exception);
+ NAPKIN_E_SUBCLASS(exception_st_port,exception_sleeptracker);
+ NAPKIN_E_SUBCLASS(exception_st_data,exception_sleeptracker);
+ NAPKIN_E_SUBCLASS(exception_st_data_envelope,exception_st_data);
+ NAPKIN_E_SUBCLASS(exception_st_data_integrity,exception_st_data_envelope);
+
+ NAPKIN_E_SUBCLASS(exception_db,exception);
+ NAPKIN_E_SUBCLASS(exception_db_already,exception_db);
+}
+
+#undef NAPKIN_E_SUBCLASS
+
+#endif /* __NAPKIN_EXCEPTION_H */
diff --git a/include/napkin/st/decode.h b/include/napkin/st/decode.h
new file mode 100644
index 0000000..e1f1d07
--- a/dev/null
+++ b/include/napkin/st/decode.h
@@ -0,0 +1,15 @@
+#ifndef __NAPKIN_ST_DECODE_H
+#define __NAPKIN_ST_DECODE_H
+
+#include <napkin/types.h>
+
+namespace napkin {
+ namespace sleeptracker {
+
+ hypnodata_t& decode(hypnodata_t& rv,const void *data,size_t data_length);
+ hypnodata_ptr_t decode(const void *data,size_t data_length);
+
+ }
+}
+
+#endif /* __NAPKIN_ST_DECODE_H */
diff --git a/include/napkin/st/download.h b/include/napkin/st/download.h
new file mode 100644
index 0000000..92d1d9d
--- a/dev/null
+++ b/include/napkin/st/download.h
@@ -0,0 +1,20 @@
+#ifndef __NAPKIN_ST_DOWNLOAD_H
+#define __NAPKIN_ST_DOWNLOAD_H
+
+#include <napkin/types.h>
+
+namespace napkin {
+ namespace sleeptracker {
+
+ int download_initiate(const char *port=0);
+ size_t download_finish(int fd,void *buffer,size_t buffer_size);
+
+ size_t download(
+ void *buffer,size_t buffer_size,
+ const char *port=0);
+ hypnodata_ptr_t download(const char *port=0);
+
+ }
+}
+
+#endif /* __NAPKIN_ST_DOWNLOAD_H */
diff --git a/include/napkin/types.h b/include/napkin/types.h
new file mode 100644
index 0000000..2bc3a0a
--- a/dev/null
+++ b/include/napkin/types.h
@@ -0,0 +1,46 @@
+#ifndef __NAPKIN_TYPES_H
+#define __NAPKIN_TYPES_H
+
+#include <time.h>
+#include <string>
+#include <vector>
+#include <tr1/memory>
+
+namespace napkin {
+ using std::vector;
+ using std::tr1::shared_ptr;
+ using std::string;
+
+ class hypnodata_t {
+ public:
+ time_t to_bed;
+ time_t alarm;
+ int window;
+ vector<time_t> almost_awakes;
+ int data_a;
+
+ void clear();
+
+ void set_to_bed(const string& w3c);
+ void set_alarm(const string& w3c);
+ void set_window(const string& str);
+ void set_data_a(const string& str);
+ void set_almost_awakes(const string& str);
+
+ const string w3c_to_bed() const;
+ const string w3c_alarm() const;
+ const string w3c_almostawakes() const;
+
+ const string str_to_bed() const;
+ const string str_alarm() const;
+ const string str_date() const;
+ const string str_data_a() const;
+
+ time_t aligned_start() const;
+ };
+
+ typedef shared_ptr<hypnodata_t> hypnodata_ptr_t;
+
+}
+
+#endif /* __NAPKIN_TYPES_H */
diff --git a/include/napkin/util.h b/include/napkin/util.h
new file mode 100644
index 0000000..bf7946d
--- a/dev/null
+++ b/include/napkin/util.h
@@ -0,0 +1,14 @@
+#ifndef __NAPKIN_UTIL_H
+#define __NAPKIN_UTIL_H
+
+#include <time.h>
+#include <string>
+
+namespace napkin {
+ using std::string;
+
+ string strftime(const char *fmt,time_t t);
+
+}
+
+#endif /* __NAPKIN_UTIL_H */
diff --git a/lib/.gitignore b/lib/.gitignore
new file mode 100644
index 0000000..b7b1941
--- a/dev/null
+++ b/lib/.gitignore
@@ -0,0 +1,11 @@
+/.deps
+/.libs
+/st-decode.lo
+/st-decode.o
+/libnapkin.la
+/st-download.lo
+/st-download.o
+/hypnodata.lo
+/hypnodata.o
+/util.lo
+/util.o
diff --git a/lib/Makefile.am b/lib/Makefile.am
new file mode 100644
index 0000000..bf55849
--- a/dev/null
+++ b/lib/Makefile.am
@@ -0,0 +1,11 @@
+lib_LTLIBRARIES = libnapkin.la
+
+INCLUDES = -I${top_builddir}/include/ -I${top_srcdir}/include/ \
+ ${MODULES_CFLAGS}
+LIBS = ${MODULES_CFLAGS}
+
+libnapkin_la_SOURCES = \
+ st-decode.cc st-download.cc \
+ hypnodata.cc \
+ util.cc
+libnapkin_la_LDFLAGS = -version-info 0:0:0
diff --git a/lib/hypnodata.cc b/lib/hypnodata.cc
new file mode 100644
index 0000000..977fb76
--- a/dev/null
+++ b/lib/hypnodata.cc
@@ -0,0 +1,92 @@
+#include <napkin/exception.h>
+#include <napkin/util.h>
+#include <napkin/types.h>
+
+namespace napkin {
+
+ void hypnodata_t::clear() {
+ to_bed = alarm = 0;
+ data_a = window = 0;
+ almost_awakes.clear();
+ }
+
+ static time_t from_minute_w3c(const string& w3c) {
+ struct tm t; memset(&t,0,sizeof(t)); t.tm_isdst=-1;
+ if(sscanf(w3c.c_str(),"%04d-%02d-%02dT%02d:%02d",
+ &t.tm_year,&t.tm_mon,&t.tm_mday,&t.tm_hour,&t.tm_min)!=5)
+ throw exception("failed to parse w3c time");
+ --t.tm_mon;t.tm_year-=1900;
+ time_t rv = mktime(&t);
+ if(rv==(time_t)-1)
+ throw exception("failed to mktime()");
+ return rv;
+ }
+
+ void hypnodata_t::set_to_bed(const string& w3c) {
+ to_bed = from_minute_w3c(w3c); }
+ void hypnodata_t::set_alarm(const string& w3c) {
+ alarm = from_minute_w3c(w3c); }
+ void hypnodata_t::set_window(const string& str) {
+ window = strtol(str.c_str(),0,10); /* TODO: check for error */
+ }
+ void hypnodata_t::set_data_a(const string& str) {
+ data_a = strtol(str.c_str(),0,10); /* TODO: check for error */
+ }
+ void hypnodata_t::set_almost_awakes(const string& str) {
+ almost_awakes.clear();
+ static const char *significants = "0123456789-T:Z";
+ string::size_type p = str.find_first_of(significants);
+ struct tm t; memset(&t,0,sizeof(t)); t.tm_isdst=-1;
+ while(p!=string::npos) {
+ string::size_type ns = str.find_first_not_of(significants,p);
+ string w3c;
+ if(ns==string::npos) {
+ w3c = str.substr(p);
+ p = string::npos;
+ }else{
+ w3c = str.substr(p,ns-p);
+ p = str.find_first_of(significants,ns);
+ }
+ if(w3c.empty()) continue;
+ if(sscanf(w3c.c_str(),"%04d-%02d-%02dT%02d:%02d:%02d",
+ &t.tm_year,&t.tm_mon,&t.tm_mday,&t.tm_hour,&t.tm_min,&t.tm_sec)!=6)
+ throw exception("failed to parse w3c time");
+ --t.tm_mon;t.tm_year-=1900;
+ time_t aa = mktime(&t);
+ if(aa==(time_t)-1)
+ throw exception("failed to mktime()");
+ almost_awakes.push_back(aa);
+ }
+ }
+
+ const string hypnodata_t::w3c_to_bed() const {
+ return strftime("%Y-%m-%dT%H:%M",to_bed); }
+ const string hypnodata_t::w3c_alarm() const {
+ return strftime("%Y-%m-%dT%H:%M",alarm); }
+ const string hypnodata_t::w3c_almostawakes() const {
+ string rv;
+ for(vector<time_t>::const_iterator i=almost_awakes.begin();i!=almost_awakes.end();++i) {
+ if(!rv.empty())
+ rv += ',';
+ rv += strftime("%Y-%m-%dT%H:%M:%S",*i);
+ }
+ return rv;
+ }
+
+ const string hypnodata_t::str_to_bed() const {
+ return strftime("%H:%M",to_bed); }
+ const string hypnodata_t::str_alarm() const {
+ return strftime("%H:%M",alarm); }
+ const string hypnodata_t::str_date() const {
+ return strftime("%Y-%m-%d, %a",alarm); }
+ const string hypnodata_t::str_data_a() const {
+ char tmp[16];
+ snprintf(tmp,sizeof(tmp),"%d:%02d:%02d",
+ data_a/3600, (data_a%3600)/60,
+ data_a % 60 );
+ return tmp; }
+
+ time_t hypnodata_t::aligned_start() const {
+ return alarm - (alarm % (24*60*60)) - 24*60*60; }
+
+}
diff --git a/lib/st-decode.cc b/lib/st-decode.cc
new file mode 100644
index 0000000..f8459ac
--- a/dev/null
+++ b/lib/st-decode.cc
@@ -0,0 +1,121 @@
+#include <stdexcept>
+#include <numeric>
+#include <napkin/exception.h>
+#include <napkin/st/decode.h>
+
+namespace napkin {
+ namespace sleeptracker {
+ using std::invalid_argument;
+ using std::runtime_error;
+
+ struct st_time_t {
+ uint8_t hour;
+ uint8_t min;
+ };
+ struct st_date_t {
+ uint8_t month;
+ uint8_t day;
+ };
+ struct st_fulltime_t {
+ uint8_t hour;
+ uint8_t min;
+ uint8_t sec;
+ };
+ struct st_data_header_t {
+ char magic;
+ st_date_t today;
+ uint8_t unknown;
+ uint8_t window;
+ st_time_t to_bed;
+ st_time_t alarm;
+ uint8_t nawakes;
+ };
+ struct st_data_footer_t {
+ uint16_t data_a;
+ uint8_t checksum;
+ uint8_t eof_mark;
+ };
+
+ static void back_a_day(struct tm& t) {
+ time_t ts = mktime(&t);
+ if(ts==(time_t)-1)
+ throw exception_st_data("failed to make up time to step back a day");
+ ts -= 60*60*24;
+ if(!localtime_r(&ts,&t))
+ throw exception_st_data("failed to localtime_r() while stepping back a day");
+ }
+
+ hypnodata_t& decode(hypnodata_t& rv,const void *data,size_t data_length) {
+ if(data_length < (sizeof(st_data_header_t)+sizeof(st_data_footer_t)))
+ throw exception_st_data_envelope("not enough sleeptracker data to decode");
+ st_data_header_t *h = (st_data_header_t*)data;
+ if(h->magic != 'V')
+ throw exception_st_data_envelope("invalid magic in the data");
+ st_data_footer_t *f = (st_data_footer_t*)(static_cast<const char *>(data)+data_length-sizeof(st_data_footer_t));
+ if( (std::accumulate((uint8_t*)&h->today,(uint8_t*)&f->checksum,0)&0xFF) != f->checksum )
+ throw exception_st_data_integrity("checksum mismatch");
+ st_fulltime_t *aawake = (st_fulltime_t*)&h[1];
+ if((void*)&aawake[h->nawakes] != (void*)f)
+ throw exception_st_data_envelope("unbelievably screwed up data");
+ rv.clear();
+ time_t now = time(0);
+ struct tm t;
+ if(!localtime_r(&now,&t))
+ throw exception_st_data("failed to localtime_r()");
+ t.tm_mon = h->today.month-1;
+ t.tm_mday = h->today.day;
+ time_t mkt = mktime(&t);
+ if(mkt == (time_t)-1)
+ throw exception_st_data("failed to mktime() for a timestamp");
+ if(mkt > now) {
+ --t.tm_year;
+ }
+ struct tm ta;
+ memmove(&ta,&t,sizeof(ta));
+ ta.tm_sec = 0;
+ ta.tm_hour = h->alarm.hour; ta.tm_min = h->alarm.min;
+ rv.alarm = mktime(&ta);
+ if(rv.alarm == (time_t)-1)
+ throw exception_st_data("failed to mktime() for alarm");
+ struct tm tb;
+ memmove(&tb,&ta,sizeof(tb));
+ tb.tm_hour = h->to_bed.hour; tb.tm_min = h->to_bed.min;
+ rv.to_bed = mktime(&tb);
+ if(rv.to_bed == (time_t)-1)
+ throw exception_st_data("failed to mktime() for 'to bed'");
+ if(rv.to_bed > rv.alarm) {
+ back_a_day(tb);
+ rv.to_bed -= 24*60*60;
+ }
+ struct tm taaw;
+ memmove(&taaw,&tb,sizeof(taaw));
+ for(int rest=h->nawakes;rest;--rest,++aawake) {
+ if(
+ taaw.tm_mday!=ta.tm_mday
+ && (
+ aawake->hour < tb.tm_hour
+ || (
+ aawake->hour==tb.tm_hour
+ && aawake->min < tb.tm_min )
+ ) )
+ memmove(&taaw,&ta,sizeof(taaw));
+ taaw.tm_hour = aawake->hour;
+ taaw.tm_min = aawake->min;
+ taaw.tm_sec = aawake->sec;
+ rv.almost_awakes.push_back( mktime(&taaw) );
+ if(rv.almost_awakes.back() == (time_t)-1)
+ throw exception_st_data("failed to mktime() for almost awake moment");
+ }
+ rv.window = h->window;
+ rv.data_a = f->data_a;
+ return rv;
+ }
+
+ hypnodata_ptr_t decode(const void *data,size_t data_length) {
+ hypnodata_ptr_t rv( new hypnodata_t );
+ decode(*rv,data,data_length);
+ return rv;
+ }
+
+ }
+}
diff --git a/lib/st-download.cc b/lib/st-download.cc
new file mode 100644
index 0000000..b56e52d
--- a/dev/null
+++ b/lib/st-download.cc
@@ -0,0 +1,70 @@
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <termios.h>
+#include <stdexcept>
+#include <napkin/exception.h>
+#include <napkin/st/download.h>
+#include <napkin/st/decode.h>
+
+namespace napkin {
+ namespace sleeptracker {
+ using std::runtime_error;
+
+ int download_initiate(const char *port) {
+ int fd = open(port?port:"/dev/sleeptracker",
+ O_RDWR|O_NOCTTY|O_NONBLOCK);
+ if(fd<0)
+ throw exception_st_port("failed to open() sleeptracker port");
+
+ if(tcflush(fd,TCIOFLUSH)) {
+ close(fd);
+ throw exception_st_port("failed to tcflush()");
+ }
+ struct termios ts;
+ ts.c_cflag = CS8|CREAD;
+ cfsetispeed(&ts,B2400); cfsetospeed(&ts,B2400);
+ ts.c_iflag = IGNPAR;
+ ts.c_oflag = ts.c_lflag = 0;
+ ts.c_cc[VMIN]=1; ts.c_cc[VTIME]=0;
+ if(tcsetattr(fd,TCSANOW,&ts)) {
+ close(fd);
+ throw exception_st_port("failed to tcsetattr()");
+ }
+
+ if(write(fd,"V",1)!=1) {
+ close(fd);
+ throw exception_st_port("failed to write() to sleeptracker");
+ }
+ return fd;
+ }
+ size_t download_finish(int fd,void *buffer,size_t buffer_size) {
+ size_t rv = read(fd,buffer,buffer_size);
+ close(fd);
+
+ if(rv==(size_t)-1)
+ throw exception_st_port("failed to read() from sleeptracker");
+ return rv;
+ }
+
+ size_t download(
+ void *buffer,size_t buffer_size,
+ const char *port) {
+ int fd = download_initiate(port);
+ /* this is not the best way to wait for data, but
+ * after all it's a sleeptracker! */
+ sleep(1);
+ return download_finish(fd,buffer,buffer_size);
+ }
+
+ hypnodata_ptr_t download(const char *port) {
+ char buffer[2048];
+ size_t rb = download(buffer,sizeof(buffer),port);
+ hypnodata_ptr_t rv( new hypnodata_t );
+ decode(*rv,buffer,rb);
+ return rv;
+ }
+
+ }
+}
diff --git a/lib/util.cc b/lib/util.cc
new file mode 100644
index 0000000..350cbac
--- a/dev/null
+++ b/lib/util.cc
@@ -0,0 +1,13 @@
+#include <napkin/util.h>
+
+namespace napkin {
+
+ string strftime(const char *fmt,time_t t) {
+ struct tm tt;
+ localtime_r(&t,&tt);// TODO: check res
+ char rv[1024];
+ strftime(rv,sizeof(rv),fmt,&tt); // TODO: check res
+ return rv;
+ }
+
+}
diff --git a/src/.gitignore b/src/.gitignore
new file mode 100644
index 0000000..1210d2d
--- a/dev/null
+++ b/src/.gitignore
@@ -0,0 +1,13 @@
+/napkin
+/napkin.o
+/.deps
+/.libs
+/schema.cc
+/schema.o
+/db.o
+/dialogs.o
+/sleep_history.o
+/sleep_timeline.o
+/widgets.o
+/COPYING.cc
+/COPYING.o
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..1fdae31
--- a/dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,32 @@
+bin_PROGRAMS = napkin
+
+AM_CXXFLAGS = ${MODULES_CFLAGS} -I${top_srcdir}/include/ -I${srcdir}
+LIBS = ${MODULES_LIBS} \
+ ${top_builddir}/lib/libnapkin.la
+
+noinst_HEADERS = sqlite.h db.h \
+ sleep_timeline.h \
+ widgets.h dialogs.h \
+ sleep_history.h
+
+napkin_SOURCES = napkin.cc \
+ db.cc \
+ sleep_timeline.cc \
+ widgets.cc dialogs.cc \
+ sleep_history.cc \
+ schema.cc COPYING.cc
+napkin_DEPENDENCIES = \
+ ${top_builddir}/lib/libnapkin.la
+
+EXTRA_DIST = schema.sql
+
+schema.cc: schema.sql
+ (\
+ echo 'namespace napkin{const char *sql_bootstrap=' &&\
+ sed -e 's/^\s\+//' -e 's/\s*--.*$$//' -e 's/^/"/' -e 's/$$/"/' $< &&\
+ echo ';}'\
+ ) >$@
+COPYING.cc: ${top_srcdir}/COPYING
+ echo "const char * COPYING =" >$@ || (rm $@;exit 1)
+ sed 's/"/\\"/g' $< | sed 's/^/\"/' | sed 's/$$/\\n\"/' >>$@ || (rm $@;exit 1)
+ echo ";" >>$@ || (rm $@;exit 1)
diff --git a/src/db.cc b/src/db.cc
new file mode 100644
index 0000000..d1e0a85
--- a/dev/null
+++ b/src/db.cc
@@ -0,0 +1,101 @@
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <cassert>
+#include <napkin/exception.h>
+#include "db.h"
+
+#include "config.h"
+
+namespace napkin {
+
+ extern const char *sql_bootstrap;
+
+ db_t::db_t() {
+ const char *h = getenv("HOME");
+ if(h) {
+ datadir = h;
+ datadir += "/."PACKAGE_NAME"/";
+ }else{
+ char *cwd = get_current_dir_name();
+ if(!cwd)
+ throw napkin::exception("failed to get_current_dir_name()");
+ datadir = cwd;
+ free(cwd);
+ datadir += "/."PACKAGE_NAME"/";
+ }
+ if(access(datadir.c_str(),R_OK|W_OK)
+ && mkdir(datadir.c_str(),0700))
+ throw napkin::exception("no access to '"+datadir+"' directory");
+ open((datadir+PACKAGE_NAME".db").c_str());
+ assert(_D);
+ char **resp; int nr,nc; char *errm;
+ if(sqlite3_get_table(
+ _D,
+ "SELECT s_tobed FROM sleeps LIMIT 0",
+ &resp,&nr,&nc,&errm)!=SQLITE_OK) {
+ if(sqlite3_exec(_D,sql_bootstrap,NULL,NULL,&errm)!=SQLITE_OK)
+ throw napkin::exception(string("failed to bootstrap sqlite database: ")+errm);
+ }else
+ sqlite3_free_table(resp);
+ }
+
+ void db_t::store(const hypnodata_t& hd) {
+ sqlite::mem_t<char*> S = sqlite3_mprintf(
+ "INSERT INTO sleeps ("
+ "s_tobed,s_alarm,"
+ "s_window,s_data_a,"
+ "s_almost_awakes,"
+ "s_timezone"
+ ") VALUES ("
+ "%Q,%Q,%d,%d,%Q,%ld"
+ ")",
+ hd.w3c_to_bed().c_str(),
+ hd.w3c_alarm().c_str(),
+ hd.window,hd.data_a,
+ hd.w3c_almostawakes().c_str(),
+ timezone );
+ try {
+ exec(S);
+ }catch(sqlite::exception& se) {
+ if(se.rcode==SQLITE_CONSTRAINT)
+ throw exception_db_already("The record seems to be already in the database");
+ throw exception_db("Well, some error occured");
+ }
+ }
+
+ void db_t::remove(const hypnodata_t& hd) {
+ sqlite::mem_t<char*> S = sqlite3_mprintf(
+ "DELETE FROM sleeps"
+ " WHERE s_tobed=%Q AND s_alarm=%Q",
+ hd.w3c_to_bed().c_str(),
+ hd.w3c_alarm().c_str() );
+ exec(S);
+ }
+
+ void db_t::load(list<hypnodata_ptr_t>& rv,
+ const string& sql) {
+ sqlite::table_t T;
+ int nr,nc;
+ get_table( string(
+ "SELECT"
+ " s_tobed, s_alarm,"
+ " s_window, s_data_a,"
+ " s_almost_awakes"
+ " FROM sleeps"
+ " "+sql).c_str(),T,&nr,&nc );
+ if(nr) {
+ assert(nc==5);
+ for(int r=1;r<=nr;++r) {
+ hypnodata_ptr_t hd(new hypnodata_t());
+ hd->set_to_bed(T.get(r,0,nc));
+ hd->set_alarm(T.get(r,1,nc));
+ hd->set_window(T.get(r,2,nc));
+ hd->set_data_a(T.get(r,3,nc));
+ hd->set_almost_awakes(T.get(r,4,nc));
+ rv.push_back(hd);
+ }
+ }
+ }
+
+}
diff --git a/src/db.h b/src/db.h
new file mode 100644
index 0000000..3794eae
--- a/dev/null
+++ b/src/db.h
@@ -0,0 +1,28 @@
+#ifndef __N_DB_H
+#define __N_DB_H
+
+#include <string>
+#include <list>
+#include <napkin/types.h>
+#include "sqlite.h"
+
+namespace napkin {
+ using std::string;
+ using std::list;
+
+ class db_t : public sqlite::db_t {
+ public:
+ string datadir;
+
+ db_t();
+
+ void store(const hypnodata_t& hd);
+ void remove(const hypnodata_t& hd);
+
+ void load(list<hypnodata_ptr_t>& rv,
+ const string& sql);
+ };
+
+}
+
+#endif /* __N_DB_H */
diff --git a/src/dialogs.cc b/src/dialogs.cc
new file mode 100644
index 0000000..efe0b36
--- a/dev/null
+++ b/src/dialogs.cc
@@ -0,0 +1,17 @@
+#include "dialogs.h"
+
+namespace napkin {
+ namespace gtk {
+
+ hypnoinfo_dialog_t::hypnoinfo_dialog_t(Gtk::Window& w)
+ : Gtk::Dialog("Sleepy information",w,true/*modal*/,true/*use separator*/)
+ {
+ Gtk::VBox *vb = get_vbox();
+ vb->set_spacing(2);
+ vb->add(w_hinfo);
+ vb->show_all();
+ }
+
+
+ }
+}
diff --git a/src/dialogs.h b/src/dialogs.h
new file mode 100644
index 0000000..0a7f1b0
--- a/dev/null
+++ b/src/dialogs.h
@@ -0,0 +1,25 @@
+#ifndef __N_DIALOGS_H
+#define __N_DIALOGS_H
+
+#include <gtkmm/dialog.h>
+#include <gtkmm/box.h>
+#include "widgets.h"
+
+namespace napkin {
+ namespace gtk {
+
+ class hypnoinfo_dialog_t : public Gtk::Dialog {
+ public:
+ hypnoinfo_t w_hinfo;
+
+ hypnoinfo_dialog_t(Gtk::Window& w);
+
+ inline void update_data(const hypnodata_ptr_t& hd) {
+ w_hinfo.update_data(hd); }
+ };
+
+
+ }
+}
+
+#endif /* __N_DIALOGS_H */
diff --git a/src/napkin.cc b/src/napkin.cc
new file mode 100644
index 0000000..d9ba0c9
--- a/dev/null
+++ b/src/napkin.cc
@@ -0,0 +1,367 @@
+#include <fcntl.h>
+#include <iostream>
+using std::cerr;
+using std::endl;
+#include <fstream>
+using std::ofstream;
+#include <cstdlib>
+using std::min;
+#include <stdexcept>
+using std::runtime_error;
+#include <list>
+using std::list;
+#include <vector>
+using std::vector;
+#include <string>
+using std::string;
+#include <gtkmm/main.h>
+#include <gtkmm/window.h>
+#include <gtkmm/box.h>
+#include <gtkmm/statusbar.h>
+#include <gtkmm/uimanager.h>
+#include <gtkmm/stock.h>
+#include <gtkmm/toolbar.h>
+#include <gtkmm/filechooserdialog.h>
+#include <gtkmm/messagedialog.h>
+#include <gtkmm/aboutdialog.h>
+#include <napkin/exception.h>
+#include <napkin/util.h>
+#include <napkin/st/decode.h>
+#include <napkin/st/download.h>
+
+#include "db.h"
+#include "sleep_timeline.h"
+#include "dialogs.h"
+#include "sleep_history.h"
+
+#include "config.h"
+
+class napkin_ui : public Gtk::Window {
+ public:
+ Gtk::VBox w_outer_box;
+ Gtk::Statusbar w_status_bar;
+ napkin::gtk::sleep_history_t w_history;
+ Glib::RefPtr<Gtk::UIManager> uiman;
+ Glib::RefPtr<Gtk::ActionGroup> agroup;
+ napkin::db_t db;
+ Glib::RefPtr<Gtk::Action> a_remove;
+
+ napkin_ui()
+ : w_history(db)
+ {
+ static char *ui_info =
+ "<ui>"
+ "<menubar name='menu_bar'>"
+ "<menu action='menu_sleep'>"
+#ifndef NDEBUG
+ "<menu action='menu_sleep_add'>"
+#endif
+ "<menuitem action='sleep_add_from_sleeptracker'/>"
+#ifndef NDEBUG
+ "<menuitem action='sleep_add_from_datafile'/>"
+ "</menu>"
+#endif
+ "<menuitem action='sleep_remove'/>"
+ "<menuitem action='exit'/>"
+ "</menu>"
+ "<menu action='menu_help'>"
+ "<menuitem action='help_about'/>"
+ "</menu>"
+ "</menubar>"
+ "<toolbar action='tool_bar'>"
+ "<toolitem action='sleep_add_from_sleeptracker'/>"
+ "<toolitem action='sleep_remove'/>"
+ "<separator expand='true'/>"
+#ifndef NDEBUG
+ "<toolitem action='debug'/>"
+ "<separator/>"
+#endif
+ "<toolitem action='exit'/>"
+ "</toolbar>"
+ "</ui>";
+ agroup = Gtk::ActionGroup::create();
+ agroup->add(Gtk::Action::create("menu_sleep","Sleep"));
+ agroup->add(Gtk::Action::create("menu_sleep_add","Add"));
+ agroup->add(Gtk::Action::create("sleep_add_from_sleeptracker",Gtk::Stock::CONNECT,
+ "from sleeptracker","import sleeptracker data from watch"),
+ Gtk::AccelKey("<Ctrl>d"),
+ sigc::mem_fun(*this,&napkin_ui::on_sleep_add_from_sleeptracker));
+#ifndef NDEBUG
+ agroup->add(Gtk::Action::create("sleep_add_from_datafile",Gtk::Stock::CONVERT,
+ "from data file","import sleeptracker data stored in a file"),
+ sigc::mem_fun(*this,&napkin_ui::on_sleep_add_from_datafile));
+#endif
+ agroup->add(a_remove=Gtk::Action::create("sleep_remove",Gtk::Stock::REMOVE,
+ "Remove","remove highlighted sleep event from the database"),
+ Gtk::AccelKey("delete"),
+ sigc::mem_fun(*this,&napkin_ui::on_remove));
+ agroup->add(Gtk::Action::create("exit",Gtk::Stock::QUIT,"Exit","Exit "PACKAGE_NAME),
+ Gtk::AccelKey("<control>w"),
+ sigc::mem_fun(*this,&napkin_ui::on_quit));
+ agroup->add(Gtk::Action::create("menu_help","Help"));
+ agroup->add(Gtk::Action::create("help_about",Gtk::Stock::ABOUT,
+ "About","About this program"),
+ sigc::mem_fun(*this,&napkin_ui::on_help_about));
+#ifndef NDEBUG
+ agroup->add(Gtk::Action::create("debug",Gtk::Stock::INFO,"Debug","debug action"),
+ sigc::mem_fun(*this,&napkin_ui::on_debug));
+#endif
+ uiman = Gtk::UIManager::create();
+ uiman->insert_action_group(agroup);
+ add_accel_group(uiman->get_accel_group());
+ uiman->add_ui_from_string(ui_info);
+ Gtk::Widget * mb = uiman->get_widget("/menu_bar");
+ if(mb)
+ w_outer_box.pack_start(*mb,Gtk::PACK_SHRINK);
+ Gtk::Widget * tb = uiman->get_widget("/tool_bar");
+ if(tb) {
+ static_cast<Gtk::Toolbar*>(tb)->set_toolbar_style(Gtk::TOOLBAR_ICONS);
+ w_outer_box.pack_start(*tb,Gtk::PACK_SHRINK);
+ }
+ w_outer_box.pack_start(w_history,true/*expand*/,true/*fill*/);
+ w_outer_box.pack_end(w_status_bar,false/*expand*/,false/*fill*/);
+ add(w_outer_box);
+ set_title(PACKAGE_STRING);
+ set_default_size(800,600);
+ show_all();
+ w_status_bar.push(" "PACKAGE_STRING);
+
+ refresh_data();
+
+ w_history.signal_cursor_changed().connect(
+ sigc::mem_fun(*this,&napkin_ui::on_history_cursor_changed));
+ on_history_cursor_changed();
+ w_history.signal_double_click().connect(
+ sigc::mem_fun(*this,&napkin_ui::on_history_double_click));
+ }
+
+ void on_help_about() {
+ Gtk::AboutDialog about;
+ about.set_authors(vector<string>(1,"Michael Krelin <hacker@klever.net>"));
+ about.set_copyright("© 2008 Klever Group");
+ extern const char *COPYING;
+ about.set_license(COPYING);
+ about.set_program_name(PACKAGE_NAME);
+ about.set_version(VERSION);
+ about.set_website("http://kin.klever.net/");
+ about.set_website_label("Klever Internet Nothings");
+ about.set_comments("The Sleeptracker PRO watch support program");
+ about.run();
+ }
+
+ void on_history_double_click() {
+ napkin::hypnodata_ptr_t hd = w_history.get_current();
+ if(!hd) return;
+ napkin::gtk::hypnoinfo_dialog_t hid(*this);
+ hid.update_data(hd);
+ hid.add_button(Gtk::Stock::OK,Gtk::RESPONSE_OK);
+ hid.run();
+ }
+
+ void refresh_data() {
+ load_data("ORDER BY s_alarm DESC");
+ }
+
+ void load_data(const string& sql) {
+ list<napkin::hypnodata_ptr_t> hds;
+ db.load(hds,sql);
+ w_history.set_data(hds);
+ }
+
+ void on_history_cursor_changed() {
+ a_remove->set_sensitive(w_history.get_current());
+ }
+
+ void on_remove() {
+ napkin::hypnodata_ptr_t hd = w_history.get_current();
+ if(!hd) return;
+ napkin::gtk::hypnoinfo_dialog_t hid(*this);
+ hid.update_data(hd);
+ hid.add_button("Remove from the database",Gtk::RESPONSE_OK);
+ hid.add_button(Gtk::Stock::CANCEL,Gtk::RESPONSE_CANCEL);
+ if(hid.run() == Gtk::RESPONSE_OK) {
+ db.remove(*hd); // TODO: handle error
+ refresh_data();
+ }
+ }
+
+ void on_quit() {
+ hide();
+ }
+
+ void import_data(const napkin::hypnodata_ptr_t& hd) {
+ napkin::gtk::hypnoinfo_dialog_t hid(*this);
+ hid.update_data(hd);
+ hid.add_button("Add to the database",Gtk::RESPONSE_OK);
+ hid.add_button(Gtk::Stock::CANCEL,Gtk::RESPONSE_CANCEL);
+ if(hid.run() == Gtk::RESPONSE_OK) {
+ try {
+ db.store(*hd);
+ refresh_data();
+ }catch(napkin::exception_db& nedb) {
+ Gtk::MessageDialog md(*this,
+ string("Failed to add data to the database... ")+nedb.what(),
+ false/*use_markup*/,Gtk::MESSAGE_ERROR,Gtk::BUTTONS_OK,
+ true/*modal*/);
+ md.run();
+ }
+ }
+ }
+
+ class st_download_t : public Gtk::Dialog {
+ public:
+ Gtk::Label hint, attempt, error;
+ int nattempt;
+ napkin::hypnodata_ptr_t rv;
+
+ st_download_t(Gtk::Window& w)
+ : Gtk::Dialog("Importing data from watch",w,true/*modal*/,false/*use separator*/),
+ hint("\nImporting data from the sleeptracker...\n\n"
+ "Set your watch to the 'data' screen "
+ " and connect to the compuer, if you haven't yet.",0.5,0.5),
+ attempt("",1,0.5), error("",0,0.5),
+ nattempt(1), fd(-1)
+ {
+ Gtk::VBox *vb = get_vbox();
+ vb->set_spacing(10);
+ hint.set_justify(Gtk::JUSTIFY_CENTER);
+ vb->pack_start(hint,Gtk::PACK_SHRINK,5);
+ vb->pack_start(attempt);
+ vb->pack_start(error);
+ add_button("Cancel",Gtk::RESPONSE_CANCEL);
+ vb->show_all();
+ }
+ ~st_download_t() {
+ if(!(fd<0)) close(fd);
+ }
+
+ void on_map() {
+ Gtk::Dialog::on_map();
+ initiate_attempt();
+ }
+
+ void initiate_attempt() {
+ Glib::signal_timeout().connect_seconds(
+ sigc::mem_fun(*this,&st_download_t::try_watch),
+ 1);
+ }
+ void show_error(const napkin::exception& e) {
+ error.set_use_markup(true);
+ error.set_markup(string()+
+ "<span color='red'>"+
+ e.what()+"</span>");
+ }
+ void next_attempt() {
+ char tmp[128];
+ snprintf(tmp,sizeof(tmp),"Trying again, attempt #%d",++nattempt);
+ attempt.set_text(tmp);
+ }
+
+ int fd;
+ char buffer[512];
+ size_t rb;
+
+ bool try_watch() {
+ try {
+ fd = napkin::sleeptracker::download_initiate(getenv("SLEEPTRACKER_PORT"));
+ Glib::signal_timeout().connect_seconds(
+ sigc::mem_fun(*this,&st_download_t::try_data),
+ 1);
+ return false;
+ }catch(napkin::exception_sleeptracker& nest) {
+ show_error(nest);
+ }
+ next_attempt();
+ return true;
+ }
+
+ bool try_data() {
+ try {
+ try {
+ rb = napkin::sleeptracker::download_finish(fd,buffer,sizeof(buffer));
+ }catch(napkin::exception_st_port& nestp) {
+ fd = -1;
+ show_error(nestp);
+ next_attempt();
+ initiate_attempt();
+ return false;
+ }
+ rv = napkin::sleeptracker::decode(buffer,rb);
+ response(Gtk::RESPONSE_OK);
+ }catch(napkin::exception_st_data_envelope& neste) {
+ show_error(neste);
+ next_attempt();
+ initiate_attempt();
+ }catch(napkin::exception_sleeptracker& nest) {
+ show_error(nest);
+ }
+ return false;
+ }
+ };
+
+ void on_sleep_add_from_sleeptracker() {
+ st_download_t sd(*this);
+ if(sd.run()==Gtk::RESPONSE_OK && sd.rv ) {
+ sd.hide();
+#ifndef NDEBUG
+ {
+ ofstream dfile(
+ (db.datadir+"/raw-"+napkin::strftime("%Y-%m-%d.st",time(0))).c_str(),
+ std::ios::binary|std::ios::out|std::ios::trunc);
+ if(dfile)
+ dfile.write(sd.buffer,sd.rb);
+ dfile.close();
+ }
+#endif
+ import_data(sd.rv);
+ }
+ }
+
+#ifndef NDEBUG
+ void on_sleep_add_from_datafile() {
+ Gtk::FileChooserDialog d("Please select a file",
+ Gtk::FILE_CHOOSER_ACTION_OPEN);
+ d.set_transient_for(*this);
+ d.add_button(Gtk::Stock::CANCEL,Gtk::RESPONSE_CANCEL);
+ d.add_button(Gtk::Stock::OPEN,Gtk::RESPONSE_OK);
+ Gtk::FileFilter stfiles;
+ stfiles.set_name("Sleeptracker files");
+ stfiles.add_pattern("*.st");
+ d.add_filter(stfiles);
+ Gtk::FileFilter allfiles;
+ allfiles.set_name("All files");
+ allfiles.add_pattern("*");
+ d.add_filter(allfiles);
+ if(d.run()==Gtk::RESPONSE_OK) {
+ d.hide();
+
+ int fd = open(d.get_filename().c_str(),O_RDONLY);
+ if(fd<0)
+ throw napkin::exception("failed to open() data");
+ unsigned char buffer[512];
+ size_t rb = read(fd,buffer,sizeof(buffer));
+ close(fd);
+ if( (rb==(size_t)-1) || rb==sizeof(buffer))
+ throw napkin::exception("error reading datafile");
+ napkin::hypnodata_ptr_t hd = napkin::sleeptracker::decode(buffer,rb);
+ import_data(hd);
+ }
+ }
+#endif
+
+#ifndef NDEBUG
+ void on_debug() {
+ }
+#endif
+};
+
+int main(int argc,char**argv) {
+ try {
+ Gtk::Main m(argc,argv);
+ napkin_ui hui;
+ m.run(hui);
+ return 0;
+ }catch(std::exception& e) {
+ cerr << "oops: " << e.what() << endl;
+ }
+}
diff --git a/src/schema.sql b/src/schema.sql
new file mode 100644
index 0000000..f0fc4dd
--- a/dev/null
+++ b/src/schema.sql
@@ -0,0 +1,9 @@
+CREATE TABLE sleeps (
+ s_tobed text NOT NULL, -- w3cish timestamp with minute precision
+ s_alarm text NOT NULL PRIMARY KEY, -- w3cish timestamp with minute precision
+ s_window integer NOT NULL, -- number of minutes
+ s_data_a integer NOT NULL, -- number of seconds
+ s_almost_awakes text NOT NULL, -- [^0-9:TZ-]-separated list of w3cish
+ -- timestamps with second precision
+ s_timezone integer NOT NULL -- seconds west of GMT, TODO: make use of it
+);
diff --git a/src/sleep_history.cc b/src/sleep_history.cc
new file mode 100644
index 0000000..1b5ce27
--- a/dev/null
+++ b/src/sleep_history.cc
@@ -0,0 +1,158 @@
+#include <gtkmm/main.h>
+#include <napkin/util.h>
+
+#include "sleep_timeline.h"
+#include "sleep_history.h"
+
+namespace napkin {
+ namespace gtk {
+
+ sleep_history_t::basic_textrenderer::basic_textrenderer() {
+ property_family().set_value("monospace");
+ property_single_paragraph_mode().set_value(true);
+ }
+ void sleep_history_t::basic_textrenderer::render_vfunc(
+ const Glib::RefPtr<Gdk::Drawable>& window, Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area, const Gdk::Rectangle& cell_area,
+ const Gdk::Rectangle& expose_area, Gtk::CellRendererState flags) {
+ hypnodata_t *hd = (hypnodata_t*)property_user_data().get_value();
+ property_text().set_value(hd?get_text(*hd).c_str():"");
+ Gtk::CellRendererText::render_vfunc(window,widget,
+ background_area,cell_area,expose_area,
+ flags);
+ }
+
+ const string sleep_history_t::date_render_t::get_text(const hypnodata_t& hd) const {
+ return hd.str_date(); }
+ const string sleep_history_t::tobed_render_t::get_text(const hypnodata_t& hd) const {
+ return hd.str_to_bed(); }
+ const string sleep_history_t::alarm_render_t::get_text(const hypnodata_t& hd) const {
+ return hd.str_alarm(); }
+ sleep_history_t::window_render_t::window_render_t() {
+ property_xalign().set_value(1); }
+ const string sleep_history_t::window_render_t::get_text(
+ const hypnodata_t& hd) const {
+ char tmp[16];
+ snprintf(tmp,sizeof(tmp),"%2d",hd.window);
+ return tmp;
+ }
+ sleep_history_t::nawakes_render_t::nawakes_render_t() {
+ property_xalign().set_value(1); }
+ const string sleep_history_t::nawakes_render_t::get_text(
+ const hypnodata_t& hd) const {
+ char tmp[16];
+ snprintf(tmp,sizeof(tmp),"%2d",(int)hd.almost_awakes.size());
+ return tmp;
+ }
+ const string sleep_history_t::data_a_render_t::get_text(const hypnodata_t& hd) const {
+ return hd.str_data_a(); }
+
+ sleep_history_t::sleep_timeline_render_t::sleep_timeline_render_t(
+ const sleep_history_t& sh) : sleep_history(sh)
+ {
+ property_xpad().set_value(2); property_ypad().set_value(2);
+ }
+ void sleep_history_t::sleep_timeline_render_t::render_vfunc(
+ const Glib::RefPtr<Gdk::Drawable>& window, Gtk::Widget&/*widget*/,
+ const Gdk::Rectangle&/*background_area*/, const Gdk::Rectangle& cell_area,
+ const Gdk::Rectangle&/*expose_area*/, Gtk::CellRendererState/*flags*/) {
+ hypnodata_t *hd = (hypnodata_t*)property_user_data().get_value();
+ if(!hd) return;
+ int xpad = property_xpad(), ypad = property_ypad();
+ int x0 = cell_area.get_x()+xpad, y0 = cell_area.get_y()+ypad,
+ dx = cell_area.get_width()-2*xpad, dy = cell_area.get_height()-2*ypad;
+ time_t a = hd->aligned_start();
+ gtk::render_sleep_timeline(
+ *hd,
+ window,
+ x0,y0, dx,dy,
+ a+sleep_history.min_tobed,
+ a+sleep_history.max_alarm );
+ }
+
+ sleep_history_t::sleep_history_t(db_t& d)
+ : store( Gtk::ListStore::create(cols) ),
+ r_sleep_timeline(*this),
+ min_tobed(24*60*60*2), max_alarm(0),
+ db(d)
+ {
+ w_tree.set_model(store);
+ add(w_tree);
+ append_c("Date",r_date);
+ append_c("To bed",r_to_bed);
+ (c_timeline=append_c("Sleep timeline",r_sleep_timeline))->set_expand(true);
+ append_c("Alarm",r_alarm);
+ append_c("Window",r_window);
+ append_c(" N",r_nawakes);
+ append_c("Data A",r_data_a);
+
+ w_tree.signal_query_tooltip().connect(
+ sigc::mem_fun(*this,&sleep_history_t::on_query_tooltip));
+ w_tree.set_has_tooltip(true);
+ w_tree.signal_button_press_event().connect(
+ sigc::mem_fun(*this,&sleep_history_t::on_button_press),false);
+ }
+
+ Gtk::TreeView::Column *sleep_history_t::append_c(const string& title,Gtk::CellRenderer& renderer) {
+ Gtk::TreeView::Column *rv = w_tree.get_column(w_tree.append_column(title,renderer)-1);
+ rv->add_attribute(renderer.property_user_data(),cols.c_hypnodata_ptr);
+ rv->set_resizable(true);
+ return rv;
+ }
+
+ bool sleep_history_t::on_button_press(GdkEventButton* geb) {
+ if(geb->type!=GDK_2BUTTON_PRESS) return false;
+ double_click_signal();
+ return true;
+ }
+
+ bool sleep_history_t::on_query_tooltip(
+ int x,int y,bool keyboard_tooltip,
+ const Glib::RefPtr<Gtk::Tooltip>& tooltip) {
+ if(keyboard_tooltip) return false;
+ int tx,ty;
+ w_tree.convert_widget_to_tree_coords(x,y,tx,ty);
+ Gtk::TreeModel::Path p;
+ Gtk::TreeViewColumn *c;
+ int cx, cy;
+ if(!w_tree.get_path_at_pos(tx,ty,p,c,cx,cy)) return false;
+ if(c != c_timeline) return false;
+ hypnodata_ptr_t hd = store->get_iter(p)->get_value(cols.c_hypnodata);
+ string mup = "Almost awake moments are:\n\n<tt>";
+ for(vector<time_t>::const_iterator i=hd->almost_awakes.begin();i!=hd->almost_awakes.end();++i)
+ mup += strftime(" %H:%M:%S\n",*i);
+ mup += "</tt>";
+ tooltip->set_markup( mup );
+ return true;
+ }
+
+ void sleep_history_t::set_data(list<napkin::hypnodata_ptr_t> data) {
+ store->clear();
+ min_tobed = 24*60*60*2; max_alarm = 0;
+ for(std::list<napkin::hypnodata_ptr_t>::const_iterator
+ i=data.begin(); i!=data.end(); ++i) {
+ Gtk::TreeModel::Row r = *(store->append());
+ r[cols.c_hypnodata] = *i;
+ r[cols.c_hypnodata_ptr] = i->get();
+ time_t a = (*i)->aligned_start();
+ time_t soff = (*i)->to_bed-a;
+ if(soff < min_tobed) min_tobed = soff;
+ time_t eoff = (*i)->alarm-a;
+ if(eoff > max_alarm) max_alarm = eoff;
+ }
+ while(Gtk::Main::events_pending()) Gtk::Main::iteration() ;
+ w_tree.columns_autosize();
+ }
+
+ const hypnodata_ptr_t sleep_history_t::get_current() {
+ Gtk::TreeModel::Path p;
+ Gtk::TreeViewColumn *c;
+ w_tree.get_cursor(p,c);
+ if( (!p.gobj()) || p.empty())
+ return hypnodata_ptr_t(); /* XXX: or throw? */
+ Gtk::ListStore::iterator i = store->get_iter(p);
+ return i->get_value(cols.c_hypnodata);
+ }
+
+ }
+}
diff --git a/src/sleep_history.h b/src/sleep_history.h
new file mode 100644
index 0000000..7837711
--- a/dev/null
+++ b/src/sleep_history.h
@@ -0,0 +1,113 @@
+#ifndef __N_SLEEP_HISTORY_H
+#define __N_SLEEP_HISTORY_H
+
+#include <string>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/liststore.h>
+#include <napkin/types.h>
+#include "db.h"
+
+namespace napkin {
+ namespace gtk {
+ using std::string;
+
+ class sleep_history_t : public Gtk::ScrolledWindow {
+ public:
+ class basic_textrenderer : public Gtk::CellRendererText {
+ public:
+ basic_textrenderer();
+ void render_vfunc(
+ const Glib::RefPtr<Gdk::Drawable>& window, Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area, const Gdk::Rectangle& cell_area,
+ const Gdk::Rectangle& expose_area, Gtk::CellRendererState flags);
+ virtual const string get_text(const hypnodata_t& hd) const = 0;
+ };
+
+ class date_render_t : public basic_textrenderer {
+ public:
+ const string get_text(const hypnodata_t& hd) const;
+ };
+ class tobed_render_t : public basic_textrenderer {
+ public:
+ const string get_text(const hypnodata_t& hd) const;
+ };
+ class alarm_render_t : public basic_textrenderer {
+ public:
+ const string get_text(const hypnodata_t& hd) const;
+ };
+ class window_render_t : public basic_textrenderer {
+ public:
+ window_render_t();
+ const string get_text(const hypnodata_t& hd) const;
+ };
+ class nawakes_render_t : public basic_textrenderer {
+ public:
+ nawakes_render_t();
+ const string get_text(const hypnodata_t& hd) const;
+ };
+ class data_a_render_t : public basic_textrenderer {
+ public:
+ const string get_text(const hypnodata_t& hd) const;
+ };
+
+ class sleep_timeline_render_t : public Gtk::CellRenderer {
+ public:
+ const sleep_history_t& sleep_history;
+
+ sleep_timeline_render_t(const sleep_history_t& sh);
+ void render_vfunc(const Glib::RefPtr<Gdk::Drawable>& window, Gtk::Widget&/*widget*/,
+ const Gdk::Rectangle&/*background_area*/, const Gdk::Rectangle& cell_area,
+ const Gdk::Rectangle&/*expose_area*/, Gtk::CellRendererState/*flags*/);
+ };
+
+ class columns_t : public Gtk::TreeModel::ColumnRecord {
+ public:
+ Gtk::TreeModelColumn<hypnodata_ptr_t> c_hypnodata;
+ Gtk::TreeModelColumn<void*> c_hypnodata_ptr;
+
+ columns_t() {
+ add(c_hypnodata); add(c_hypnodata_ptr);
+ }
+ };
+
+ columns_t cols;
+ Gtk::TreeView w_tree;
+ Glib::RefPtr<Gtk::ListStore> store;
+ date_render_t r_date;
+ tobed_render_t r_to_bed;
+ alarm_render_t r_alarm;
+ window_render_t r_window;
+ nawakes_render_t r_nawakes;
+ data_a_render_t r_data_a;
+ sleep_timeline_render_t r_sleep_timeline;
+ Gtk::TreeView::Column *c_timeline;
+
+ sigc::signal<void> double_click_signal;
+ sigc::signal<void>& signal_double_click() { return double_click_signal; }
+
+ time_t min_tobed, max_alarm;
+
+ db_t& db;
+
+ sleep_history_t(db_t& d);
+
+ Gtk::TreeView::Column *append_c(const string& title,Gtk::CellRenderer& renderer);
+
+ bool on_button_press(GdkEventButton* geb);
+ bool on_query_tooltip(int x,int y,bool keyboard_tooltip, const Glib::RefPtr<Gtk::Tooltip>& tooltip);
+
+
+ void set_data(list<napkin::hypnodata_ptr_t> data);
+
+ Glib::SignalProxy0<void> signal_cursor_changed() {
+ return w_tree.signal_cursor_changed();
+ }
+
+ const hypnodata_ptr_t get_current();
+
+ };
+ }
+}
+
+#endif /* __N_SLEEP_HISTORY_H */
diff --git a/src/sleep_timeline.cc b/src/sleep_timeline.cc
new file mode 100644
index 0000000..e5d4146
--- a/dev/null
+++ b/src/sleep_timeline.cc
@@ -0,0 +1,100 @@
+#include <cstdlib>
+#include <vector>
+#include <gtkmm/widget.h>
+
+#include "sleep_timeline.h"
+
+namespace napkin {
+ namespace gtk {
+ using std::vector;
+ using std::min;
+
+ void render_sleep_timeline(
+ const hypnodata_t& hd,
+ const Glib::RefPtr<Gdk::Drawable>& w,
+ int x0,int y0,int dx,int dy,
+ time_t _t0,time_t _t1) {
+ static Gdk::Color c_tobed("darkgreen"), c_alarm("red"),
+ c_almostawake("maroon"), c_midnight("blue"), c_hour("#606060"),
+ c_timeline("#404040"), c_window("red"),
+ c_background("#ffffc0"), c_border("gray");
+ static bool beenthere=false;
+ if(!beenthere) {
+ Glib::RefPtr<Gdk::Colormap> cm(Gtk::Widget::get_default_colormap());
+ cm->alloc_color(c_tobed); cm->alloc_color(c_alarm);
+ cm->alloc_color(c_almostawake);
+ cm->alloc_color(c_midnight); cm->alloc_color(c_hour);
+ cm->alloc_color(c_timeline); cm->alloc_color(c_window);
+ cm->alloc_color(c_background); cm->alloc_color(c_border);
+ beenthere = true;
+ }
+ Glib::RefPtr<Gdk::GC> gc = Gdk::GC::create(w);
+
+ gc->set_foreground(c_background);
+ w->draw_rectangle(gc,true, x0,y0, dx,dy+1 );
+ gc->set_foreground(c_border);
+ w->draw_rectangle(gc,false, x0,y0, dx,dy+1 );
+ x0+=3; dx-=6;
+
+ time_t t0, t1;
+ if(_t0 && _t1 && _t0!=_t1 && _t0<=hd.to_bed && hd.alarm<=_t1)
+ t0 = _t0, t1 = _t1;
+ else
+ t0 = hd.to_bed, t1 = hd.alarm;
+ time_t dt = t1-t0;
+
+ time_t tb = hd.to_bed; time_t ta = hd.alarm;
+ int xb = x0+dx*(tb-t0)/dt,
+ xa = x0+dx*(ta-t0)/dt;
+ int ym = y0+dy/2;
+ gc->set_line_attributes(1,Gdk::LINE_SOLID,Gdk::CAP_BUTT,Gdk::JOIN_MITER);
+ gc->set_foreground(c_timeline);
+ w->draw_line(gc, xb,ym, xa,ym );
+ time_t ws = ta-hd.window*60;
+ int xws = x0+dx*(ws-t0)/dt;
+ gc->set_foreground(c_window);
+ w->draw_rectangle(gc, true, xws,ym-1, xa-xws,3 );
+ gc->set_foreground(c_almostawake);
+ int tl2 = min(dy/2 - 3, 7);
+ int yt0 = ym-tl2, yt1 = ym+tl2+1;
+ for(vector<time_t>::const_iterator i=hd.almost_awakes.begin();i!=hd.almost_awakes.end();++i) {
+ int x = x0+dx*(*i-t0)/dt;
+ w->draw_line(gc, x,yt0, x,yt1 );
+ }
+ tl2 = min(dy/5, 5);
+ yt0 = ym-tl2; yt1 = ym+tl2+1;
+ gc->set_foreground(c_hour);
+ time_t midnight = hd.aligned_start()+24*60*60;
+ for(time_t h = tb-tb%3600 + 3600; h<ta ; h+=3600) {
+ if(h==midnight) gc->set_foreground(c_midnight);
+ int x = x0+dx*(h-t0)/dt;
+ w->draw_line(gc, x,yt0, x,yt1 );
+ if(h==midnight) gc->set_foreground(c_hour);
+ }
+ gc->set_line_attributes(2,Gdk::LINE_SOLID,Gdk::CAP_BUTT,Gdk::JOIN_MITER);
+ gc->set_foreground(c_tobed);
+ w->draw_line(gc, xb,yt0, xb,yt1 );
+ gc->set_foreground(c_alarm);
+ w->draw_line(gc, xa,yt0, xa,yt1 );
+ }
+
+
+ bool sleep_timeline_t::on_expose_event(GdkEventExpose*) {
+ if(!hd) return true;
+ Glib::RefPtr<Gdk::Window> w = get_window();
+ int x0,y0,dx,dy,wd;
+ w->get_geometry(x0,y0,dx,dy,wd);
+ render_sleep_timeline(
+ *hd,
+ w,
+ 0,0, dx-2,dy-2 );
+ return true;
+ }
+
+ void sleep_timeline_t::set_data(const hypnodata_ptr_t& _hd) {
+ hd = _hd;
+ queue_draw();
+ }
+
+ }
+}
diff --git a/src/sleep_timeline.h b/src/sleep_timeline.h
new file mode 100644
index 0000000..3264fd6
--- a/dev/null
+++ b/src/sleep_timeline.h
@@ -0,0 +1,29 @@
+#ifndef __N_SLEEP_TIMELINE_H
+#define __N_SLEEP_TIMELINE_H
+
+#include <time.h>
+#include <gtkmm/drawingarea.h>
+#include <napkin/types.h>
+
+namespace napkin {
+ namespace gtk {
+
+ void render_sleep_timeline(
+ const hypnodata_t& hd,
+ const Glib::RefPtr<Gdk::Drawable>& w,
+ int x0,int y0,int dx,int dy,
+ time_t _t0=0,time_t _t1=0);
+
+ class sleep_timeline_t : public Gtk::DrawingArea {
+ public:
+ hypnodata_ptr_t hd;
+
+ bool on_expose_event(GdkEventExpose*);
+ void set_data(const hypnodata_ptr_t& _hd);
+ };
+
+
+ }
+}
+
+#endif /* __N_SLEEP_TIMELINE_H */
diff --git a/src/sqlite.h b/src/sqlite.h
new file mode 100644
index 0000000..ad276ee
--- a/dev/null
+++ b/src/sqlite.h
@@ -0,0 +1,103 @@
+#ifndef __SQLITE_H
+#define __SQLITE_H
+
+#include <cassert>
+#include <stdexcept>
+#include <string>
+#include <sqlite3.h>
+
+namespace sqlite {
+ using std::string;
+
+ class exception : public std::runtime_error {
+ public:
+ int rcode;
+ explicit exception(const string& w,int rc=-1)
+ : std::runtime_error(w), rcode(rc) { }
+ ~exception() throw() { }
+ };
+
+ class db_t {
+ public:
+ sqlite3 *_D;
+
+ db_t()
+ : _D(0) { }
+ db_t(const char *f)
+ : _D(0) { open(f); }
+ ~db_t() { close(); }
+
+ operator const sqlite3*(void) const { return _D; }
+ operator sqlite3*(void) { return _D; }
+
+ void close() {
+ if(_D) {
+ sqlite3_close(_D);
+ _D = 0;
+ }
+ }
+ void open(const char *f) {
+ close();
+ int r = sqlite3_open(f,&_D);
+ if(r!=SQLITE_OK) {
+ string msg = sqlite3_errmsg(_D); sqlite3_close(_D);
+ throw exception("Failed to open SQLite database: "+msg,r);
+ }
+ }
+
+ void exec(const char *sql) {
+ assert(_D);
+ char *errm;
+ int r = sqlite3_exec(_D,sql,NULL,NULL,&errm);
+ if(r!=SQLITE_OK)
+ throw exception(string("Failed to sqlite3_exec():")+errm,r);
+ }
+ void get_table(const char *sql,char ***resp,int *nr,int *nc) {
+ assert(_D);
+ char *errm;
+ int r = sqlite3_get_table(_D,sql,resp,nr,nc,&errm);
+ if(r!=SQLITE_OK)
+ throw exception(string("Failed to sqlite3_get_table():")+errm,r);
+ }
+ };
+
+ template<typename T>
+ class mem_t {
+ public:
+ T _M;
+
+ mem_t(T M) :_M(M) { }
+ ~mem_t() { if(_M) sqlite3_free(_M); }
+
+ operator const T&(void) const { return _M; }
+ operator T&(void) { return _M; }
+
+ mem_t operator=(T M) {
+ if(_M) sqlite3_free(_M);
+ _M = M;
+ }
+ };
+
+ class table_t {
+ public:
+ char **_T;
+
+ table_t() : _T(0) { }
+ table_t(char **T) : _T(T) { }
+ ~table_t() { if(_T) sqlite3_free_table(_T); }
+
+ operator char**&(void) { return _T; }
+
+ operator char ***(void) {
+ if(_T) sqlite3_free_table(_T);
+ return &_T; }
+
+ const char *get(int r,int c,int nc) {
+ assert(_T);
+ return _T[r*nc+c];
+ }
+ };
+
+}
+
+#endif /* __SQLITE_H */
diff --git a/src/widgets.cc b/src/widgets.cc
new file mode 100644
index 0000000..ea85bc8
--- a/dev/null
+++ b/src/widgets.cc
@@ -0,0 +1,63 @@
+#include <napkin/util.h>
+#include "widgets.h"
+
+namespace napkin {
+ namespace gtk {
+
+ hypnoinfo_t::hypnoinfo_t()
+ : w_upper(4,3,false/*homogeneous*/),
+ lc_tobed("To bed:",0.5,0.5),
+ lc_timeline("Sleep timeline:",0.5,0.5),
+ lc_alarm("Alarm:",0.5,0.5), lc_window("Window:",0.5,0.5),
+ l_data_a("",0.9,0.5)
+ {
+ add(l_date);
+ add(l_hseparator);
+ w_upper.set_col_spacings(5);
+ w_upper.attach(lc_tobed,0,1,0,1, Gtk::SHRINK);
+ w_upper.attach(lc_timeline,1,2,0,1, Gtk::SHRINK);
+ w_upper.attach(lc_alarm,2,3,0,1, Gtk::SHRINK);
+ w_upper.attach(lf_tobed,0,1,1,4, Gtk::SHRINK);
+ w_upper.attach(st_timeline,1,2,1,4,
+ Gtk::FILL|Gtk::EXPAND,Gtk::FILL|Gtk::EXPAND,0,0);
+ w_upper.attach(lf_alarm,2,3,1,2, Gtk::SHRINK);
+ w_upper.attach(lc_window,2,3,2,3, Gtk::SHRINK);
+ w_upper.attach(lf_window,2,3,3,4, Gtk::SHRINK);
+ add(w_upper);
+ add(lc_almost_awakes);
+ add(lf_almost_awakes);
+ add(l_data_a);
+ show_all();
+ }
+
+ void hypnoinfo_t::update_data(const hypnodata_ptr_t& hd) {
+ l_date.set_use_markup(true);
+ l_date.set_markup("<b>"+hd->str_date()+"</b>");
+ lf_tobed.set_use_markup(true);
+ lf_tobed.set_markup("<b>"+hd->str_to_bed()+"</b>");
+ lf_alarm.set_use_markup(true);
+ lf_alarm.set_markup("<b>"+hd->str_alarm()+"</b>");
+ char tmp[64];
+ snprintf(tmp,sizeof(tmp),"<b>%d mins</b>",hd->window);
+ lf_window.set_use_markup(true);
+ lf_window.set_markup(tmp);
+ snprintf(tmp,sizeof(tmp),"<b>%d</b> almost awake moments:",(int)hd->almost_awakes.size());
+ lc_almost_awakes.set_use_markup(true);
+ lc_almost_awakes.set_markup(tmp);
+ string awlist;
+ for(vector<time_t>::const_iterator i=hd->almost_awakes.begin();i!=hd->almost_awakes.end();++i) {
+ if(!awlist.empty())
+ awlist += ", ";
+ awlist += strftime("<b>%H:%M:%S</b>",*i);
+ }
+ lf_almost_awakes.set_use_markup(true);
+ lf_almost_awakes.set_line_wrap(true);
+ lf_almost_awakes.set_line_wrap_mode(Pango::WRAP_WORD);
+ lf_almost_awakes.set_markup("<tt>"+awlist+"</tt>");
+ l_data_a.set_use_markup(true);
+ l_data_a.set_markup("Data A is <b>"+hd->str_data_a()+"</b>");
+ st_timeline.set_data(hd);
+ }
+
+ }
+}
diff --git a/src/widgets.h b/src/widgets.h
new file mode 100644
index 0000000..99936ff
--- a/dev/null
+++ b/src/widgets.h
@@ -0,0 +1,33 @@
+#ifndef __N_WIDGETS_H
+#define __N_WIDGETS_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/label.h>
+#include <gtkmm/separator.h>
+#include <gtkmm/table.h>
+#include "sleep_timeline.h"
+
+namespace napkin {
+ namespace gtk {
+
+ class hypnoinfo_t : public Gtk::VBox {
+ public:
+ Gtk::Label l_date;
+ Gtk::HSeparator l_hseparator;
+ Gtk::Table w_upper;
+ Gtk::Label lc_tobed, lc_timeline, lc_alarm, lc_window;
+ Gtk::Label lf_tobed, lf_alarm, lf_window;
+ sleep_timeline_t st_timeline;
+ Gtk::Label lc_almost_awakes;
+ Gtk::Label lf_almost_awakes;
+ Gtk::Label l_data_a;
+
+ hypnoinfo_t();
+
+ void update_data(const hypnodata_ptr_t& hd);
+ };
+
+ }
+}
+
+#endif /* __N_WIDGETS_H */
diff --git a/test/.gitignore b/test/.gitignore
new file mode 100644
index 0000000..579d0cc
--- a/dev/null
+++ b/test/.gitignore
@@ -0,0 +1,4 @@
+/sleeptracker-decode
+/sleeptracker-decode.o
+/.libs
+/.deps
diff --git a/test/Makefile.am b/test/Makefile.am
new file mode 100644
index 0000000..09c3ed7
--- a/dev/null
+++ b/test/Makefile.am
@@ -0,0 +1,7 @@
+noinst_PROGRAMS = sleeptracker-decode
+
+INCLUDES = -I${top_srcdir}/include/ ${MODULES_CFLAGS}
+LIBS = ${top_builddir}/lib/libnapkin.la
+
+sleeptracker_decode_SOURCES = sleeptracker-decode.cc
+sleeptracker_decode_DEPENDENCIES = ${LIBS}
diff --git a/test/sleeptracker-decode.cc b/test/sleeptracker-decode.cc
new file mode 100644
index 0000000..77d4fbd
--- a/dev/null
+++ b/test/sleeptracker-decode.cc
@@ -0,0 +1,49 @@
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <termios.h>
+#include <iostream>
+#include <stdexcept>
+#include <algorithm>
+#include <iterator>
+using namespace std;
+#include <napkin/st/decode.h>
+
+string str_f_time(const char *fmt,time_t t) {
+ struct tm tt;
+ localtime_r(&t,&tt);
+ char rv[1024];
+ strftime(rv,sizeof(rv),fmt,&tt);
+ return rv;
+}
+
+ostream& operator<<(ostream& o,const napkin::hypnodata_t& hd) {
+ o
+ << "Window is " << hd.window << endl
+ << "'To bed' time is " << str_f_time("%Y-%m-%d %H:%M",hd.to_bed) << endl
+ << "Alarm time is " << str_f_time("%Y-%m-%d %H:%M",hd.alarm) << endl
+ << "Data A is " << hd.data_a/60 << ":" << hd.data_a%60 << endl;
+ for(vector<time_t>::const_iterator i=hd.almost_awakes.begin();i!=hd.almost_awakes.end();++i)
+ o << " almost awake at " << str_f_time("%Y-%m-%d %H:%M:%S",*i) << endl;
+ return o;
+}
+
+int main(int/*argc*/,char **argv) {
+ try {
+ int fd = open(argv[1],O_RDONLY);
+ if(fd<0)
+ throw runtime_error("failed to open() data");
+ unsigned char buffer[1024];
+ int rb = read(fd,buffer,sizeof(buffer));
+ if(!(rb>0))
+ throw runtime_error("failed to read() data");
+ close(fd);
+
+ napkin::hypnodata_t hd;
+ cout << napkin::sleeptracker::decode(hd,buffer,rb);
+ }catch(exception& e) {
+ cerr << "oops: " << e.what() << endl;
+ }
+}
+