-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e59da4d
commit 46de1ed
Showing
9 changed files
with
401 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" "$@" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)' |
Oops, something went wrong.