Skip to content

Commit

Permalink
Add source and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelmhoffman committed Nov 15, 2024
1 parent e59da4d commit 46de1ed
Show file tree
Hide file tree
Showing 9 changed files with 401 additions and 0 deletions.
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Copyright 2024 Michael M. Hoffman <[email protected]>

bindir = $(HOME)/.local/bin

scripts = bin/gcaledit bin/gcaledit-deadlines bin/gcaledit-meals bin/gcaledit-zoom bin/gcal-tab2xlsx bin/gcal-xlsx2tab

install:
install --target-directory=$(bindir) $(scripts)

install-prereqs:
pip install gcalcli pandas XlsxWriter

.PHONY: install install-prereqs
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# gcaledit

Bash and Python scripts to demonstrate the gcalcli `agendaupdate` command.

## Installation

`make install-prereqs` to install the Python prerequisites needed.

`make install` to install the scripts. Two of them are Python scripts but it's easier to deal with it this way than to make a proper package that could be installed with pip.

## Scripts

### gcaledit

`gcaledit` is the main script. It uses `gcalcli agenda --tsv` to get event info from your Google Calendar and formats it as a table with one row per event and a column for each event detail. Then, it converts the table to XLSX spreadsheet format and uses an `open` command to open the file in Excel or your spreadsheet application of choice. You will need to supply the `open` command.

Then, `gcaledit` uses `inotifywait` to watch the file for saves. Any save will cause the changes to be written back to your Google Calendar through `gcalcli agendaupdate --tsv`.

### gcaledit workflows

`gcaledit-deadlines`: edit events on a particular calendar where you store all of your deadlines

`gcaledit-meals`: meal planning: edit events on your family calendar that match `Breakfast|Lunch|Dinner|Brunch|Cook`

`gcaledit-zoom`: edit events that match `.zoom.us`. I use this to bulk eliminate extra cruft that goes into these events by default

### helper scripts

`gcal-tab2xlsx`: convert output of `gcalcli agenda --tsv` to XLSX

`gcal-xlsx2csv`: convert XLSX created by `gcal-tab2xlsx` to be a TSV suitable as input to `gcalcli agendaupdate`

## Support

There is no support guaranteed! This may not work at all on any system other than my own. This is mainly here to provide examples. Pull requests might be considered.

## License

MIT License
95 changes: 95 additions & 0 deletions bin/gcal-tab2xlsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env python
"""gcaltab2xlsx: save gcal TSV as XLSX."""

__version__ = "0.1"

# Copyright 2021, 2023, 2024 Michael M. Hoffman <[email protected]>

from argparse import Namespace
from os import EX_OK
from pathlib import Path
import sys
from typing import TextIO

from pandas import read_csv, to_timedelta
from xlsxwriter import Workbook


WORKSHEET_NAME = "Schedule"
INFILE_SEP = "\t"

DTYPE = {"location": "string"}

COL_WIDTHS = {"id": 44,
"start_date": 10,
"end_date": 10,
"title": 40}

FORMAT_HEADER = {"bold": True}
FORMAT_LENGTH = {"num_format": "h:mm"}


def gcal_save_xlsx(infile: TextIO, outfilepath: Path) -> int:
"""Save gcal from standard input as XLSX."""
dataframe = read_csv(infile, sep=INFILE_SEP, header=0, dtype=DTYPE,
na_filter=False)
columns = dataframe.columns

if "length" in columns:
dataframe.length = to_timedelta(dataframe.length)

with Workbook(outfilepath) as workbook:
format_header = workbook.add_format(FORMAT_HEADER)
format_length = workbook.add_format(FORMAT_LENGTH)

worksheet = workbook.add_worksheet(WORKSHEET_NAME)

worksheet.write_row(0, 0, columns, format_header)

for col_index, (col_name, column) \
in enumerate(dataframe.items()):
worksheet.write_column(1, col_index, column)

width = COL_WIDTHS.get(col_name)
if col_name == "length":
col_format = format_length
else:
col_format = None

worksheet.set_column(col_index, col_index, width, col_format)

return EX_OK


def parse_args(args: list[str]) -> Namespace:
"""Parse arguments."""
from argparse import (ArgumentDefaultsHelpFormatter, ArgumentParser,
FileType)

description = __doc__.splitlines()[0].partition(": ")[2]
parser = ArgumentParser(description=description,
formatter_class=ArgumentDefaultsHelpFormatter)

version = f"%(prog)s {__version__}"
parser.add_argument("--version", action="version", version=version)

parser.add_argument("infile", nargs="?", metavar="TSV", type=FileType(),
default="-",
help="path of input TSV table ('-' means stdin)")

parser.add_argument("outfile", metavar="XLSX", type=Path,
help="path of output XLSX workbook")

return parser.parse_args(args)


def main(argv: list[str] = sys.argv[1:]) -> int:
"""Provide command-line interface."""
args = parse_args(argv)

with args.infile as infile:
return gcal_save_xlsx(infile, args.outfile)


if __name__ == "__main__":
sys.exit(main())
94 changes: 94 additions & 0 deletions bin/gcal-xlsx2tab
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env python
"""xlsx2gcaltab: Convert a workbook to tab-separated values for gcalcli."""

__version__ = "0.1"

# Copyright 2021, 2023, 2024 Michael M. Hoffman <[email protected]>

from argparse import Namespace
from os import EX_OK
from pathlib import Path
import sys

from pandas import DataFrame, NaT, read_excel, to_datetime


CSV_SEP = "\t"

DATE_FORMAT_DATE = "ISO8601"

# https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior
# hour (24-hour clock, zero padded):minute (zero-padded)
DATE_FORMAT_TIME = "%H:%M"

# XXX: instead of hardcoding the two date columns, infer this from columns with
# names ending in _date
DATE_FORMAT: dict[str, str] = {"start_date": DATE_FORMAT_DATE,
"end_date": DATE_FORMAT_DATE}
PARSE_DATES: list[str] = list(map(str, DATE_FORMAT))

EXCEL_DTYPE = {"start_date": str, "start_time": str,
"end_date": str, "end_time": str}


def update_datetime(dataframe: DataFrame, colname: str) -> None:
colname_type = colname.rpartition("_")[2]

match colname_type:
case "date" | "time":
pass
case _:
return

col = to_datetime(dataframe[colname], format="mixed")
dataframe[colname] = getattr(col.dt, colname_type)


def xlsx2tab(filepath: Path) -> int:
"""Convert a workbook to tab-separated values."""
dataframe = read_excel(filepath, dtype=EXCEL_DTYPE)

for colname in dataframe.columns:
update_datetime(dataframe, colname)

try:
# XXX: add this back in when `gcalcli agendaupdate` can handle it
del dataframe["length"]
except KeyError:
# gcalcli 4.5.0 doesn't support `--details length`
# https://github.com/insanum/gcalcli/issues/791
pass

dataframe.replace(NaT, "")

dataframe.to_csv(sys.stdout, sep=CSV_SEP, index=False)

return EX_OK


def parse_args(args: list[str]) -> Namespace:
"""Parse arguments."""
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser

description = __doc__.splitlines()[0].partition(": ")[2]
parser = ArgumentParser(description=description,
formatter_class=ArgumentDefaultsHelpFormatter)

version = f"%(prog)s {__version__}"
parser.add_argument("--version", action="version", version=version)

parser.add_argument("infile", metavar="XLSX", type=Path,
help="path of input XLSX workbook")

return parser.parse_args(args)


def main(argv: list[str] = sys.argv[1:]) -> int:
"""Provide command-line interface."""
args = parse_args(argv)

return xlsx2tab(args.infile)


if __name__ == "__main__":
sys.exit(main())
90 changes: 90 additions & 0 deletions bin/gcaledit
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash

## gcaledit: edit Google Calendar in spreadsheet

## Copyright 2020, 2021, 2023, 2024 Michael M. Hoffman <[email protected]>

set -o nounset -o pipefail -o errexit

if [[ $# -lt 4 || "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
printf >&2 \
"usage: %s --calendar EDITCAL [--calendar OTHERCAL]... START END [GREPARGS...]" \
"$0"
exit 2
fi

calendar_args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--calendar)
if [[ -n "$2" ]]; then
calendar_args+=("--calendar" "$2")
shift 2
else
echo >&2 "error: --calendar requires argument"
exit 1
fi
;;
*)
break
;;
esac
done

if [[ $# -lt 2 ]]; then
echo >&2 "error: missing START and END"
exit 1
fi

start="$1"
end="$2"

shift 2

grepargs=("$@")

if [[ ${#grepargs[@]} -eq 0 ]]; then
# default grepargs of nothing
grepargs+=("")
fi

file="$(mktemp --tmpdir "$(basename "$0").XXXXXXXXXX.xlsx")"

on_exit ()
{
rm -rf "$file"
}

trap on_exit EXIT

# https://unix.stackexchange.com/questions/11856/sort-but-keep-header-line-at-the-top
body() {
IFS= read -r header
printf '%s\n' "$header"
"$@"
}

# XXX: add more details, especially notes
# XXX: grep with exit status 1 should not break the pipe
# add a || true in there

# XXX: this won't actually work for >1 calendars until agendaupdate can
# write or ignore the calendar detail

gcalcli "${calendar_args[@]}" agenda \
--details id --details length --details action --details location \
--details description --tsv "$start" "$end" \
| body grep --perl-regexp "${grepargs[@]}" \
| gcal-tab2xlsx "$file"

open "$file"

echo "Press enter to finish after next save." >&2
while inotifywait -e close_write -e modify "$file"; do
# only supply the first calendar argument
gcal-xlsx2tab "$file" | gcalcli "${calendar_args[@]:0:2}" agendaupdate
if read -t 0; then
read # eat up whatever was provided
break
fi
done
23 changes: 23 additions & 0 deletions bin/gcaledit-deadlines
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash

## deadlines: put deadlines into spreadsheet

## Copyright 2020-2021, 2023-2024 Michael M. Hoffman <[email protected]>

set -o nounset -o pipefail -o errexit

if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
echo usage: "$0" [START] [END] [GREPARGS...]
exit 2
fi

calendar="${GCALEDIT_DEADLINES_CALENDAR:-Deadlines}"

start="${1:-3 days ago}" # suggest 3 days ago
end="${2:-1 year}" # suggest 1 year

shift 2

# grepargs: suggest .

exec gcaledit --calendar "$calendar" "$start 12am" "$end 11:59pm" "$@"
19 changes: 19 additions & 0 deletions bin/gcaledit-meals
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash

## meals: put meals into spreadsheet

## Copyright 2020-2021, 2023-2024 Michael M. Hoffman <[email protected]>

set -o nounset -o pipefail -o errexit

if [[ $# -gt 1 || "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
echo usage: "$0" [NUMDAYS]
exit 2
fi

num_days="${1:-9}"
calendar="${GCALEDIT_MEALS_CALENDAR:-Family}"

exec gcaledit --calendar "$calendar" \
"today 12am" "$num_days days 11:59pm" \
-i -e $'(\t|; )(Breakfast|Lunch|Dinner|Brunch|Cook)'
Loading

0 comments on commit 46de1ed

Please sign in to comment.