├── .coveragerc ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── AUTHORS.txt ├── CHANGELOG.rst ├── CODE_OF_CONDUCT.rst ├── CONTRIBUTING.rst ├── COPYING ├── MANIFEST.in ├── README.rst ├── bin ├── ikhal └── khal ├── codecov.yml ├── doc ├── Makefile ├── source │ ├── changelog.rst │ ├── conf.py │ ├── configure.rst │ ├── faq.rst │ ├── feedback.rst │ ├── hacking.rst │ ├── images │ │ └── rss.png │ ├── index.rst │ ├── install.rst │ ├── license.rst │ ├── man.rst │ ├── news.rst │ ├── news │ │ ├── 30c3.rst │ │ ├── 31c3.rst │ │ ├── callfortesting.rst │ │ ├── khal01.rst │ │ ├── khal0100.rst │ │ ├── khal011.rst │ │ ├── khal02.rst │ │ ├── khal03.rst │ │ ├── khal031.rst │ │ ├── khal04.rst │ │ ├── khal05.rst │ │ ├── khal06.rst │ │ ├── khal07.rst │ │ ├── khal071.rst │ │ ├── khal08.rst │ │ ├── khal081.rst │ │ ├── khal082.rst │ │ ├── khal083.rst │ │ ├── khal084.rst │ │ ├── khal09.rst │ │ ├── khal091.rst │ │ ├── khal092.rst │ │ ├── khal093.rst │ │ ├── khal094.rst │ │ ├── khal095.rst │ │ ├── khal096.rst │ │ ├── khal097.rst │ │ └── khal098.rst │ ├── standards.rst │ ├── usage.rst │ ├── ystatic │ │ └── .gitignore │ └── ytemplates │ │ └── layout.html └── webpage │ └── src │ └── new_rss_url.rst ├── khal.conf.sample ├── khal ├── __init__.py ├── __main__.py ├── _compat.py ├── calendar_display.py ├── cli.py ├── cli_utils.py ├── configwizard.py ├── controllers.py ├── custom_types.py ├── exceptions.py ├── icalendar.py ├── khalendar │ ├── __init__.py │ ├── backend.py │ ├── event.py │ ├── exceptions.py │ ├── khalendar.py │ ├── typing.py │ └── vdir.py ├── parse_datetime.py ├── plugins.py ├── settings │ ├── __init__.py │ ├── exceptions.py │ ├── khal.spec │ ├── settings.py │ └── utils.py ├── terminal.py ├── ui │ ├── __init__.py │ ├── base.py │ ├── calendarwidget.py │ ├── colors.py │ ├── editor.py │ └── widgets.py └── utils.py ├── misc ├── khal.desktop └── mutt2khal ├── publish-release.yaml ├── pyproject.toml ├── tests ├── __init__.py ├── backend_test.py ├── cal_display_test.py ├── cli_test.py ├── configs │ ├── nocalendars.conf │ ├── one_level_calendars.conf │ ├── simple.conf │ └── small.conf ├── configwizard_test.py ├── conftest.py ├── controller_test.py ├── event_test.py ├── icalendar_test.py ├── ics │ ├── cal_d.ics │ ├── cal_dt_two_tz.ics │ ├── cal_lots_of_timezones.ics │ ├── cal_no_dst.ics │ ├── event_d.ics │ ├── event_d_15.ics │ ├── event_d_long.ics │ ├── event_d_no_value.ics │ ├── event_d_rdate.ics │ ├── event_d_rr.ics │ ├── event_d_same_start_end.ics │ ├── event_dt_description.ics │ ├── event_dt_duration.ics │ ├── event_dt_floating.ics │ ├── event_dt_local_missing_tz.ics │ ├── event_dt_london.ics │ ├── event_dt_long.ics │ ├── event_dt_mixed_awareness.ics │ ├── event_dt_multi_recuid_no_master.ics │ ├── event_dt_multi_uid.ics │ ├── event_dt_no_end.ics │ ├── event_dt_partstat.ics │ ├── event_dt_rd.ics │ ├── event_dt_recuid_no_master.ics │ ├── event_dt_rr.ics │ ├── event_dt_rrule_invalid_until.ics │ ├── event_dt_rrule_invalid_until2.ics │ ├── event_dt_rrule_until_before_start.ics │ ├── event_dt_simple.ics │ ├── event_dt_simple_inkl_vtimezone.ics │ ├── event_dt_simple_nocat.ics │ ├── event_dt_simple_updated.ics │ ├── event_dt_simple_zulu.ics │ ├── event_dt_status_confirmed.ics │ ├── event_dt_two_rd.ics │ ├── event_dt_two_tz.ics │ ├── event_dt_url.ics │ ├── event_dtr_exdatez.ics │ ├── event_dtr_no_tz_exdatez.ics │ ├── event_dtr_notz_untilz.ics │ ├── event_invalid_exdate.ics │ ├── event_no_dst.ics │ ├── event_r_past.ics │ ├── event_rdate_no_value.ics │ ├── event_rrule_no_occurence.ics │ ├── event_rrule_recuid.ics │ ├── event_rrule_recuid_cancelled.ics │ ├── event_rrule_recuid_invalid_tzid.ics │ ├── event_rrule_recuid_update.ics │ ├── invalid_tzoffset.ics │ ├── mult_uids_and_recuid_no_order.ics │ ├── non_dst_error.ics │ ├── part0.ics │ ├── part1.ics │ ├── tz_windows_format.ics │ └── without_uid.ics ├── khalendar_test.py ├── khalendar_utils_test.py ├── parse_datetime_test.py ├── settings_test.py ├── terminal_test.py ├── ui │ ├── __init__.py │ ├── canvas_render.py │ ├── test_calendarwidget.py │ ├── test_editor.py │ ├── test_walker.py │ └── test_widgets.py ├── utils.py ├── utils_test.py ├── vdir_test.py └── vtimezone_test.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit=khal/ui/* 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **If applicable: Stack Trace** 14 | Please copy the stack trace if khal crashes. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **OS, version, khal version and how you installed it:** 26 | - The output of khal --version: [e.g. `khal, version 0.11.2.dev20+g0c47162.d20230530` ] 27 | - Installation method [e.g. PyPI, git, OS repo] 28 | - python version [e.g. python 3.9] 29 | - OS [e.g. arch] 30 | - Your khal config file 31 | - The versions of your other python packages [e.g. the output of `pip freeze`] 32 | 33 | **Additional context** 34 | Add any other context about the problem here. Especially, if the issue came up when reading an .ics file, please provide the content of that file (anonymize if needed). 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: ["master"] 7 | pull_request: 8 | branches: ["master"] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | tests: 13 | name: "Python ${{ matrix.python-version }} ${{ matrix.tox-test }}" 14 | runs-on: "ubuntu-latest" 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.9", "3.10", "3.11", "3.12"] 20 | tox-test: ["default"] 21 | 22 | steps: 23 | - uses: "actions/checkout@v3" 24 | - uses: "actions/setup-python@v4" 25 | with: 26 | python-version: "${{ matrix.python-version }}" 27 | - name: "Install test locales" 28 | run: | 29 | echo "de_DE.UTF-8 UTF-8" | sudo tee -a /etc/locale.gen 30 | echo "cs_CZ.UTF-8 UTF-8" | sudo tee -a /etc/locale.gen 31 | echo "el_GR.UTF-8 UTF-8" | sudo tee -a /etc/locale.gen 32 | echo "fr_FR.UTF-8 UTF-8" | sudo tee -a /etc/locale.gen 33 | sudo locale-gen 34 | - name: "Install dependencies" 35 | run: | 36 | set -xe 37 | python -VV 38 | python -m site 39 | python -m pip install --upgrade pip setuptools wheel 40 | python -m pip install --upgrade coverage[toml] virtualenv tox tox-gh-actions 41 | 42 | - name: "Run tox targets for ${{ matrix.python-version }}" 43 | if: ${{ matrix.tox-test == 'default' }} 44 | # Fake a TTY 45 | shell: 'script -q -e -c "bash --noprofile --norc -eo pipefail {0}"' 46 | run: "python -m tox" 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | khal/version.py 4 | khal.egg-info/ 5 | build/ 6 | dist/ 7 | .eggs/* 8 | *.egg 9 | *.db 10 | .tox 11 | .coverage 12 | .cache 13 | htmlcov 14 | doc/source/configspec.rst 15 | .mypy_cache/ 16 | .env 17 | .venv 18 | env/ 19 | venv/ 20 | .hypothesis/ 21 | .python-version 22 | .dmypy.json 23 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: trailing-whitespace 6 | args: [--markdown-linebreak-ext=md] 7 | - id: end-of-file-fixer 8 | - id: check-toml 9 | - id: check-added-large-files 10 | - id: debug-statements 11 | - repo: https://github.com/pre-commit/mirrors-mypy 12 | rev: v1.7.0 13 | hooks: 14 | - id: mypy 15 | additional_dependencies: 16 | - types-tzlocal 17 | - types-freezegun 18 | - types-pytz 19 | - types-setuptools 20 | - types-python-dateutil 21 | - repo: https://github.com/astral-sh/ruff-pre-commit 22 | rev: 'v0.1.5' 23 | hooks: 24 | - id: ruff 25 | args: ["--fix"] 26 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | 3 | version: 2 4 | sphinx: 5 | configuration: doc/source/conf.py 6 | 7 | build: 8 | os: "ubuntu-22.04" 9 | tools: 10 | python: "3.11" 11 | 12 | python: 13 | install: 14 | - method: pip 15 | path: . 16 | extra_requirements: 17 | - docs 18 | 19 | # This is the default, you can omit this 20 | formats: [] 21 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Christian Geier 2 | David Soulayrol - david.soulayrol [at] gmail [dot] com - http://david.soulayrol.name 3 | Aaron Bishop - abishop [at] linux [dot] com 4 | Thomas Dwyer - github [at] tomd [dot] tel - http://tomd.tel 5 | Thomas Tschager - github [at] tschager [dot] net - https://thomas.tschager.net 6 | Patrice Peterson - runiq [at] archlinux [dot] us 7 | Eric Scheibler - email [at] eric-scheibler [dot] de - http://eric-scheibler.de 8 | Pierre David - pdagog [at] gmail [dot] com 9 | Markus Unterwaditzer - markus [at] unterwaditzer [dot] net - https://unterwaditzer.net 10 | Hugo Osvaldo Barrera - hugo@whynothugo.nl - https://whynothugo.nl 11 | Bradley Jones - caffeinatedbrad [at] gmail [dot] com - http://caffeinatedbrad.com 12 | Micah Nordland - micah [at] rehack [dot] me - https://w3.thoughtfuldragon.com/ 13 | Thomas Glanzmann - thomas [at] glanzmann [dot] de - https://thomas.glanzmann.de/ 14 | John Shea - coachshea [at] fastmail [dot] com 15 | Dominik Joe Pantůček - joe [at] joe [dot] cz - http://joe.cz/ 16 | Thomas Schaper - libreman [at] libremail [dot] nl 17 | Oliver Kiddle - okiddle [at] yahoo [dot] co [dot] uk 18 | Filip Pytloun - filip [at] pytloun [dot] cz - https://fpy.cz 19 | Sebastian Hamann 20 | Lucas Hoffmann 21 | Johannes Wienke - languitar [at] semipol [dot] de - https://www.semipol.de 22 | Laurent Arnoud - laurent [at] spkdev [dot] net - http://spkdev.net/ 23 | Julian Mehne 24 | Stephan Weller 25 | Max Voit - max.voit+dvkh [at] with-eyes [dot] net 26 | Taylor L Money - - http://taylorlmoney.com 27 | Troy Sankey - sankeytms [at] gmail [dot] com 28 | Mart Lubbers - mart [at] martlubbers [dot] net 29 | Paweł Fertyk - pfertyk [at] openmailbox [dot] org 30 | Moritz Kobel - moritz [at] kobelnet [dot] ch - http://www.kobelnet.ch 31 | Guilhem Saurel - guilhem [at] saurel [dot] me - https://saurel.me 32 | Stefan Siegel - ssiegel [at] sdas [dot] net 33 | August Lindberg 34 | Thomas Kluyver - thomas [at] kluyver [dot] me [dot] uk 35 | Tobias Brummer - hallo [at] t0bybr.de - https://t0bybr.de 36 | Amanda Hickman - amanda [at] velociraptor [dot] info 37 | Raef Coles - raefcoles [at] gmail [dot] com 38 | Nito Martinez - nito [at] qindel [dot] com - http://qindel.com http://theqvd.com 39 | Florian Wehner - florian [at] whnr [dot] de 40 | Martin Stone 41 | Maxime Ocafrain 42 | Axel Danguin 43 | Yorick Barbanneau - git [at] epha [dot] se - https://xieme-art.org 44 | Florian Lassenay - pizzacoca [at] aquilenet [dot] fr 45 | Simon Crespeau - simon [at] art-e-toile [dot] com - https://www.art-e-toile.com 46 | Fred Thomsen - me [at] fredthomsen [dot] net - http://fredthomsen.net 47 | Robin Schubert 48 | Axel Gschaider - axel.gschaider [at] posteo [dot] de 49 | Anuragh - kpanuragh [at] gmail [dot] com 50 | Henning Ullrich - github {at] henning-ullrich [dot] de 51 | Jason Cox - me [at] jasoncarloscox [dot] com - https://jasoncarloscox.com 52 | Michael Tretter - michael.tretter [at] posteo [dot] net 53 | Raúl Medina - raulmgcontact [at] gmail (dot] com 54 | Matthew Rademaker - matthew.rademaker [at] gmail [dot] com 55 | Valentin Iovene - val [at] too [dot] gy 56 | Julian Wollrath 57 | Mattori Birnbaum - me [at] mattori [dot] com - https://mattori.com 58 | Pi R 59 | Alnoman Kamil - noman [at] kamil [dot] gr - https://kamil.gr 60 | Leonardo Taccari - iamleot [at] gmail [dot] com 61 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.rst: -------------------------------------------------------------------------------- 1 | See `the pimutils CoC `_. 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Please see `the documentation 2 | `_ for how to 3 | contribute to this project. 4 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2022 khal contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include khal.conf.sample 2 | include README.rst 3 | include CONTRIBUTING.txt 4 | include AUTHORS.txt 5 | include COPYING 6 | include CHANGELOG.rst 7 | include khal/settings/khal.spec 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | khal 2 | ==== 3 | .. image:: https://github.com/pimutils/khal/actions/workflows/ci.yml/badge.svg?branch=master&event=push 4 | :target: https://github.com/pimutils/khal/actions/workflows/ci.yml 5 | 6 | .. image:: https://codecov.io/github/pimutils/khal/coverage.svg?branch=master 7 | :target: https://codecov.io/github/pimutils/khal?branch=master 8 | 9 | .. image:: https://readthedocs.org/projects/khal/badge/?version=latest&style=flat 10 | :target: https://khal.readthedocs.io/en/latest/ 11 | 12 | *Khal* is a standards based CLI and terminal calendar program, able to synchronize 13 | with CalDAV_ servers through vdirsyncer_. 14 | 15 | .. image:: http://lostpackets.de/images/khal.png 16 | 17 | Features 18 | -------- 19 | (or rather: limitations) 20 | 21 | - khal can read and write events/icalendars to vdir_, so vdirsyncer_ can be 22 | used to `synchronize calendars with a variety of other programs`__, for 23 | example CalDAV_ servers. 24 | - fast and easy way to add new events 25 | - ikhal (interactive khal) lets you browse and edit calendars and events 26 | - no support for editing the timezones of events yet 27 | - works with python 3.9+ 28 | - khal should run on all major operating systems [1]_ 29 | 30 | .. [1] except for Microsoft Windows 31 | 32 | Feedback 33 | -------- 34 | Please do provide feedback if *khal* works for you or even more importantly if 35 | it doesn't. The preferred way to get in contact (especially if something isn't 36 | working) is via github or via IRC (#pimutils on Libera.Chat). 37 | 38 | .. _vdir: https://vdirsyncer.readthedocs.org/en/stable/vdir.html 39 | .. _vdirsyncer: https://github.com/pimutils/vdirsyncer 40 | .. _CalDAV: http://en.wikipedia.org/wiki/CalDAV 41 | .. _github: https://github.com/pimutils/khal/ 42 | .. __: http://en.wikipedia.org/wiki/Comparison_of_CalDAV_and_CardDAV_implementations 43 | 44 | 45 | Documentation 46 | ------------- 47 | For khal's documentation have a look at readthedocs_. 48 | 49 | .. _readthedocs: http://khal.readthedocs.org/ 50 | 51 | Installation 52 | ------------ 53 | khal is packaged for most `operating systems`__ and should be installable with 54 | your standard package manager. 55 | 56 | .. __: https://repology.org/project/python:khal/versions 57 | 58 | For some exemplary OS you can find installation instructions below. Otherwise 59 | see the documentation_ for more information. 60 | 61 | .. _documentation: https://khal.readthedocs.io/en/latest/install.html 62 | 63 | Debian/Ubuntu 64 | ~~~~~~~~~~~~~ 65 | 66 | apt install khal 67 | 68 | Nix 69 | ~~~ 70 | 71 | nix-env -i khal 72 | 73 | Arch 74 | ~~~~ 75 | 76 | pacman -S khal 77 | 78 | Brew 79 | ~~~~ 80 | 81 | brew install khal 82 | 83 | Fedora 84 | ~~~~~~ 85 | 86 | dnf install khal 87 | 88 | FreeBSD 89 | ~~~~~~~ 90 | 91 | pkg install py-khal 92 | 93 | 94 | Install latest version 95 | ~~~~~~~~~~~~~~~~~~~~~~ 96 | 97 | pip install git+https://github.com/pimutils/khal 98 | 99 | 100 | Alternatives 101 | ------------ 102 | Projects with similar aims you might want to check out are calendar-cli_ (no 103 | offline storage and a bit different scope) and gcalcli_ (only works with 104 | google's calendar). 105 | 106 | .. _calendar-cli: https://github.com/tobixen/calendar-cli 107 | .. _gcalcli: https://github.com/insanum/gcalcli 108 | 109 | Contributing 110 | ------------ 111 | You want to contribute to *khal*? Awesome! 112 | 113 | The most appreciated way of contributing is by supplying code or documentation, 114 | reporting bugs, creating packages for your favorite operating system, making 115 | khal better known by telling your friends about it, etc. 116 | 117 | License 118 | ------- 119 | khal is released under the Expat/MIT License:: 120 | 121 | Copyright (c) 2013-2022 khal contributors 122 | 123 | Permission is hereby granted, free of charge, to any person obtaining a copy of 124 | this software and associated documentation files (the "Software"), to deal in 125 | the Software without restriction, including without limitation the rights to 126 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 127 | the Software, and to permit persons to whom the Software is furnished to do so, 128 | subject to the following conditions: 129 | 130 | The above copyright notice and this permission notice shall be included in all 131 | copies or substantial portions of the Software. 132 | 133 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 134 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 135 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 136 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 137 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 138 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 139 | -------------------------------------------------------------------------------- /bin/ikhal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from khal.cli import main_ikhal 3 | 4 | if __name__ == "__main__": 5 | main_ikhal() 6 | -------------------------------------------------------------------------------- /bin/khal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from khal.cli import main_khal 3 | 4 | if __name__ == "__main__": 5 | main_khal() 6 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: false 4 | project: false 5 | comment: false 6 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettextp source 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/khal.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/khal.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/khal" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/khal" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /doc/source/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | .. include:: ../../CHANGELOG.rst 4 | -------------------------------------------------------------------------------- /doc/source/configure.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | :command:`khal` reads configuration files in the *ini* syntax, meaning it understands 4 | keys separated from values by a **=**, while section and subsection names are 5 | enclosed by single or double square brackets (like **[sectionname]** and 6 | **[[subsectionname]]**). Any line beginning with a **#** will be treated as a 7 | comment. 8 | 9 | Help with initial configuration 10 | ------------------------------- 11 | If you do not have a configuration file yet, running :command:`khal configure` 12 | will launch a small, interactive tool that should help you with initial 13 | configuration of :command:`khal`. 14 | 15 | Location of configuration file 16 | ------------------------------ 17 | :command:`khal` is looking for configuration files in the following places and 18 | order: :file:`$XDG_CONFIG_HOME/khal/config` (on most systems this is 19 | :file:`~/.config/khal/config`), :file:`~/.khal/khal.conf` (deprecated) and a 20 | file called :file:`khal.conf` in the current directory (deprecated). 21 | Alternatively you can specify which configuration file to use with :option:`-c 22 | path/to/config` at runtime. 23 | 24 | .. include:: configspec.rst 25 | 26 | A minimal sample configuration could look like this: 27 | 28 | Example 29 | ------- 30 | .. literalinclude:: ../../tests/configs/simple.conf 31 | :language: ini 32 | 33 | Exemplary discover usage 34 | ------------------------- 35 | If you have the following directory layout:: 36 | 37 | ~/calendars 38 | ├- work/ 39 | ├- home/ 40 | └─ family/ 41 | 42 | where `work`, `home` and `family` are all different vdirs, each containing one 43 | calendar, a matching calendar section could look like this: 44 | 45 | .. highlight:: ini 46 | 47 | :: 48 | 49 | [[calendars]] 50 | path = ~/calendars/* 51 | type = discover 52 | color = dark green 53 | 54 | 55 | Syncing 56 | ------- 57 | To get :command:`khal` working with CalDAV you will first need to setup 58 | vdirsyncer_. After each start :command:`khal` will automatically check if 59 | anything has changed and automatically update its caching db (this may take some 60 | time after the initial sync, especially for large calendar collections). 61 | Therefore, you might want to execute :command:`khal` automatically after syncing 62 | with :command:`vdirsyncer` (e.g. via :command:`cron`). 63 | 64 | .. _vdirsyncer: https://github.com/pimutils/vdirsyncer 65 | -------------------------------------------------------------------------------- /doc/source/faq.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | === 3 | 4 | Frequently asked questions: 5 | 6 | * **Installation stops with an error: source/str_util.c:25:20: fatal error: Python.h: No such file or directory** 7 | You do not have the Python development headers installed, on Debian based 8 | Distributions you can install them via *aptitude install python-dev*. 9 | 10 | * **unknown key "default_command"** 11 | This key was deprecated by f8d9135. 12 | See https://github.com/pimutils/khal/issues/648 for the rationale behind this removal. 13 | -------------------------------------------------------------------------------- /doc/source/feedback.rst: -------------------------------------------------------------------------------- 1 | Feedback 2 | ======== 3 | 4 | .. note:: 5 | 6 | All participants must follow the `pimutils Code of Conduct 7 | `_. 8 | 9 | Please do provide feedback if *khal* works for you or even more importantly, 10 | if it doesn't. Feature requests and other ideas on how to improve khal are also 11 | welcome (see below). 12 | 13 | In case you are not satisfied with khal, there are at least two other 14 | projects with similar aims you might want to check out: calendar-cli_ (no 15 | offline storage and a bit different scope) and gcalcli_ (only works with 16 | google's calendar). 17 | 18 | .. _calendar-cli: https://github.com/tobixen/calendar-cli 19 | .. _gcalcli: https://github.com/insanum/gcalcli 20 | 21 | Submitting a Bug 22 | ---------------- 23 | If you found a bug or any part of khal isn't working as you expected, please 24 | check if that bug is also present in the latest version from github (see 25 | :doc:`install`) and is not already reported_ (you still might want to comment on 26 | an already open issue). 27 | 28 | If it isn't, please open a new bug. In case you submit a new bug report, 29 | please include: 30 | 31 | * how you ran khal (please run in verbose mode with `-v DEBUG`) 32 | * what you expected khal to do 33 | * what it did instead 34 | * everything khal printed to the screen (you may redact private details) 35 | * in case khal complains about a specific .ics file, please include that as 36 | well (or create a .ics which leads to the same error without any private 37 | information) 38 | * the version of khal and python you are using, which operating system you are 39 | using and how you installed khal 40 | 41 | Suggesting Features 42 | ------------------- 43 | If you believe khal is lacking a useful feature or some part of khal is not 44 | working the way you think it should, please first check if there isn't already 45 | a relevant issue_ for it and otherwise open a new one. 46 | 47 | .. _contact: 48 | 49 | Contact 50 | ------- 51 | * You might get quick answers on the `#pimutils`_ IRC channel on Libera.Chat, if 52 | nobody is answering you, please hang around for a bit. You can also use this 53 | channel for general discussions about :command:`khal` and `related tools`_. 54 | * Open a github issue_ 55 | * If the above mentioned methods do not work, you can always contact the `main 56 | developer`_. 57 | 58 | .. _#pimutils: irc://#pimutils@Libera.Chat 59 | .. _related tools: https://github.com/pimutils/ 60 | .. _issue: https://github.com/pimutils/khal/issues 61 | .. _reported: https://github.com/pimutils/khal/issues 62 | .. _main developer: https://lostpackets.de 63 | -------------------------------------------------------------------------------- /doc/source/images/rss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimutils/khal/df6545645740f5ffe6ec00a97f7b5ff195ff60f4/doc/source/images/rss.png -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | khal 2 | ==== 3 | 4 | *Khal* is a standards based CLI (console) calendar program, able to synchronize 5 | with CalDAV_ servers through vdirsyncer_. 6 | 7 | .. image:: http://lostpackets.de/images/khal.png 8 | 9 | Features 10 | -------- 11 | (or rather: limitations) 12 | 13 | - khal can read and write events/icalendars to vdir_, so vdirsyncer_ can be 14 | used to `synchronize calendars with a variety of other programs`__, for 15 | example CalDAV_ servers. 16 | - fast and easy way to add new events 17 | - ikhal (interactive khal) lets you browse and edit calendars and events 18 | - only rudimentary support for creating and editing recursion rules 19 | - you cannot edit the timezones of events 20 | - works with python 3.9+ 21 | - khal should run on all major operating systems [1]_ 22 | 23 | .. [1] except for Microsoft Windows 24 | 25 | 26 | .. _vdir: https://vdirsyncer.readthedocs.org/en/stable/vdir.html 27 | .. _vdirsyncer: https://github.com/pimutils/vdirsyncer 28 | .. _CalDAV: http://en.wikipedia.org/wiki/CalDAV 29 | .. _github: https://github.com/pimutils/khal/ 30 | .. __: http://en.wikipedia.org/wiki/Comparison_of_CalDAV_and_CardDAV_implementations 31 | 32 | 33 | Table of Contents 34 | ================= 35 | 36 | .. toctree:: 37 | :maxdepth: 1 38 | 39 | install 40 | configure 41 | usage 42 | standards 43 | feedback 44 | hacking 45 | changelog 46 | faq 47 | license 48 | news 49 | -------------------------------------------------------------------------------- /doc/source/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | If khal is packaged for your OS/distribution, using your system's 5 | standard package manager is probably the easiest way to install khal. 6 | khal has been packaged for, among others: Arch Linux (stable_ and development_ 7 | versions), Debian_, Fedora_, FreeBSD_, Guix_, and pkgsrc_ (or see repology_ for 8 | a more complete list). 9 | 10 | .. _stable: https://www.archlinux.org/packages/community/any/khal/ 11 | .. _development: https://aur.archlinux.org/packages/khal-git/ 12 | .. _Debian: https://packages.debian.org/search?keywords=khal&searchon=names 13 | .. _Fedora: https://admin.fedoraproject.org/pkgdb/package/rpms/khal/ 14 | .. _FreeBSD: https://www.freshports.org/deskutils/py-khal/ 15 | .. _Guix: http://www.gnu.org/software/guix/packages/ 16 | .. _pkgsrc: http://pkgsrc.se/time/khal 17 | .. _repology: https://repology.org/project/python:khal/versions 18 | 19 | If a package isn't available (or it is outdated) you need to fall back to one 20 | of the methods mentioned below. 21 | 22 | Install via Python's Package Managers 23 | ------------------------------------- 24 | 25 | Since *khal* is written in python, you can use one of the package managers 26 | available to install python packages, e.g. *pip*. 27 | 28 | You can install the latest released version of *khal* by executing:: 29 | 30 | pip install khal 31 | 32 | or the latest development version by executing:: 33 | 34 | pip install git+git://github.com/pimutils/khal.git 35 | 36 | This should also take care of installing all required dependencies. If in 37 | doubt, do not use `sudo pip install` but install `pip install khal --user`. 38 | Especially if using the `--user` flag, *khal* might be installed to 39 | `~/.local/bin`. So if your shell cannot find *khal*, you might want to check 40 | there and add that `folder to your $PATH 41 | `_. 42 | 43 | Otherwise, you can always download the latest release from pypi_ and execute:: 44 | 45 | python setup.py install 46 | 47 | or better:: 48 | 49 | pip install . 50 | 51 | in the unpacked distribution folder. 52 | 53 | Since version 0.12.0, *khal* **only supports python 3.9+**. If you have 54 | python 2 and 3 installed in parallel you might need to use `pip3` instead of 55 | `pip` and `python3` instead of `python`. In case your operating system cannot 56 | deal with python 2 and 3 packages concurrently, we suggest installing *khal* in 57 | a virtualenv_ (e.g. by using virtualenvwrapper_ or with the help of pipsi_) and 58 | then starting khal from that virtual environment. 59 | 60 | .. _pipsi: https://github.com/mitsuhiko/pipsi 61 | .. _pypi: https://pypi.python.org/pypi/khal 62 | .. _virtualenv: https://virtualenv.pypa.io 63 | .. _virtualenvwrapper: http://virtualenvwrapper.readthedocs.org/ 64 | 65 | Shell Completion 66 | ---------------- 67 | khal uses click_ for it's commpand line interface. click can automatically 68 | generate completion_ files for bash, fish, and zsh which can either be generated 69 | and sourced each time your shell is started or, generated once, save to a file 70 | and sourced from there. The latter version is much more efficient. 71 | 72 | .. note:: 73 | If you package khal, please generate and include the completion file in the 74 | package if possible. 75 | 76 | .. _click: https://click.palletsprojects.com 77 | .. _completion: https://click.palletsprojects.com/en/8.1.x/shell-completion/ 78 | 79 | bash 80 | ~~~~ 81 | For bash, you can either add the following line to your ``.bashrc``:: 82 | 83 | eval "$(_KHAL_COMPLETE=bash_source khal)" 84 | 85 | Or, save the file somewhere:: 86 | 87 | _KHAL_COMPLETE=bash_source khal > ~/.khal-complete.bash 88 | 89 | and then source it from your ``.bashrc``:: 90 | 91 | . ~/.khal-complete.bash 92 | 93 | zsh 94 | ~~~ 95 | For zsh, you can either add the following line to your ``.zshrc``:: 96 | 97 | eval "$(_KHAL_COMPLETE=zsh_source khal)" 98 | 99 | Or, save the file somewhere:: 100 | 101 | _KHAL_COMPLETE=zsh_source khal > ~/.khal-complete.zsh 102 | 103 | and then source it from your ``.zshrc``:: 104 | 105 | . ~/.khal-complete.zsh 106 | 107 | fish 108 | ~~~~ 109 | For fish, add this to ``~/.config/fish/completions/khal.fish``:: 110 | 111 | eval (env _KHAL_COMPLETE=fish_source khal) 112 | 113 | Or save the script to ``~/.config/fish/completions/khal.fish``:: 114 | 115 | _KHAL_COMPLETE=fish_source khal > ~/.config/fish/completions/khal.fish 116 | 117 | Note, that the latter basically does the same as the former and no efficiency is 118 | gained. 119 | 120 | .. _requirements: 121 | 122 | Requirements 123 | ------------ 124 | 125 | *khal* is written in python and can run on Python 3.9+. It requires a Python 126 | with ``sqlite3`` support enabled (which is usually the case). 127 | 128 | If you are installing python via *pip* or from source, be aware that since 129 | *khal* indirectly depends on lxml_ you need to either install it via your 130 | system's package manager or have python's libxml2's and libxslt1's headers 131 | (included in a separate "development package" on some distributions) installed. 132 | 133 | .. _icalendar: https://github.com/collective/icalendar 134 | .. _vdirsyncer: https://github.com/pimutils/vdirsyncer 135 | .. _lxml: http://lxml.de/ 136 | 137 | Packaging 138 | --------- 139 | 140 | If your packages are generated by running ``setup.py install`` or some similar 141 | mechanism, you'll end up with very slow entry points (eg: ``/usr/bin/khal``). 142 | Package managers should use the files included in ``bin`` as a replacement for 143 | those. 144 | 145 | The root cause of the issue is really how python's setuptools generates these 146 | and outside of the scope of this project 147 | 148 | If your packages are generated using python wheels, this should not be an issue 149 | (much like it won't be an issue for users installing via ``pip``). 150 | -------------------------------------------------------------------------------- /doc/source/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ------- 3 | khal is released under the Expat/MIT License:: 4 | 5 | Copyright (c) 2013-2022 khal contributors 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software is furnished to do so, 12 | subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /doc/source/man.rst: -------------------------------------------------------------------------------- 1 | khal 2 | ==== 3 | 4 | Khal is a calendar program for the terminal for viewing, adding and editing 5 | events and calendars. Khal is build on the iCalendar and vdir (allowing the 6 | use of :manpage:`vdirsyncer(1)` for CalDAV compatibility) standards. 7 | 8 | 9 | Table of Contents 10 | ================= 11 | 12 | .. toctree:: 13 | :maxdepth: 1 14 | 15 | usage 16 | configure 17 | standards 18 | faq 19 | license 20 | -------------------------------------------------------------------------------- /doc/source/news.rst: -------------------------------------------------------------------------------- 1 | News 2 | ==== 3 | 4 | Below is a list of new releases and other khal related news. This is also 5 | available as an `rss feed `_ |rss|. 6 | 7 | .. |rss| image:: images/rss.png 8 | :target: https://lostpackets.de/khal/index.rss 9 | 10 | .. feed:: 11 | :rss: index.rss 12 | :title: khal news 13 | :link: http://lostpackets.de/khal/ 14 | 15 | news/khal0100 16 | news/khal098 17 | news/khal097 18 | news/khal096 19 | news/khal095 20 | news/khal094 21 | news/khal093 22 | news/khal092 23 | news/khal091 24 | news/khal09 25 | news/khal071 26 | news/khal084 27 | news/khal083 28 | news/khal082 29 | news/khal081 30 | news/khal08 31 | news/khal07 32 | news/khal06 33 | news/khal05 34 | news/khal04 35 | news/31c3 36 | news/khal031 37 | news/khal03 38 | news/khal02 39 | news/khal011 40 | news/khal01 41 | news/30c3 42 | news/callfortesting 43 | -------------------------------------------------------------------------------- /doc/source/news/30c3.rst: -------------------------------------------------------------------------------- 1 | pycarddav and khal at 30c3 2 | ========================== 3 | 4 | .. feed-entry:: 5 | :date: 2013-12-13 6 | 7 | If you will be 30C3_ and would like to discuss the faults and merits of khal or 8 | pycarddav, commandline calendaring/addressbooking in general, your ideas or just 9 | have a beer or mate, I'd love to meet up. You can find my contact details under 10 | *Feedback*. 11 | 12 | .. _30C3: https://events.ccc.de/congress/2013/wiki/Main_Page 13 | -------------------------------------------------------------------------------- /doc/source/news/31c3.rst: -------------------------------------------------------------------------------- 1 | pycarddav and khal at 31c3 2 | ========================== 3 | 4 | .. feed-entry:: 5 | :date: 2014-12-09 6 | 7 | If you will be at 31C3_ and would like to discuss the faults and merits of khal 8 | or pycarddav, commandline calendaring/addressbooking in general, your ideas or 9 | just have a beer or mate, I'd love to meet up. You can find my contact details 10 | under *Feedback*. 11 | 12 | .. _31C3: https://events.ccc.de/congress/2014/wiki/Main_Page 13 | -------------------------------------------------------------------------------- /doc/source/news/callfortesting.rst: -------------------------------------------------------------------------------- 1 | Call for Testing 2 | ================= 3 | 4 | .. feed-entry:: 5 | :date: 2013-11-19 6 | 7 | While there isn't a release yet, *khal* is, at least partly, in a usable shape 8 | by now. Please report any errors you stumble upon and improvement suggestions 9 | you have either via email or github_ (if you don't have any privacy concerns 10 | etc. I'd prefer you use github since it is public, but I'll soon set up a 11 | mailing list). TODO.rst_ gives you an idea about the plans I currently have 12 | for *khal*'s near future. 13 | 14 | .. _github: https://github.com/geier/khal/ 15 | .. _TODO.rst: https://github.com/geier/khal/blob/master/TODO.rst 16 | -------------------------------------------------------------------------------- /doc/source/news/khal01.rst: -------------------------------------------------------------------------------- 1 | khal v0.1 released 2 | ================== 3 | 4 | .. feed-entry:: 5 | :date: 2014-04-03 6 | 7 | The first release of khal is here: `khal v0.1.0`__ (and also available on pypi_ 8 | now). 9 | 10 | __ https://lostpackets.de/khal/downloads/khal-0.1.0.tar.gz 11 | 12 | The next release, hopefully coming rather sooner than later, will get rid of its 13 | own CalDAV implementation, but instead use vdirsyncer_; you can 14 | already try it out via checking out the branch *vdir* at github_. 15 | 16 | .. _pypi: https://pypi.python.org/pypi/khal/ 17 | .. _vdirsyncer: https://github.com/pimutils/vdirsyncer/ 18 | .. _github: https://github.com/geier/khal/tree/vdir 19 | -------------------------------------------------------------------------------- /doc/source/news/khal0100.rst: -------------------------------------------------------------------------------- 1 | khal v0.10.0 released 2 | ===================== 3 | 4 | .. feed-entry:: 5 | :date: 2019-03-25 6 | 7 | This is not only the first bugfix release in more than a year, but also the 8 | first release containing new features in nearly two years. 9 | 10 | v0.10.0 contains some breaking changes from earlier versions of khal, most 11 | notably the removal of the default command [1]_. For users who want the old 12 | functionality back, a shell function seems to be the best option. Please use the 13 | wiki_ to share your solutions. Have a look at the Changelog_ for more changes 14 | and fixes. 15 | 16 | With this release I want to bring the aforementioned changes to more people, 17 | find (and fix) as many more bugs as possible and then release a version 1.0. 18 | 19 | For convenience issues, khal will only be available on pypi_ in the future, with 20 | minor versions (v0.10.x) not getting announced here any more. Users and packagers 21 | who want to stay on a current version of khal are therefore advised to watch 22 | pypi_ for new versions. 23 | 24 | .. Important:: 25 | 26 | **Contributors and maintainers wanted** 27 | 28 | As I find myself having less and less time to devout to khal, I'm looking for 29 | more developers and maintainers. Even if you are not a python developer, your 30 | help with helping new users and triaging and prioritizing bugs is very much 31 | appreciated. 32 | 33 | If you don't know how to contact the current team, open an `issue on github`_. 34 | 35 | **vdirsyncer**, which many khal users are probably dependend on, is also 36 | looking for new maintainers_. 37 | 38 | 39 | .. [1] The implementation of the default command proved to be a source of 40 | constant headache for users and developers alike. This was due to the library chosen 41 | for handling argument parsing. 42 | 43 | Feel free to share other suggestions in the wiki_. 44 | 45 | .. _pypi: https://pypi.python.org/pypi/khal/ 46 | .. _issue on github: https://github.com/pimutils/khal/issues 47 | .. _issues: https://github.com/pimutils/khal/issues 48 | .. _wiki: https://github.com/pimutils/khal/wiki/Default-command-alternatives 49 | .. _changelog: changelog.html#id2 50 | .. _vdirsyncer: https://github.com/pimutils/vdirsyncer 51 | .. _maintainers: https://github.com/pimutils/vdirsyncer/issues/790 52 | -------------------------------------------------------------------------------- /doc/source/news/khal011.rst: -------------------------------------------------------------------------------- 1 | khal v0.1.1 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2014-05-07 6 | 7 | A small bugfix release: `khal v0.1.0`__ 8 | Example config file now in source dist. 9 | 10 | __ https://lostpackets.de/khal/downloads/khal-0.1.1.tar.gz 11 | -------------------------------------------------------------------------------- /doc/source/news/khal02.rst: -------------------------------------------------------------------------------- 1 | khal v0.2 released 2 | ================== 3 | 4 | .. feed-entry:: 5 | :date: 2014-06-27 6 | 7 | A new release of khal is here: `khal v0.2.0`__ (also available on pypi_). 8 | 9 | __ https://lostpackets.de/khal/downloads/khal-0.2.0.tar.gz 10 | 11 | 12 | If you want to update your installation from pypi_, you can run `sudo pip 13 | install --upgrade khal`. 14 | 15 | From now on *khal* relies on vdirsyncer_ for CalDAV sync. While this makes 16 | *khal* a bit more complicated to setup, *vdirsyncer* is much better tested than 17 | *khal* and also the `bus factor`__ increased (at least for parts of the 18 | project). 19 | 20 | __ http://en.wikipedia.org/wiki/Bus_factor 21 | 22 | You might want to head over to the tutorial_ on how to setup *vdirsyncer*. 23 | Afterwards you will need to re-setup your *khal* configuration (copy the new 24 | example config file), also you will need to delete your old (local) database, so 25 | please make sure you did sync everything. 26 | 27 | Also *khal*'s command line syntax changed quite a bit, so you might want to head over the documentation_. 28 | 29 | .. _pypi: https://pypi.python.org/pypi/khal/ 30 | .. _vdirsyncer: https://github.com/pimutils/vdirsyncer/ 31 | .. _tutorial: https://vdirsyncer.readthedocs.org/en/latest/tutorial.html 32 | .. _documentation: http://lostpackets.de/khal/pages/usage.html 33 | -------------------------------------------------------------------------------- /doc/source/news/khal03.rst: -------------------------------------------------------------------------------- 1 | khal v0.3 released 2 | ================== 3 | 4 | .. feed-entry:: 5 | :date: 2014-09-03 6 | 7 | A new release of khal is here: `khal v0.3.0`__ (also available on pypi_). 8 | 9 | __ https://lostpackets.de/khal/downloads/khal-0.3.0.tar.gz 10 | 11 | 12 | If you want to update your installation from pypi_, you can run `sudo pip 13 | install --upgrade khal`. 14 | 15 | CHANGELOG 16 | --------- 17 | * new unified documentation 18 | * html documentation (website) and man pages are all generated from the same 19 | sources via sphinx (type `make html` or `make man` in doc/, the result 20 | will be build in *build/html* or *build/man* respectively (also available 21 | on `Read the Docs`__) 22 | * the new documentation lives in doc/ 23 | * the package sphinxcontrib-newsfeed is needed for generating the html 24 | version (for generating an RSS feed) 25 | * the man pages live doc/build/man/, they can be build by running 26 | `make man` in doc/sphinx/ 27 | * new dependencies: configobj, tzlocal>=1.0 28 | * **IMPORTANT**: the configuration file's syntax changed (again), have a look at the new 29 | documentation for details 30 | * local_timezone and default_timezone will now be set to the timezone the 31 | computer is set to (if they are not set in the configuration file) 32 | 33 | __ https://khal.readthedocs.org 34 | 35 | .. _pypi: https://pypi.python.org/pypi/khal/ 36 | .. _vdirsyncer: https://github.com/pimutils/vdirsyncer/ 37 | -------------------------------------------------------------------------------- /doc/source/news/khal031.rst: -------------------------------------------------------------------------------- 1 | khal v0.3.1 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2014-09-08 6 | 7 | A new release of khal is here: `khal v0.3.1`__ (also available on pypi_). 8 | 9 | __ https://lostpackets.de/khal/downloads/khal-0.3.1.tar.gz 10 | 11 | 12 | This is a bugfix release, bringing no new features. The last release suffered 13 | from a major bug, where events deleted on the server (and in the vdir) were not 14 | deleted in khal's caching database and therefore still displayed in khal. 15 | Therefore, after updating please delete your local database. 16 | 17 | For more information on other fixed bugs, see :ref:`changelog`. 18 | 19 | 20 | .. _pypi: https://pypi.python.org/pypi/khal/ 21 | -------------------------------------------------------------------------------- /doc/source/news/khal04.rst: -------------------------------------------------------------------------------- 1 | khal v0.4.0 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2015-02-02 6 | 7 | A new release of khal is here: `khal v0.4.0`__ (also available on pypi_). 8 | 9 | __ https://lostpackets.de/khal/downloads/khal-0.4.0.tar.gz 10 | 11 | This release offers several functional improvements like better support for 12 | recurring events or a major speedup when creating the caching database and some 13 | new features like week number support or creating recurring events with `khal 14 | new --repeat`. 15 | 16 | Note to users 17 | ------------- 18 | 19 | khal now requires click_ instead of docopt_ and, as usual, the local database 20 | will need to be deleted. 21 | 22 | For a more detailed list of changes, please have a look at the :ref:`changelog`. 23 | 24 | .. _click: http://click.pocoo.org/ 25 | .. _docopt: http://docopt.org/ 26 | .. _pypi: https://pypi.python.org/pypi/khal/ 27 | -------------------------------------------------------------------------------- /doc/source/news/khal05.rst: -------------------------------------------------------------------------------- 1 | khal v0.5.0 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2015-06-01 6 | 7 | A new release of khal is here: `khal v0.5.0`__ (also available on pypi_). 8 | 9 | __ https://lostpackets.de/khal/downloads/khal-0.5.0.tar.gz 10 | 11 | This release brings a lot of new features (like rudimentary search support, 12 | user changeable keybindings in ikhal, new command `at`), python 3 support and 13 | some assorted bugfixes. 14 | 15 | Thanks to everybody who contributed with bug reports, suggestions and code, 16 | especially to everyone contributing for the first time! 17 | 18 | For a more detailed list of changes, please have a look at the changelog_. 19 | 20 | .. _click: http://click.pocoo.org/ 21 | .. _docopt: http://docopt.org/ 22 | .. _pypi: https://pypi.python.org/pypi/khal/ 23 | .. _changelog: changelog.html#id2 24 | -------------------------------------------------------------------------------- /doc/source/news/khal06.rst: -------------------------------------------------------------------------------- 1 | khal v0.6.0 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2015-07-15 6 | 7 | 8 | Only six weeks after the last version `khal v0.6.0`__ is now available (yes, 9 | also on pypi_). 10 | 11 | __ https://lostpackets.de/khal/downloads/khal-0.6.0.tar.gz 12 | 13 | This release fixes an unfortunate bug which could lead to wrong shifts in other 14 | events when inserting complicated recurring events. All users are therefore 15 | advised to quickly upgrade to khal 0.6. 16 | 17 | There are also quite a bunch of new features, among other nicer editing 18 | capabilities in ikhal's text edits and import of .ics files. For a more 19 | detailed list of changes, please have a look at the changelog_ (especially if 20 | you package khal). 21 | 22 | .. _pypi: https://pypi.python.org/pypi/khal/ 23 | .. _changelog: changelog.html#id2 24 | -------------------------------------------------------------------------------- /doc/source/news/khal07.rst: -------------------------------------------------------------------------------- 1 | khal v0.7.0 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2015-11-24 6 | 7 | The latest version of khal has been released: `khal v0.7.0`__ 8 | (as always, also on pypi_). 9 | 10 | __ https://lostpackets.de/khal/downloads/khal-0.7.0.tar.gz 11 | 12 | This release brings a lot of new features, by an ever increasing number of new 13 | contributors (welcome everyone!). With highlighting of days that have events we 14 | now have one of the most requested features implemented (because it does 15 | noticeably slow down khal's start it is disabled by default, startup performance 16 | will hopefully be increased soon). Among the other new features are duplicating 17 | events (in ikhal), prettier event display (also in ikhal) and a better zsh 18 | completion file. 19 | 20 | Have a look at the changelog_ for more complete list of new features. 21 | 22 | .. _pypi: https://pypi.python.org/pypi/khal/ 23 | .. _changelog: changelog.html#id2 24 | -------------------------------------------------------------------------------- /doc/source/news/khal071.rst: -------------------------------------------------------------------------------- 1 | khal v0.7.1 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2016-10-11 6 | 7 | `khal v0.7.1`_ (pypi_) is a bugfix release that fixes a **critical bug** in 8 | `khal import`. This is a backport of the fix that got released with v0.8.4_, for 9 | those users than cannot (or *really* don't want to) upgrade to a more recent 10 | version of khal (most likely because of the dropped support for python 2). 11 | Please note, that khal v0.7.x is generally *not maintained* anymore, will not 12 | receive any new features, and any non-critical bugs will not be fixed either. 13 | 14 | See the `0.8.4 release announcement`_ for more details regarding the fixed bug. 15 | 16 | 17 | .. _khal v0.7.1: https://lostpackets.de/khal/downloads/khal-0.7.1.tar.gz 18 | .. _pypi: https://pypi.python.org/pypi?:action=display&name=khal&version=0.7.1 19 | .. _v0.8.4: https://lostpackets.de/khal/news/khal084.html 20 | .. _0.8.4 release announcement: https://lostpackets.de/khal/news/khal084.html 21 | -------------------------------------------------------------------------------- /doc/source/news/khal08.rst: -------------------------------------------------------------------------------- 1 | khal v0.8.0 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2016-04-13 6 | 7 | The latest version of khal has been released: `khal v0.8.0`__ 8 | (as always, also on pypi_). 9 | 10 | __ https://lostpackets.de/khal/downloads/khal-0.8.0.tar.gz 11 | 12 | We have recently dropped python 2 support, so this release is the first one that 13 | only supports python 3 (3.3+). 14 | 15 | There is one more backwards incompatible change: 16 | The color `grey` has been renamed to `gray`, if you use it in your configuration 17 | file, you will need to update to `gray`. 18 | 19 | There are some new features that should be configuring khal easier, especially 20 | for new users (e.g., new command `configure` helps with the initial 21 | configuration). Also alarms can now be entered either when creating new events 22 | with `new` or when editing them in ikhal. 23 | 24 | Have a look at the changelog_ for more complete list of new features (of which 25 | there are many). 26 | 27 | .. _pypi: https://pypi.python.org/pypi/khal/ 28 | .. _changelog: changelog.html#id2 29 | -------------------------------------------------------------------------------- /doc/source/news/khal081.rst: -------------------------------------------------------------------------------- 1 | khal v0.8.1 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2016-04-13 6 | 7 | The second version released today (`khal v0.8.1`__, yes, also on pypi_) fixes a 8 | bug in the CalendarWidget() that probably would not have trigged but made the 9 | tests fail. 10 | 11 | __ https://lostpackets.de/khal/downloads/khal-0.8.1.tar.gz 12 | 13 | .. _pypi: https://pypi.python.org/pypi/khal/ 14 | -------------------------------------------------------------------------------- /doc/source/news/khal082.rst: -------------------------------------------------------------------------------- 1 | khal v0.8.2 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2016-05-16 6 | 7 | `khal v0.8.2`__ (pypi_) is a maintenance release that fixes several bugs in 8 | `configure` that would lead to crashes during the initial configuration and 9 | following runs of khal (due to an invalid configuration file getting written to 10 | disk) and improves the detection of the installed icalendar version. 11 | 12 | If khal currently works for you, there is no need for an upgrade. 13 | 14 | __ https://lostpackets.de/khal/downloads/khal-0.8.2.tar.gz 15 | 16 | .. _pypi: https://pypi.python.org/pypi/khal/ 17 | -------------------------------------------------------------------------------- /doc/source/news/khal083.rst: -------------------------------------------------------------------------------- 1 | khal v0.8.3 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2016-08-28 6 | 7 | `khal v0.8.3`__ (pypi_) is a maintenance release that fixes several bugs, mostly 8 | in the test suite. If khal is working fine for you, there is no need to upgrade. 9 | 10 | 11 | __ https://lostpackets.de/khal/downloads/khal-0.8.3.tar.gz 12 | 13 | .. _pypi: https://pypi.python.org/pypi/khal/ 14 | -------------------------------------------------------------------------------- /doc/source/news/khal084.rst: -------------------------------------------------------------------------------- 1 | khal v0.8.4 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2016-10-06 6 | 7 | `khal v0.8.4`_ (pypi_) is a bugfix release that fixes a **critical bug** in `khal 8 | import`. **All users are advised to upgrade as soon as possible**. 9 | 10 | Details 11 | ~~~~~~~ 12 | If importing events from `.ics` files, any VTIMEZONEs (specifications of the 13 | timezone) would *not* be imported with those events. 14 | As khal understands Olson DB timezone specifiers (such as "Europe/Berlin" or 15 | "America/New_York", events using those timezones are displayed in the correct 16 | timezone, but all other events are displayed as if they were in the configured 17 | *default timezone*. 18 | **This can lead to imported events being shown at wrong times!** 19 | 20 | 21 | Solution 22 | ~~~~~~~~ 23 | First, please upgrade khal to either v0.8.4 or, if you are using a version of khal directly 24 | from the git repository, upgrade to the latest version from github_. 25 | 26 | To see if you are affected by this bug, delete your local khal caching db, 27 | (usually `~/.local/share/khal/khal.db`), re-run khal and watch out for lines 28 | looking like this: 29 | ``warning: $PROPERTY has invalid or incomprehensible timezone information in 30 | $long_uid.ics in $my_collection``. 31 | You will then need to edit these files by hand and either replace the timezone 32 | identifiers with the corresponding one from the Olson DB (e.g., change 33 | `Europe_Berlin` to `Europe/Berlin`) or copy original VTIMZONE definition in. 34 | 35 | If you have any problems with this, please either open an `issue at github`_ or come into 36 | our `irc channel`_ (`#pimutils` on Libera.Chat). 37 | 38 | We are sorry for any inconveniences this is causing you! 39 | 40 | 41 | .. _khal v0.8.4: https://lostpackets.de/khal/downloads/khal-0.8.4.tar.gz 42 | .. _github: https://github.com/pimutils/khal/ 43 | .. _issue at github: https://github.com/pimutils/khal/issues 44 | .. _pypi: https://pypi.python.org/pypi/khal/ 45 | .. _irc channel: irc://#pimutils@Libera.Chat 46 | -------------------------------------------------------------------------------- /doc/source/news/khal09.rst: -------------------------------------------------------------------------------- 1 | khal v0.9.0 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2017-01-24 6 | 7 | 8 | This is probably the biggest release of khal to date, that is, the one with the 9 | most changes since the last release. This changes are made up of a bunch of bug 10 | fixes and enhancements. Unfortunately, some of these break backwards 11 | compatibility, many of which will make themselves noticeable, because your config 12 | file will no longer be valid, consult the changelog_ or the documentation_. 13 | Noticable is also, that command `agenda` has been renamed to `list` 14 | 15 | Some of the larger changes include, among others, new configuration options on 16 | how events are printed (thanks to first time contributor Taylor Money) and a new 17 | look for ikhal's event-list column. 18 | 19 | Have a look at the changelog_ for more complete list of new features (of which 20 | there are many). 21 | 22 | Get `khal v0.9.0`__ from this site, or from pypi_. 23 | 24 | __ https://lostpackets.de/khal/downloads/khal-0.9.0.tar.gz 25 | 26 | .. _pypi: https://pypi.python.org/pypi/khal/ 27 | .. _changelog: changelog.html#id2 28 | .. _documentation: https://lostpackets.de/khal/ 29 | -------------------------------------------------------------------------------- /doc/source/news/khal091.rst: -------------------------------------------------------------------------------- 1 | khal v0.9.1 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2017-01-25 6 | 7 | This is a bug fix release for python 3.6. 8 | 9 | Under python 3.6, datetimes with timezone information that is missing from the 10 | icalendar file would be treated if they were in the system's local timezone, not 11 | as if they were in khal's configured default timezone. This could therefore lead 12 | to erroneous offsets in start and end times for those events. 13 | 14 | To check if you are affected by this bug, delete khal's database file (usually 15 | :file:`~/.local/share/khal/khal.db`), rerun khal and watch for error messages 16 | that look like the one below: 17 | 18 | warning: DTSTART localized in invalid or incomprehensible timezone `FOO` in 19 | events/event_dt_local_missing_tz.ics. This could lead to this event being 20 | wrongly displayed. 21 | 22 | 23 | All users (of python 3.6) are advised to upgrade as soon as possible. 24 | 25 | Get `khal v0.9.1`__ from this site, or from pypi_. 26 | 27 | __ https://lostpackets.de/khal/downloads/khal-0.9.1.tar.gz 28 | 29 | .. _pypi: https://pypi.python.org/pypi/khal/ 30 | -------------------------------------------------------------------------------- /doc/source/news/khal092.rst: -------------------------------------------------------------------------------- 1 | khal v0.9.2 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2017-02-13 6 | 7 | 8 | This is an **important bug fix release**, that fixes a bunch of different bugs, 9 | but most importantly: 10 | 11 | * if weekstart != 0 ikhal would show wrong weekday names 12 | * allday events added with `khal new DATE TIMEDELTA` (e.g., 2017-01-18 3d) 13 | were lasting one day too long 14 | 15 | Special thanks to Tom Rushworth for finding and reporting both bugs! 16 | 17 | All other fixed bugs would be rather obvious if you happened to run into them, 18 | as they would lead to khal crashing in one way or another. 19 | 20 | One new feature made its way into this release as well, which is good news for 21 | all users pining for the way ikhal's right column behaved in pre 0.9.0 days: 22 | setting new configuration option [view]dynamic_days=False, will make that column 23 | behave similar as it used to. 24 | 25 | .. Warning:: 26 | 27 | All users of khal 0.9.x are advised to **upgrade as soon as possible**. 28 | 29 | Users of khal 0.8.x are not affected by either bug. 30 | 31 | Get `khal v0.9.2`__ from this site, or from pypi_. 32 | 33 | __ https://lostpackets.de/khal/downloads/khal-0.9.2.tar.gz 34 | 35 | .. _pypi: https://pypi.python.org/pypi/khal/ 36 | -------------------------------------------------------------------------------- /doc/source/news/khal093.rst: -------------------------------------------------------------------------------- 1 | khal v0.9.3 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2017-03-06 6 | 7 | Sadly, the biggest release in khal's history, also brought the most bugs. 8 | The latest release, khal version 0.9.3, fixes some more of them. 9 | The good news: while most of these bugs lead to khal crashing, no harm was done, 10 | that is all (calendar) related data shown was correct. 11 | 12 | Again, some new features sneaked in, for those and for the complete list of 13 | fixed bugs, have a look at the changelog_. 14 | 15 | Get `khal v0.9.3`__ from this site, or from pypi_. 16 | 17 | 18 | .. _pypi: https://pypi.python.org/pypi/khal/ 19 | .. _changelog: changelog.html#id2 20 | .. _documentation: https://lostpackets.de/khal/ 21 | __ https://lostpackets.de/khal/downloads/khal-0.9.3.tar.gz 22 | -------------------------------------------------------------------------------- /doc/source/news/khal094.rst: -------------------------------------------------------------------------------- 1 | khal v0.9.4 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2017-03-30 6 | 7 | Another minor release, this time bringing some features, mostly for ikhal: 8 | among others are an improved light color scheme, an improved editor for 9 | recurrence rules and the ability to detect updates the underlying vdirs and 10 | refreshing the user interface. 11 | 12 | Furthermore, the configuration wizard helping new users generating a configuration 13 | file got streamlined, making it's use much easier. 14 | 15 | Special thanks to first time contributors August Lindberg and Thomas Kluyver. 16 | 17 | For a more complete list of changes, have a look at the changelog_. 18 | 19 | Get `khal v0.9.4`__ from this site, or from pypi_. 20 | 21 | 22 | .. _pypi: https://pypi.python.org/pypi/khal/ 23 | .. _changelog: changelog.html#id2 24 | .. _documentation: https://lostpackets.de/khal/ 25 | __ https://lostpackets.de/khal/downloads/khal-0.9.4.tar.gz 26 | -------------------------------------------------------------------------------- /doc/source/news/khal095.rst: -------------------------------------------------------------------------------- 1 | khal v0.9.5 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2017-04-10 6 | 7 | Another minor release, some non-serious bugs this time. 8 | 9 | For a more complete list of changes, have a look at the changelog_. 10 | 11 | Get `khal v0.9.5`__ from this site, or from pypi_. 12 | 13 | 14 | .. _pypi: https://pypi.python.org/pypi/khal/ 15 | .. _changelog: changelog.html#id2 16 | .. _documentation: https://lostpackets.de/khal/ 17 | __ https://lostpackets.de/khal/downloads/khal-0.9.5.tar.gz 18 | -------------------------------------------------------------------------------- /doc/source/news/khal096.rst: -------------------------------------------------------------------------------- 1 | khal v0.9.6 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2017-06-13 6 | 7 | Another minor release, some non-serious bugs and some minor features. 8 | 9 | For a more complete list of changes, have a look at the changelog_. 10 | 11 | Get `khal v0.9.6`__ from this site, or from pypi_. 12 | 13 | 14 | .. _pypi: https://pypi.python.org/pypi/khal/ 15 | .. _changelog: changelog.html#id2 16 | .. _documentation: https://lostpackets.de/khal/ 17 | __ https://lostpackets.de/khal/downloads/khal-0.9.6.tar.gz 18 | -------------------------------------------------------------------------------- /doc/source/news/khal097.rst: -------------------------------------------------------------------------------- 1 | khal v0.9.7 released 2 | ==================== 3 | 4 | .. feed-entry:: 5 | :date: 2017-09-15 6 | 7 | `khal v0.9.7`_ comes with two fixes (no more crashing on datetime events with 8 | UNTIL properties and no more crashes on search finding events with overwritten 9 | subevents) and one change: `search` will now print subevents of matching events 10 | once, i.e., that is one line for the master event, and one line for every 11 | different overwritten event. 12 | 13 | 14 | Get `khal v0.9.7`_ from this site, or from pypi_. 15 | 16 | 17 | .. _pypi: https://pypi.python.org/pypi/khal/ 18 | .. _changelog: changelog.html#id2 19 | .. _documentation: https://lostpackets.de/khal/ 20 | .. _khal v0.9.7: https://lostpackets.de/khal/downloads/khal-0.9.7.tar.gz 21 | -------------------------------------------------------------------------------- /doc/source/news/khal098.rst: -------------------------------------------------------------------------------- 1 | khal v0.9.8 released with an IMPORTANT BUGFIX 2 | ============================================= 3 | 4 | .. feed-entry:: 5 | :date: 2017-10-05 6 | 7 | `khal v0.9.8`_ comes with an **IMPORTANT BUGFIX**: 8 | If editing an event in ikhal and not editing the end time but moving the cursor 9 | through the end time field, the end time could be moved to the start time + 1 10 | hour (the end *date* was not affected). 11 | 12 | 13 | .. Warning:: 14 | 15 | All users of khal are advised to **upgrade as soon as possible!** 16 | 17 | Users of khal v0.9.3 and earlier are not affected. 18 | 19 | Get `khal v0.9.8`_ from this site, or from pypi_. 20 | 21 | 22 | .. _pypi: https://pypi.python.org/pypi/khal/ 23 | .. _khal v0.9.8: https://lostpackets.de/khal/downloads/khal-0.9.8.tar.gz 24 | -------------------------------------------------------------------------------- /doc/source/standards.rst: -------------------------------------------------------------------------------- 1 | Standards 2 | ========= 3 | 4 | *khal* tries to follow standards and RFCs (most importantly :rfc:`5545` 5 | *iCalendar*) wherever possible. Known intentional and unintentional deviations 6 | are listed below. 7 | 8 | RDATE;VALUE=PERIOD 9 | ------------------ 10 | 11 | `RDATE` s with `PERIOD` values are currently not supported, as icalendar_ does 12 | not support it yet. Please submit any real world examples of events with 13 | `RDATE;VALUE=PERIOD` you might encounter (khal will print warnings if you have 14 | any in your calendars). 15 | 16 | RANGE=THISANDPRIOR 17 | ------------------ 18 | 19 | Recurrent events with the `RANGE=THISANDPRIOR` are and will not be [1]_ 20 | supported by khal, as applications supporting the latest standard_ MUST NOT 21 | create those. khal will print a warning if it encounters an event containing 22 | `RANGE=THISANDPRIOR`. 23 | 24 | .. [1] unless a lot of users request this feature 25 | 26 | .. _standard: http://tools.ietf.org/html/rfc5546 27 | 28 | Events with neither END nor DURATION 29 | ------------------------------------ 30 | 31 | While the RFC states:: 32 | 33 | A calendar entry with a "DTSTART" property but no "DTEND" 34 | property does not take up any time. It is intended to represent 35 | an event that is associated with a given calendar date and time 36 | of day, such as an anniversary. Since the event does not take up 37 | any time, it MUST NOT be used to record busy time no matter what 38 | the value for the "TRANSP" property. 39 | 40 | khal transforms those events into all-day events lasting for one day (the start 41 | date). As long a those events do not get edited, these changes will not be 42 | written to the vdir (and with that to the CalDAV server). Any timezone 43 | information that was associated with the start date gets discarded. 44 | 45 | .. note:: 46 | While the main rationale for this behaviour was laziness on part of khal's 47 | main author, other calendar software shows the same behaviour (e.g. Google 48 | Calendar and Evolution). 49 | 50 | Timezones 51 | --------- 52 | Getting localized time right, seems to be the most difficult part about 53 | calendaring (and messing it up ends in missing the one important meeting of the 54 | week). So I'll briefly describe here, how khal tries to handle timezone 55 | information, which information it can handle and which it can't. 56 | 57 | In general, there are two different type of events. *Localized events* (with 58 | *localized* start and end datetimes) which have timezone information attached to 59 | their start and end datetimes, and *floating* events (with *floating* start and end 60 | datetimes), which have no timezone information attached (all-day events, events that 61 | last for complete days are floating as well). Localized events are always 62 | observed at the same UTC_ (no matter what time zone the observer is in), but 63 | different local times. On the other hand, floating events are always observed at 64 | the same local time, which might be different in UTC. 65 | 66 | In khal all localized datetimes are saved to the local database as UTC. 67 | Datetimes that are already UTC, e.g. ``19980119T070000Z``, are saved as such, 68 | others are converted to UTC (but don't worry, the timezone information does not 69 | get lost). Floating events get saved in floating time, independently of the 70 | localized events. 71 | 72 | If you want to look up which events take place at a specified datetime, khal 73 | always expects that you want to know what events take place at that *local* 74 | datetime. Therefore, the (local) datetime you asked for gets converted to UTC, the 75 | appropriate *localized* events get selected and presented with their start and 76 | end datetimes *converted* to *your local datetime*. For floating events no 77 | conversion is necessary. 78 | 79 | Khal (i.e. icalendar_) can understand all timezone identifiers as used in the 80 | `Olson DB`_ and custom timezone definitions, if those VTIMEZONE components are 81 | placed before the VEVENTS that make use of them (as most calendar programs seem 82 | to do). In case an unknown (or unsupported) timezone is found, khal will assume 83 | you want that event to be placed in the *default timezone* (which can be 84 | configured in the configuration file as well). 85 | 86 | khal expects you *always* want *all* start and end datetimes displayed in 87 | *local time* (which can be set in the configuration file as well, otherwise 88 | your computer's timezone is used). 89 | 90 | .. _Olson DB: https://en.wikipedia.org/wiki/Tz_database 91 | .. _UTC: https://en.wikipedia.org/wiki/Coordinated_Universal_Time 92 | .. _icalendar: https://github.com/collective/icalendar 93 | -------------------------------------------------------------------------------- /doc/source/ystatic/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimutils/khal/df6545645740f5ffe6ec00a97f7b5ff195ff60f4/doc/source/ystatic/.gitignore -------------------------------------------------------------------------------- /doc/source/ytemplates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block linktags %} 4 | {{ super() }} 5 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /doc/webpage/src/new_rss_url.rst: -------------------------------------------------------------------------------- 1 | New RSS URL 2 | =========== 3 | :date: 20.08.2014 4 | :category: News 5 | 6 | The newsfeed will now be available under a `new url`__, for now there will be 7 | only an RSS feed. 8 | 9 | __ https://lostpackets.de/khal/index.rss 10 | -------------------------------------------------------------------------------- /khal.conf.sample: -------------------------------------------------------------------------------- 1 | #/etc/khal/khal.conf.sample 2 | [calendars] 3 | [[home]] 4 | path = ~/.khal/calendars/home/ 5 | color = dark blue 6 | 7 | [[work]] 8 | path = ~/.khal/calendars/work/ 9 | readonly = True 10 | 11 | [sqlite] 12 | path = ~/.khal/khal.db 13 | 14 | [locale] 15 | local_timezone = Europe/Berlin 16 | default_timezone = America/New_York 17 | 18 | # If you use certain characters (e.g. commas) in these formats you may need to 19 | # enclose them in "" to ensure that they are loaded as strings. 20 | timeformat = %H:%M 21 | dateformat = %d.%m. 22 | longdateformat = %d.%m.%Y 23 | datetimeformat = %d.%m. %H:%M 24 | longdatetimeformat = %d.%m.%Y %H:%M 25 | 26 | firstweekday = 0 27 | monthdisplay = firstday 28 | 29 | [default] 30 | default_calendar = home 31 | timedelta = 2d # the default timedelta that list uses 32 | highlight_event_days = True # the default is False 33 | enable_mouse = True # mouse is enabled by default in interactive mode 34 | -------------------------------------------------------------------------------- /khal/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2022 khal contributors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | try: 23 | from khal.version import version 24 | except ImportError: 25 | version = 'invalid' 26 | import sys 27 | sys.exit('Failed to find (autogenerated) version.py. This might be due to ' 28 | 'using GitHub\'s tarballs or svn access. Either clone ' 29 | 'from GitHub via git or get a tarball from PyPI.') 30 | 31 | __productname__ = 'khal' 32 | __version__ = version 33 | __author__ = 'Christian Geier' 34 | __copyright__ = 'Copyright (c) 2013-2022 khal contributors' 35 | __author_email__ = 'khal@lostpackets.de' 36 | __description__ = 'A standards based terminal calendar' 37 | __license__ = 'Expat/MIT, see COPYING' 38 | __homepage__ = 'https://lostpackets.de/khal/' 39 | -------------------------------------------------------------------------------- /khal/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2022 khal contributors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | from khal.cli import main_khal 23 | 24 | if __name__ == '__main__': 25 | main_khal() 26 | -------------------------------------------------------------------------------- /khal/_compat.py: -------------------------------------------------------------------------------- 1 | __all__ = ["importlib_metadata"] 2 | 3 | import sys 4 | 5 | if sys.version_info >= (3, 10): # pragma: no cover 6 | from importlib import metadata as importlib_metadata 7 | else: # pragma: no cover 8 | import importlib_metadata 9 | -------------------------------------------------------------------------------- /khal/calendar_display.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2022 khal contributors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | import calendar 23 | import datetime as dt 24 | from locale import LC_ALL, LC_TIME, getlocale, setlocale 25 | from typing import Optional, Union 26 | 27 | from click import style 28 | 29 | from .khalendar import CalendarCollection 30 | from .terminal import colored 31 | from .utils import get_month_abbr_len 32 | 33 | setlocale(LC_ALL, '') 34 | 35 | 36 | def get_weekheader(firstweekday: int) -> str: 37 | try: 38 | mylocale = '.'.join(getlocale(LC_TIME)) # type: ignore 39 | except TypeError: 40 | mylocale = 'C' 41 | 42 | _calendar = calendar.LocaleTextCalendar(firstweekday, locale=mylocale) # type: ignore 43 | return _calendar.formatweekheader(2) 44 | 45 | 46 | def getweeknumber(date: dt.date) -> int: 47 | """return iso week number for datetime.date object 48 | :param date: date 49 | :return: weeknumber 50 | """ 51 | return dt.date.isocalendar(date)[1] 52 | 53 | 54 | def get_calendar_color(calendar: str, default_color: str, collection: CalendarCollection) -> str: 55 | """Because multi-line lambdas would be un-Pythonic 56 | """ 57 | if collection._calendars[calendar]['color'] == '': 58 | return default_color 59 | return collection._calendars[calendar]['color'] 60 | 61 | 62 | def get_color_list( 63 | calendars: list[str], 64 | default_color: str, 65 | collection: CalendarCollection 66 | ) -> list[str]: 67 | """Get the list of possible colors for the day, taking into account priority""" 68 | dcolors = [ 69 | ( 70 | get_calendar_color(x, default_color, collection), 71 | collection._calendars[x]["priority"], 72 | ) 73 | for x in calendars 74 | ] 75 | 76 | dcolors.sort(key=lambda x: x[1], reverse=True) 77 | 78 | maxPriority = dcolors[0][1] 79 | return list({x[0] for x in filter(lambda x: x[1] == maxPriority, dcolors)}) 80 | 81 | 82 | def str_highlight_day( 83 | day: dt.date, 84 | calendars: list[str], 85 | hmethod: Optional[str], 86 | default_color: str, 87 | multiple: str, 88 | multiple_on_overflow: bool, 89 | color: str, 90 | bold_for_light_color: bool, 91 | collection: CalendarCollection, 92 | ) -> str: 93 | """returns a string with day highlighted according to configuration 94 | """ 95 | dstr = str(day.day).rjust(2) 96 | if color == '': 97 | dcolors = get_color_list(calendars, default_color, collection) 98 | if len(dcolors) > 1: 99 | if multiple == '' or (multiple_on_overflow and len(dcolors) == 2): 100 | if hmethod == "foreground" or hmethod == "fg": 101 | return colored(dstr[:1], fg=dcolors[0], 102 | bold_for_light_color=bold_for_light_color) + \ 103 | colored(dstr[1:], fg=dcolors[1], bold_for_light_color=bold_for_light_color) 104 | else: 105 | return colored(dstr[:1], bg=dcolors[0], 106 | bold_for_light_color=bold_for_light_color) + \ 107 | colored(dstr[1:], bg=dcolors[1], bold_for_light_color=bold_for_light_color) 108 | else: 109 | dcolor = multiple 110 | else: 111 | dcolor = dcolors[0] or default_color 112 | else: 113 | dcolor = color 114 | if dcolor != '': 115 | if hmethod == "foreground" or hmethod == "fg": 116 | return colored(dstr, fg=dcolor, bold_for_light_color=bold_for_light_color) 117 | else: 118 | return colored(dstr, bg=dcolor, bold_for_light_color=bold_for_light_color) 119 | return dstr 120 | 121 | 122 | def str_week( 123 | week: list[dt.date], 124 | today: dt.date, 125 | collection: Optional[CalendarCollection]=None, 126 | hmethod: Optional[str]=None, 127 | default_color: str='', 128 | multiple: str='', 129 | multiple_on_overflow: bool=False, 130 | color: str='', 131 | highlight_event_days: bool=False, 132 | locale=None, 133 | bold_for_light_color: bool=True, 134 | ) -> str: 135 | """returns a string representing one week, 136 | if for day == today color is reversed 137 | 138 | :param week: list of 7 datetime.date objects (one week) 139 | :param today: the date of today 140 | :return: string, which if printed on terminal appears to have length 20, 141 | but may contain ascii escape sequences 142 | """ 143 | strweek = '' 144 | if highlight_event_days and collection is None: 145 | raise ValueError( 146 | 'if `highlight_event_days` is True, `collection` must be a CalendarCollection' 147 | ) 148 | for day in week: 149 | if day == today: 150 | day_str = style(str(day.day).rjust(2), reverse=True) 151 | elif highlight_event_days: 152 | assert collection is not None 153 | devents = list(collection.get_calendars_on(day)) 154 | if len(devents) > 0: 155 | day_str = str_highlight_day( 156 | day, devents, hmethod, default_color, multiple, 157 | multiple_on_overflow, color, bold_for_light_color, 158 | collection, 159 | ) 160 | else: 161 | day_str = str(day.day).rjust(2) 162 | else: 163 | day_str = str(day.day).rjust(2) 164 | strweek = strweek + day_str + ' ' 165 | return strweek 166 | 167 | 168 | def vertical_month(month: Optional[int]=None, 169 | year: Optional[int]=None, 170 | today: Optional[dt.date]=None, 171 | weeknumber: Union[bool, str]=False, 172 | count: int=3, 173 | firstweekday: int=0, 174 | monthdisplay: str='firstday', 175 | collection=None, 176 | hmethod: str='fg', 177 | default_color: str='', 178 | multiple: str='', 179 | multiple_on_overflow: bool=False, 180 | color: str='', 181 | highlight_event_days: bool=False, 182 | locale=None, 183 | bold_for_light_color: bool=True, 184 | ) -> list[str]: 185 | """ 186 | returns a list() of str() of weeks for a vertical arranged calendar 187 | 188 | :param month: first month of the calendar, 189 | if non given, current month is assumed 190 | :param year: year of the first month included, 191 | if non given, current year is assumed 192 | :param today: day highlighted, if non is given, current date is assumed 193 | :param weeknumber: if not False the iso weeknumber will be shown for each 194 | week, if weeknumber is 'right' it will be shown in its 195 | own column, if it is 'left' it will be shown interleaved 196 | with the month names 197 | :returns: calendar strings, may also include some 198 | ANSI (color) escape strings 199 | """ 200 | if month is None: 201 | month = dt.date.today().month 202 | if year is None: 203 | year = dt.date.today().year 204 | if today is None: 205 | today = dt.date.today() 206 | 207 | khal = [] 208 | w_number = ' ' if weeknumber == 'right' else '' 209 | calendar.setfirstweekday(firstweekday) 210 | weekheaders = get_weekheader(firstweekday) 211 | month_abbr_len = get_month_abbr_len() 212 | khal.append(style(' ' * month_abbr_len + weekheaders + ' ' + w_number, bold=True)) 213 | _calendar = calendar.Calendar(firstweekday) 214 | for _ in range(count): 215 | for week in _calendar.monthdatescalendar(year, month): 216 | if monthdisplay == 'firstday': 217 | new_month = len([day for day in week if day.day == 1]) 218 | else: 219 | new_month = len(week if week[0].day <= 7 else []) 220 | strweek = str_week(week, today, collection, hmethod, default_color, 221 | multiple, multiple_on_overflow, color, highlight_event_days, locale, 222 | bold_for_light_color) 223 | if new_month: 224 | m_name = style(calendar.month_abbr[week[6].month].ljust(month_abbr_len), bold=True) 225 | elif weeknumber == 'left': 226 | m_name = style(str(getweeknumber(week[0])).center(month_abbr_len), bold=True) 227 | else: 228 | m_name = ' ' * month_abbr_len 229 | if weeknumber == 'right': 230 | w_number = style(f'{getweeknumber(week[0]):2}', bold=True) 231 | else: 232 | w_number = '' 233 | 234 | sweek = m_name + strweek + w_number 235 | if sweek != khal[-1]: 236 | khal.append(sweek) 237 | month = month + 1 238 | if month > 12: 239 | month = 1 240 | year = year + 1 241 | return khal 242 | -------------------------------------------------------------------------------- /khal/cli_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2022 khal contributors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | # 22 | import logging 23 | import sys 24 | 25 | import click 26 | import click_log 27 | 28 | from . import __version__, khalendar 29 | from .exceptions import FatalError 30 | from .settings import InvalidSettingsError, NoConfigFile, get_config 31 | 32 | logger = logging.getLogger('khal') 33 | click_log.basic_config('khal') 34 | 35 | days_option = click.option('--days', default=None, type=int, help='How many days to include.') 36 | week_option = click.option('--week', '-w', help='Include all events in one week.', is_flag=True) 37 | events_option = click.option('--events', default=None, type=int, help='How many events to include.') 38 | dates_arg = click.argument('dates', nargs=-1) 39 | 40 | 41 | def time_args(f): 42 | return dates_arg(events_option(week_option(days_option(f)))) 43 | 44 | 45 | def multi_calendar_select(ctx, include_calendars, exclude_calendars): 46 | if include_calendars and exclude_calendars: 47 | raise click.UsageError('Can\'t use both -a and -d.') 48 | 49 | selection = set() 50 | 51 | if include_calendars: 52 | for cal_name in include_calendars: 53 | if cal_name not in ctx.obj['conf']['calendars']: 54 | raise click.BadParameter( 55 | f'Unknown calendar {cal_name}, run `khal printcalendars` ' 56 | 'to get a list of all configured calendars.' 57 | ) 58 | 59 | selection.update(include_calendars) 60 | elif exclude_calendars: 61 | selection.update(ctx.obj['conf']['calendars'].keys()) 62 | for value in exclude_calendars: 63 | selection.remove(value) 64 | 65 | return selection or None 66 | 67 | 68 | def multi_calendar_option(f): 69 | a = click.option('--include-calendar', '-a', multiple=True, metavar='CAL', 70 | help=('Include the given calendar. Can be specified ' 71 | 'multiple times.')) 72 | d = click.option('--exclude-calendar', '-d', multiple=True, metavar='CAL', 73 | help=('Exclude the given calendar. Can be specified ' 74 | 'multiple times.')) 75 | 76 | return d(a(f)) 77 | 78 | 79 | def mouse_option(f): 80 | o = click.option( 81 | '--mouse/--no-mouse', 82 | is_flag=True, 83 | default=None, 84 | help='Disable mouse in interactive UI' 85 | ) 86 | return o(f) 87 | 88 | 89 | def _select_one_calendar_callback(ctx, option, calendar): 90 | if isinstance(calendar, tuple): 91 | if len(calendar) > 1: 92 | raise click.UsageError( 93 | 'Can\'t use "--include-calendar" / "-a" more than once for this command.') 94 | elif len(calendar) == 1: 95 | calendar = calendar[0] 96 | return _calendar_select_callback(ctx, option, calendar) 97 | 98 | 99 | def _calendar_select_callback(ctx, option, calendar): 100 | if calendar and calendar not in ctx.obj['conf']['calendars']: 101 | raise click.BadParameter( 102 | f'Unknown calendar {calendar}, run `khal printcalendars` to get a ' 103 | 'list of all configured calendars.' 104 | ) 105 | return calendar 106 | 107 | 108 | def calendar_option(f): 109 | return click.option('--calendar', '-a', metavar='CAL', callback=_calendar_select_callback)(f) 110 | 111 | 112 | def global_options(f): 113 | def color_callback(ctx, option, value): 114 | ctx.color = value 115 | 116 | def logfile_callback(ctx, option, path): 117 | ctx.logfilepath = path 118 | 119 | config = click.option( 120 | '--config', '-c', 121 | help='The config file to use.', 122 | default=None, metavar='PATH' 123 | ) 124 | color = click.option( 125 | '--color/--no-color', 126 | help=('Use colored/uncolored output. Default is to only enable colors ' 127 | 'when not part of a pipe.'), 128 | expose_value=False, default=None, 129 | callback=color_callback 130 | ) 131 | 132 | logfile = click.option( 133 | '--logfile', '-l', 134 | help='The logfile to use [defaults to stdout]', 135 | type=click.Path(), 136 | callback=logfile_callback, 137 | default=None, 138 | expose_value=False, 139 | metavar='LOGFILE', 140 | ) 141 | 142 | version = click.version_option(version=__version__) 143 | 144 | return logfile(config(color(version(f)))) 145 | 146 | 147 | def build_collection(conf, selection): 148 | """build and return a khalendar.CalendarCollection from the configuration""" 149 | try: 150 | props = {} 151 | for name, cal in conf['calendars'].items(): 152 | if selection is None or name in selection: 153 | props[name] = { 154 | 'name': name, 155 | 'path': cal['path'], 156 | 'readonly': cal['readonly'], 157 | 'color': cal['color'], 158 | 'priority': cal['priority'], 159 | 'ctype': cal['type'], 160 | 'addresses': cal['addresses'] if 'addresses' in cal else '', 161 | } 162 | collection = khalendar.CalendarCollection( 163 | calendars=props, 164 | color=conf['highlight_days']['color'], 165 | locale=conf['locale'], 166 | dbpath=conf['sqlite']['path'], 167 | hmethod=conf['highlight_days']['method'], 168 | default_color=conf['highlight_days']['default_color'], 169 | multiple=conf['highlight_days']['multiple'], 170 | multiple_on_overflow=conf['highlight_days']['multiple_on_overflow'], 171 | highlight_event_days=conf['default']['highlight_event_days'], 172 | ) 173 | except FatalError as error: 174 | logger.debug(error, exc_info=True) 175 | logger.fatal(error) 176 | sys.exit(1) 177 | 178 | collection._default_calendar_name = conf['default']['default_calendar'] 179 | return collection 180 | 181 | 182 | class _NoConfig: 183 | def __getitem__(self, key): 184 | logger.fatal( 185 | 'Cannot find a config file. If you have no configuration file ' 186 | 'yet, you might want to run `khal configure`.') 187 | sys.exit(1) 188 | 189 | 190 | def prepare_context(ctx, config): 191 | assert ctx.obj is None 192 | 193 | logger.debug('khal %s', __version__) 194 | try: 195 | conf = get_config(config) 196 | except NoConfigFile: 197 | conf = _NoConfig() 198 | except InvalidSettingsError: 199 | logger.info('If your configuration file used to work, please have a ' 200 | 'look at the Changelog to see what changed.') 201 | sys.exit(1) 202 | else: 203 | logger.debug('Using config:') 204 | logger.debug(stringify_conf(conf)) 205 | 206 | ctx.obj = {'conf_path': config, 'conf': conf} 207 | 208 | 209 | def stringify_conf(conf): 210 | # since we have only two levels of recursion, a recursive function isn't 211 | # really worth it 212 | out = [] 213 | for key, value in conf.items(): 214 | out.append(f'[{key}]') 215 | for subkey, subvalue in value.items(): 216 | if isinstance(subvalue, dict): 217 | out.append(f' [[{subkey}]]') 218 | for subsubkey, subsubvalue in subvalue.items(): 219 | out.append(f' {subsubkey}: {subsubvalue}') 220 | else: 221 | out.append(f' {subkey}: {subvalue}') 222 | return '\n'.join(out) 223 | -------------------------------------------------------------------------------- /khal/custom_types.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import os 3 | from typing import Literal, Optional, Protocol, TypedDict, Union 4 | 5 | import pytz 6 | 7 | 8 | class CalendarConfiguration(TypedDict): 9 | name: str 10 | path: str 11 | readonly: bool 12 | color: str 13 | priority: int 14 | ctype: str 15 | addresses: str 16 | 17 | 18 | class LocaleConfiguration(TypedDict): 19 | local_timezone: pytz.BaseTzInfo 20 | default_timezone: pytz.BaseTzInfo 21 | timeformat: str 22 | dateformat: str 23 | longdateformat: str 24 | datetimeformat: str 25 | longdatetimeformat: str 26 | weeknumbers: Union[str, bool] 27 | firstweekday: int 28 | unicode_symbols: bool 29 | 30 | 31 | class SupportsRaw(Protocol): 32 | @property 33 | def uid(self) -> Optional[str]: 34 | ... 35 | 36 | @property 37 | def raw(self) -> str: 38 | ... 39 | 40 | 41 | # set this to TypeAlias once we support that python version (PEP613) 42 | EventTuple = tuple[ 43 | str, 44 | str, 45 | Union[dt.date, dt.datetime], 46 | Union[dt.date, dt.datetime], 47 | str, 48 | str, 49 | str, 50 | ] 51 | 52 | 53 | # Only need for RRuleMapType 54 | class RRuleMapBase(TypedDict): 55 | freq: str 56 | 57 | 58 | class RRuleMapType(RRuleMapBase, total=False): 59 | # not required keys go in here 60 | # TODO remove if either `NotRequired` is supported by mypy or the oldest 61 | # python we support is 3.11 (see PEP 655) 62 | until: dt.datetime 63 | 64 | 65 | class EventCreationTypes(TypedDict): 66 | dtstart: dt.date 67 | dtend: dt.date 68 | summary: str 69 | description: str 70 | allday: bool 71 | location: Optional[str] 72 | categories: Optional[Union[str, list[str]]] 73 | repeat: Optional[str] 74 | until: str 75 | alarms: str 76 | timezone: pytz.BaseTzInfo 77 | url: str 78 | 79 | 80 | PathLike = Union[str, os.PathLike] 81 | 82 | WeekNumbersType = Literal['left', 'right', False] 83 | MonthDisplayType = Literal['firstday', 'firstfullweek'] 84 | -------------------------------------------------------------------------------- /khal/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2022 khal contributors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | 23 | class Error(Exception): 24 | 25 | """base class for all of khal's Exceptions""" 26 | pass 27 | 28 | 29 | class FatalError(Error): 30 | 31 | """execution cannot continue""" 32 | pass 33 | 34 | 35 | class DateTimeParseError(FatalError): 36 | pass 37 | 38 | 39 | class ConfigurationError(FatalError): 40 | pass 41 | 42 | 43 | class UnsupportedFeatureError(Error): 44 | 45 | """something Failed but we know why""" 46 | pass 47 | 48 | 49 | class UnsupportedRecurrence(Error): 50 | 51 | """raised if the RRULE is not understood by dateutil.rrule""" 52 | pass 53 | 54 | 55 | class InvalidDate(Error): 56 | pass 57 | -------------------------------------------------------------------------------- /khal/khalendar/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2022 khal contributors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | from .khalendar import CalendarCollection # noqa: F401 # type: ignore 23 | -------------------------------------------------------------------------------- /khal/khalendar/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2022 khal contributors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | from typing import Optional # noqa 23 | 24 | from ..exceptions import Error, FatalError, UnsupportedFeatureError 25 | 26 | 27 | class UnsupportedRruleExceptionError(UnsupportedFeatureError): 28 | 29 | """we do not support exceptions that do not delete events yet""" 30 | 31 | def __init__(self, message='') -> None: 32 | x = 'This kind of recurrence exception is currently unsupported' 33 | if message: 34 | x += f': {message.strip()}' 35 | UnsupportedFeatureError.__init__(self, x) 36 | 37 | 38 | class ReadOnlyCalendarError(Error): 39 | 40 | """this calendar is readonly and should not be modifiable from within 41 | khal""" 42 | 43 | 44 | class EtagMissmatch(Error): 45 | 46 | """An event is trying to be modified from khal which has also been externally 47 | modified""" 48 | 49 | 50 | class OutdatedDbVersionError(FatalError): 51 | 52 | """the db file has an older version and needs to be deleted""" 53 | 54 | 55 | class CouldNotCreateDbDir(FatalError): 56 | 57 | """the db directory could not be created. Abort.""" 58 | 59 | 60 | class UpdateFailed(Error): 61 | 62 | """could not update the event in the database""" 63 | 64 | 65 | class DuplicateUid(Error): 66 | 67 | """an event with this UID already exists""" 68 | existing_href = None # type: Optional[str] 69 | 70 | 71 | class NonUniqueUID(Error): 72 | 73 | """the .ics file contains more than one UID""" 74 | -------------------------------------------------------------------------------- /khal/khalendar/typing.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Callable 2 | 3 | if TYPE_CHECKING: 4 | from khal.khalendar.event import Event 5 | 6 | # There should be `khal.khalendar.Event` instead of `Any` 7 | # here and in `Postprocess` below but that results in recursive typing 8 | # which mypy doesn't support until 9 | # https://github.com/python/mypy/issues/731 is implemented. 10 | Render = Callable[ 11 | ["Event", Any], 12 | str, 13 | ] 14 | 15 | Postprocess = Callable[[str, "Event", Any], str] 16 | -------------------------------------------------------------------------------- /khal/plugins.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping 2 | from typing import Callable 3 | 4 | from khal._compat import importlib_metadata 5 | 6 | # This is a shameless ripoff of mdformat's plugin extension API. 7 | # see: 8 | # https://github.com/executablebooks/mdformat/blob/master/src/mdformat/plugins.py 9 | # https://setuptools.pypa.io/en/latest/userguide/entry_point.html 10 | 11 | 12 | def _load_formatters() -> dict[str, Callable[[str], str]]: 13 | formatter_entrypoints = importlib_metadata.entry_points(group="khal.formatter") 14 | return {ep.name: ep.load() for ep in formatter_entrypoints} 15 | 16 | 17 | FORMATTERS: Mapping[str, Callable[[str], str]] = _load_formatters() 18 | 19 | def _load_color_themes() -> dict[str, list[tuple[str, ...]]]: 20 | color_theme_entrypoints = importlib_metadata.entry_points(group="khal.color_theme") 21 | return {ep.name: ep.load() for ep in color_theme_entrypoints} 22 | 23 | THEMES: dict[str, list[tuple[str, ...]],] = _load_color_themes() 24 | 25 | 26 | def _load_commands() -> dict[str, Callable]: 27 | command_entrypoints = importlib_metadata.entry_points(group="khal.commands") 28 | return {ep.name: ep.load() for ep in command_entrypoints} 29 | 30 | COMMANDS: dict[str, Callable] = _load_commands() 31 | -------------------------------------------------------------------------------- /khal/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .exceptions import InvalidSettingsError # noqa # type: ignore 2 | from .exceptions import NoConfigFile # noqa # type: ignore 3 | from .settings import find_configuration_file # noqa # type: ignore 4 | from .settings import get_config # noqa # type: ignore 5 | -------------------------------------------------------------------------------- /khal/settings/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2022 khal contributors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | from ..exceptions import Error 23 | 24 | 25 | class InvalidSettingsError(Error): 26 | """Invalid Settings detected""" 27 | pass 28 | 29 | 30 | class CannotParseConfigFileError(InvalidSettingsError): 31 | pass 32 | 33 | 34 | class NoConfigFile(InvalidSettingsError): 35 | pass 36 | -------------------------------------------------------------------------------- /khal/settings/settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2022 khal contributors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | # 22 | 23 | import logging 24 | import os 25 | 26 | import xdg.BaseDirectory 27 | from configobj import ConfigObj, ConfigObjError, flatten_errors, get_extra_values 28 | 29 | from khal import __productname__ 30 | 31 | try: 32 | # Available from configobj 5.1.0 33 | from configobj.validate import Validator 34 | except ModuleNotFoundError: 35 | from validate import Validator 36 | 37 | from typing import Callable, Optional 38 | 39 | from .exceptions import CannotParseConfigFileError, InvalidSettingsError, NoConfigFile 40 | from .utils import ( 41 | config_checks, 42 | expand_db_path, 43 | expand_path, 44 | get_color_from_vdir, 45 | get_vdir_type, 46 | is_color, 47 | is_timedelta, 48 | is_timezone, 49 | monthdisplay_option, 50 | weeknumber_option, 51 | ) 52 | 53 | logger = logging.getLogger('khal') 54 | SPECPATH = os.path.join(os.path.dirname(__file__), 'khal.spec') 55 | 56 | 57 | def find_configuration_file() -> Optional[str]: 58 | """Return the configuration filename. 59 | 60 | Check all the paths for configuration files defined in the XDG Base Directory 61 | Standard, and return the first one that exists, if any. 62 | 63 | For the common case, this will return ~/.config/khal/config, assuming that it 64 | exists. 65 | """ 66 | 67 | for dir in xdg.BaseDirectory.xdg_config_dirs: 68 | path = os.path.join(dir, __productname__, 'config') 69 | if os.path.exists(path): 70 | return path 71 | 72 | return None 73 | 74 | 75 | def get_config( 76 | config_path: Optional[str]=None, 77 | _get_color_from_vdir: Callable=get_color_from_vdir, 78 | _get_vdir_type: Callable=get_vdir_type) -> ConfigObj: 79 | """reads the config file, validates it and return a config dict 80 | 81 | :param config_path: path to a custom config file, if none is given the 82 | default locations will be searched 83 | :param _get_color_from_vdir: override get_color_from_vdir for testing purposes 84 | :param _get_vdir_type: override get_vdir_type for testing purposes 85 | :returns: configuration 86 | """ 87 | if config_path is None: 88 | config_path = find_configuration_file() 89 | if config_path is None or not os.path.exists(config_path): 90 | raise NoConfigFile() 91 | 92 | logger.debug(f'using the config file at {config_path}') 93 | 94 | try: 95 | user_config = ConfigObj(config_path, 96 | configspec=SPECPATH, 97 | interpolation=False, 98 | file_error=True, 99 | ) 100 | except ConfigObjError as error: 101 | logger.fatal('parsing the config file with the following error: ' 102 | f'{error}') 103 | logger.fatal('if you recently updated khal, the config file format ' 104 | 'might have changed, in that case please consult the ' 105 | 'CHANGELOG or other documentation') 106 | raise CannotParseConfigFileError() 107 | 108 | fdict = {'timezone': is_timezone, 109 | 'timedelta': is_timedelta, 110 | 'expand_path': expand_path, 111 | 'expand_db_path': expand_db_path, 112 | 'weeknumbers': weeknumber_option, 113 | 'monthdisplay': monthdisplay_option, 114 | 'color': is_color, 115 | } 116 | validator = Validator(fdict) 117 | results = user_config.validate(validator, preserve_errors=True) 118 | 119 | abort = False 120 | for section, subsection, config_error in flatten_errors(user_config, results): 121 | abort = True 122 | if isinstance(config_error, Exception): 123 | logger.fatal( 124 | f'config error:\n' 125 | f'in [{section[0]}] {subsection}: {config_error}') 126 | else: 127 | for key in config_error: 128 | if isinstance(config_error[key], Exception): 129 | logger.fatal( 130 | 'config error:\n' 131 | f'in {sectionize(section + [subsection])} {key}: ' 132 | f'{str(config_error[key])}' 133 | ) 134 | 135 | if abort or not results: 136 | raise InvalidSettingsError() 137 | 138 | config_checks(user_config, _get_color_from_vdir, _get_vdir_type) 139 | 140 | extras = get_extra_values(user_config) 141 | for section, value in extras: 142 | if section == (): 143 | logger.warning(f'unknown section "{value}" in config file') 144 | elif section == ('palette',): 145 | # we don't validate the palette section, because there is no way to 146 | # automatically extract valid attributes from the ui module 147 | continue 148 | else: 149 | section = sectionize(section) 150 | logger.warning( 151 | f'unknown key or subsection "{value}" in section "{section}"') 152 | return user_config 153 | 154 | 155 | def sectionize(sections: list[str], depth: int=1) -> str: 156 | """converts list of string into [list][[of]][[[strings]]]""" 157 | this_part = depth * '[' + sections[0] + depth * ']' 158 | if len(sections) > 1: 159 | return this_part + sectionize(sections[1:], depth=depth + 1) 160 | else: 161 | return this_part 162 | -------------------------------------------------------------------------------- /khal/terminal.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2022 khal contributors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | # 22 | """all functions related to terminal display are collected here""" 23 | 24 | from itertools import zip_longest 25 | from typing import NamedTuple, Optional 26 | 27 | 28 | class NamedColor(NamedTuple): 29 | color_index: int 30 | light: bool 31 | 32 | 33 | RTEXT = '\x1b[7m' # reverse 34 | NTEXT = '\x1b[0m' # normal 35 | BTEXT = '\x1b[1m' # bold 36 | RESET = '\33[0m' 37 | COLORS: dict[str, NamedColor] = { 38 | 'black': NamedColor(color_index=0, light=False), 39 | 'dark red': NamedColor(color_index=1, light=False), 40 | 'dark green': NamedColor(color_index=2, light=False), 41 | 'brown': NamedColor(color_index=3, light=False), 42 | 'dark blue': NamedColor(color_index=4, light=False), 43 | 'dark magenta': NamedColor(color_index=5, light=False), 44 | 'dark cyan': NamedColor(color_index=6, light=False), 45 | 'white': NamedColor(color_index=7, light=False), 46 | 'light gray': NamedColor(color_index=7, light=True), 47 | 'dark gray': NamedColor(color_index=0, light=True), # actually light black 48 | 'light red': NamedColor(color_index=1, light=True), 49 | 'light green': NamedColor(color_index=2, light=True), 50 | 'yellow': NamedColor(color_index=3, light=True), 51 | 'light blue': NamedColor(color_index=4, light=True), 52 | 'light magenta': NamedColor(color_index=5, light=True), 53 | 'light cyan': NamedColor(color_index=6, light=True) 54 | } 55 | 56 | 57 | def get_color( 58 | fg: Optional[str]=None, 59 | bg: Optional[str]=None, 60 | bold_for_light_color: bool=False, 61 | ) -> str: 62 | """convert foreground and/or background color in ANSI color codes 63 | 64 | colors can be a color name from the ANSI color palette (e.g. 'dark green'), 65 | a number between 0 and 255 (still pass them as a string) or an HTML color in 66 | the style `#00FF00` or `#ABC` 67 | 68 | :param fg: foreground color 69 | :param bg: background color 70 | :returns: ANSI color code 71 | """ 72 | 73 | result = '' 74 | for colorstring, is_bg in ((fg, False), (bg, True)): 75 | if colorstring: 76 | color = '\33[' 77 | if colorstring in COLORS: 78 | # 16 color palette 79 | if not is_bg: 80 | # foreground color 81 | c = 30 + COLORS[colorstring].color_index 82 | if COLORS[colorstring].light: 83 | if bold_for_light_color: 84 | color += '1;' 85 | else: 86 | c += 60 87 | else: 88 | # background color 89 | c = 40 + COLORS[colorstring].color_index 90 | if COLORS[colorstring].light: 91 | if not bold_for_light_color: 92 | c += 60 93 | color += str(c) 94 | elif colorstring.isdigit(): 95 | # 256 color palette 96 | if not is_bg: 97 | color += '38;5;' + colorstring 98 | else: 99 | color += '48;5;' + colorstring 100 | else: 101 | # HTML-style 24-bit color 102 | if len(colorstring) == 4: 103 | # e.g. #ABC, equivalent to #AABBCC 104 | r = int(colorstring[1] * 2, 16) 105 | g = int(colorstring[2] * 2, 16) 106 | b = int(colorstring[3] * 2, 16) 107 | else: 108 | # e.g. #AABBCC 109 | r = int(colorstring[1:3], 16) 110 | g = int(colorstring[3:5], 16) 111 | b = int(colorstring[5:7], 16) 112 | if not is_bg: 113 | color += f'38;2;{r!s};{g!s};{b!s}' 114 | else: 115 | color += f'48;2;{r!s};{g!s};{b!s}' 116 | color += 'm' 117 | result += color 118 | return result 119 | 120 | 121 | def colored( 122 | string: str, 123 | fg: Optional[str]=None, 124 | bg: Optional[str]=None, 125 | bold_for_light_color: bool=True, 126 | ) -> str: 127 | """colorize `string` with ANSI color codes 128 | 129 | see get_color for description of `fg`, `bg` and `bold_for_light_color` 130 | :param string: string to be colorized 131 | :returns: colorized string 132 | """ 133 | result = get_color(fg, bg, bold_for_light_color) 134 | result += string 135 | if fg or bg: 136 | result += RESET 137 | return result 138 | 139 | 140 | def merge_columns(lcolumn: list[str], rcolumn: list[str], width: int=25) -> list[str]: 141 | """merge two lists elementwise together 142 | 143 | Wrap right columns to terminal width. 144 | If the right list(column) is longer, first lengthen the left one. 145 | We assume that the left column has width `width`, we cannot find 146 | out its (real) width automatically since it might contain ANSI 147 | escape sequences. 148 | """ 149 | missing = len(rcolumn) - len(lcolumn) 150 | if missing > 0: 151 | lcolumn = lcolumn + missing * [width * ' '] 152 | 153 | return [' '.join(one) for one in zip_longest(lcolumn, rcolumn, fillvalue='')] 154 | -------------------------------------------------------------------------------- /khal/ui/colors.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2022 khal contributors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | 23 | dark = [ 24 | ('header', 'white', 'black'), 25 | ('footer', 'white', 'black'), 26 | ('line header', 'black', 'white', 'bold'), 27 | ('alt header', 'white', '', 'bold'), 28 | ('bright', 'dark blue', 'white', 'bold,standout'), 29 | ('list', 'black', 'white'), 30 | ('list focused', 'white', 'light blue', 'bold'), 31 | ('edit', 'black', 'white'), 32 | ('edit focus', 'white', 'light blue', 'bold'), 33 | ('button', 'black', 'dark cyan'), 34 | ('button focused', 'white', 'light blue', 'bold'), 35 | 36 | ('reveal focus', 'black', 'light gray'), 37 | ('today focus', 'white', 'dark magenta'), 38 | ('today', 'dark gray', 'dark green',), 39 | 40 | ('date header', 'light gray', 'black'), 41 | ('date header focused', 'black', 'white'), 42 | ('date header selected', 'dark gray', 'light gray'), 43 | 44 | ('dayname', 'light gray', ''), 45 | ('monthname', 'light gray', ''), 46 | ('weeknumber_right', 'light gray', ''), 47 | ('alert', 'white', 'dark red'), 48 | ('mark', 'white', 'dark green'), 49 | ('frame', 'white', 'black'), 50 | ('frame focus', 'light red', 'black'), 51 | ('frame focus color', 'dark blue', 'black'), 52 | ('frame focus top', 'dark magenta', 'black'), 53 | 54 | ('eventcolumn', '', '', ''), 55 | ('eventcolumn focus', '', '', ''), 56 | ('calendar', '', '', ''), 57 | ('calendar focus', '', '', ''), 58 | 59 | ('editbx', 'light gray', 'dark blue'), 60 | ('editcp', 'black', 'light gray', 'standout'), 61 | ('popupbg', 'white', 'black', 'bold'), 62 | ('popupper', 'white', 'dark cyan'), 63 | ('caption', 'white', '', 'bold'), 64 | ] 65 | light = [ 66 | ('header', 'black', 'white'), 67 | ('footer', 'black', 'white'), 68 | ('line header', 'black', 'white', 'bold'), 69 | ('alt header', 'black', '', 'bold'), 70 | ('bright', 'dark blue', 'white', 'bold,standout'), 71 | ('list', 'black', 'white'), 72 | ('list focused', 'white', 'light blue', 'bold'), 73 | ('edit', 'black', 'white'), 74 | ('edit focus', 'white', 'light blue', 'bold'), 75 | ('button', 'black', 'dark cyan'), 76 | ('button focused', 'white', 'light blue', 'bold'), 77 | 78 | ('reveal focus', 'black', 'dark cyan', 'standout'), 79 | ('today focus', 'white', 'dark cyan', 'standout'), 80 | ('today', 'black', 'light gray'), 81 | 82 | ('date header', '', 'white'), 83 | ('date header focused', 'white', 'dark gray', 'bold,standout'), 84 | ('date header selected', 'dark gray', 'light cyan'), 85 | 86 | ('dayname', 'dark gray', 'white'), 87 | ('monthname', 'dark gray', 'white'), 88 | ('weeknumber_right', 'dark gray', 'white'), 89 | ('alert', 'white', 'dark red'), 90 | ('mark', 'white', 'dark green'), 91 | ('frame', 'dark gray', 'white'), 92 | ('frame focus', 'light red', 'white'), 93 | ('frame focus color', 'dark blue', 'white'), 94 | ('frame focus top', 'dark magenta', 'white'), 95 | 96 | ('eventcolumn', '', '', ''), 97 | ('eventcolumn focus', '', '', ''), 98 | ('calendar', '', '', ''), 99 | ('calendar focus', '', '', ''), 100 | 101 | ('editbx', 'light gray', 'dark blue'), 102 | ('editcp', 'black', 'light gray', 'standout'), 103 | ('popupbg', 'white', 'black', 'bold'), 104 | ('popupper', 'black', 'light gray'), 105 | ('caption', 'black', '', ''), 106 | ] 107 | 108 | themes: dict[str, list[tuple[str, ...]]] = {'light': light, 'dark': dark} 109 | -------------------------------------------------------------------------------- /misc/khal.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=ikhal 3 | Categories=Calendar;ConsoleOnly; 4 | GenericName=Calendar application 5 | Comment=Terminal CLI calendar application 6 | Exec=ikhal 7 | Terminal=true 8 | Type=Application 9 | -------------------------------------------------------------------------------- /misc/mutt2khal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | # mutt2khal is designed to be used in conjunction with vcalendar-filter (https://github.com/terabyte/mutt-filters/blob/master/vcalendar-filter) 3 | # and was inspired by the work of Jason Ryan (https://bitbucket.org/jasonwryan/shiv/src/tip/Scripts/mutt2khal) 4 | # example muttrc: macro attach A "vcalendar-filter | mutt2khal" 5 | 6 | 7 | /^Summary/ { 8 | sub(/^Summary[ ]*:[ ]*/, "") 9 | gsub("'", "'\\''") 10 | summ = sprintf("'%s'", $0) 11 | next 12 | } 13 | 14 | /^Location/ { 15 | sub(/^Location[ ]*:[ ]*/,"") 16 | gsub("'", "'\\''") 17 | loc = sprintf("'%s'", $0) 18 | next 19 | } 20 | 21 | /^Desc/ { 22 | sub(/^Description[ ]*:[ ]*/, "") 23 | gsub("'", "'\\''") 24 | desc = sprintf("'%s'", $0) 25 | next 26 | } 27 | 28 | /^Dtstart/ { 29 | split($3, a, "-") 30 | t_st = $4 31 | d_st = sprintf("%s.%s.%s", a[3], a[2], a[1]) 32 | next 33 | } 34 | 35 | /^Dtend/ { 36 | split($3, a, "-") 37 | t_end = $4 38 | d_end = sprintf("%s.%s.%s", a[3], a[2], a[1]) 39 | next 40 | } 41 | 42 | END { 43 | printf("khal new --location %s %s %s %s %s %s :: %s", loc, d_st, t_st, d_end, t_end, summ, desc) | "sh" 44 | } 45 | 46 | ## IMPORTANT ## 47 | 48 | # the d_st and d_end variables assume the default datetimeformat variable of 49 | #%d.%m.%Y, if another format is in use, the sprintf variables must be changed 50 | #accordingly. For example, if the datetimeforma is set to %m.%d.%Y, use: 51 | #sprintf("%s.%s.%s", a[2], a[3], a[1]) 52 | -------------------------------------------------------------------------------- /publish-release.yaml: -------------------------------------------------------------------------------- 1 | # Push new version to PyPI. 2 | # 3 | # Usage: hut builds submit publish-release.yaml --follow 4 | 5 | image: alpine/edge 6 | packages: 7 | - py3-build 8 | - py3-pip 9 | - py3-setuptools 10 | - py3-setuptools_scm 11 | - py3-wheel 12 | - twine 13 | sources: 14 | - https://github.com/pimutils/khal 15 | secrets: 16 | - 18ea84e2-ada3-4aea-b823-309a367c8359 # PyPI token. 17 | environment: 18 | CI: true 19 | tasks: 20 | - check-tag: | 21 | cd khal 22 | git fetch --tags 23 | 24 | # Stop here unless this is a tag. 25 | git describe --exact-match --tags || complete-build 26 | - publish: | 27 | cd khal 28 | python -m build --no-isolation 29 | twine upload --non-interactive dist/* 30 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "khal" 3 | dynamic = ["version"] 4 | description = "Standards based terminal calendar" 5 | readme = "README.rst" 6 | authors = [ 7 | {name = "khal contributors", email = "khal@lostpackets.de"}, 8 | ] 9 | license = "MIT" 10 | license-files = ["doc/source/license.rst"] 11 | classifiers = [ 12 | "Development Status :: 4 - Beta", 13 | "Environment :: Console :: Curses", 14 | "Intended Audience :: End Users/Desktop", 15 | "Operating System :: POSIX", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3 :: Only", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | "Topic :: Communications", 24 | "Topic :: Utilities", 25 | ] 26 | requires-python = ">=3.9,<3.14" 27 | dependencies = [ 28 | "click>=3.2", 29 | "click_log>=0.2.0", 30 | "icalendar>=6.0.0", 31 | "urwid>=2.6.15", 32 | "pyxdg", 33 | "pytz", 34 | "python-dateutil", 35 | "configobj", 36 | "tzlocal>=1.0", 37 | ] 38 | [project.optional-dependencies] 39 | proctitle = [ 40 | "setproctitle" 41 | ] 42 | test = [ 43 | "pytest", 44 | "freezegun", 45 | "hypothesis", 46 | "packaging", 47 | "vdirsyncer", 48 | "importlib-metadata; python_version <= '3.9'", # importlib.metadata is in stdlib since 3.10 49 | ] 50 | docs = [ 51 | "sphinx!=1.6.1", 52 | "sphinxcontrib-newsfeed", 53 | "sphinx-rtd-theme", 54 | ] 55 | 56 | # install all optional dependencies 57 | all = ["khal[proctitle,test,docs]"] 58 | 59 | [project.urls] 60 | homepage = "http://lostpackets.de/khal/" 61 | repository = "https://github.com/pimutils/khal" 62 | 63 | [project.scripts] 64 | khal = "khal.cli:main_khal" 65 | ikhal = "khal.cli:main_ikhal" 66 | 67 | [build-system] 68 | requires = ["setuptools>=77", "setuptools_scm>=8"] 69 | build-backend = "setuptools.build_meta" 70 | 71 | [tool.setuptools.packages.find] 72 | include = ["khal*"] 73 | 74 | [tool.setuptools_scm] 75 | version_file = "khal/version.py" 76 | 77 | [tool.ruff] 78 | line-length = 100 79 | target-version = "py39" 80 | 81 | [tool.ruff.lint] 82 | ignore = ["B008"] 83 | select = ["E", "F", "W", "I", "B0", "UP", "C4"] 84 | 85 | [tool.coverage.report] 86 | exclude_lines = [ 87 | "if TYPE_CHECKING:", 88 | ] 89 | 90 | [tool.mypy] 91 | ignore_missing_imports = true 92 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimutils/khal/df6545645740f5ffe6ec00a97f7b5ff195ff60f4/tests/__init__.py -------------------------------------------------------------------------------- /tests/configs/nocalendars.conf: -------------------------------------------------------------------------------- 1 | [locale] 2 | local_timezone= Europe/Berlin 3 | default_timezone= Europe/Berlin 4 | timeformat= %H:%M 5 | dateformat= %d.%m. 6 | longdateformat= %d.%m.%Y 7 | datetimeformat= %d.%m. %H:%M 8 | longdatetimeformat= %d.%m.%Y %H:%M 9 | -------------------------------------------------------------------------------- /tests/configs/one_level_calendars.conf: -------------------------------------------------------------------------------- 1 | [calendars] 2 | path = /home/user/.nextcloud/ 3 | type = discover 4 | -------------------------------------------------------------------------------- /tests/configs/simple.conf: -------------------------------------------------------------------------------- 1 | [calendars] 2 | [[home]] 3 | path = ~/.calendars/home/ 4 | 5 | [[work]] 6 | path = ~/.calendars/work/ 7 | 8 | [locale] 9 | local_timezone= Europe/Berlin 10 | default_timezone= Europe/Berlin 11 | timeformat= %H:%M 12 | dateformat= %d.%m. 13 | longdateformat= %d.%m.%Y 14 | datetimeformat= %d.%m. %H:%M 15 | longdatetimeformat= %d.%m.%Y %H:%M 16 | -------------------------------------------------------------------------------- /tests/configs/small.conf: -------------------------------------------------------------------------------- 1 | [calendars] 2 | 3 | [[home]] 4 | path = ~/.calendars/home/ 5 | color = dark green 6 | priority = 20 7 | 8 | [[work]] 9 | path = ~/.calendars/work/ 10 | readonly = True 11 | addresses = user@example.com 12 | -------------------------------------------------------------------------------- /tests/configwizard_test.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pytest 3 | 4 | from khal.configwizard import get_collection_names_from_vdirs, validate_int 5 | 6 | 7 | def test_validate_int(): 8 | assert validate_int('3', 0, 3) == 3 9 | with pytest.raises(click.UsageError): 10 | validate_int('3', 0, 2) 11 | with pytest.raises(click.UsageError): 12 | validate_int('two', 0, 2) 13 | 14 | 15 | def test_default_vdir(metavdirs): 16 | names = get_collection_names_from_vdirs([('found', f'{metavdirs}/**/', 'discover')]) 17 | assert names == [ 18 | 'my private calendar', 'my calendar', 'public', 'home', 'public1', 'work', 19 | 'cfgcolor', 'cfgcolor_again', 'cfgcolor_once_more', 'dircolor', 'singlecollection', 20 | ] 21 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from time import sleep 4 | 5 | import pytest 6 | 7 | from khal.custom_types import CalendarConfiguration 8 | from khal.khalendar import CalendarCollection 9 | from khal.khalendar.vdir import Vdir 10 | 11 | from .utils import LOCALE_BERLIN, CollVdirType, cal1, example_cals 12 | 13 | 14 | @pytest.fixture 15 | def metavdirs(tmpdir): 16 | tmpdir = str(tmpdir) 17 | dirstructure = [ 18 | '/cal1/public/', 19 | '/cal1/private/', 20 | '/cal2/public/', 21 | '/cal3/public/', 22 | '/cal3/work/', 23 | '/cal3/home/', 24 | '/cal4/cfgcolor/', 25 | '/cal4/dircolor/', 26 | '/cal4/cfgcolor_again/', 27 | '/cal4/cfgcolor_once_more/', 28 | '/singlecollection/', 29 | ] 30 | for one in dirstructure: 31 | os.makedirs(tmpdir + one) 32 | filestructure = [ 33 | ('/cal1/public/displayname', 'my calendar'), 34 | ('/cal1/public/color', 'dark blue'), 35 | ('/cal1/private/displayname', 'my private calendar'), 36 | ('/cal1/private/color', '#FF00FF'), 37 | ('/cal4/dircolor/color', 'dark blue'), 38 | ] 39 | for filename, content in filestructure: 40 | with open(tmpdir + filename, 'w') as metafile: 41 | metafile.write(content) 42 | return tmpdir 43 | 44 | 45 | @pytest.fixture 46 | def coll_vdirs(tmpdir) -> CollVdirType: 47 | calendars, vdirs = {}, {} 48 | for name in example_cals: 49 | path = str(tmpdir) + '/' + name 50 | os.makedirs(path, mode=0o770) 51 | readonly = True if name == 'a_calendar' else False 52 | calendars[name] = CalendarConfiguration( 53 | name=name, 54 | path=path, 55 | readonly=readonly, 56 | color='dark blue', 57 | priority=10, 58 | ctype='calendar', 59 | addresses='user@example.com', 60 | ) 61 | vdirs[name] = Vdir(path, '.ics') 62 | coll = CalendarCollection(calendars=calendars, dbpath=':memory:', locale=LOCALE_BERLIN) 63 | coll.default_calendar_name = cal1 64 | return coll, vdirs 65 | 66 | 67 | @pytest.fixture 68 | def coll_vdirs_birthday(tmpdir): 69 | calendars, vdirs = {}, {} 70 | for name in example_cals: 71 | path = str(tmpdir) + '/' + name 72 | os.makedirs(path, mode=0o770) 73 | readonly = True if name == 'a_calendar' else False 74 | calendars[name] = {'name': name, 'path': path, 'color': 'dark blue', 75 | 'readonly': readonly, 'unicode_symbols': True, 'ctype': 'birthdays', 76 | 'addresses': 'user@example.com'} 77 | vdirs[name] = Vdir(path, '.vcf') 78 | coll = CalendarCollection(calendars=calendars, dbpath=':memory:', locale=LOCALE_BERLIN) 79 | coll.default_calendar_name = cal1 80 | return coll, vdirs 81 | 82 | 83 | @pytest.fixture(autouse=True) 84 | def never_echo_bytes(monkeypatch): 85 | '''Click's echo function will not strip colorcodes if we call `click.echo` 86 | with a bytestring message. The reason for this that bytestrings may contain 87 | arbitrary binary data (such as images). 88 | 89 | Khal is not concerned with such data at all, but may contain a few 90 | instances where it explicitly encodes its output into the configured 91 | locale. This in turn would break the functionality of the global 92 | `--color/--no-color` flag. 93 | ''' 94 | from click import echo as old_echo 95 | 96 | def echo(msg=None, *a, **kw): 97 | assert not isinstance(msg, bytes) 98 | return old_echo(msg, *a, **kw) 99 | 100 | monkeypatch.setattr('click.echo', echo) 101 | 102 | class Result: 103 | @staticmethod 104 | def undo(): 105 | monkeypatch.setattr('click.echo', old_echo) 106 | 107 | return Result 108 | 109 | 110 | @pytest.fixture(scope='session') 111 | def sleep_time(tmpdir_factory): 112 | """ 113 | Returns the filesystem's mtime precision 114 | 115 | Returns how long we need to sleep for the filesystem's mtime precision to 116 | pick up differences. 117 | 118 | This keeps test fast on systems with high precisions, but makes them pass 119 | on those that don't. 120 | """ 121 | tmpfile = tmpdir_factory.mktemp('sleep').join('touch_me') 122 | 123 | def touch_and_mtime(): 124 | tmpfile.open('w').close() 125 | stat = os.stat(str(tmpfile)) 126 | return getattr(stat, 'st_mtime_ns', stat.st_mtime) 127 | 128 | i = 0.00001 129 | while i < 100: 130 | # Measure three times to avoid things like 12::18:11.9994 [mis]passing 131 | first = touch_and_mtime() 132 | sleep(i) 133 | second = touch_and_mtime() 134 | sleep(i) 135 | third = touch_and_mtime() 136 | 137 | if first != second != third: 138 | return i * 1.1 139 | i = i * 10 140 | 141 | # This should never happen, but oh, well: 142 | raise Exception( 143 | 'Filesystem does not seem to save modified times of files. \n' 144 | 'Cannot run tests that depend on this.' 145 | ) 146 | 147 | 148 | @pytest.fixture 149 | def fix_caplog(monkeypatch): 150 | """Temporarily undoes the logging setup by click-log such that the caplog fixture can be used""" 151 | logger = logging.getLogger('khal') 152 | monkeypatch.setattr(logger, 'handlers', []) 153 | monkeypatch.setattr(logger, 'propagate', True) 154 | -------------------------------------------------------------------------------- /tests/controller_test.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | from textwrap import dedent 3 | 4 | import pytest 5 | from freezegun import freeze_time 6 | 7 | from khal import exceptions 8 | from khal.controllers import import_ics, khal_list, start_end_from_daterange 9 | from khal.khalendar.vdir import Item 10 | 11 | from . import utils 12 | from .utils import _get_text 13 | 14 | today = dt.date.today() 15 | yesterday = today - dt.timedelta(days=1) 16 | tomorrow = today + dt.timedelta(days=1) 17 | 18 | event_allday_template = """BEGIN:VEVENT 19 | SEQUENCE:0 20 | UID:uid3@host1.com 21 | DTSTART;VALUE=DATE:{} 22 | DTEND;VALUE=DATE:{} 23 | SUMMARY:a meeting 24 | DESCRIPTION:short description 25 | LOCATION:LDB Lobby 26 | END:VEVENT""" 27 | 28 | event_today = event_allday_template.format(today.strftime('%Y%m%d'), 29 | tomorrow.strftime('%Y%m%d')) 30 | item_today = Item(event_today) 31 | 32 | event_format = '{calendar-color}{start-end-time-style:16} {title}' 33 | event_format += '{repeat-symbol}{description-separator}{description}{calendar-color}' 34 | 35 | conf = {'locale': utils.LOCALE_BERLIN, 36 | 'default': {'timedelta': dt.timedelta(days=2), 'show_all_days': False} 37 | } 38 | 39 | 40 | class TestGetAgenda: 41 | def test_new_event(self, coll_vdirs): 42 | coll, vdirs = coll_vdirs 43 | event = coll.create_event_from_ics(event_today, utils.cal1) 44 | coll.insert(event) 45 | assert [' a meeting :: short description\x1b[0m'] == \ 46 | khal_list(coll, [], conf, agenda_format=event_format, day_format="") 47 | 48 | def test_new_event_day_format(self, coll_vdirs): 49 | coll, vdirs = coll_vdirs 50 | event = coll.create_event_from_ics(event_today, utils.cal1) 51 | coll.insert(event) 52 | assert ['Today\x1b[0m', 53 | ' a meeting :: short description\x1b[0m'] == \ 54 | khal_list(coll, [], conf, agenda_format=event_format, day_format="{name}") 55 | 56 | def test_agenda_default_day_format(self, coll_vdirs): 57 | with freeze_time('2016-04-10 12:33'): 58 | today = dt.date.today() 59 | event_today = event_allday_template.format( 60 | today.strftime('%Y%m%d'), tomorrow.strftime('%Y%m%d')) 61 | coll, vdirs = coll_vdirs 62 | event = coll.create_event_from_ics(event_today, utils.cal1) 63 | coll.insert(event) 64 | out = khal_list( 65 | coll, conf=conf, agenda_format=event_format, datepoint=[]) 66 | assert [ 67 | '\x1b[1m10.04.2016 12:33\x1b[0m\x1b[0m', 68 | '↦ a meeting :: short description\x1b[0m'] == out 69 | 70 | def test_agenda_fail(self, coll_vdirs): 71 | with freeze_time('2016-04-10 12:33'): 72 | coll, vdirs = coll_vdirs 73 | with pytest.raises(exceptions.FatalError): 74 | khal_list(coll, conf=conf, agenda_format=event_format, datepoint=['xyz']) 75 | with pytest.raises(exceptions.FatalError): 76 | khal_list(coll, conf=conf, agenda_format=event_format, datepoint=['today']) 77 | 78 | def test_empty_recurrence(self, coll_vdirs): 79 | coll, vidrs = coll_vdirs 80 | coll.insert(coll.create_event_from_ics(dedent( 81 | 'BEGIN:VEVENT\r\n' 82 | 'UID:no_recurrences\r\n' 83 | 'SUMMARY:No recurrences\r\n' 84 | 'RRULE:FREQ=DAILY;COUNT=2;INTERVAL=1\r\n' 85 | 'EXDATE:20110908T130000\r\n' 86 | 'EXDATE:20110909T130000\r\n' 87 | 'DTSTART:20110908T130000\r\n' 88 | 'DTEND:20110908T170000\r\n' 89 | 'END:VEVENT\r\n' 90 | ), utils.cal1)) 91 | assert '\n'.join(khal_list(coll, [], conf, 92 | agenda_format=event_format, day_format="{name}")).lower() == '' 93 | 94 | 95 | class TestImport: 96 | def test_import(self, coll_vdirs): 97 | coll, vdirs = coll_vdirs 98 | view = {'event_format': '{title}'} 99 | conf = {'locale': utils.LOCALE_BERLIN, 'view': view} 100 | import_ics(coll, conf, _get_text('event_rrule_recuid'), batch=True) 101 | start_date = utils.BERLIN.localize(dt.datetime(2014, 4, 30)) 102 | end_date = utils.BERLIN.localize(dt.datetime(2014, 9, 26)) 103 | events = list(coll.get_localized(start_date, end_date)) 104 | assert len(events) == 6 105 | events = sorted(events) 106 | assert events[1].start_local == utils.BERLIN.localize(dt.datetime(2014, 7, 7, 9, 0)) 107 | assert utils.BERLIN.localize(dt.datetime(2014, 7, 14, 7, 0)) in \ 108 | [ev.start for ev in events] 109 | 110 | import_ics(coll, conf, _get_text('event_rrule_recuid_update'), batch=True) 111 | events = list(coll.get_localized(start_date, end_date)) 112 | for ev in events: 113 | print(ev.start) 114 | assert ev.calendar == 'foobar' 115 | assert len(events) == 5 116 | assert utils.BERLIN.localize(dt.datetime(2014, 7, 14, 7, 0)) not in \ 117 | [ev.start_local for ev in events] 118 | 119 | def test_mix_datetime_types(self, coll_vdirs): 120 | """ 121 | Test importing events with mixed tz-aware and tz-naive datetimes. 122 | """ 123 | coll, vdirs = coll_vdirs 124 | view = {'event_format': '{title}'} 125 | import_ics( 126 | coll, 127 | {'locale': utils.LOCALE_BERLIN, 'view': view}, 128 | _get_text('event_dt_mixed_awareness'), 129 | batch=True 130 | ) 131 | start_date = utils.BERLIN.localize(dt.datetime(2015, 5, 29)) 132 | end_date = utils.BERLIN.localize(dt.datetime(2015, 6, 3)) 133 | events = list(coll.get_localized(start_date, end_date)) 134 | assert len(events) == 2 135 | events = sorted(events) 136 | assert events[0].start_local == \ 137 | utils.BERLIN.localize(dt.datetime(2015, 5, 30, 12, 0)) 138 | assert events[0].end_local == \ 139 | utils.BERLIN.localize(dt.datetime(2015, 5, 30, 16, 0)) 140 | assert events[1].start_local == \ 141 | utils.BERLIN.localize(dt.datetime(2015, 6, 2, 12, 0)) 142 | assert events[1].end_local == \ 143 | utils.BERLIN.localize(dt.datetime(2015, 6, 2, 16, 0)) 144 | 145 | 146 | def test_start_end(): 147 | with freeze_time('2016-04-10'): 148 | start = dt.datetime(2016, 4, 10, 0, 0) 149 | end = dt.datetime(2016, 4, 11, 0, 0) 150 | assert (start, end) == start_end_from_daterange(('today',), locale=utils.LOCALE_BERLIN) 151 | 152 | 153 | def test_start_end_default_delta(): 154 | with freeze_time('2016-04-10'): 155 | start = dt.datetime(2016, 4, 10, 0, 0) 156 | end = dt.datetime(2016, 4, 11, 0, 0) 157 | assert (start, end) == start_end_from_daterange(('today',), utils.LOCALE_BERLIN) 158 | 159 | 160 | def test_start_end_delta(): 161 | with freeze_time('2016-04-10'): 162 | start = dt.datetime(2016, 4, 10, 0, 0) 163 | end = dt.datetime(2016, 4, 12, 0, 0) 164 | assert (start, end) == start_end_from_daterange(('today', '2d'), utils.LOCALE_BERLIN) 165 | 166 | 167 | def test_start_end_empty(): 168 | with freeze_time('2016-04-10'): 169 | start = dt.datetime(2016, 4, 10, 0, 0) 170 | end = dt.datetime(2016, 4, 11, 0, 0) 171 | assert (start, end) == start_end_from_daterange([], utils.LOCALE_BERLIN) 172 | 173 | 174 | def test_start_end_empty_default(): 175 | with freeze_time('2016-04-10'): 176 | start = dt.datetime(2016, 4, 10, 0, 0) 177 | end = dt.datetime(2016, 4, 13, 0, 0) 178 | assert (start, end) == start_end_from_daterange( 179 | [], utils.LOCALE_BERLIN, 180 | default_timedelta_date=dt.timedelta(days=3), 181 | default_timedelta_datetime=dt.timedelta(hours=1), 182 | ) 183 | -------------------------------------------------------------------------------- /tests/icalendar_test.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import random 3 | import textwrap 4 | 5 | import icalendar 6 | from freezegun import freeze_time 7 | 8 | from khal.icalendar import new_vevent, split_ics 9 | 10 | from .utils import LOCALE_BERLIN, _get_text, _replace_uid, normalize_component 11 | 12 | 13 | def _get_TZIDs(lines): 14 | """from a list of strings, get all unique strings that start with TZID""" 15 | return sorted(line for line in lines if line.startswith('TZID')) 16 | 17 | 18 | def test_normalize_component(): 19 | assert normalize_component(textwrap.dedent(""" 20 | BEGIN:VEVENT 21 | DTSTART;TZID=Europe/Berlin:20140409T093000 22 | END:VEVENT 23 | """)) != normalize_component(textwrap.dedent(""" 24 | BEGIN:VEVENT 25 | DTSTART;TZID=Oyrope/Berlin:20140409T093000 26 | END:VEVENT 27 | """)) 28 | 29 | 30 | def test_new_vevent(): 31 | with freeze_time('20220702T1400'): 32 | vevent = _replace_uid(new_vevent( 33 | LOCALE_BERLIN, 34 | dt.date(2022, 7, 2), 35 | dt.date(2022, 7, 3), 36 | 'An Event', 37 | allday=True, 38 | repeat='weekly', 39 | )) 40 | assert vevent.to_ical().decode('utf-8') == '\r\n'.join([ 41 | 'BEGIN:VEVENT', 42 | 'SUMMARY:An Event', 43 | 'DTSTART;VALUE=DATE:20220702', 44 | 'DTEND;VALUE=DATE:20220703', 45 | 'DTSTAMP:20220702T140000Z', 46 | 'UID:E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA', 47 | 'RRULE:FREQ=WEEKLY', 48 | 'END:VEVENT', 49 | '' 50 | ]) 51 | 52 | 53 | def test_split_ics(): 54 | cal = _get_text('cal_lots_of_timezones') 55 | vevents = split_ics(cal) 56 | 57 | vevents0 = vevents[0].split('\r\n') 58 | vevents1 = vevents[1].split('\r\n') 59 | 60 | part0 = _get_text('part0').split('\n') 61 | part1 = _get_text('part1').split('\n') 62 | 63 | assert _get_TZIDs(vevents0) == _get_TZIDs(part0) 64 | assert _get_TZIDs(vevents1) == _get_TZIDs(part1) 65 | 66 | assert sorted(vevents0) == sorted(part0) 67 | assert sorted(vevents1) == sorted(part1) 68 | 69 | 70 | def test_split_ics_random_uid(): 71 | random.seed(123) 72 | cal = _get_text('cal_lots_of_timezones') 73 | vevents = split_ics(cal, random_uid=True) 74 | 75 | part0 = _get_text('part0').split('\n') 76 | part1 = _get_text('part1').split('\n') 77 | 78 | for item in icalendar.Calendar.from_ical(vevents[0]).walk(): 79 | if item.name == 'VEVENT': 80 | assert item['UID'] == 'DRF0RGCY89VVDKIV9VPKA1FYEAU2GCFJIBS1' 81 | for item in icalendar.Calendar.from_ical(vevents[1]).walk(): 82 | if item.name == 'VEVENT': 83 | assert item['UID'] == '4Q4CTV74N7UAZ618570X6CLF5QKVV9ZE3YVB' 84 | 85 | # after replacing the UIDs, everything should be as above 86 | vevents0 = vevents[0].replace('DRF0RGCY89VVDKIV9VPKA1FYEAU2GCFJIBS1', '123').split('\r\n') 87 | vevents1 = vevents[1].replace('4Q4CTV74N7UAZ618570X6CLF5QKVV9ZE3YVB', 'abcde').split('\r\n') 88 | 89 | assert _get_TZIDs(vevents0) == _get_TZIDs(part0) 90 | assert _get_TZIDs(vevents1) == _get_TZIDs(part1) 91 | 92 | assert sorted(vevents0) == sorted(part0) 93 | assert sorted(vevents1) == sorted(part1) 94 | 95 | 96 | def test_split_ics_missing_timezone(): 97 | """testing if we detect the missing timezone in splitting""" 98 | cal = _get_text('event_dt_local_missing_tz') 99 | split_ics(cal, random_uid=True, default_timezone=LOCALE_BERLIN['default_timezone']) 100 | 101 | 102 | def test_windows_timezone(caplog): 103 | """Test if a windows tz format works""" 104 | cal = _get_text("tz_windows_format") 105 | split_ics(cal) 106 | assert "Cannot find timezone `Pacific/Auckland`" not in caplog.text 107 | 108 | 109 | def test_split_ics_without_uid(): 110 | cal = _get_text('without_uid') 111 | vevents = split_ics(cal) 112 | assert vevents 113 | vevents2 = split_ics(cal) 114 | assert vevents[0] == vevents2[0] 115 | -------------------------------------------------------------------------------- /tests/ics/cal_d.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | SUMMARY:An Event 6 | DTSTART;VALUE=DATE:20140409 7 | DTEND;VALUE=DATE:20140410 8 | DTSTAMP:20140401T234817Z 9 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 10 | END:VEVENT 11 | END:VCALENDAR 12 | -------------------------------------------------------------------------------- /tests/ics/cal_dt_two_tz.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VTIMEZONE 5 | TZID:Europe/Berlin 6 | BEGIN:DAYLIGHT 7 | DTSTART:20140330T030000 8 | TZNAME:CEST 9 | TZOFFSETFROM:+0100 10 | TZOFFSETTO:+0200 11 | END:DAYLIGHT 12 | BEGIN:STANDARD 13 | DTSTART:20141026T020000 14 | TZNAME:CET 15 | TZOFFSETFROM:+0200 16 | TZOFFSETTO:+0100 17 | END:STANDARD 18 | END:VTIMEZONE 19 | BEGIN:VTIMEZONE 20 | TZID:America/New_York 21 | BEGIN:DAYLIGHT 22 | DTSTART:20140309T030000 23 | TZNAME:EDT 24 | TZOFFSETFROM:-0500 25 | TZOFFSETTO:-0400 26 | END:DAYLIGHT 27 | BEGIN:STANDARD 28 | DTSTART:20141102T010000 29 | TZNAME:EST 30 | TZOFFSETFROM:-0400 31 | TZOFFSETTO:-0500 32 | END:STANDARD 33 | END:VTIMEZONE 34 | BEGIN:VEVENT 35 | SUMMARY:An Event 36 | DTSTART;TZID=Europe/Berlin:20140409T093000 37 | DTEND;TZID=America/New_York:20140409T103000 38 | DTSTAMP:20140401T234817Z 39 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 40 | END:VEVENT 41 | END:VCALENDAR 42 | -------------------------------------------------------------------------------- /tests/ics/cal_lots_of_timezones.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VTIMEZONE 5 | TZID:IndianReunion 6 | BEGIN:STANDARD 7 | TZOFFSETFROM:+034152 8 | TZOFFSETTO:+0400 9 | TZNAME:RET 10 | DTSTART:19110601T000000 11 | RDATE:19110601T000000 12 | END:STANDARD 13 | END:VTIMEZONE 14 | BEGIN:VTIMEZONE 15 | TZID:Will_not_appear 16 | BEGIN:STANDARD 17 | TZOFFSETFROM:+034152 18 | TZOFFSETTO:+0400 19 | TZNAME:RET 20 | DTSTART:19110601T000000 21 | RDATE:19110601T000000 22 | END:STANDARD 23 | END:VTIMEZONE 24 | BEGIN:VTIMEZONE 25 | TZID:Europe_Amsterdam 26 | BEGIN:DAYLIGHT 27 | TZOFFSETFROM:+0100 28 | TZOFFSETTO:+0200 29 | TZNAME:CEST 30 | DTSTART:19810329T020000 31 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 32 | END:DAYLIGHT 33 | BEGIN:STANDARD 34 | TZOFFSETFROM:+0200 35 | TZOFFSETTO:+0100 36 | TZNAME:CET 37 | DTSTART:19961027T030000 38 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 39 | END:STANDARD 40 | END:VTIMEZONE 41 | BEGIN:VTIMEZONE 42 | TZID:Europe_Berlin 43 | BEGIN:STANDARD 44 | DTSTART:20141026T020000 45 | TZNAME:CET 46 | TZOFFSETFROM:+0200 47 | TZOFFSETTO:+0100 48 | RDATE:20151025T020000 49 | END:STANDARD 50 | BEGIN:DAYLIGHT 51 | DTSTART:20140330T030000 52 | RDATE:20150329T030000,20160327T030000 53 | TZNAME:CEST 54 | TZOFFSETFROM:+0100 55 | TZOFFSETTO:+0200 56 | END:DAYLIGHT 57 | END:VTIMEZONE 58 | BEGIN:VTIMEZONE 59 | TZID:America_New_York 60 | BEGIN:STANDARD 61 | DTSTART:20141102T010000 62 | RDATE:20151101T010000 63 | TZNAME:EST 64 | TZOFFSETFROM:-0400 65 | TZOFFSETTO:-0500 66 | END:STANDARD 67 | BEGIN:DAYLIGHT 68 | DTSTART:20140309T030000 69 | RDATE:20150308T030000,20160313T030000 70 | TZNAME:EDT 71 | TZOFFSETFROM:-0500 72 | TZOFFSETTO:-0400 73 | END:DAYLIGHT 74 | END:VTIMEZONE 75 | BEGIN:VTIMEZONE 76 | TZID:America_Bogota 77 | BEGIN:STANDARD 78 | TZOFFSETFROM:-0400 79 | TZOFFSETTO:-0500 80 | TZNAME:COT 81 | DTSTART:19930404T000000 82 | RDATE:19930404T000000 83 | END:STANDARD 84 | END:VTIMEZONE 85 | BEGIN:VTIMEZONE 86 | TZID:Europe_London 87 | BEGIN:DAYLIGHT 88 | TZOFFSETFROM:+0000 89 | TZOFFSETTO:+0100 90 | TZNAME:BST 91 | DTSTART:19810329T010000 92 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 93 | END:DAYLIGHT 94 | BEGIN:STANDARD 95 | TZOFFSETFROM:+0100 96 | TZOFFSETTO:+0000 97 | TZNAME:GMT 98 | DTSTART:19961027T020000 99 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 100 | END:STANDARD 101 | END:VTIMEZONE 102 | BEGIN:VEVENT 103 | SUMMARY:An Event 104 | DTSTART;TZID=Europe_Berlin:20140409T093000 105 | DTEND;TZID=America_New_York:20140409T103000 106 | RDATE;TZID=America_Bogota:20140411T113000,20140413T113000 107 | RDATE;TZID=America_Bogota:20140415T113000 108 | RDATE;TZID=IndianReunion:20140418T113000 109 | RRULE:FREQ=MONTHLY;COUNT=6 110 | DTSTAMP:20140401T234817Z 111 | UID:abcde 112 | END:VEVENT 113 | BEGIN:VEVENT 114 | SUMMARY:An Event 115 | DTSTART;TZID=Europe_London:20140509T193000 116 | DTEND;TZID=Europe_London:20140509T203000 117 | DTSTAMP:20140401T234817Z 118 | UID:123 119 | END:VEVENT 120 | BEGIN:VEVENT 121 | SUMMARY:An Updated Event 122 | DTSTART;TZID=Europe_Berlin:20140409T093000 123 | DTEND;TZID=America_New_York:20140409T103000 124 | DTSTAMP:20140401T234817Z 125 | UID:abcde 126 | RECURRENCE-ID;TZID=Europe_Amsterdam:20140707T070000 127 | END:VEVENT 128 | END:VCALENDAR 129 | -------------------------------------------------------------------------------- /tests/ics/cal_no_dst.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VTIMEZONE 5 | TZID:America/Bogota 6 | BEGIN:STANDARD 7 | DTSTART:19930206T230000 8 | TZNAME:COT 9 | TZOFFSETFROM:-0400 10 | TZOFFSETTO:-0500 11 | END:STANDARD 12 | END:VTIMEZONE 13 | BEGIN:VEVENT 14 | SUMMARY:An Event 15 | DTSTART;TZID=America/Bogota:20140409T093000 16 | DTEND;TZID=America/Bogota:20140409T103000 17 | DTSTAMP:20140401T234817Z 18 | UID:event_no_dst 19 | END:VEVENT 20 | END:VCALENDAR 21 | -------------------------------------------------------------------------------- /tests/ics/event_d.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:An Event 3 | DTSTART;VALUE=DATE:20140409 4 | DTEND;VALUE=DATE:20140410 5 | DTSTAMP:20140401T234817Z 6 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 7 | END:VEVENT 8 | -------------------------------------------------------------------------------- /tests/ics/event_d_15.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:An Event 3 | DTSTART;VALUE=DATE:20150409 4 | DTEND;VALUE=DATE:20150410 5 | DTSTAMP;VALUE=DATE-TIME:20140401T234817Z 6 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 7 | END:VEVENT 8 | -------------------------------------------------------------------------------- /tests/ics/event_d_long.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:Another Event 3 | DTSTART;VALUE=DATE:20140409 4 | DTEND;VALUE=DATE:20140412 5 | DTSTAMP;VALUE=DATE-TIME:20140401T234817Z 6 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 7 | END:VEVENT 8 | -------------------------------------------------------------------------------- /tests/ics/event_d_no_value.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:Another Event 3 | DTSTART:20140409 4 | DTEND:20140410 5 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 6 | END:VEVENT 7 | -------------------------------------------------------------------------------- /tests/ics/event_d_rdate.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | UID:d_rdate 5 | SUMMARY:this events last for a day and recurrs on four subsequent days 6 | DTSTART;VALUE=DATE:20150812 7 | DTEND;VALUE=DATE:20150813 8 | RDATE;VALUE=DATE:20150812,20150813,20150814,20150815 9 | END:VEVENT 10 | END:VCALENDAR 11 | -------------------------------------------------------------------------------- /tests/ics/event_d_rr.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:Another Event 3 | DTSTART;VALUE=DATE:20140409 4 | DTEND;VALUE=DATE:20140410 5 | RRULE:FREQ=DAILY;COUNT=10 6 | DTSTAMP;VALUE=DATE-TIME:20140401T234817Z 7 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 8 | END:VEVENT 9 | -------------------------------------------------------------------------------- /tests/ics/event_d_same_start_end.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:Another Event 3 | DTSTART;VALUE=DATE:20140409 4 | DTEND;VALUE=DATE:20140409 5 | DTSTAMP;VALUE=DATE-TIME:20140401T234817Z 6 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 7 | END:VEVENT 8 | -------------------------------------------------------------------------------- /tests/ics/event_dt_description.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:An Event 3 | DESCRIPTION;LANG=en_US:Hey, \n\nJust setting aside some dedicated time to talk about redacted. 4 | DTSTART:20230409T093000 5 | DTEND:20230409T103000 6 | DTSTAMP:20230401T234817Z 7 | UID:floating_with_description_and_parameter 8 | END:VEVENT 9 | -------------------------------------------------------------------------------- /tests/ics/event_dt_duration.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:An Event 3 | DTSTART;TZID=Europe/Berlin:20140409T093000 4 | DURATION:PT1H0M0S 5 | DTSTAMP:20140401T234817Z 6 | ORGANIZER;CN=Frank Nord:mailto:frank@nord.tld 7 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 8 | END:VEVENT 9 | -------------------------------------------------------------------------------- /tests/ics/event_dt_floating.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:An Event 3 | DESCRIPTION:Search for me 4 | DTSTART:20140409T093000 5 | DTEND:20140409T103000 6 | DTSTAMP:20140401T234817Z 7 | UID:floating1234567890 8 | END:VEVENT 9 | -------------------------------------------------------------------------------- /tests/ics/event_dt_local_missing_tz.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | SUMMARY:An Event 6 | DTSTART;TZID=FOO;VALUE=DATE-TIME:20140409T093000 7 | DTEND;TZID=FOO;VALUE=DATE-TIME:20140409T103000 8 | DTSTAMP;VALUE=DATE-TIME:20140401T234817Z 9 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 10 | END:VEVENT 11 | END:VCALENDAR 12 | -------------------------------------------------------------------------------- /tests/ics/event_dt_london.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | SUMMARY:An Event 6 | DTSTART;TZID=Europe/London:20140409T140000 7 | DTEND;TZID=Europe/London:20140409T190000 8 | DTSTAMP:20140401T234817Z 9 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 10 | END:VEVENT 11 | END:VCALENDAR 12 | -------------------------------------------------------------------------------- /tests/ics/event_dt_long.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:An Event 3 | DTSTART;VALUE=DATE-TIME:20140409T093000 4 | DTEND;VALUE=DATE-TIME:20140412T103000 5 | DTSTAMP;VALUE=DATE-TIME:20140401T234817Z 6 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 7 | END:VEVENT 8 | -------------------------------------------------------------------------------- /tests/ics/event_dt_mixed_awareness.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:ownCloud Calendar 0.7.3 4 | BEGIN:VEVENT 5 | SUMMARY:Termin:Junghackertag 6 | URL:http://wiki.hamburg.ccc.de/Termin:Junghackertag 7 | UID:http://wiki.hamburg.ccc.de/Termin:Junghackertag 8 | DTSTART:20150530T120000 9 | DTEND;TZID=Europe/Berlin:20150530T160000Z 10 | DESCRIPTION:Junghackertag 11 | DTSTAMP:20150526T182050 12 | SEQUENCE:10172 13 | END:VEVENT 14 | BEGIN:VEVENT 15 | SUMMARY:An event with mixed tz-aware/tz-naive dates 16 | UID:S9HZETRQne 17 | DTSTART;TZID=Europe/Berlin:20150602T120000Z 18 | DTEND:20150602T160000 19 | DESCRIPTION:Junghackertag 20 | DTSTAMP:20150626T182050 21 | SEQUENCE:10172 22 | END:VEVENT 23 | END:VCALENDAR 24 | -------------------------------------------------------------------------------- /tests/ics/event_dt_multi_recuid_no_master.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | UID:abe304dc-f7b1-4f2b-997d-5c8bcfaa7e0b 5 | SUMMARY:Arbeit 6 | RRULE:FREQ=WEEKLY;UNTIL=20160925T053000 7 | RECURRENCE-ID:20140714T053000 8 | DTSTART:20140630T073000 9 | DURATION:PT4H30M 10 | END:VEVENT 11 | BEGIN:VEVENT 12 | UID:abe304dc-f7b1-4f2b-997d-5c8bcfaa7e0b 13 | SUMMARY:Arbeit 14 | RECURRENCE-ID:20140707T053000 15 | RRULE:FREQ=WEEKLY;UNTIL=20160925T053000 16 | DTSTART:20140707T083000 17 | DTEND:20140707T120000 18 | END:VEVENT 19 | END:VCALENDAR 20 | -------------------------------------------------------------------------------- /tests/ics/event_dt_multi_uid.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | SUMMARY:Test original 6 | DTSTART:20171228T190000 7 | DTEND:20171228T200000 8 | DTSTAMP:20171228T190731Z 9 | UID:abc 10 | SEQUENCE:0 11 | END:VEVENT 12 | BEGIN:VEVENT 13 | SUMMARY:Test next 14 | DTSTART:20180104T220000 15 | DTEND:20180104T230000 16 | UID:def 17 | END:VEVENT 18 | BEGIN:VEVENT 19 | SUMMARY:Test last 20 | DTSTART:20180104T220000 21 | DTEND:20180104T230000 22 | UID:def 23 | END:VEVENT 24 | END:VCALENDAR 25 | -------------------------------------------------------------------------------- /tests/ics/event_dt_no_end.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | BEGIN:VEVENT 3 | SUMMARY:Test 4 | DTSTART:20160116T070000Z 5 | UID:nodtend 6 | END:VEVENT 7 | END:VCALENDAR 8 | -------------------------------------------------------------------------------- /tests/ics/event_dt_partstat.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | SUMMARY:An Event 6 | DTSTART;TZID=Europe/Berlin:20140409T093000 7 | DTEND;TZID=Europe/Berlin:20140409T103000 8 | DTSTAMP:20140401T234817Z 9 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 10 | ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;DELEGATED-FROM= 11 | "mailto:iamboss@example.com";CN=Henry Cabot:mailto:hcabot@ 12 | example.com 13 | ATTENDEE;ROLE=NON-PARTICIPANT;PARTSTAT=DELEGATED;DELEGATED-TO= 14 | "mailto:hcabot@example.com";CN=The Big Cheese:mailto:iamboss 15 | @example.com 16 | ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Jane Doe 17 | :mailto:jdoe@example.com 18 | ATTENDEE;PARTSTAT=ACCEPTED:mailto:jqpublic@example.com 19 | ATTENDEE;PARTSTAT=DECLINED:mailto:another@example.com 20 | ATTENDEE;PARTSTAT=TENTATIVE:mailto:tent@example.com 21 | END:VEVENT 22 | END:VCALENDAR 23 | -------------------------------------------------------------------------------- /tests/ics/event_dt_rd.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:An Event 3 | DTSTART;VALUE=DATE-TIME:20140409T093000 4 | DTEND;VALUE=DATE-TIME:20140409T103000 5 | RDATE;VALUE=DATE-TIME:20140410T093000 6 | DTSTAMP;VALUE=DATE-TIME:20140401T234817Z 7 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 8 | END:VEVENT 9 | -------------------------------------------------------------------------------- /tests/ics/event_dt_recuid_no_master.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | DTSTART:20170329T160000 5 | DTEND:20170329T162500 6 | DTSTAMP:20170322T171834Z 7 | RECURRENCE-ID:20170329T160000 8 | SUMMARY:Infrastructure Planning 9 | UID:406336 10 | END:VEVENT 11 | END:VCALENDAR 12 | -------------------------------------------------------------------------------- /tests/ics/event_dt_rr.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:An Event 3 | DTSTART;VALUE=DATE-TIME:20140409T093000 4 | DTEND;VALUE=DATE-TIME:20140409T103000 5 | RRULE:FREQ=DAILY;COUNT=10 6 | DTSTAMP;VALUE=DATE-TIME:20140401T234817Z 7 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 8 | END:VEVENT 9 | -------------------------------------------------------------------------------- /tests/ics/event_dt_rrule_invalid_until.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | SUMMARY:This event is invalid but should still be supported by khal 6 | DTSTART;VALUE=DATE:20071201 7 | DTEND;VALUE=DATE:20071202 8 | UID:invalidRRULEUNTIL 9 | RRULE:FREQ=MONTHLY;UNTIL=20080202T000000Z 10 | END:VEVENT 11 | END:VCALENDAR 12 | -------------------------------------------------------------------------------- /tests/ics/event_dt_rrule_invalid_until2.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | SUMMARY:This event is invalid but should still be supported by khal 6 | DTSTART;TZID=Europe/Berlin;VALUE=DATE-TIME:20140409T093000 7 | DTEND;TZID=Europe/Berlin;VALUE=DATE-TIME:20140409T103000 8 | UID:invalidRRULEUNTIL2 9 | RRULE:FREQ=WEEKLY;UNTIL=20141205 10 | END:VEVENT 11 | END:VCALENDAR 12 | -------------------------------------------------------------------------------- /tests/ics/event_dt_rrule_until_before_start.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:Stop recurring before we start 3 | DTSTART;VALUE=DATE-TIME:19801109T193000 4 | DTEND;VALUE=DATE-TIME:19801109T203000 5 | RRULE:FREQ=DAILY;UNTIL=19701119T203000Z 6 | DTSTAMP;VALUE=DATE-TIME:20140401T234817Z 7 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOV 8 | END:VEVENT 9 | -------------------------------------------------------------------------------- /tests/ics/event_dt_simple.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | SUMMARY:An Event 6 | DTSTART;TZID=Europe/Berlin:20140409T093000 7 | DTEND;TZID=Europe/Berlin:20140409T103000 8 | DTSTAMP:20140401T234817Z 9 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 10 | END:VEVENT 11 | END:VCALENDAR 12 | -------------------------------------------------------------------------------- /tests/ics/event_dt_simple_inkl_vtimezone.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VTIMEZONE 5 | TZID:Europe/Berlin 6 | BEGIN:STANDARD 7 | DTSTART:20141026T020000 8 | TZNAME:CET 9 | TZOFFSETFROM:+0200 10 | TZOFFSETTO:+0100 11 | END:STANDARD 12 | BEGIN:DAYLIGHT 13 | DTSTART:20140330T030000 14 | TZNAME:CEST 15 | TZOFFSETFROM:+0100 16 | TZOFFSETTO:+0200 17 | END:DAYLIGHT 18 | END:VTIMEZONE 19 | BEGIN:VEVENT 20 | SUMMARY:An Event 21 | DTSTART;TZID=Europe/Berlin:20140409T093000 22 | DTEND;TZID=Europe/Berlin:20140409T103000 23 | DTSTAMP:20140401T234817Z 24 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 25 | END:VEVENT 26 | END:VCALENDAR 27 | -------------------------------------------------------------------------------- /tests/ics/event_dt_simple_nocat.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | SUMMARY:A not so simple Event 6 | DESCRIPTION:Everything has changed 7 | LOCATION:anywhere 8 | DTSTART;TZID=Europe/Berlin:20140409T093000 9 | DTEND;TZID=Europe/Berlin:20140409T103000 10 | DTSTAMP:20140401T234817Z 11 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 12 | END:VEVENT 13 | END:VCALENDAR 14 | -------------------------------------------------------------------------------- /tests/ics/event_dt_simple_updated.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | SUMMARY:A not so simple Event 6 | DESCRIPTION:Everything has changed 7 | LOCATION:anywhere 8 | CATEGORIES:meeting 9 | DTSTART;TZID=Europe/Berlin:20140409T093000 10 | DTEND;TZID=Europe/Berlin:20140409T103000 11 | DTSTAMP:20140401T234817Z 12 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 13 | END:VEVENT 14 | END:VCALENDAR 15 | -------------------------------------------------------------------------------- /tests/ics/event_dt_simple_zulu.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | SUMMARY:An Event 6 | DTSTART:20140409T093000Z 7 | DTEND:20140409T103000Z 8 | DTSTAMP:20140401T234817Z 9 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 10 | END:VEVENT 11 | END:VCALENDAR 12 | -------------------------------------------------------------------------------- /tests/ics/event_dt_status_confirmed.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | SUMMARY:An Event 6 | DTSTART;TZID=Europe/Berlin:20140409T093000 7 | DTEND;TZID=Europe/Berlin:20140409T103000 8 | DTSTAMP:20140401T234817Z 9 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 10 | STATUS:CONFIRMED 11 | END:VEVENT 12 | END:VCALENDAR 13 | -------------------------------------------------------------------------------- /tests/ics/event_dt_two_rd.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:An Event 3 | DTSTART;VALUE=DATE-TIME:20140409T093000 4 | DTEND;VALUE=DATE-TIME:20140409T103000 5 | RDATE;VALUE=DATE-TIME:20140410T093000 6 | RDATE;VALUE=DATE-TIME:20140411T093000,20140412T093000 7 | DTSTAMP;VALUE=DATE-TIME:20140401T234817Z 8 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 9 | END:VEVENT 10 | -------------------------------------------------------------------------------- /tests/ics/event_dt_two_tz.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | SUMMARY:An Event 3 | DTSTART;TZID=Europe/Berlin:20140409T093000 4 | DTEND;TZID=America/New_York:20140409T103000 5 | DTSTAMP:20140401T234817Z 6 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 7 | END:VEVENT 8 | -------------------------------------------------------------------------------- /tests/ics/event_dt_url.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | SUMMARY:An Event 6 | URL:https://github.com/pimutils/khal 7 | DTSTART;TZID=Europe/Berlin:20140409T093000 8 | DTEND;TZID=Europe/Berlin:20140409T103000 9 | DTSTAMP:20140401T234817Z 10 | UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU 11 | END:VEVENT 12 | END:VCALENDAR 13 | -------------------------------------------------------------------------------- /tests/ics/event_dtr_exdatez.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | UID:event_dtr_exdatez 5 | SUMMARY:event_dtr_exdatez 6 | RRULE:FREQ=WEEKLY;UNTIL=20140725T053000Z 7 | EXDATE:20140721T053000Z 8 | DTSTART;TZID=Europe/Berlin:20140630T073000 9 | DURATION:PT4H31M 10 | DESCRIPTION:An recurring datetime event with excluded dates in Zulu time 11 | END:VEVENT 12 | END:VCALENDAR 13 | -------------------------------------------------------------------------------- /tests/ics/event_dtr_no_tz_exdatez.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | UID:5624b646-ba12-4dbd-ba9f-8d66319b0776 5 | RRULE:FREQ=MONTHLY;COUNT=6 6 | SUMMARY:Conf Call 7 | DESCRIPTION:event with not understood timezone for dtstart and zulu time form exdate 8 | DTSTART;TZID="LOLOLOL":20120403T100000 9 | DTEND;TZID="LOLOLOL":20120403T103000 10 | EXDATE:20120603T080000Z 11 | END:VEVENT 12 | END:VCALENDAR 13 | -------------------------------------------------------------------------------- /tests/ics/event_dtr_notz_untilz.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | UID:273A4F5B7 5 | RRULE:FREQ=WEEKLY;UNTIL=20121101T035959Z;INTERVAL=2;BYDAY=TH 6 | SUMMARY:Storage Meeting 7 | DESCRIPTION:Meeting every other week 8 | DTSTART;TZID="GMT-05.00/-04.00":20120726T130000 9 | DTEND;TZID="GMT-05.00/-04.00":20120726T140000 10 | END:VEVENT 11 | END:VCALENDAR 12 | -------------------------------------------------------------------------------- /tests/ics/event_invalid_exdate.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | BEGIN:VEVENT 3 | UID:00D6A925-90F7-4663-AE13-E8BD1655EF77 4 | SUMMARY:Soccer 5 | RRULE:FREQ=WEEKLY;UNTIL=20111206T205000Z 6 | EXDATE:20110924T195000Z 7 | EXDATE:20111126T205000Z 8 | EXDATE:20111008T195000Z 9 | DTSTART;TZID=America/New_York:20111112T155000 10 | DTEND;TZID=America/New_York:20111112T170000 11 | END:VEVENT 12 | END:VCALENDAR 13 | -------------------------------------------------------------------------------- /tests/ics/event_no_dst.ics: -------------------------------------------------------------------------------- 1 | 2 | BEGIN:VEVENT 3 | SUMMARY:An Event 4 | DTSTART;TZID=America/Bogota:20140409T093000 5 | DTEND;TZID=America/Bogota:20140409T103000 6 | DTSTAMP:20140401T234817Z 7 | UID:event_no_dst 8 | END:VEVENT 9 | -------------------------------------------------------------------------------- /tests/ics/event_r_past.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | DTSTART;VALUE=DATE:19650423 5 | DURATION:P1D 6 | UID:date_event_past 7 | RRULE:FREQ=YEARLY 8 | SUMMARY:Dummy's Birthday (1965) 9 | END:VEVENT 10 | END:VCALENDAR 11 | -------------------------------------------------------------------------------- /tests/ics/event_rdate_no_value.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | LOCATION:Mumble channel 5 | RDATE;TZID=Europe/Berlin:20150831T113000 6 | RDATE;TZID=Europe/Berlin:20150914T113000 7 | RRULE:FREQ=WEEKLY;UNTIL=20161231;INTERVAL=2;BYDAY=MO 8 | DTSTART;TZID=Europe/Berlin:20160125T113000 9 | DTEND;TZID=Europe/Berlin:20160125T123000 10 | UID:rdatenovalue 11 | SUMMARY:An event with rdates which have no VALUE 12 | END:VEVENT 13 | END:VCALENDAR 14 | -------------------------------------------------------------------------------- /tests/ics/event_rrule_no_occurence.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | DTSTART;VALUE=DATE-TIME:20210101T093000Z 3 | DTEND;VALUE=DATE-TIME:20210101T094500Z 4 | RRULE:FREQ=WEEKLY;UNTIL=20210104T090000Z;BYDAY=TU 5 | DTSTAMP;VALUE=DATE-TIME:20210101T000000Z 6 | SUMMARY:This event will never happen 7 | UID:OW1M4APLKA2WTEOJLIGRHVSEHJHDZLLVXI2N 8 | END:VEVENT 9 | -------------------------------------------------------------------------------- /tests/ics/event_rrule_recuid.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | UID:event_rrule_recurrence_id 6 | SUMMARY:Arbeit 7 | RRULE:FREQ=WEEKLY;UNTIL=20140806T060000Z 8 | DTSTART;TZID=Europe/Berlin:20140630T070000 9 | DTEND;TZID=Europe/Berlin:20140630T120000 10 | END:VEVENT 11 | BEGIN:VEVENT 12 | UID:event_rrule_recurrence_id 13 | SUMMARY:Arbeit 14 | RECURRENCE-ID:20140707T050000Z 15 | DTSTART;TZID=Europe/Berlin:20140707T090000 16 | DTEND;TZID=Europe/Berlin:20140707T140000 17 | END:VEVENT 18 | END:VCALENDAR 19 | -------------------------------------------------------------------------------- /tests/ics/event_rrule_recuid_cancelled.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | UID:event_rrule_recurrence_id 6 | SUMMARY:Arbeit 7 | RRULE:FREQ=WEEKLY;UNTIL=20140806T060000Z 8 | DTSTART;TZID=Europe/Berlin:20140630T070000 9 | DTEND;TZID=Europe/Berlin:20140630T120000 10 | END:VEVENT 11 | BEGIN:VEVENT 12 | UID:event_rrule_recurrence_id 13 | SUMMARY:Arbeit 14 | RECURRENCE-ID:20140707T050000Z 15 | DTSTART;TZID=Europe/Berlin:20140707T090000 16 | DTEND;TZID=Europe/Berlin:20140707T140000 17 | END:VEVENT 18 | BEGIN:VEVENT 19 | UID:event_rrule_recurrence_id 20 | SUMMARY:Arbeit 21 | RECURRENCE-ID:20140714T050000Z 22 | DTSTART;TZID=Europe/Berlin:20140714T070000 23 | DTEND;TZID=Europe/Berlin:20140714T120000 24 | STATUS:CANCELLED 25 | END:VEVENT 26 | END:VCALENDAR 27 | -------------------------------------------------------------------------------- /tests/ics/event_rrule_recuid_invalid_tzid.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | UID:event_rrule_recurrence_id 6 | SUMMARY:Arbeit 7 | RRULE:FREQ=WEEKLY;UNTIL=20140806T060000Z 8 | DTSTART;TZID=foo:20140630T070000 9 | DTEND;TZID=foo:20140630T120000 10 | END:VEVENT 11 | BEGIN:VEVENT 12 | UID:event_rrule_recurrence_id 13 | SUMMARY:Arbeit 14 | RECURRENCE-ID;TZID=foo:20140707T070000 15 | DTSTART;TZID=foo:20140707T090000 16 | DTEND;TZID=foo:20140707T140000 17 | END:VEVENT 18 | END:VCALENDAR 19 | -------------------------------------------------------------------------------- /tests/ics/event_rrule_recuid_update.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | BEGIN:VEVENT 3 | UID:event_rrule_recurrence_id 4 | SUMMARY:ArbeitXXX 5 | RRULE:FREQ=WEEKLY;UNTIL=20140806T060000Z 6 | DTSTART;TZID=Europe/Berlin:20140630T070000 7 | DTEND;TZID=Europe/Berlin:20140630T120000 8 | EXDATE;TZID=Europe/Berlin:20140714T070000 9 | END:VEVENT 10 | END:VCALENDAR 11 | -------------------------------------------------------------------------------- /tests/ics/invalid_tzoffset.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VTIMEZONE 4 | TZID:Europe/Berlin 5 | BEGIN:STANDARD 6 | DTSTART:18930401T000000 7 | RDATE:18930401T000000 8 | TZNAME:CEST 9 | TZOFFSETFROM:+5328 10 | TZOFFSETTO:+0100 11 | END:STANDARD 12 | BEGIN:DAYLIGHT 13 | DTSTART:19810329T020000 14 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 15 | TZNAME:CEST 16 | TZOFFSETFROM:+0100 17 | TZOFFSETTO:+0200 18 | END:DAYLIGHT 19 | BEGIN:STANDARD 20 | DTSTART:19961027T030000 21 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 22 | TZNAME:CET 23 | TZOFFSETFROM:+0200 24 | TZOFFSETTO:+0100 25 | END:STANDARD 26 | END:VTIMEZONE 27 | BEGIN:VEVENT 28 | CREATED:20121120T195219Z 29 | UID:1234567890DC8-468D-B140-D2B41B1013E9 30 | DTEND;TZID=Europe/Berlin:20121202T093000 31 | SUMMARY:Some event 32 | DTSTART;TZID=Europe/Berlin:20121202T080000 33 | DTSTAMP:20121120T195239Z 34 | END:VEVENT 35 | END:VCALENDAR 36 | -------------------------------------------------------------------------------- /tests/ics/mult_uids_and_recuid_no_order.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | UID:event_rrule_recurrence_id 5 | SUMMARY:Arbeit 6 | RECURRENCE-ID:20140707T050000Z 7 | DTSTART;TZID=Europe/Berlin:20140707T090000 8 | DTEND;TZID=Europe/Berlin:20140707T140000 9 | END:VEVENT 10 | BEGIN:VEVENT 11 | UID:date123 12 | DTSTART;VALUE=DATE:20130301 13 | DTEND;VALUE=DATE:20130302 14 | RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=6 15 | SUMMARY:Event with Ümläutß 16 | END:VEVENT 17 | BEGIN:VEVENT 18 | UID:event_rrule_recurrence_id 19 | SUMMARY:Arbeit 20 | RRULE:FREQ=WEEKLY;UNTIL=20140806T060000Z 21 | DTSTART;TZID=Europe/Berlin:20140630T070000 22 | DTEND;TZID=Europe/Berlin:20140630T120000 23 | END:VEVENT 24 | END:VCALENDAR 25 | -------------------------------------------------------------------------------- /tests/ics/non_dst_error.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//NS Internationaal B.V.//sales-v2//en 4 | BEGIN:VTIMEZONE 5 | TZID:Europe/Brussels 6 | LAST-MODIFIED:20221105T024525Z 7 | TZURL:http://www.tzurl.org/zoneinfo/Europe/Brussels 8 | X-LIC-LOCATION:Europe/Brussels 9 | X-PROLEPTIC-TZNAME:LMT 10 | BEGIN:STANDARD 11 | TZNAME:BMT 12 | TZOFFSETFROM:+0017 13 | TZOFFSETTO:+0017 14 | DTSTART:18800101T000000 15 | END:STANDARD 16 | BEGIN:STANDARD 17 | TZNAME:WET 18 | TZOFFSETFROM:+0017 19 | TZOFFSETTO:+0000 20 | DTSTART:18920501T001730 21 | END:STANDARD 22 | BEGIN:STANDARD 23 | TZNAME:CET 24 | TZOFFSETFROM:+0000 25 | TZOFFSETTO:+0100 26 | DTSTART:19141108T000000 27 | END:STANDARD 28 | BEGIN:STANDARD 29 | TZNAME:CET 30 | TZOFFSETFROM:+0200 31 | TZOFFSETTO:+0100 32 | DTSTART:19161001T010000 33 | RDATE:19421102T030000 34 | RDATE:19431004T030000 35 | RDATE:19440917T030000 36 | RDATE:19450916T030000 37 | RDATE:19461007T030000 38 | RDATE:19770925T030000 39 | RDATE:19781001T030000 40 | END:STANDARD 41 | BEGIN:STANDARD 42 | TZNAME:CET 43 | TZOFFSETFROM:+0200 44 | TZOFFSETTO:+0100 45 | DTSTART:19170917T030000 46 | RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYDAY=3MO;BYMONTH=9 47 | END:STANDARD 48 | BEGIN:STANDARD 49 | TZNAME:WET 50 | TZOFFSETFROM:+0100 51 | TZOFFSETTO:+0000 52 | DTSTART:19181111T120000 53 | RDATE:19191005T000000 54 | RDATE:19201024T000000 55 | RDATE:19211026T000000 56 | RDATE:19391119T030000 57 | END:STANDARD 58 | BEGIN:STANDARD 59 | TZNAME:WET 60 | TZOFFSETFROM:+0100 61 | TZOFFSETTO:+0000 62 | DTSTART:19221008T000000 63 | RRULE:FREQ=YEARLY;UNTIL=19271001T230000Z;BYDAY=SU;BYMONTHDAY=2,3,4,5,6,7,8; 64 | BYMONTH=10 65 | END:STANDARD 66 | BEGIN:STANDARD 67 | TZNAME:WET 68 | TZOFFSETFROM:+0100 69 | TZOFFSETTO:+0000 70 | DTSTART:19281007T030000 71 | RRULE:FREQ=YEARLY;UNTIL=19381002T020000Z;BYDAY=SU;BYMONTHDAY=2,3,4,5,6,7,8; 72 | BYMONTH=10 73 | END:STANDARD 74 | BEGIN:STANDARD 75 | TZNAME:CET 76 | TZOFFSETFROM:+0100 77 | TZOFFSETTO:+0100 78 | DTSTART:19770101T000000 79 | END:STANDARD 80 | BEGIN:STANDARD 81 | TZNAME:CET 82 | TZOFFSETFROM:+0200 83 | TZOFFSETTO:+0100 84 | DTSTART:19790930T030000 85 | RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYDAY=-1SU;BYMONTH=9 86 | END:STANDARD 87 | BEGIN:STANDARD 88 | TZNAME:CET 89 | TZOFFSETFROM:+0200 90 | TZOFFSETTO:+0100 91 | DTSTART:19961027T030000 92 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 93 | END:STANDARD 94 | BEGIN:DAYLIGHT 95 | TZNAME:CEST 96 | TZOFFSETFROM:+0100 97 | TZOFFSETTO:+0200 98 | DTSTART:19160501T000000 99 | RDATE:19400520T030000 100 | RDATE:19430329T020000 101 | RDATE:19440403T020000 102 | RDATE:19450402T020000 103 | RDATE:19460519T020000 104 | END:DAYLIGHT 105 | BEGIN:DAYLIGHT 106 | TZNAME:CEST 107 | TZOFFSETFROM:+0100 108 | TZOFFSETTO:+0200 109 | DTSTART:19170416T020000 110 | RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYDAY=3MO;BYMONTH=4 111 | END:DAYLIGHT 112 | BEGIN:DAYLIGHT 113 | TZNAME:WEST 114 | TZOFFSETFROM:+0000 115 | TZOFFSETTO:+0100 116 | DTSTART:19190301T230000 117 | RDATE:19200214T230000 118 | RDATE:19210314T230000 119 | RDATE:19220325T230000 120 | RDATE:19230421T230000 121 | RDATE:19240329T230000 122 | RDATE:19250404T230000 123 | RDATE:19260417T230000 124 | RDATE:19270409T230000 125 | RDATE:19280414T230000 126 | RDATE:19290421T020000 127 | RDATE:19300413T020000 128 | RDATE:19310419T020000 129 | RDATE:19320403T020000 130 | RDATE:19330326T020000 131 | RDATE:19340408T020000 132 | RDATE:19350331T020000 133 | RDATE:19360419T020000 134 | RDATE:19370404T020000 135 | RDATE:19380327T020000 136 | RDATE:19390416T020000 137 | RDATE:19400225T020000 138 | END:DAYLIGHT 139 | BEGIN:DAYLIGHT 140 | TZNAME:CEST 141 | TZOFFSETFROM:+0200 142 | TZOFFSETTO:+0200 143 | DTSTART:19440903T000000 144 | END:DAYLIGHT 145 | BEGIN:DAYLIGHT 146 | TZNAME:CEST 147 | TZOFFSETFROM:+0100 148 | TZOFFSETTO:+0200 149 | DTSTART:19770403T020000 150 | RRULE:FREQ=YEARLY;UNTIL=19800406T010000Z;BYDAY=1SU;BYMONTH=4 151 | END:DAYLIGHT 152 | BEGIN:DAYLIGHT 153 | TZNAME:CEST 154 | TZOFFSETFROM:+0100 155 | TZOFFSETTO:+0200 156 | DTSTART:19810329T020000 157 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 158 | END:DAYLIGHT 159 | END:VTIMEZONE 160 | BEGIN:VEVENT 161 | UID:4e3e3186-28c6-42d4-81cf-eb4f56d587f1 162 | DTSTAMP:20230122T142045Z 163 | SUMMARY:Trip from Utrecht to Bruxelles Central/Brussel Centraal 164 | DESCRIPTION:Trip from Utrecht to Bruxelles Central/Brussel Centraal 165 | LOCATION:Utrecht 166 | URL:https://www.nsinternational.com/en/traintickets-v3/ 167 | DTSTART;TZID=Europe/Brussels:20230203T151500 168 | DTEND;TZID=Europe/Brussels:20230203T180600 169 | BEGIN:VALARM 170 | ACTION:DISPLAY 171 | TRIGGER;RELATED=START:PT-2H 172 | END:VALARM 173 | END:VEVENT 174 | BEGIN:VEVENT 175 | UID:28d25a55-84cd-4564-910b-088066fd5244 176 | DTSTAMP:20230122T142045Z 177 | SUMMARY:Trip from Bruxelles Central/Brussel Centraal to Utrecht 178 | DESCRIPTION:Trip from Bruxelles Central/Brussel Centraal to Utrecht 179 | LOCATION:Bruxelles Central/Brussel Centraal 180 | URL:https://www.nsinternational.com/en/traintickets-v3/ 181 | DTSTART;TZID=Europe/Brussels:20230206T095000 182 | DTEND;TZID=Europe/Brussels:20230206T125000 183 | BEGIN:VALARM 184 | ACTION:DISPLAY 185 | TRIGGER;RELATED=START:PT-2H 186 | END:VALARM 187 | END:VEVENT 188 | END:VCALENDAR 189 | -------------------------------------------------------------------------------- /tests/ics/part0.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VTIMEZONE 5 | TZID:Europe_London 6 | BEGIN:DAYLIGHT 7 | TZOFFSETFROM:+0000 8 | TZOFFSETTO:+0100 9 | TZNAME:BST 10 | DTSTART:19810329T010000 11 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 12 | END:DAYLIGHT 13 | BEGIN:STANDARD 14 | TZOFFSETFROM:+0100 15 | TZOFFSETTO:+0000 16 | TZNAME:GMT 17 | DTSTART:19961027T020000 18 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 19 | END:STANDARD 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | SUMMARY:An Event 23 | DTSTART;TZID=Europe_London:20140509T193000 24 | DTEND;TZID=Europe_London:20140509T203000 25 | DTSTAMP:20140401T234817Z 26 | UID:123 27 | END:VEVENT 28 | END:VCALENDAR 29 | -------------------------------------------------------------------------------- /tests/ics/part1.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VTIMEZONE 5 | TZID:IndianReunion 6 | BEGIN:STANDARD 7 | TZOFFSETFROM:+034152 8 | TZOFFSETTO:+0400 9 | TZNAME:RET 10 | DTSTART:19110601T000000 11 | RDATE:19110601T000000 12 | END:STANDARD 13 | END:VTIMEZONE 14 | BEGIN:VTIMEZONE 15 | TZID:Europe_Amsterdam 16 | BEGIN:DAYLIGHT 17 | TZOFFSETFROM:+0100 18 | TZOFFSETTO:+0200 19 | TZNAME:CEST 20 | DTSTART:19810329T020000 21 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 22 | END:DAYLIGHT 23 | BEGIN:STANDARD 24 | TZOFFSETFROM:+0200 25 | TZOFFSETTO:+0100 26 | TZNAME:CET 27 | DTSTART:19961027T030000 28 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 29 | END:STANDARD 30 | END:VTIMEZONE 31 | BEGIN:VTIMEZONE 32 | TZID:Europe_Berlin 33 | BEGIN:STANDARD 34 | DTSTART:20141026T020000 35 | TZNAME:CET 36 | TZOFFSETFROM:+0200 37 | TZOFFSETTO:+0100 38 | RDATE:20151025T020000 39 | END:STANDARD 40 | BEGIN:DAYLIGHT 41 | DTSTART:20140330T030000 42 | RDATE:20150329T030000,20160327T030000 43 | TZNAME:CEST 44 | TZOFFSETFROM:+0100 45 | TZOFFSETTO:+0200 46 | END:DAYLIGHT 47 | END:VTIMEZONE 48 | BEGIN:VTIMEZONE 49 | TZID:America_New_York 50 | BEGIN:STANDARD 51 | DTSTART:20141102T010000 52 | RDATE:20151101T010000 53 | TZNAME:EST 54 | TZOFFSETFROM:-0400 55 | TZOFFSETTO:-0500 56 | END:STANDARD 57 | BEGIN:DAYLIGHT 58 | DTSTART:20140309T030000 59 | RDATE:20150308T030000,20160313T030000 60 | TZNAME:EDT 61 | TZOFFSETFROM:-0500 62 | TZOFFSETTO:-0400 63 | END:DAYLIGHT 64 | END:VTIMEZONE 65 | BEGIN:VTIMEZONE 66 | TZID:America_Bogota 67 | BEGIN:STANDARD 68 | TZOFFSETFROM:-0400 69 | TZOFFSETTO:-0500 70 | TZNAME:COT 71 | DTSTART:19930404T000000 72 | RDATE:19930404T000000 73 | END:STANDARD 74 | END:VTIMEZONE 75 | BEGIN:VEVENT 76 | SUMMARY:An Event 77 | DTSTART;TZID=Europe_Berlin:20140409T093000 78 | DTEND;TZID=America_New_York:20140409T103000 79 | RDATE;TZID=IndianReunion:20140418T113000 80 | RDATE;TZID=America_Bogota:20140411T113000,20140413T113000 81 | RDATE;TZID=America_Bogota:20140415T113000 82 | RRULE:FREQ=MONTHLY;COUNT=6 83 | DTSTAMP:20140401T234817Z 84 | UID:abcde 85 | END:VEVENT 86 | BEGIN:VEVENT 87 | SUMMARY:An Updated Event 88 | DTSTART;TZID=Europe_Berlin:20140409T093000 89 | DTEND;TZID=America_New_York:20140409T103000 90 | DTSTAMP:20140401T234817Z 91 | UID:abcde 92 | RECURRENCE-ID;TZID=Europe_Amsterdam:20140707T070000 93 | END:VEVENT 94 | END:VCALENDAR 95 | -------------------------------------------------------------------------------- /tests/ics/tz_windows_format.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VTIMEZONE 5 | TZID:New Zealand Standard Time 6 | BEGIN:STANDARD 7 | DTSTART;VALUE=DATE-TIME:20191027T020000 8 | TZNAME:CET 9 | TZOFFSETFROM:+0200 10 | TZOFFSETTO:+0100 11 | END:STANDARD 12 | BEGIN:DAYLIGHT 13 | DTSTART;VALUE=DATE-TIME:20200329T030000 14 | TZNAME:CEST 15 | TZOFFSETFROM:+1200 16 | TZOFFSETTO:+1300 17 | END:DAYLIGHT 18 | END:VTIMEZONE 19 | BEGIN:VEVENT 20 | SUMMARY:Event with Windows TZ name 21 | DTSTART;TZID=New Zealand Standard Time;VALUE=DATE-TIME:20191120T130000 22 | DTEND;TZID=New Zealand Standard Time;VALUE=DATE-TIME:20191120T143000 23 | DTSTAMP;VALUE=DATE-TIME:20191105T094904Z 24 | UID:PERSOYPOYGVR55JMICVAFVDWWBIKPC9PTAG6SN4B2YT 25 | END:VEVENT 26 | END:VCALENDAR 27 | -------------------------------------------------------------------------------- /tests/ics/without_uid.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN 4 | BEGIN:VEVENT 5 | SUMMARY:An Event 6 | DTSTART;VALUE=DATE:20140409 7 | DTEND;VALUE=DATE:20140410 8 | DTSTAMP;VALUE=DATE-TIME:20140401T234817Z 9 | END:VEVENT 10 | END:VCALENDAR 11 | -------------------------------------------------------------------------------- /tests/terminal_test.py: -------------------------------------------------------------------------------- 1 | from khal.terminal import colored, merge_columns 2 | 3 | 4 | def test_colored(): 5 | assert colored('test', 'light cyan') == '\33[1;36mtest\x1b[0m' 6 | assert colored('täst', 'white') == '\33[37mtäst\x1b[0m' 7 | assert colored('täst', 'white', 'dark green') == '\x1b[37m\x1b[42mtäst\x1b[0m' 8 | assert colored('täst', 'light magenta', 'dark green', True) == '\x1b[1;35m\x1b[42mtäst\x1b[0m' 9 | assert colored('täst', 'light magenta', 'dark green', False) == '\x1b[95m\x1b[42mtäst\x1b[0m' 10 | assert colored('täst', 'light magenta', 'light green', True) == '\x1b[1;35m\x1b[42mtäst\x1b[0m' 11 | assert colored('täst', 'light magenta', 'light green', False) == '\x1b[95m\x1b[102mtäst\x1b[0m' 12 | assert colored('täst', '5', '20') == '\x1b[38;5;5m\x1b[48;5;20mtäst\x1b[0m' 13 | assert colored('täst', '#F0F', '#00AABB') == \ 14 | '\x1b[38;2;255;0;255m\x1b[48;2;0;170;187mtäst\x1b[0m' 15 | 16 | 17 | class TestMergeColumns: 18 | 19 | def test_longer_right(self): 20 | left = ['uiae', 'nrtd'] 21 | right = ['123456', '234567', '345678'] 22 | out = ['uiae 123456', 23 | 'nrtd 234567', 24 | ' 345678'] 25 | assert merge_columns(left, right, width=4) == out 26 | 27 | def test_longer_left(self): 28 | left = ['uiae', 'nrtd', 'xvlc'] 29 | right = ['123456', '234567'] 30 | out = ['uiae 123456', 'nrtd 234567', 'xvlc '] 31 | assert merge_columns(left, right, width=4) == out 32 | -------------------------------------------------------------------------------- /tests/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimutils/khal/df6545645740f5ffe6ec00a97f7b5ff195ff60f4/tests/ui/__init__.py -------------------------------------------------------------------------------- /tests/ui/canvas_render.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import io 4 | 5 | import click 6 | 7 | 8 | class CanvasTranslator: 9 | """Translates a canvas object into a printable string.""" 10 | 11 | def __init__(self, canvas, palette: dict[str, str] | None = None) -> None: 12 | """currently only support foreground colors, so palette is 13 | a dictionary of attributes and foreground colors""" 14 | self._canvas = canvas 15 | self._palette : dict[str, tuple[bool, str]]= {} 16 | if palette: 17 | for key, color in palette.items(): 18 | self.add_color(key, color) 19 | 20 | def add_color(self, key: str, color: str) -> None: 21 | if color.startswith('#'): # RGB colour 22 | r = color[1:3] 23 | g = color[3:5] 24 | b = color[5:8] 25 | rgb = int(r, 16), int(g, 16), int(b, 16) 26 | value = True, '\33[38;2;{!s};{!s};{!s}m'.format(*rgb) 27 | else: 28 | color = color.split(' ')[-1] 29 | if color == 'gray': 30 | color = 'white' # click will insist on US-english 31 | value = False, color 32 | 33 | self._palette[key] = value # (is_ansi, color) 34 | 35 | def transform(self) -> str: 36 | self.output = io.StringIO() 37 | for row in self._canvas.content(): 38 | # self.spaces = 0 39 | for col in row[:-1]: 40 | self._process_char(*col) 41 | # the last column has all the trailing whitespace, which deforms 42 | # everything if the terminal is resized: 43 | col = row[-1] 44 | self._process_char(col[0], col[1], col[2].rstrip()) 45 | 46 | self.output.write('\n') 47 | 48 | return self.output.getvalue() 49 | 50 | def _process_char(self, fmt, _, b): 51 | text = b.decode() 52 | if not fmt: 53 | self.output.write(text) 54 | else: 55 | fmt = self._palette[fmt] 56 | if fmt[0]: 57 | self.output.write(f'{fmt[1]}{click.style(text)}') 58 | else: 59 | self.output.write(click.style(text, fg=fmt[1])) 60 | -------------------------------------------------------------------------------- /tests/ui/test_calendarwidget.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | 3 | from freezegun import freeze_time 4 | 5 | from khal.ui.calendarwidget import CalendarWidget 6 | 7 | on_press: dict = {} 8 | 9 | keybindings = { 10 | 'today': ['T'], 11 | 'left': ['left', 'h', 'backspace'], 12 | 'up': ['up', 'k'], 13 | 'right': ['right', 'l', ' '], 14 | 'down': ['down', 'j'], 15 | } 16 | 17 | 18 | def test_initial_focus_today(): 19 | today = dt.date.today() 20 | frame = CalendarWidget(on_date_change=lambda _: None, 21 | keybindings=keybindings, 22 | on_press=on_press, 23 | weeknumbers='right') 24 | assert frame.focus_date == today 25 | 26 | 27 | def test_set_focus_date(): 28 | today = dt.date.today() 29 | for diff in range(-10, 10, 1): 30 | frame = CalendarWidget(on_date_change=lambda _: None, 31 | keybindings=keybindings, 32 | on_press=on_press, 33 | weeknumbers='right') 34 | day = today + dt.timedelta(days=diff) 35 | frame.set_focus_date(day) 36 | assert frame.focus_date == day 37 | 38 | 39 | def test_set_focus_date_weekstart_6(): 40 | 41 | with freeze_time('2016-04-10'): 42 | today = dt.date.today() 43 | for diff in range(-21, 21, 1): 44 | frame = CalendarWidget(on_date_change=lambda _: None, 45 | keybindings=keybindings, 46 | on_press=on_press, 47 | firstweekday=6, 48 | weeknumbers='right') 49 | day = today + dt.timedelta(days=diff) 50 | frame.set_focus_date(day) 51 | assert frame.focus_date == day 52 | 53 | with freeze_time('2016-04-23'): 54 | today = dt.date.today() 55 | for diff in range(10): 56 | frame = CalendarWidget(on_date_change=lambda _: None, 57 | keybindings=keybindings, 58 | on_press=on_press, 59 | firstweekday=6, 60 | weeknumbers='right') 61 | day = today + dt.timedelta(days=diff) 62 | frame.set_focus_date(day) 63 | assert frame.focus_date == day 64 | 65 | 66 | def test_set_focus_far_future(): 67 | future_date = dt.date.today() + dt.timedelta(days=1000) 68 | frame = CalendarWidget(on_date_change=lambda _: None, 69 | keybindings=keybindings, 70 | on_press=on_press, 71 | weeknumbers='right') 72 | frame.set_focus_date(future_date) 73 | assert frame.focus_date == future_date 74 | 75 | def test_set_focus_far_past(): 76 | future_date = dt.date.today() - dt.timedelta(days=1000) 77 | frame = CalendarWidget(on_date_change=lambda _: None, 78 | keybindings=keybindings, 79 | on_press=on_press, 80 | weeknumbers='right') 81 | frame.set_focus_date(future_date) 82 | assert frame.focus_date == future_date 83 | -------------------------------------------------------------------------------- /tests/ui/test_editor.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | 3 | import icalendar 4 | 5 | from khal.ui.editor import RecurrenceEditor, StartEndEditor 6 | 7 | from ..utils import BERLIN, LOCALE_BERLIN 8 | from .canvas_render import CanvasTranslator 9 | 10 | CONF = {'locale': LOCALE_BERLIN, 'keybindings': {}, 'view': {'monthdisplay': 'firstday'}} 11 | 12 | START = BERLIN.localize(dt.datetime(2015, 4, 26, 22, 23)) 13 | END = BERLIN.localize(dt.datetime(2015, 4, 27, 23, 23)) 14 | 15 | palette = { 16 | 'date header focused': 'blue', 17 | 'date header': 'green', 18 | 'default': 'black', 19 | 'edit focused': 'red', 20 | 'edit': 'blue', 21 | } 22 | 23 | 24 | def test_popup(monkeypatch): 25 | """making sure the popup calendar gets callend with the right inital value 26 | 27 | #405 28 | """ 29 | class FakeCalendar: 30 | def store(self, *args, **kwargs): 31 | self.args = args 32 | self.kwargs = kwargs 33 | 34 | fake = FakeCalendar() 35 | monkeypatch.setattr( 36 | 'khal.ui.calendarwidget.CalendarWidget.__init__', fake.store) 37 | see = StartEndEditor(START, END, CONF) 38 | see.widgets.startdate.keypress((22, ), 'enter') 39 | assert fake.kwargs['initial'] == dt.date(2015, 4, 26) 40 | see.widgets.enddate.keypress((22, ), 'enter') 41 | assert fake.kwargs['initial'] == dt.date(2015, 4, 27) 42 | 43 | 44 | def test_check_understood_rrule(): 45 | assert RecurrenceEditor.check_understood_rrule( 46 | icalendar.vRecur.from_ical('FREQ=MONTHLY;BYDAY=1SU') 47 | ) 48 | assert RecurrenceEditor.check_understood_rrule( 49 | icalendar.vRecur.from_ical('FREQ=MONTHLY;BYMONTHDAY=1') 50 | ) 51 | assert RecurrenceEditor.check_understood_rrule( 52 | icalendar.vRecur.from_ical('FREQ=MONTHLY;BYDAY=TH;BYSETPOS=1') 53 | ) 54 | assert RecurrenceEditor.check_understood_rrule( 55 | icalendar.vRecur.from_ical('FREQ=MONTHLY;BYDAY=TU,TH;BYSETPOS=1') 56 | ) 57 | assert RecurrenceEditor.check_understood_rrule( 58 | icalendar.vRecur.from_ical('FREQ=MONTHLY;INTERVAL=2;BYDAY=MO,TU,WE,TH,FR,SA,SU;BYSETPOS=1') 59 | ) 60 | assert RecurrenceEditor.check_understood_rrule( 61 | icalendar.vRecur.from_ical('FREQ=MONTHLY;INTERVAL=2;BYDAY=WE,SU,MO,TH,FR,TU,SA;BYSETPOS=1') 62 | ) 63 | assert RecurrenceEditor.check_understood_rrule( 64 | icalendar.vRecur.from_ical('FREQ=MONTHLY;INTERVAL=2;BYDAY=WE,MO,TH,FR,TU,SA;BYSETPOS=1') 65 | ) 66 | assert not RecurrenceEditor.check_understood_rrule( 67 | icalendar.vRecur.from_ical('FREQ=MONTHLY;BYDAY=-1SU') 68 | ) 69 | assert not RecurrenceEditor.check_understood_rrule( 70 | icalendar.vRecur.from_ical('FREQ=MONTHLY;BYDAY=TH;BYMONTHDAY=1,2,3,4,5,6,7') 71 | ) 72 | assert not RecurrenceEditor.check_understood_rrule( 73 | icalendar.vRecur.from_ical('FREQ=MONTHLY;BYDAY=TH;BYMONTHDAY=-1') 74 | ) 75 | assert not RecurrenceEditor.check_understood_rrule( 76 | icalendar.vRecur.from_ical('FREQ=MONTHLY;BYDAY=TH;BYSETPOS=3') 77 | ) 78 | 79 | 80 | def test_editor(): 81 | """test for the issue in #666""" 82 | editor = StartEndEditor( 83 | BERLIN.localize(dt.datetime(2017, 10, 2, 13)), 84 | BERLIN.localize(dt.datetime(2017, 10, 4, 18)), 85 | conf=CONF 86 | ) 87 | assert editor.startdt == BERLIN.localize(dt.datetime(2017, 10, 2, 13)) 88 | assert editor.enddt == BERLIN.localize(dt.datetime(2017, 10, 4, 18)) 89 | assert editor.changed is False 90 | for _ in range(3): 91 | editor.keypress((10, ), 'tab') 92 | for _ in range(3): 93 | editor.keypress((10, ), 'shift tab') 94 | assert editor.startdt == BERLIN.localize(dt.datetime(2017, 10, 2, 13)) 95 | assert editor.enddt == BERLIN.localize(dt.datetime(2017, 10, 4, 18)) 96 | assert editor.changed is False 97 | 98 | 99 | def test_convert_to_date(): 100 | """test for the issue in #666""" 101 | editor = StartEndEditor( 102 | BERLIN.localize(dt.datetime(2017, 10, 2, 13)), 103 | BERLIN.localize(dt.datetime(2017, 10, 4, 18)), 104 | conf=CONF 105 | ) 106 | canvas = editor.render((50, ), True) 107 | assert CanvasTranslator(canvas, palette).transform() == ( 108 | '[ ] Allday\nFrom: \x1b[31m2.10.2017 \x1b[0m \x1b[34m13:00 \x1b[0m\n' 109 | 'To: \x1b[34m04.10.2017\x1b[0m \x1b[34m18:00 \x1b[0m\n' 110 | ) 111 | 112 | assert editor.startdt == BERLIN.localize(dt.datetime(2017, 10, 2, 13)) 113 | assert editor.enddt == BERLIN.localize(dt.datetime(2017, 10, 4, 18)) 114 | assert editor.changed is False 115 | assert editor.allday is False 116 | 117 | # set to all day event 118 | editor.keypress((10, ), 'shift tab') 119 | editor.keypress((10, ), ' ') 120 | for _ in range(3): 121 | editor.keypress((10, ), 'tab') 122 | for _ in range(3): 123 | editor.keypress((10, ), 'shift tab') 124 | 125 | canvas = editor.render((50, ), True) 126 | assert CanvasTranslator(canvas, palette).transform() == ( 127 | '[X] Allday\nFrom: \x1b[34m02.10.2017\x1b[0m \n' 128 | 'To: \x1b[34m04.10.2017\x1b[0m \n' 129 | ) 130 | 131 | assert editor.changed is True 132 | assert editor.allday is True 133 | assert editor.startdt == dt.date(2017, 10, 2) 134 | assert editor.enddt == dt.date(2017, 10, 4) 135 | -------------------------------------------------------------------------------- /tests/ui/test_walker.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | 3 | from freezegun import freeze_time 4 | 5 | from khal.ui import DayWalker, DListBox, StaticDayWalker 6 | 7 | from ..utils import LOCALE_BERLIN 8 | from .canvas_render import CanvasTranslator 9 | 10 | CONF = {'locale': LOCALE_BERLIN, 'keybindings': {}, 11 | 'view': {'monthdisplay': 'firstday'}, 12 | 'default': {'timedelta': dt.timedelta(days=3)}, 13 | } 14 | 15 | 16 | palette = { 17 | 'date header focused': 'blue', 18 | 'date header': 'green', 19 | 'default': 'black', 20 | } 21 | 22 | 23 | @freeze_time('2017-6-7') 24 | def test_daywalker(coll_vdirs): 25 | collection, _ = coll_vdirs 26 | this_date = dt.date.today() 27 | daywalker = DayWalker(this_date, None, CONF, collection, delete_status={}) 28 | elistbox = DListBox( 29 | daywalker, parent=None, conf=CONF, 30 | delete_status=lambda: False, 31 | toggle_delete_all=None, 32 | toggle_delete_instance=None, 33 | dynamic_days=True, 34 | ) 35 | canvas = elistbox.render((50, 6), True) 36 | assert CanvasTranslator(canvas, palette).transform() == \ 37 | """\x1b[34mToday (Wednesday, 07.06.2017)\x1b[0m 38 | \x1b[32mTomorrow (Thursday, 08.06.2017)\x1b[0m 39 | \x1b[32mFriday, 09.06.2017 (2 days from now)\x1b[0m 40 | \x1b[32mSaturday, 10.06.2017 (3 days from now)\x1b[0m 41 | \x1b[32mSunday, 11.06.2017 (4 days from now)\x1b[0m 42 | \x1b[32mMonday, 12.06.2017 (5 days from now)\x1b[0m 43 | """ 44 | 45 | 46 | @freeze_time('2017-6-7') 47 | def test_staticdaywalker(coll_vdirs): 48 | collection, _ = coll_vdirs 49 | this_date = dt.date.today() 50 | daywalker = StaticDayWalker(this_date, None, CONF, collection, delete_status={}) 51 | elistbox = DListBox( 52 | daywalker, parent=None, conf=CONF, 53 | delete_status=lambda: False, 54 | toggle_delete_all=None, 55 | toggle_delete_instance=None, 56 | dynamic_days=False, 57 | ) 58 | canvas = elistbox.render((50, 10), True) 59 | assert CanvasTranslator(canvas, palette).transform() == \ 60 | """\x1b[34mToday (Wednesday, 07.06.2017)\x1b[0m 61 | \x1b[32mTomorrow (Thursday, 08.06.2017)\x1b[0m 62 | \x1b[32mFriday, 09.06.2017 (2 days from now)\x1b[0m 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | """ 71 | 72 | 73 | @freeze_time('2017-6-7') 74 | def test_staticdaywalker_3(coll_vdirs): 75 | collection, _ = coll_vdirs 76 | this_date = dt.date.today() 77 | conf = {} 78 | conf.update(CONF) 79 | conf['default'] = {'timedelta': dt.timedelta(days=1)} 80 | daywalker = StaticDayWalker(this_date, None, conf, collection, delete_status={}) 81 | elistbox = DListBox( 82 | daywalker, parent=None, conf=conf, 83 | delete_status=lambda: False, 84 | toggle_delete_all=None, 85 | toggle_delete_instance=None, 86 | dynamic_days=False, 87 | ) 88 | canvas = elistbox.render((50, 10), True) 89 | assert CanvasTranslator(canvas, palette).transform() == \ 90 | '\x1b[34mToday (Wednesday, 07.06.2017)\x1b[0m\n\n\n\n\n\n\n\n\n\n' 91 | -------------------------------------------------------------------------------- /tests/ui/test_widgets.py: -------------------------------------------------------------------------------- 1 | from khal.ui.widgets import delete_last_word 2 | 3 | 4 | def test_delete_last_word(): 5 | 6 | tests = [ 7 | ('Fü1ü Bär!', 'Fü1ü Bär', 1), 8 | ('Füü Bär1', 'Füü ', 1), 9 | ('Füü1 Bär1', 'Füü1 ', 1), 10 | (' Füü Bär', ' Füü ', 1), 11 | ('Füü Bär.Füü', 'Füü Bär.', 1), 12 | ('Füü Bär.(Füü)', 'Füü Bär.(Füü', 1), 13 | ('Füü ', '', 1), 14 | ('Füü ', '', 1), 15 | ('Füü', '', 1), 16 | ('', '', 1), 17 | 18 | ('Füü Bär.(Füü)', 'Füü Bär.', 3), 19 | ('Füü Bär1', '', 2), 20 | ('Lorem ipsum dolor sit amet, consetetur sadipscing elitr, ' 21 | 'sed diam nonumy eirmod tempor invidunt ut labore et dolore ' 22 | 'magna aliquyam erat, sed diam volest.', 23 | 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, ' 24 | 'sed diam nonumy eirmod tempor invidunt ut labore ', 25 | 10) 26 | ] 27 | 28 | for org, short, number in tests: 29 | assert delete_last_word(org, number) == short 30 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import icalendar 4 | import pytz 5 | 6 | from khal.custom_types import LocaleConfiguration 7 | from khal.khalendar import CalendarCollection 8 | from khal.khalendar.vdir import Vdir 9 | 10 | CollVdirType = tuple[CalendarCollection, dict[str, Vdir]] 11 | 12 | cal0 = 'a_calendar' 13 | cal1 = 'foobar' 14 | cal2 = "Dad's calendar" 15 | cal3 = 'private' 16 | 17 | example_cals = [cal0, cal1, cal2, cal3] 18 | 19 | BERLIN = pytz.timezone('Europe/Berlin') 20 | NEW_YORK = pytz.timezone('America/New_York') 21 | LONDON = pytz.timezone('Europe/London') 22 | SAMOA = pytz.timezone('Pacific/Samoa') 23 | SYDNEY = pytz.timezone('Australia/Sydney') 24 | GMTPLUS3 = pytz.timezone('Etc/GMT+3') 25 | # the lucky people in Bogota don't know the pain that is DST 26 | BOGOTA = pytz.timezone('America/Bogota') 27 | 28 | 29 | LOCALE_BERLIN: LocaleConfiguration = { 30 | 'default_timezone': BERLIN, 31 | 'local_timezone': BERLIN, 32 | 'dateformat': '%d.%m.', 33 | 'longdateformat': '%d.%m.%Y', 34 | 'timeformat': '%H:%M', 35 | 'datetimeformat': '%d.%m. %H:%M', 36 | 'longdatetimeformat': '%d.%m.%Y %H:%M', 37 | 'unicode_symbols': True, 38 | 'firstweekday': 0, 39 | 'weeknumbers': False, 40 | } 41 | 42 | LOCALE_NEW_YORK: LocaleConfiguration = { 43 | 'default_timezone': NEW_YORK, 44 | 'local_timezone': NEW_YORK, 45 | 'timeformat': '%H:%M', 46 | 'dateformat': '%Y/%m/%d', 47 | 'longdateformat': '%Y/%m/%d', 48 | 'datetimeformat': '%Y/%m/%d-%H:%M', 49 | 'longdatetimeformat': '%Y/%m/%d-%H:%M', 50 | 'firstweekday': 6, 51 | 'unicode_symbols': True, 52 | 'weeknumbers': False, 53 | } 54 | 55 | LOCALE_SAMOA = { 56 | 'local_timezone': SAMOA, 57 | 'default_timezone': SAMOA, 58 | 'unicode_symbols': True, 59 | } 60 | LOCALE_SYDNEY = {'local_timezone': SYDNEY, 'default_timezone': SYDNEY} 61 | 62 | LOCALE_BOGOTA = LOCALE_BERLIN.copy() 63 | LOCALE_BOGOTA['local_timezone'] = BOGOTA 64 | LOCALE_BOGOTA['default_timezone'] = BOGOTA 65 | 66 | LOCALE_MIXED = LOCALE_BERLIN.copy() 67 | LOCALE_MIXED['local_timezone'] = BOGOTA 68 | 69 | LOCALE_FLOATING = LOCALE_BERLIN.copy() 70 | LOCALE_FLOATING['default_timezone'] = None # type: ignore 71 | LOCALE_FLOATING['local_timezone'] = None # type: ignore 72 | 73 | 74 | def normalize_component(x): 75 | x = icalendar.cal.Component.from_ical(x) 76 | 77 | def inner(c): 78 | contentlines = icalendar.cal.Contentlines() 79 | for name, value in c.property_items(sorted=True, recursive=False): 80 | contentlines.append(c.content_line(name, value, sorted=True)) 81 | contentlines.append('') 82 | 83 | return (c.name, contentlines.to_ical(), 84 | frozenset(inner(sub) for sub in c.subcomponents)) 85 | 86 | return inner(x) 87 | 88 | 89 | def _get_text(event_name): 90 | directory = '/'.join(__file__.split('/')[:-1]) + '/ics/' 91 | if directory == '/ics/': 92 | directory = './ics/' 93 | 94 | with open(os.path.join(directory, event_name + '.ics'), 'rb') as f: 95 | rv = f.read().decode('utf-8') 96 | 97 | return rv 98 | 99 | 100 | def _get_vevent_file(event_path): 101 | directory = '/'.join(__file__.split('/')[:-1]) + '/ics/' 102 | with open(os.path.join(directory, event_path + '.ics'), 'rb') as f: 103 | ical = icalendar.Calendar.from_ical( 104 | f.read() 105 | ) 106 | for component in ical.walk(): 107 | if component.name == 'VEVENT': 108 | return component 109 | 110 | 111 | def _get_ics_filepath(event_name): 112 | directory = '/'.join(__file__.split('/')[:-1]) + '/ics/' 113 | if directory == '/ics/': 114 | directory = './ics/' 115 | return os.path.join(directory, event_name + '.ics') 116 | 117 | 118 | def _get_all_vevents_file(event_path): 119 | directory = '/'.join(__file__.split('/')[:-1]) + '/ics/' 120 | ical = icalendar.Calendar.from_ical( 121 | open(os.path.join(directory, event_path + '.ics'), 'rb').read() 122 | ) 123 | for component in ical.walk(): 124 | if component.name == 'VEVENT': 125 | yield component 126 | 127 | 128 | def _replace_uid(event): 129 | """ 130 | Replace an event's UID with E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA. 131 | """ 132 | event.pop('uid') 133 | event.add('uid', 'E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA') 134 | return event 135 | 136 | 137 | class DumbItem: 138 | def __init__(self, raw, uid) -> None: 139 | self.raw = raw 140 | self.uid = uid 141 | -------------------------------------------------------------------------------- /tests/utils_test.py: -------------------------------------------------------------------------------- 1 | """testing functions from the khal.utils""" 2 | import datetime as dt 3 | 4 | from click import style 5 | from freezegun import freeze_time 6 | 7 | from khal import utils 8 | 9 | 10 | def test_relative_timedelta_str(): 11 | with freeze_time('2016-9-19'): 12 | assert utils.relative_timedelta_str(dt.date(2016, 9, 24)) == '5 days from now' 13 | assert utils.relative_timedelta_str(dt.date(2016, 9, 29)) == '~1 week from now' 14 | assert utils.relative_timedelta_str(dt.date(2017, 9, 29)) == '~1 year from now' 15 | assert utils.relative_timedelta_str(dt.date(2016, 7, 29)) == '~7 weeks ago' 16 | 17 | 18 | weekheader = """ Mo Tu We Th Fr Sa Su """ 19 | today_line = """Today""" 20 | calendarline = ( 21 | "Nov 31  1  2 " 22 | " 3  4  5  6" 23 | ) 24 | 25 | 26 | def test_last_reset(): 27 | assert utils.find_last_reset(weekheader) == (31, 35, '\x1b[0m') 28 | assert utils.find_last_reset(today_line) == (13, 17, '\x1b[0m') 29 | assert utils.find_last_reset(calendarline) == (99, 103, '\x1b[0m') 30 | assert utils.find_last_reset('Hello World') == (-2, -1, '') 31 | 32 | 33 | def test_last_sgr(): 34 | assert utils.find_last_sgr(weekheader) == (0, 4, '\x1b[1m') 35 | assert utils.find_last_sgr(today_line) == (0, 4, '\x1b[1m') 36 | assert utils.find_last_sgr(calendarline) == (92, 97, '\x1b[32m') 37 | assert utils.find_last_sgr('Hello World') == (-2, -1, '') 38 | 39 | 40 | def test_find_unmatched_sgr(): 41 | assert utils.find_unmatched_sgr(weekheader) is None 42 | assert utils.find_unmatched_sgr(today_line) is None 43 | assert utils.find_unmatched_sgr(calendarline) is None 44 | assert utils.find_unmatched_sgr('\x1b[31mHello World') == '\x1b[31m' 45 | assert utils.find_unmatched_sgr('\x1b[31mHello\x1b[0m \x1b[32mWorld') == '\x1b[32m' 46 | assert utils.find_unmatched_sgr('foo\x1b[1;31mbar') == '\x1b[1;31m' 47 | assert utils.find_unmatched_sgr('\x1b[0mfoo\x1b[1;31m') == '\x1b[1;31m' 48 | 49 | 50 | def test_color_wrap(): 51 | text = ( 52 | "Lorem ipsum \x1b[31mdolor sit amet, consetetur sadipscing " 53 | "elitr, sed diam nonumy\x1b[0m eirmod tempor" 54 | ) 55 | expected = [ 56 | "Lorem ipsum \x1b[31mdolor sit amet,\x1b[0m", 57 | "\x1b[31mconsetetur sadipscing elitr, sed\x1b[0m", 58 | "\x1b[31mdiam nonumy\x1b[0m eirmod tempor", 59 | ] 60 | 61 | assert utils.color_wrap(text, 35) == expected 62 | 63 | 64 | def test_color_wrap_256(): 65 | text = ( 66 | "\x1b[38;2;17;255;0mLorem ipsum dolor sit amet, consetetur sadipscing " 67 | "elitr, sed diam nonumy\x1b[0m" 68 | ) 69 | expected = [ 70 | "\x1b[38;2;17;255;0mLorem ipsum\x1b[0m", 71 | "\x1b[38;2;17;255;0mdolor sit amet, consetetur\x1b[0m", 72 | "\x1b[38;2;17;255;0msadipscing elitr, sed diam\x1b[0m", 73 | "\x1b[38;2;17;255;0mnonumy\x1b[0m" 74 | ] 75 | 76 | assert utils.color_wrap(text, 30) == expected 77 | 78 | 79 | def test_color_wrap_multiple_colors_and_tabs(): 80 | text = ( 81 | "\x1b[31m14:00-14:50 AST-1002-102 INTRO AST II/STAR GALAX (R) Classes", 82 | "15:30-16:45 PHL-2000-104 PHILOSOPHY, SOCIETY & ETHICS (R) Classes", 83 | "\x1b[38;2;255;0m17:00-18:00 Pay Ticket Deadline Calendar", 84 | "09:30-10:45 PHL-1501-101 MIND, KNOWLEDGE & REALITY (R) Classes", 85 | "\x1b[38;2;255;0m11:00-14:00 Rivers Street (noodles and pizza) (R) Calendar", 86 | ) 87 | expected = [ 88 | '\x1b[31m14:00-14:50 AST-1002-102 INTRO AST II/STAR GALAX (R)\x1b[0m', 89 | '\x1b[31mClasses\x1b[0m', 90 | '15:30-16:45 PHL-2000-104 PHILOSOPHY, SOCIETY & ETHICS (R)', 91 | 'Classes', 92 | '\x1b[38;2;255;0m17:00-18:00 Pay Ticket Deadline Calendar\x1b[0m', 93 | '09:30-10:45 PHL-1501-101 MIND, KNOWLEDGE & REALITY (R)', 94 | 'Classes', 95 | '\x1b[38;2;255;0m11:00-14:00 Rivers Street (noodles and\x1b[0m', 96 | '\x1b[38;2;255;0mpizza) (R) Calendar\x1b[0m' 97 | ] 98 | actual = [] 99 | for line in text: 100 | actual += utils.color_wrap(line, 60) 101 | assert actual == expected 102 | 103 | 104 | def test_get_weekday_occurrence(): 105 | assert utils.get_weekday_occurrence(dt.datetime(2017, 3, 1)) == (2, 1) 106 | assert utils.get_weekday_occurrence(dt.datetime(2017, 3, 2)) == (3, 1) 107 | assert utils.get_weekday_occurrence(dt.datetime(2017, 3, 3)) == (4, 1) 108 | assert utils.get_weekday_occurrence(dt.datetime(2017, 3, 4)) == (5, 1) 109 | assert utils.get_weekday_occurrence(dt.datetime(2017, 3, 5)) == (6, 1) 110 | assert utils.get_weekday_occurrence(dt.datetime(2017, 3, 6)) == (0, 1) 111 | assert utils.get_weekday_occurrence(dt.datetime(2017, 3, 7)) == (1, 1) 112 | assert utils.get_weekday_occurrence(dt.datetime(2017, 3, 8)) == (2, 2) 113 | assert utils.get_weekday_occurrence(dt.datetime(2017, 3, 9)) == (3, 2) 114 | assert utils.get_weekday_occurrence(dt.datetime(2017, 3, 10)) == (4, 2) 115 | 116 | assert utils.get_weekday_occurrence(dt.datetime(2017, 3, 31)) == (4, 5) 117 | 118 | assert utils.get_weekday_occurrence(dt.date(2017, 5, 1)) == (0, 1) 119 | assert utils.get_weekday_occurrence(dt.date(2017, 5, 7)) == (6, 1) 120 | assert utils.get_weekday_occurrence(dt.date(2017, 5, 8)) == (0, 2) 121 | assert utils.get_weekday_occurrence(dt.date(2017, 5, 28)) == (6, 4) 122 | assert utils.get_weekday_occurrence(dt.date(2017, 5, 29)) == (0, 5) 123 | 124 | 125 | def test_human_formatter_width(): 126 | formatter = utils.human_formatter('{red}{title}', width=10) 127 | output = formatter({'title': 'morethan10characters', 'red': style('', reset=False, fg='red')}) 128 | assert output.startswith('\x1b[31mmoret\x1b[0m') 129 | -------------------------------------------------------------------------------- /tests/vdir_test.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2022 khal contributors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | import os 23 | from time import sleep 24 | 25 | from khal.khalendar import vdir 26 | 27 | 28 | def test_etag(tmpdir, sleep_time): 29 | fpath = os.path.join(str(tmpdir), 'foo') 30 | 31 | file_ = open(fpath, 'w') 32 | file_.write('foo') 33 | file_.close() 34 | 35 | old_etag = vdir.get_etag_from_file(fpath) 36 | sleep(sleep_time) 37 | 38 | file_ = open(fpath, 'w') 39 | file_.write('foo') 40 | file_.close() 41 | 42 | new_etag = vdir.get_etag_from_file(fpath) 43 | 44 | assert old_etag != new_etag 45 | 46 | 47 | def test_etag_sync(tmpdir, sleep_time): 48 | fpath = os.path.join(str(tmpdir), 'foo') 49 | 50 | file_ = open(fpath, 'w') 51 | file_.write('foo') 52 | file_.close() 53 | os.sync() 54 | old_etag = vdir.get_etag_from_file(fpath) 55 | sleep(sleep_time) 56 | 57 | file_ = open(fpath, 'w') 58 | file_.write('foo') 59 | file_.close() 60 | 61 | new_etag = vdir.get_etag_from_file(fpath) 62 | 63 | assert old_etag != new_etag 64 | 65 | 66 | def test_get_href_from_uid(): 67 | # Test UID with unsafe characters 68 | uid = "V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWÈÉ@pimutils.org" 69 | first_href = vdir._generate_href(uid) 70 | second_href = vdir._generate_href(uid) 71 | assert first_href == second_href 72 | 73 | # test UID with safe characters 74 | uid = "V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU@pimutils.org" 75 | href = vdir._generate_href(uid) 76 | assert href == uid 77 | 78 | href = vdir._generate_href() 79 | assert href is not None 80 | -------------------------------------------------------------------------------- /tests/vtimezone_test.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | 3 | import pytz 4 | from packaging import version 5 | 6 | from khal.khalendar.event import create_timezone 7 | 8 | berlin = pytz.timezone('Europe/Berlin') 9 | bogota = pytz.timezone('America/Bogota') 10 | 11 | atime = dt.datetime(2014, 10, 28, 10, 10) 12 | btime = dt.datetime(2016, 10, 28, 10, 10) 13 | 14 | 15 | def test_berlin(): 16 | vberlin_std = b'\r\n'.join( 17 | [b'BEGIN:STANDARD', 18 | b'DTSTART:20141026T020000', 19 | b'TZNAME:CET', 20 | b'TZOFFSETFROM:+0200', 21 | b'TZOFFSETTO:+0100', 22 | b'END:STANDARD', 23 | ]) 24 | 25 | vberlin_dst = b'\r\n'.join( 26 | [b'BEGIN:DAYLIGHT', 27 | b'DTSTART:20150329T030000', 28 | b'TZNAME:CEST', 29 | b'TZOFFSETFROM:+0100', 30 | b'TZOFFSETTO:+0200', 31 | b'END:DAYLIGHT', 32 | ]) 33 | 34 | vberlin = create_timezone(berlin, atime, atime).to_ical() 35 | assert b'TZID:Europe/Berlin' in vberlin 36 | assert vberlin_std in vberlin 37 | assert vberlin_dst in vberlin 38 | 39 | 40 | def test_berlin_rdate(): 41 | vberlin_std = b'\r\n'.join( 42 | [b'BEGIN:STANDARD', 43 | b'DTSTART:20141026T020000', 44 | b'RDATE:20151025T020000,20161030T020000', 45 | b'TZNAME:CET', 46 | b'TZOFFSETFROM:+0200', 47 | b'TZOFFSETTO:+0100', 48 | b'END:STANDARD', 49 | ]) 50 | 51 | vberlin_dst = b'\r\n'.join( 52 | [b'BEGIN:DAYLIGHT', 53 | b'DTSTART:20150329T030000', 54 | b'RDATE:20160327T030000', 55 | b'TZNAME:CEST', 56 | b'TZOFFSETFROM:+0100', 57 | b'TZOFFSETTO:+0200', 58 | b'END:DAYLIGHT', 59 | ]) 60 | 61 | vberlin = create_timezone(berlin, atime, btime).to_ical() 62 | assert b'TZID:Europe/Berlin' in vberlin 63 | assert vberlin_std in vberlin 64 | assert vberlin_dst in vberlin 65 | 66 | 67 | def test_bogota(): 68 | vbogota = [b'BEGIN:VTIMEZONE', 69 | b'TZID:America/Bogota', 70 | b'BEGIN:STANDARD', 71 | b'DTSTART:19930206T230000', 72 | b'TZNAME:COT', 73 | b'TZOFFSETFROM:-0400', 74 | b'TZOFFSETTO:-0500', 75 | b'END:STANDARD', 76 | b'END:VTIMEZONE', 77 | b''] 78 | if version.parse(pytz.__version__) > version.Version('2017.1'): 79 | vbogota[4] = b'TZNAME:-05' 80 | if version.parse(pytz.__version__) < version.Version('2022.7'): 81 | vbogota.insert(4, b'RDATE:20380118T221407') 82 | 83 | assert create_timezone(bogota, atime, atime).to_ical().split(b'\r\n') == vbogota 84 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = {py39,py310,py311,py312,py313}-tests,py39-tests-{pytz2018.7,pytz_latest},vermin 3 | skip_missing_interpreters = True 4 | 5 | [testenv] 6 | ignore_errors = True 7 | passenv = 8 | LANG 9 | CI 10 | 11 | extras = 12 | test 13 | pytz20187: pytz==2018.7 14 | pytz_latest: pytz 15 | commands = 16 | py.test {posargs} 17 | 18 | [gh-actions] 19 | python = 20 | 3.9: py39 21 | 3.10: py310 22 | 3.11: py311 23 | 3.12: py312 24 | 3.13: py313 25 | 26 | [testenv:docs] 27 | allowlist_externals = make 28 | extras = docs 29 | commands = 30 | make -C doc html 31 | make -C doc man 32 | 33 | [testenv:vermin] 34 | deps = vermin 35 | commands = 36 | vermin -t=3.9- --eval-annotations --backport enum --backport importlib --backport typing --no-parse-comments --violations . 37 | skip_install = true 38 | --------------------------------------------------------------------------------