summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--nix/default.nix3
-rw-r--r--nix/notmuch-links.nix24
-rw-r--r--nix/notmuch-tcp.nix22
-rw-r--r--notmuch-links30
-rw-r--r--notmuch-tcp-client61
-rw-r--r--notmuch-tcp-server142
-rwxr-xr-xworktime.py53
7 files changed, 305 insertions, 30 deletions
diff --git a/nix/default.nix b/nix/default.nix
index 097d092..5665a9d 100644
--- a/nix/default.nix
+++ b/nix/default.nix
@@ -1,6 +1,6 @@
1self: super: 1self: super:
2 2
3{ 3rec {
4 cliparg = self.callPackage ./cliparg.nix {}; 4 cliparg = self.callPackage ./cliparg.nix {};
5 rebuild-system = self.callPackage ./rebuild-system.nix {}; 5 rebuild-system = self.callPackage ./rebuild-system.nix {};
6 pulseaudio-ctl = self.callPackage ./pulseaudio-ctl.nix {}; 6 pulseaudio-ctl = self.callPackage ./pulseaudio-ctl.nix {};
@@ -8,6 +8,7 @@ self: super:
8 rolling-directory = self.callPackage ./rolling-directory.nix {}; 8 rolling-directory = self.callPackage ./rolling-directory.nix {};
9 recv = self.callPackage ./recv.nix {}; 9 recv = self.callPackage ./recv.nix {};
10 notmuch-tcp = self.callPackage ./notmuch-tcp.nix {}; 10 notmuch-tcp = self.callPackage ./notmuch-tcp.nix {};
11 notmuch-links = self.callPackage ./notmuch-links.nix { inherit notmuch-tcp; };
11 persistent-nix-shell = self.callPackage ./persistent-nix-shell.nix {}; 12 persistent-nix-shell = self.callPackage ./persistent-nix-shell.nix {};
12 worktime = self.callPackage ./worktime.nix {}; 13 worktime = self.callPackage ./worktime.nix {};
13} 14}
diff --git a/nix/notmuch-links.nix b/nix/notmuch-links.nix
new file mode 100644
index 0000000..67ebe3b
--- /dev/null
+++ b/nix/notmuch-links.nix
@@ -0,0 +1,24 @@
1{ stdenv
2, notmuch-tcp
3, zsh, coreutils, gawk, gnused
4}:
5
6stdenv.mkDerivation rec {
7 pname = "notmuch-links";
8 version = "0.1";
9 src = ../notmuch-links;
10
11 phases = [ "buildPhase" "installPhase" ];
12
13 inherit zsh coreutils gawk gnused;
14 notmuchtcp = notmuch-tcp;
15
16 buildPhase = ''
17 substituteAll $src notmuch-links
18 '';
19
20 installPhase = ''
21 install -m 0755 -D -t $out/bin \
22 notmuch-links
23 '';
24}
diff --git a/nix/notmuch-tcp.nix b/nix/notmuch-tcp.nix
index 59d2e39..1a1947b 100644
--- a/nix/notmuch-tcp.nix
+++ b/nix/notmuch-tcp.nix
@@ -1,22 +1,30 @@
1{ stdenv 1{ stdenv
2, zsh, socat, nettools, coreutils 2, python38
3}: 3}:
4 4
5stdenv.mkDerivation rec { 5stdenv.mkDerivation rec {
6 pname = "notmuch-tcp"; 6 pname = "notmuch-tcp";
7 version = "0.1"; 7 version = "1.0";
8 src = ../notmuch-tcp; 8 src = [../notmuch-tcp-client ../notmuch-tcp-server];
9 9
10 phases = [ "buildPhase" "installPhase" ]; 10 phases = [ "unpackPhase" "buildPhase" "installPhase" ];
11 11
12 inherit zsh socat nettools coreutils; 12 python = python38;
13
14 unpackPhase = ''
15 for srcFile in $src; do
16 cp $srcFile $(stripHash $srcFile)
17 done
18 '';
13 19
14 buildPhase = '' 20 buildPhase = ''
15 substituteAll $src notmuch-tcp 21 substituteAllInPlace notmuch-tcp-client
22 substituteAllInPlace notmuch-tcp-server
16 ''; 23 '';
17 24
18 installPhase = '' 25 installPhase = ''
19 install -m 0755 -D -t $out/bin \ 26 install -m 0755 -D -t $out/bin \
20 notmuch-tcp 27 notmuch-tcp-client notmuch-tcp-server
28 ln -s notmuch-tcp-client $out/bin/notmuch-tcp
21 ''; 29 '';
22} 30}
diff --git a/notmuch-links b/notmuch-links
new file mode 100644
index 0000000..16cc01c
--- /dev/null
+++ b/notmuch-links
@@ -0,0 +1,30 @@
1#!@zsh@/bin/zsh
2
3set -xe
4
5function notmuch {
6 NOTMUCH_TCP=${NOTMUCH_TCP:-2011} @notmuchtcp@/bin/notmuch-tcp ${@}
7}
8
9function browser {
10 ${BROWSER:-firefox} ${@}
11}
12
13maxCount=0
14if [[ -n "$1" && "$1" == <-> ]]; then
15 maxCount="$1"
16fi
17
18count=0
19for thread (${(z)$(notmuch search --sort=oldest-first 'tag:inbox AND is:link AND is:unread AND NOT (is:killed or tag:later)' | @coreutils@/bin/tee >(cat >&2) | @gawk@/bin/awk '{ print $1; }')}); do
20 url=$(notmuch show --format=mbox $thread | @gnused@/bin/sed -r '/^X-RSS-URL: /!d; s/^X-RSS-URL: (.*)$/\1/')
21 count=$((count + 1))
22
23 if [[ -n "${url}" ]]; then
24 browser ${url} && notmuch tag -unread -inbox -- $thread
25 fi
26
27 if [[ ${maxCount} -gt 0 && ${count} -ge ${maxCount} ]]; then
28 exit 0
29 fi
30done
diff --git a/notmuch-tcp-client b/notmuch-tcp-client
new file mode 100644
index 0000000..b29d6b2
--- /dev/null
+++ b/notmuch-tcp-client
@@ -0,0 +1,61 @@
1#!@python@/bin/python
2
3from os import environ
4from os.path import expanduser
5import socket
6import ssl
7from shlex import quote
8from sys import argv, stdin, stdout
9from multiprocessing import Process
10from select import select
11
12
13port = environ.get('NOTMUCH_TCP')
14host = environ.get('NOTMUCH_HOST')
15hostname = socket.gethostname()
16
17if port is None:
18 port = 2010
19if host is None:
20 host = "odin.asgard.yggdrasil"
21
22
23sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
24sslcontext.load_verify_locations(cafile = expanduser('~/.notmuch-tcp/ca.pem'))
25sslcontext.load_cert_chain(certfile = expanduser(f"~/.notmuch-tcp/{hostname}.pem"), keyfile = expanduser(f"~/.notmuch-tcp/{hostname}.key"))
26
27with socket.create_connection((host, port)) as sock:
28 with sslcontext.wrap_socket(sock, server_hostname = host) as ssock:
29 def send_args():
30 escaped_args = ' '.join(map(quote, argv[1:]))
31 ssock.sendall(f"{escaped_args}\n".encode())
32
33 def send_stdin():
34 with open(0, 'rb') as stdin_bin:
35 while True:
36 to_send = stdin_bin.read(256)
37
38 if to_send:
39 ssock.sendall(to_send)
40 else:
41 break
42
43 def recv_stdout():
44 with open(1, 'wb') as stdout_bin:
45 while True:
46 ready = select([ssock], [], [], 5)
47 if ready[0]:
48 received = ssock.recv(256)
49 if len(received) <= 0:
50 break
51 stdout_bin.write(received)
52 else:
53 break
54
55 send_args()
56 send = Process(target = send_stdin)
57 recv = Process(target = recv_stdout)
58 send.start()
59 recv.start()
60 recv.join()
61 send.terminate()
diff --git a/notmuch-tcp-server b/notmuch-tcp-server
new file mode 100644
index 0000000..328b72c
--- /dev/null
+++ b/notmuch-tcp-server
@@ -0,0 +1,142 @@
1#!@python@/bin/python
2
3from os import environ
4from os.path import expanduser
5import socket
6import ssl
7import shlex
8from sys import argv, stdin, stdout, exit
9from multiprocessing import Process
10from select import select
11from io import BytesIO
12import subprocess
13from time import sleep
14
15import logging
16
17
18logger = logging.getLogger('notmuch-tcp-server')
19logger.setLevel(logging.DEBUG)
20lh = logging.StreamHandler()
21lh.setLevel(logging.DEBUG)
22lh.setFormatter(logging.Formatter( fmt = '{levelname} - {message}', style = '{' ))
23logger.addHandler(lh)
24
25
26port = environ.get('NOTMUCH_TCP')
27host = environ.get('NOTMUCH_HOST')
28hostname = socket.gethostname()
29
30if port is None:
31 port = 2010
32if host is None:
33 host = "odin.asgard.yggdrasil"
34
35sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
36sslcontext.load_verify_locations(cafile = expanduser('~/.notmuch-tcp/ca.pem'))
37sslcontext.load_cert_chain(certfile = expanduser(f"~/.notmuch-tcp/{hostname}.pem"), keyfile = expanduser(f"~/.notmuch-tcp/{hostname}.key"))
38
39s = None
40for res in socket.getaddrinfo(host, int(port), socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
41 af, socktype, proto, canonname, sa = res
42 try:
43 s = socket.socket(af, socktype, proto)
44 except OSError as msg:
45 logger.error(f"Could not create socket: {msg}")
46 s = None
47 continue
48 try:
49 s.bind(sa)
50 s.listen()
51 except OSError as msg:
52 logger.error(f"Could not bind socket: {msg}")
53 s.close()
54 s = None
55 continue
56 break
57if s is None:
58 logger.error('Could not open any sockets')
59 exit(1)
60
61with s as sock:
62 with sslcontext.wrap_socket(sock, server_side = True) as ssock:
63 while True:
64 logger.debug('waiting...')
65 conn = None
66 try:
67 conn, client_addr = ssock.accept()
68 client_host, client_port = client_addr
69 logger.info(f"Connected: {client_host}:{client_port}")
70
71 with BytesIO() as buffer:
72 while True:
73 resp = conn.recv(256)
74 buffer.write(resp)
75 buffer.seek(0)
76 start_index = 0
77 for line in buffer:
78 start_index += len(line)
79 escaped_args = line[:-1].decode()
80 break
81 if start_index:
82 buffer.seek(start_index)
83 remaining = buffer.read()
84 buffer.truncate(0)
85 buffer.seek(0)
86 buffer.write(remaining)
87 break
88 else:
89 buffer.seek(0, 2)
90
91 logger.info(f"Arguments: {escaped_args}")
92
93 sproc = subprocess.Popen(["notmuch"] + shlex.split(escaped_args), shell = False, stdin = subprocess.PIPE, stdout = subprocess.PIPE)
94
95 def send_stdout():
96 while True:
97 to_send = sproc.stdout.read(256)
98 logger.debug(f"Sending: {to_send}")
99
100 if to_send:
101 conn.sendall(to_send)
102 else:
103 logger.debug(f"Done sending")
104 break
105
106 def recv_stdin():
107 remaining = buffer.read()
108 logger.debug(f"Received (buffered): {remaining}")
109 sproc.stdin.write(remaining)
110
111 while True:
112 logger.debug("Waiting on input...")
113 ready = select([conn], [], [], 5)
114 if ready[0]:
115 resp = conn.recv(256)
116 logger.debug(f"Received: {resp}")
117
118 if len(resp) <= 0:
119 break
120
121 sproc.stdin.write(resp)
122 else:
123 break
124 logger.debug(f"Done receiving")
125 sproc.stdin.close()
126
127 sleep(5)
128
129 sproc.kill()
130
131 send = Process(target = send_stdout)
132 recv = Process(target = recv_stdin)
133 send.start()
134 recv.start()
135 sproc_res = sproc.wait()
136 logger.info(f"Subprocess result: {sproc_res}")
137 send.join()
138 recv.terminate()
139 logger.debug(f"Handled I/O")
140 finally:
141 if conn is not None:
142 conn.close()
diff --git a/worktime.py b/worktime.py
index 9c72d30..19e2186 100755
--- a/worktime.py
+++ b/worktime.py
@@ -60,7 +60,6 @@ class TogglAPI(object):
60 return now - start if start <= now else None 60 return now - start if start <= now else None
61 61
62class Worktime(object): 62class Worktime(object):
63 time_to_work = timedelta()
64 time_worked = timedelta() 63 time_worked = timedelta()
65 running_entry = None 64 running_entry = None
66 now = datetime.now(tzlocal()) 65 now = datetime.now(tzlocal())
@@ -82,34 +81,40 @@ class Worktime(object):
82 81
83 hours_per_week = float(config.get('WORKTIME', 'HoursPerWeek', fallback=40)) 82 hours_per_week = float(config.get('WORKTIME', 'HoursPerWeek', fallback=40))
84 workdays = set([int(d.strip()) for d in config.get('WORKTIME', 'Workdays', fallback='1,2,3,4,5').split(',')]) 83 workdays = set([int(d.strip()) for d in config.get('WORKTIME', 'Workdays', fallback='1,2,3,4,5').split(',')])
85 hours_per_day = hours_per_week / len(workdays) 84 time_per_day = timedelta(hours = hours_per_week) / len(workdays)
86 85
87 holidays = set() 86 holidays = dict()
88 87
89 for year in range(start_date.year, end_date.year + 1): 88 for year in range(start_date.year, end_date.year + 1):
90 y_easter = datetime.combine(easter(year), time(), tzinfo=tzlocal()) 89 y_easter = datetime.combine(easter(year), time(), tzinfo=tzlocal())
91 90
92 # Legal holidays in munich, bavaria 91 # Legal holidays in munich, bavaria
93 holidays.add(datetime(year, 1, 1, tzinfo=tzlocal()).date()) 92 holidays[datetime(year, 1, 1, tzinfo=tzlocal()).date()] = time_per_day
94 holidays.add(datetime(year, 1, 6, tzinfo=tzlocal()).date()) 93 holidays[datetime(year, 1, 6, tzinfo=tzlocal()).date()] = time_per_day
95 holidays.add((y_easter+timedelta(days=-2)).date()) 94 holidays[(y_easter+timedelta(days=-2)).date()] = time_per_day
96 holidays.add((y_easter+timedelta(days=+1)).date()) 95 holidays[(y_easter+timedelta(days=+1)).date()] = time_per_day
97 holidays.add(datetime(year, 5, 1, tzinfo=tzlocal()).date()) 96 holidays[datetime(year, 5, 1, tzinfo=tzlocal()).date()] = time_per_day
98 holidays.add((y_easter+timedelta(days=+39)).date()) 97 holidays[(y_easter+timedelta(days=+39)).date()] = time_per_day
99 holidays.add((y_easter+timedelta(days=+50)).date()) 98 holidays[(y_easter+timedelta(days=+50)).date()] = time_per_day
100 holidays.add((y_easter+timedelta(days=+60)).date()) 99 holidays[(y_easter+timedelta(days=+60)).date()] = time_per_day
101 holidays.add(datetime(year, 8, 15, tzinfo=tzlocal()).date()) 100 holidays[datetime(year, 8, 15, tzinfo=tzlocal()).date()] = time_per_day
102 holidays.add(datetime(year, 10, 3, tzinfo=tzlocal()).date()) 101 holidays[datetime(year, 10, 3, tzinfo=tzlocal()).date()] = time_per_day
103 holidays.add(datetime(year, 11, 1, tzinfo=tzlocal()).date()) 102 holidays[datetime(year, 11, 1, tzinfo=tzlocal()).date()] = time_per_day
104 holidays.add(datetime(year, 12, 25, tzinfo=tzlocal()).date()) 103 holidays[datetime(year, 12, 25, tzinfo=tzlocal()).date()] = time_per_day
105 holidays.add(datetime(year, 12, 26, tzinfo=tzlocal()).date()) 104 holidays[datetime(year, 12, 26, tzinfo=tzlocal()).date()] = time_per_day
106 105
107 try: 106 try:
108 with open(f"{config_dir}/excused", 'r') as excused: 107 with open(f"{config_dir}/excused", 'r') as excused:
109 for line in excused: 108 for line in excused:
110 stripped_line = line.strip() 109 stripped_line = line.strip()
111 if stripped_line: 110 if stripped_line:
112 holidays.add(datetime.strptime(stripped_line, date_format).replace(tzinfo=tzlocal()).date()) 111 splitLine = stripped_line.split(' ')
112 if len(splitLine) == 2:
113 [hours, date] = splitLine
114 day = datetime.strptime(date, date_format).replace(tzinfo=tzlocal()).date()
115 holidays[day] = timedelta(hours = float(hours))
116 else:
117 holidays[datetime.strptime(stripped_line, date_format).replace(tzinfo=tzlocal()).date()] = time_per_day
113 except IOError as e: 118 except IOError as e:
114 if e.errno != 2: 119 if e.errno != 2:
115 raise e 120 raise e
@@ -130,7 +135,7 @@ class Worktime(object):
130 raise e 135 raise e
131 136
132 137
133 days_to_work = set() 138 days_to_work = dict()
134 139
135 start_day = start_date.date() 140 start_day = start_date.date()
136 end_day = end_date.date() 141 end_day = end_date.date()
@@ -138,14 +143,18 @@ class Worktime(object):
138 end_day = max(end_day, max(list(pull_forward))) 143 end_day = max(end_day, max(list(pull_forward)))
139 144
140 for day in [start_day + timedelta(days = x) for x in range(0, (end_day - start_day).days + 1)]: 145 for day in [start_day + timedelta(days = x) for x in range(0, (end_day - start_day).days + 1)]:
141 if day.isoweekday() in workdays and not day in holidays: 146 if day.isoweekday() in workdays:
142 days_to_work.add(day) 147 time_to_work = time_per_day
148 if day in holidays.keys():
149 time_to_work -= holidays[day]
150 if time_to_work > timedelta():
151 days_to_work[day] = time_to_work
143 152
144 self.is_workday = self.now.date() in days_to_work 153 self.is_workday = self.now.date() in days_to_work
145 154
146 self.time_to_work = timedelta(hours = len([day for day in days_to_work if day <= end_date.date()]) * hours_per_day) 155 self.time_to_work = sum([days_to_work[day] for day in days_to_work.keys() if day <= end_date.date()], timedelta())
147 for day in list(pull_forward): 156 for day in list(pull_forward):
148 days_forward = set([d for d in days_to_work if d >= end_date.date() and d < day and not d in pull_forward]) 157 days_forward = set([d for d in days_to_work.keys() if d >= end_date.date() and d < day and not d in pull_forward])
149 hours_per_day_forward = pull_forward[day] / len(days_forward) if len(days_forward) > 0 else timedelta() 158 hours_per_day_forward = pull_forward[day] / len(days_forward) if len(days_forward) > 0 else timedelta()
150 days_forward.discard(end_date.date()) 159 days_forward.discard(end_date.date())
151 self.time_pulled_forward += pull_forward[day] - hours_per_day_forward * len(days_forward) 160 self.time_pulled_forward += pull_forward[day] - hours_per_day_forward * len(days_forward)