URI: 
       split the role of parsing and formatting through a simple TSV intermediate format - ics2txt - convert icalendar .ics file to plain text
  HTML git clone git://bitreich.org/ics2txt git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws65d7roiv6bfj7d652fid.onion/ics2txt
   DIR Log
   DIR Files
   DIR Refs
   DIR Tags
   DIR README
       ---
   DIR commit 37b4e99568c76d39376244f8f85fcf0dcfc95bd9
   DIR parent 7ef52e239bfc8757d45f3d868920dba32dcb5b61
  HTML Author: Josuah Demangeon <me@josuah.net>
       Date:   Mon,  2 Mar 2020 01:15:03 +0100
       
       split the role of parsing and formatting through a simple TSV intermediate format
       
       Diffstat:
         M Makefile                            |       2 +-
         M README                              |      84 ++-----------------------------
         D ics2txt                             |     168 -------------------------------
         A ics2txt-back                        |      81 ++++++++++++++++++++++++++++++
         A ics2txt-ics                         |      91 +++++++++++++++++++++++++++++++
         A ics2txt-tsv                         |      64 +++++++++++++++++++++++++++++++
         A ics2txt-txt                         |      97 ++++++++++++++++++++++++++++++
         M ics2txt.1                           |      49 +++++++++++++++++++++++--------
       
       8 files changed, 374 insertions(+), 262 deletions(-)
       ---
   DIR diff --git a/Makefile b/Makefile
       @@ -1,4 +1,4 @@
       -BIN        = ics2txt
       +BIN        = ics2txt-*
        MAN1        = ics2txt.1
        
        all:
   DIR diff --git a/README b/README
       @@ -1,83 +1,7 @@
       -ics2txt
       -=======
       +sical
       +=====
        
       -*ics2txt* is an awk scripts to deal with iCal [1] format to publish,
       -display and convert *.ics files.
       +*sical* is set of awk scripts to deal with iCal [1] format to publish,
       +display and convert *.ics files, though a simple central TSV format.
        
        [1]: https://tools.ietf.org/rfc/rfc5545.txt
       -
       -Sample output:
       -
       -2019-02-02
       -
       -07:30        Welcome to FOSDEM 2019
       -07:55        Janson
       -        FOSDEM welcome and opening talk.
       -
       -08:30        The State of Go
       -09:00        UD2.120 (Chavanne)
       -        Go 1.12 is planned to be released in February 2019 and this talk
       -        covers what's coming up with it.We'll talk about Go Modules, the
       -        proposals for Go 2, and all of the new things you might have missed.
       -
       -09:30        HTTP/3
       -10:30        UD2.208 (Decroly)
       -        HTTP/3 is the next coming HTTP version. This time TCP is replaced by
       -        the new transport protocol QUIC and things are different yet again!
       -
       -10:05        Minimalism matters
       -10:25        K.4.201
       -        Minimalism matters in computing. To trust systems we need to be able
       -        to understand them completely. Openssl heartbleed disaster was caused
       -        by code no longer being minimalistic, even if it is free and open
       -        source software. Hardware manfucturers and proprietary closed source
       -        solutions make things even worse with expectations of intrusion to
       -        privacy and backdoors if we don't aim for free hardware, software and
       -        minimalism. In this talk I will discuss minimalism in a broad context
       -        and narrow down on what the free software community can aim for.
       -
       -2019-02-03
       -
       -07:55        Microkernel virtualization under one roof
       -08:30        AW1.121
       -        Today's off-the-shell virtualization solution is ridden with
       -        complexity. Application of virtualization call for trustworthy
       -        solutions. Complexity defeats trust.Microkernels with virtualization
       -        extensions and user-level VMMs on top are a approach to mitigate
       -        complexity. Modern microkernels like seL4, the NOVA microhypervisor,
       -        Genode's -hw- kernel or Fiasco.OC are such promising candidates.
       -        Fortunately and unfortunately, the diversity come with fragmentation
       -        of the small microkernel community. There are several VMMs for each
       -        platform tight to a specific microkernel, rendering it unusable
       -        across various kernels.Genode supports several kernels already, so
       -        that unification of virtualization interfaces for VMMs across kernels
       -        seem to come into reach. Does it ? The talk will cover the venture
       -        and current state of harmonization hardware-assisted virtualization
       -        interfaces to fit into the Genode OS framework.
       -
       -14:40        FOSDEM infrastructure review
       -14:55        H.2215 (Ferrer)
       -        Informational and fun.
       -
       -15:00        2019 - Fifty years of Unix and Linux advances
       -15:50        Janson
       -        2019 marks the fiftieth anniversary of Unix, but it is also the
       -        fiftieth anniversary of the ArpaNet/Internet, and people walking on
       -        the moon.  It marks the 50th anniversary of Woodstock, the beginning
       -        of America's LGBTQ movement at the Stonewall Inn in New York City,
       -        and maddog wrote his first program fifty years ago.  It was also in
       -        1969 that he shaved for the last time.2019 marks the 30th year of the
       -        World Wide Web, the 25th anniversary of V1.0 of the Linux kernel, and
       -        of many GNU/Linux distributions starting.  2019 also marks the
       -        twentieth anniversary of the Linux Professional Institute.All of
       -        these years, and anniversaries.....but why has Unix (and its younger
       -        offspring Linux) lasted so long?   What was different about Unix that
       -        caused it to survive and flourish?   Why is it important today, and
       -        how can we take it further?  How should we celebrate 2019?  While
       -        maddog does not have all the answers, he tries to make the answers he
       -        does have interesting and fun to know.
       -
       -15:55        Closing FOSDEM 2019
       -16:00        Janson
       -        Some closing words.  Don't miss it!
       -
   DIR diff --git a/ics2txt b/ics2txt
       @@ -1,168 +0,0 @@
       -#!/usr/bin/awk -f
       -
       -# display iCal entries in plain text
       -
       -function leap(yrs)
       -{
       -        return (yrs % 4 == 0) && (yrs % 100 != 0) || (yrs % 400 == 0)
       -}
       -
       -function days_per_month(mth, yrs)
       -{
       -        if (mth == 2)
       -                return 28 + leap(yrs)
       -        else
       -                return 30 + (mth - (mth > 7)) % 2
       -}
       -
       -function to_sec(yrs, mth, day, hrs, min, sec)
       -{
       -        while (--mth >= 1)
       -                day += days_per_month(mth, yrs)
       -        while (--yrs >= 1970)
       -                day += 365 + leap(yrs)
       -        return (((((day - 1) * 24) + hrs) * 60) + min) * 60 + sec
       -}
       -
       -function to_date(fmt, sec)
       -{
       -        for (yrs = 1970; sec >= (s = 3600 * 24 * (365 + leap(yrs))); yrs++)
       -                sec -= s
       -        for (mth = 1; sec >= (s = 3600 * 24 * days_per_month(mth, yrs)); mth++)
       -                sec -= s
       -        for (day = 1; sec >= (s = 3600 * 24); day++)
       -                sec -= s
       -        for (hrs = 0; sec >= 3600; hrs++)
       -                sec -= 3600
       -        for (min = 0; sec >= 60; min++)
       -                sec -= 60
       -        return sprintf(fmt, yrs, mth, day, hrs, min, sec)
       -}
       -
       -function date_ical(str, offset) {
       -        yrs = substr(str,  1, 4)
       -        mth = substr(str,  5, 2)
       -        day = substr(str,  7, 2)
       -        hrs = substr(str, 10, 2)
       -        min = substr(str, 12, 2)
       -        if (substr(str, 16, 1) == "Z")
       -                return to_sec(yrs, mth, day, hrs, min, 0)
       -        else
       -                return to_sec(yrs, mth, day, hrs, min, 0) - offset
       -}
       -
       -function date_iso8601(date, offset)
       -{
       -        yrs = substr(date,  1, 4)
       -        mth = substr(date,  6, 2)
       -        day = substr(date,  9, 2)
       -        hrs = substr(date, 12, 2)
       -        min = substr(date, 15, 2)
       -        return to_sec(yrs, mth, day, hrs, min, 0) - offset
       -}
       -
       -function swap(array, a, b)
       -{
       -        tmp = array[a]
       -        array[a] = array[b]
       -        array[b] = tmp
       -}
       -
       -function sort(array, beg, end)
       -{
       -        if (beg >= end)                                        # end recursion
       -                return
       -
       -        a = beg + 1;                                        # #1 is the pivot
       -        b = end
       -        while (a < b) {
       -                while (a < b && array[a] <= array[beg])        # beg: skip lesser
       -                        a++
       -                while (a < b && array[b] > array[beg])        # end: skip greater
       -                        b--
       -                swap(array, a, b);                        # found 2 misplaced
       -        }
       -
       -        if (array[beg] > array[a])                        # put the pivot back
       -                swap(array, beg, a)
       -
       -        sort(array, beg, a - 1);                        # sort lower half
       -        sort(array, a, end);                                # sort higher half
       -}
       -
       -function parse_ical(list, offset)
       -{
       -        FS = "[:;]"
       -
       -        while (getline) {
       -                gsub("\r", " "); gsub("\\\\[ntr]", "  "); gsub("\\\\", "")
       -                gsub("^ *", ""); gsub(" *$", "")
       -                gsub(" *<[a-zA-Z0-9/]*>* *", "")
       -
       -                if (match($0, "^ ")) {
       -                        event[type] = event[type] substr($0, 2, length($0) - 1)
       -                } else {
       -                        type = $1
       -                        i = index($0, ":")
       -                        event[type] = substr($0, i + 1, length($0) - i)
       -                }
       -
       -                if ($0 ~ /END:VEVENT/)
       -                        list[++n] = sprintf("%d\t%d\t%s\t%s\t%s\t%s",
       -                            date_ical(event["DTSTART"], offset),
       -                            date_ical(event["DTEND"], offset),
       -                            event["SUMMARY"],
       -                            event["LOCATION"],
       -                            event["DESCRIPTION"])
       -        }
       -        sort(list, 1, n)
       -        return n
       -}
       -
       -function print_fold(prefix, s, n)
       -{
       -        while (s != "") {
       -                line = substr(s, 1, n)
       -                if (length(s) > n) sub(" +[^ \t\r\n]*$", "", line)
       -                print prefix line
       -                s = substr(s, length(line) + 2)
       -        }
       -}
       -
       -function print_entry(beg, end, summary, location, description, offset)
       -{
       -        b = to_date("%04d-%02d-%02d %02d:%02d", beg + offset)
       -        e = to_date("%04d-%02d-%02d %02d:%02d", end + offset)
       -        date = substr(b, 1, 10)
       -        hour_beg = substr(b, 12)
       -        hour_end = substr(e, 12)
       -
       -        if (date != last_date) print "\n" date
       -        print "\n" hour_beg "\t" summary
       -        done = 0
       -        if (category) printf("%s\t%s\n", !done++ ? hour_end : "", category)
       -        if (location) printf("%s\t%s\n", !done++ ? hour_end : "", location)
       -        if (description) {
       -                printf("%s", !done++ ? hour_end : "")
       -                print_fold("\t", description, 70)
       -        }
       -
       -        last_date = date
       -}
       -
       -BEGIN {
       -        "date +%z" | getline offset_str
       -        close("date +%z")
       -
       -        offset = substr(offset_str, 2, 2) * 3600
       -        offset += substr(offset_str, 4, 2) * 60
       -        if (substr(offset_str, 1, 1) == "-")
       -                offset *= -1
       -
       -        n = parse_ical(list, offset)
       -        for (i = 1; i <= n; i++) {
       -                split(list[i], arr, "\t")
       -                print_entry(arr[1], arr[2], arr[3], arr[4], arr[5], arr[6], offset)
       -        }
       -        print ""
       -}
   DIR diff --git a/ics2txt-back b/ics2txt-back
       @@ -0,0 +1,81 @@
       +#!/usr/bin/awk -f
       +
       +function isleap(year)
       +{
       +        return (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)
       +}
       +
       +function mdays(mon, year)
       +{
       +        return (mon == 2) ? (28 + isleap(year)) : (30 + (mon + (mon > 7)) % 2)
       +}
       +
       +function timegm(year, mon, mday, hour, min, sec)
       +{
       +        while (--mon >= 1)
       +                mday += mdays(mon, year)
       +        while (--year >= 1970)
       +                mday += 365 + isleap(year)
       +        return (((((mday - 1) * 24) + hour) * 60) + min) * 60 + sec
       +}
       +
       +function date_text(str, offset,
       +        year, mon, mday, hour, min)
       +{
       +        year = substr(str,  1, 4)
       +        mon  = substr(str,  6, 2)
       +        mday = substr(str,  9, 2)
       +        hour = substr(str, 12, 2)
       +        min  = substr(str, 15, 2)
       +        return timegm(year, mon, mday, hour, min, 0) - offset
       +}
       +
       +{
       +        gsub(/\t/, " ")
       +}
       +
       +/^TZ[+-]/ {
       +        hour = substr($0, 4, 2)
       +        min = substr($0, 6, 2)
       +        tzoffset = substr(zone, 3, 1) hour * 3600 + min * 60
       +        next
       +}
       +
       +/^[0-9]+-[0-9]+-[0-9]+ / {
       +        time = date_text($1 " " $2, tzoffset)
       +        row++
       +}
       +
       +/^ / {
       +        d = $0
       +        sub(/^ */, "", d)
       +        des = des " " d
       +}
       +
       +/^$/ {
       +        if (beg)
       +                printf "%d\t%d\t%s\t%s\t%s\t%s\n", beg, end, cat, loc, sum, des
       +        beg = end = cat = loc = sum = des = "" 
       +}
       +
       +row == 1 {
       +        beg = time
       +        sum = $0
       +        sub(/^[^ ]+ +[^ ]+ +/, "", sum)
       +}
       +
       +row == 2 {
       +        end = time
       +
       +        line = $0
       +        sub(/^[^ ]+ +[^ ]+ +/, "", line)
       +
       +        cat = line
       +        sub(/\].*/, "", cat)
       +        sub(/^\[/, "", cat)
       +
       +        loc = line
       +        sub(/[^]]*\] */, "", loc)
       +
       +        row = 0
       +}
   DIR diff --git a/ics2txt-ics b/ics2txt-ics
       @@ -0,0 +1,91 @@
       +#!/usr/bin/awk -f
       +
       +function isleap(year)
       +{
       +        return (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)
       +}
       +
       +function mdays(mon, year)
       +{
       +        return (mon == 2) ? (28 + isleap(year)) : (30 + (mon + (mon > 7)) % 2)
       +}
       +
       +# Split the time in seconds since epoch into a table, with fields
       +# named as with gmtime(3): tm["year"], tm["mon"], tm["mday"],
       +# tm["hour"], tm["min"], tm["sec"]
       +function gmtime(sec, tm,
       +        s)
       +{
       +        tm["year"] = 1970
       +        while (sec >= (s = 86400 * (365 + isleap(tm["year"])))) {
       +                tm["year"]++
       +                sec -= s
       +        }
       +
       +        tm["mon"] = 1
       +        while (sec >= (s = 86400 * mdays(tm["mon"], tm["year"]))) {
       +                tm["mon"]++
       +                sec -= s
       +        }
       +
       +        tm["mday"] = 1
       +        while (sec >= (s = 86400)) {
       +                tm["mday"]++
       +                sec -= s
       +        }
       +
       +        tm["hour"] = 0
       +        while (sec >= 3600) {
       +                tm["hour"]++
       +                sec -= 3600
       +        }
       +
       +        tm["min"] = 0
       +        while (sec >= 60) {
       +                tm["min"]++
       +                sec -= 60
       +        }
       +
       +        tm["sec"] = sec
       +}
       +
       +function print_fold(prefix, s, n)
       +{
       +        while (s != "") {
       +                line = substr(s, 1, n)
       +                if (length(s) > n) sub(" +[^ \t\r\n]*$", "", line)
       +                print prefix line
       +                s = substr(s, length(line) + 2)
       +        }
       +}
       +
       +BEGIN {
       +        print "BEGIN:VCALENDAR"
       +        print "VERSION:2.0"
       +        print "CALSCALE:GREGORIAN"
       +        print "METHOD:PUBLISH"
       +}
       +
       +{
       +        split($0, a, "\t")
       +        gmtime(a[1] + offset, beg)
       +        gmtime(a[2] + offset, end)
       +        cat = a[3]; loc = a[4]; sum = a[5]; des = a[6]
       +
       +        print ""
       +        print "BEGIN:VEVENT"
       +        printf "DTSTART:%04d%02d%02dT%02d%02d00Z\n",
       +          beg["year"], beg["mon"], beg["mday"], beg["hour"], beg["min"]
       +        printf "DTEND:%04d%02d%02dT%02d%02d00Z\n",
       +          end["year"], end["mon"], end["mday"], end["hour"], end["min"]
       +        print "SUMMARY:"        sum
       +        print "DESCRIPTION:"        des
       +        print "CATEGORIES:"        cat
       +        print "LOCATION:"        loc
       +        print "END:VEVENT"
       +}
       +
       +END {
       +        print ""
       +        print "END:VCALENDAR"
       +}
   DIR diff --git a/ics2txt-tsv b/ics2txt-tsv
       @@ -0,0 +1,64 @@
       +#!/usr/bin/awk -f
       +
       +function isleap(year)
       +{
       +        return (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)
       +}
       +
       +function mdays(mon, year)
       +{
       +        return (mon == 2) ? (28 + isleap(year)) : (30 + (mon + (mon > 7)) % 2)
       +}
       +
       +function timegm(year, mon, mday, hour, min, sec)
       +{
       +        while (--mon >= 1)
       +                mday += mdays(mon, year)
       +        while (--year >= 1970)
       +                mday += 365 + isleap(year)
       +        return (((((mday - 1) * 24) + hour) * 60) + min) * 60 + sec
       +}
       +
       +function date_ical(str, offset,
       +        year, mon, mday, hour, min)
       +{
       +        year = substr(str,  1, 4)
       +        mon  = substr(str,  5, 2)
       +        mday = substr(str,  7, 2)
       +        hour = substr(str, 10, 2)
       +        min  = substr(str, 12, 2)
       +        offset = (substr(str, 16, 1) == "Z" ? 0 : offset)
       +        return timegm(year, mon, mday, hour, min, 0) - offset
       +}
       +
       +BEGIN {
       +        "date +%z" | getline offset_str
       +        close("date +%z")
       +        hour = substr($0, 4, 2)
       +        min = substr($0, 6, 2)
       +        tzoffset = substr(zone, 3, 1) hour * 3600 + min * 60
       +
       +        FS = "[:;]"
       +}
       +
       +{
       +        gsub("\r", ""); gsub("\t", "\\\\t")
       +        gsub("^ *", ""); gsub(" *$", "")
       +
       +        if (match($0, "^ ")) {
       +                event[type] = event[type] substr($0, 2, length($0) - 1)
       +        } else {
       +                type = $1
       +                i = index($0, ":")
       +                event[type] = substr($0, i + 1, length($0) - i)
       +        }
       +
       +        if ($0 ~ /^END:VEVENT/)
       +                printf("%d\t%d\t%s\t%s\t%s\t%s\n",
       +                  date_ical(event["DTSTART"], offset),
       +                  date_ical(event["DTEND"], offset),
       +                  event["CATEGORIES"],
       +                  event["LOCATION"],
       +                  event["SUMMARY"],
       +                  event["DESCRIPTION"])
       +}
   DIR diff --git a/ics2txt-txt b/ics2txt-txt
       @@ -0,0 +1,97 @@
       +#!/usr/bin/awk -f
       +
       +function isleap(year)
       +{
       +        return (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)
       +}
       +
       +function mdays(mon, year)
       +{
       +        return (mon == 2) ? (28 + isleap(year)) : (30 + (mon + (mon > 7)) % 2)
       +}
       +
       +# Split the time in seconds since epoch into a table, with fields
       +# named as with gmtime(3): tm["year"], tm["mon"], tm["mday"],
       +# tm["hour"], tm["min"], tm["sec"]
       +function gmtime(sec, tm,
       +        s)
       +{
       +        tm["year"] = 1970
       +        while (sec >= (s = 86400 * (365 + isleap(tm["year"])))) {
       +                tm["year"]++
       +                sec -= s
       +        }
       +
       +        tm["mon"] = 1
       +        while (sec >= (s = 86400 * mdays(tm["mon"], tm["year"]))) {
       +                tm["mon"]++
       +                sec -= s
       +        }
       +
       +        tm["mday"] = 1
       +        while (sec >= (s = 86400)) {
       +                tm["mday"]++
       +                sec -= s
       +        }
       +
       +        tm["hour"] = 0
       +        while (sec >= 3600) {
       +                tm["hour"]++
       +                sec -= 3600
       +        }
       +
       +        tm["min"] = 0
       +        while (sec >= 60) {
       +                tm["min"]++
       +                sec -= 60
       +        }
       +
       +        tm["sec"] = sec
       +}
       +
       +function print_fold(prefix, s, n)
       +{
       +        while (s != "") {
       +                line = substr(s, 1, n)
       +                if (length(s) > n) sub(" +[^ \t\r\n]*$", "", line)
       +                print prefix line
       +                s = substr(s, length(line) + 2)
       +        }
       +}
       +
       +BEGIN {
       +        cmd = "date +%z"
       +        cmd | getline zone
       +        close(cmd)
       +
       +        hour = substr(zone, 2, 2)
       +        min = substr(zone, 4, 2)
       +
       +        offset = (substr(zone, 1, 1) "1") * (hour * 3600 + min * 60)
       +        print "TZ" zone
       +}
       +
       +{
       +        split($0, a, "\t")
       +        gmtime(a[1] + offset, beg)
       +        gmtime(a[2] + offset, end)
       +        cat = a[3]; loc = a[4]; sum = a[5]; des = a[6]
       +
       +        print ""
       +        printf "%04d-%02d-%02d %02d:%02d  ",
       +          beg["year"], beg["mon"], beg["mday"], beg["hour"], beg["min"]
       +        print sum
       +
       +        printf "%04d-%02d-%02d %02d:%02d  ",
       +          end["year"], end["mon"], end["mday"], end["hour"], end["min"]
       +        print "[" cat "] " loc
       +
       +        sub("^ *", "", des)
       +        sub(" *$", "", des)
       +        if (des)
       +                print_fold("   ", des, 80)
       +}
       +
       +END {
       +        print ""
       +}
   DIR diff --git a/ics2txt.1 b/ics2txt.1
       @@ -1,4 +1,4 @@
       -.Dd $Mdocdate: May 21 2018$
       +.Dd $Mdocdate: Mar 1 2020$
        .Dt ICS2TXT 1
        .Os
        .
       @@ -6,32 +6,54 @@
        .Sh NAME
        .
        .Nm ics2txt
       -.Nd convert ics file to a simple plain text format
       +.Nd convert ics file to simpler tsv or txt formats
        .
        .
        .Sh SYNOPSIS
        .
       -.Nm Ar ics-file...
       -.
       +.Nm ics2txt-tsv Ar <file.ics >file.tsv
       +.Nm ics2txt-txt Ar <file.tsv >file.txt
       +.Nm ics2txt-ics Ar <file.tsv >file.ics
       +.Nm ics2txt-back Ar <file.txt >file.tsv
        .
        .Sh DESCRIPTION
        .
        .Nm
       -displays iCalendar 
       -.Pq ical, Pa .ics
       -.Ar file
       -or stdin if not specified in the format described by the command:
       +convert iCalendar 
       +.Pq ical
       +.Ar file.ics
       +or stdin if not specified to a tab separated value format, with one
       +line per entry, and one column per field:
       +.
       +.Bl -offset 1n -width 1n -enum -compact
       +.
       +.It
       +Begining (epoch)
        .
       +.It
       +End (epoch)
        .
       -.Sh ENVIRONMENT
       +.It
       +Category
        .
       -.Bl -tag -width 6n
       +.It
       +Location
        .
       -.It Ev TZ
       -Timezone to use for printing the dates.
       +.It
       +Summary
       +.
       +.It description
       +description
        .
        .El
        .
       +.Pp
       +The
       +.Sq \en
       +and
       +.Sq \et
       +charaters may represent newlines and tabs.
       +.
        .
        .Sh SEE ALSO
        .
       @@ -39,6 +61,7 @@ Timezone to use for printing the dates.
        .Xr calendar 1 ,
        .Xr date 1
        .
       +.
        .Sh STANDARDS
        .
        .Rs
       @@ -51,4 +74,4 @@ Timezone to use for printing the dates.
        .
        .Sh AUTHORS
        .
       -.An Josuah Demangeon Aq Mt mail@josuah.net
       +.An Josuah Demangeon Aq Mt me@josuah.net