ian

package module
v0.0.0-...-59ec2ee Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2024 License: GPL-3.0 Imports: 21 Imported by: 0

README

ian logo

ian

(gregor)ian is a UNIX-oriented and file-based calendar system CLIent and server. Its purpose is versatile and organized calendar manipulation and management. A server is a replacement for proprietary and opaque SaaS applications (e.g. Google Calendar).

Events are stored in a (preferably version controlled) directory, with subdirectories for each calendar. Each event is its own TOML file, which makes manual event manipulation easy.

This project strives for iCalendar RFC 5545 compatability.

Both CalDAV and static iCalendar calendars are supported.

Synchronization is done via configuring command hooks.

Installation

Clone the repository:

$ git clone https://github.com/truecrunchyfrog/ian
$ cd ian

Build the program:

$ go build -o ian github.com/truecrunchyfrog/ian/main

There should now be a binary in the working directory (ian).

For a single-user installation, you can add the source directory to your $PATH, by editing your ~/.profile (or, e.g. ~/.bashrc):

$ echo 'export PATH=$PATH:'$PWD >> ~/.profile

For a multi-user installation, you can place the binary in a system-wide $PATH directory like /usr/bin.

Now the program should be available in your shell. Try opening a new shell and running ian.

Structure

Your calendars and their events are stored in a directory root, along with the calendar configuration.

The root defaults to .ian in your home directory (~/.ian).

The calendars are purely subdirectories inside the root. Creating a new calendar can be done by merely creating a directory (in the root). Events are readable TOML files inside respective calendar.

This plain approach to calendaring makes calendar manipulation trivial, as it can be managed in a filesystem. There are no hidden ties between the client and the event files: the events can be freely moved around and renamed.

An ian instance may be structured like this:

.ian
├── .config.toml
├── work
│   └── Harass co-workers
└── home
    ├── Something I'll get around to
    └── Be with Molgan

In the above example there are two calendars (work and home), with a total of three events.

Every calendar is configurable.

Configuration

ian works without configuration. But it is probably for the configuration that you use ian, so you can call it essential.

There are two configuration files used in the client:

  • Your preferences (defaults to ~/.ian.toml).
  • The calendar configuration (.config.toml inside ian root).
Preference configuration

Your personal preferences (first day of the week, time zone, etc.) can be configured in your local preference file, which defaults to ~/.ian.toml. Here is an example configuration that lists all available preferences.

# ~/.ian.toml
root = "~/.ian" # ian directory that the client should use
timezone = "UTC" # defaults to your machine's time zone
no-validation = false # if true, disables event validation. helpful to effectively remedy corrupt events.
no-collision = false # if true, does not allow you to create events that chronologically collide with another event
collision-exceptions = ["birthdays"] # a list of calendars that will be ignored by the collision checker
no-collision-warnings = false # if true, warnings about colisions will not appear
first-weekday = 2 # first weekday of the week; 1 = Sunday, 2 = Monday, ... 7 = Saturday
weeks = true # if true, week numbers will be displayed where relevant in the calendar
months = 3 # amount of months to show at once in the calendar. to only show the current month, set to `1`.
no-timeline = false # if true, the timeline next to the calendar will be hidden
no-event-coloring = false # if true, the calendar day numbers (1-31) will not be colored according to the calendar's color of the events occurring that day
daywidth = 3 # the width each calendar day gets. the 'cal' UNIX command has a daywidth of 2.
no-legend = false # if true, the legend displaying the events' calendars and their colors is hidden

Any preferences here can also be overrridden per command with flags. For example, weeks = true in the configuration can be enabled temporarily with ian --weeks, or disabled temporarily with ian --weeks=false.

Calendar configuration

The calendar configuration (.config.toml inside ian root), is more relevant to your workflow.

This configuration houses how your calendar works, like subscribed calendars, synchronization hooks, etc. If you will be running an ian server, it is all configured in this file, meaning this file is shared between both client and server.

Sources

Sources are calendars that are not managed in your local instance. They are listed by name in sources.

A source can be a static iCalendar file (like a schedule, or a someone's shared calendar), or a CalDAV calendar (like someone's shared calendar that you can edit). Each source is cached and updated. When a cached calendar has reached its lifetime, it will be downloaded anew.

[sources.joe]
  source = "https://calendar.example.com/share/3497503452398461/joes-calendar"
  type = "ical" # a static calendar
  lifetime = "47h30m" # optional: interval between cache updates (defaults to 2 hours)

# one more time!
[sources.mary]
  source = "webcal://canoga-park.net/caldav/mary"
  type = "caldav" # an editable calendar
Attribute Value Description Example Required Default
source iCal/WebCal URL URL to download cache from, or CalDAV server. https://example.com/schedule.ics
type ical or caldav Type of source. ical
lifetime _h_m_s lifetime For how long the source should be cached. 3h40m optional 2h
Calendars

In calendars, you can configure the behavior of both local and cached calendars (from sources).

[calendars.work]
  color = { r = 153, g = 90, b = 209 }
Attribute Value Description Example Required Default
color RGB color Color for calendar recognition. { r = 130, g = 49, b = 168 } optional white
Hooks

Hooks are commands that perform wanted operations when the calendar is updated.

The main use for hooks is synchronizing your instance with a version control system (VCS), like git, but it can be used for anything that your shell can do.

Each hook is listed by name in hooks (e.g. hooks.peter).

Attribute Value Description Example Required Default
precommand Shell command Shell command executed before files are updated. echo "before: $(date) $MESSAGE" >> log optional
postcommand Shell command Shell command executed after files are updated. echo "after: $(date) $MESSAGE" >> log optional
type Bitmask (integer) What type of updates the hook should react on; 0 = any, 1 = ping (manual sync), 2 = event created, 4 = event updated, 8 = event deleted. Sum multiple to combine them. 10 (only on creation and deletion)
cooldown _h_m_s cooldown Time to wait before executing again. 1h optional 0s

Keep in mind that manual file operations do not trigger these hooks. Only operations performed by the ian client or server do.

A manual sync to trigger these commands is possible with ian sync. If the --ignore-cooldowns (-i) flag is passed, all hooks will be triggered regardless of their cooldown status.

Cooldowns information is kept in the file .cooldown-journal.toml. Delete the file to reset the cooldowns.

Commands

The precommand command is executed BEFORE the changes are made, and postcommand AFTER. Both commands are given a set of context environment variables:

  • MESSAGE, a detailed message demonstrating what happened. Great for a git commit message.
  • FILES, a list of space-separated files affected. If the type is a manual sync (1), this is empty.
  • TYPE, the type of event that occured. This is not a bitmask, only one value (1, 2, 4, or 8). The commands are executed inside the ian root directory.

Usage

Documentation

Index

Constants

View Source
const CacheCalendar string = ".sources"
View Source
const CacheJournalFileName string = ".cache-journal.toml"
View Source
const ConfigFilename string = ".config.toml"
View Source
const CooldownJournalFilename string = ".cooldown-journal.toml"
View Source
const DefaultCacheLifetime time.Duration = 2 * time.Hour
View Source
const DefaultTimeLayout string = "_2 Jan 15:04 MST 2006"
View Source
const IcalPropGrabTimestamp string = "X-IAN-GRABBED"

Variables

View Source
var TimeZone *time.Location
View Source
var Verbose bool

Functions

func CreateDir

func CreateDir(name string) error

func CreateFileIfMissing

func CreateFileIfMissing(name string) error

func DisplayCalendar

func DisplayCalendar(
	location *time.Location,
	fromYear int, fromMonth time.Month,
	months int,
	firstWeekday time.Weekday,
	showWeeks bool,
	widthPerDay int,
	dayFmt func(y int, m time.Month, d int) (format string, fmtEntireSlot bool),
) (output string)

func DisplayCalendarLegend

func DisplayCalendarLegend(instance *Instance, events []Event) string

func DisplayTimeline

func DisplayTimeline(instance *Instance, events []Event, showDates bool, lastShownDate time.Time, location *time.Location) string

func DisplayUnsatisfiedRecurrences

func DisplayUnsatisfiedRecurrences(instance *Instance, unsatisfiedRecurrences []*Event) string

func DoPeriodsMeet

func DoPeriodsMeet(period1, period2 TimeRange) bool

DoPeriodsMeet compares two periods and returns true if they collide at some point, otherwise false. Period ends are non-inclusive, meaning that if one period's end touches the start of the other period, it does not count.

func DurationToString

func DurationToString(d time.Duration) string

DurationToString because time's implementation is ugly.

func GenerateUid

func GenerateUid() string

func GetEventRgbAnsiSeq

func GetEventRgbAnsiSeq(event *Event, instance *Instance, background bool) string

GetEventRgbAnsiSeq is a helper function to quickly get the color of an event based on its container.

func GetTimeZone

func GetTimeZone() *time.Location

func IsPeriodConfinedToPeriod

func IsPeriodConfinedToPeriod(period1, period2 TimeRange) bool

IsPeriodConfinedToPeriod returns true if period1 start is at or after (i.e. inclusive) period2 start, and period1 end is before (i.e. non-inclusive) period2 end.

func IsTimeWithinPeriod

func IsTimeWithinPeriod(t time.Time, period TimeRange) bool

IsTimeWithinPeriod returns true if the start of t is at or after (i.e. inclusive) the start of period, and the end of t is before (i.e. non-inclusive) the end of period.

func ParseDateTime

func ParseDateTime(input string, timeZone *time.Location) (time.Time, error)

ParseDateTime parses a string against many different formats. If timezone is omitted, the local is assumed (from global variable `UseTimezone`). If year is omitted, the current one is used.

func ParseIcal

func ParseIcal(r io.Reader) (*ical.Calendar, error)

func ParseTimeOnly

func ParseTimeOnly(input string) (time.Time, error)

func RgbToAnsiSeq

func RgbToAnsiSeq(rgb color.RGBA, background bool) string

func SanitizeFilepath

func SanitizeFilepath(p string) string

SanitizeFilepath escapes a filepath. It prevents root traversal (/) and parent traversal (..), and just cleans it too.

func SanitizePath

func SanitizePath(p string) string

SanitizePath escapes a path. It prevents root traversal (/) and parent traversal (..), and just cleans it too.

func SerializeIcal

func SerializeIcal(ics *ical.Calendar) (bytes.Buffer, error)

func ToIcal

func ToIcal(events []Event, calendarName string) *ical.Calendar

func WriteConfig

func WriteConfig(root string, config Config) error

Types

type CacheJournal

type CacheJournal struct {
	Sources map[string]CacheJournalSource
}

type CacheJournalSource

type CacheJournalSource struct {
	LastUpdate time.Time
}

type CalendarConfig

type CalendarConfig struct {
	Color color.RGBA
}

func (*CalendarConfig) GetColor

func (conf *CalendarConfig) GetColor() color.RGBA

type CalendarSource

type CalendarSource struct {
	Source string
	// Type can be:
	// "native" for a native, dynamic ian calendar.
	// "caldav" for a dynamic CalDAV.
	// "ical" for a static HTTP iCalendar.
	Type string
	// Parsed with time.ParseDuration...
	Lifetime string
	// and inserted here:
	Lifetime_ time.Duration
}

func (*CalendarSource) Import

func (i *CalendarSource) Import(name string) ([]EventProperties, error)

func (*CalendarSource) ImportAndUse

func (i *CalendarSource) ImportAndUse(instance *Instance, name string) error

type Config

type Config struct {
	Calendars map[string]CalendarConfig
	Sources   map[string]CalendarSource
	Hooks     map[string]Hook
}

func ReadConfig

func ReadConfig(root string) (Config, error)

func (*Config) GetContainerConfig

func (conf *Config) GetContainerConfig(container string) (*CalendarConfig, error)

type Event

type Event struct {
	Path EventPath

	Props EventProperties
	Type  EventType
	// Constant is true if the event should not be changed. Used for source events (cache) or the event is generated from a recurrance (RRule).
	Constant bool
	// Parent is the parent event if this event is generated from a recurrence rule. Otherwise nil.
	Parent *Event
}

func BuildEvent

func BuildEvent(path EventPath, props EventProperties, eType EventType) (Event, error)

func FilterEvents

func FilterEvents(events *[]Event, filter func(*Event) bool) []Event

func GetEvent

func GetEvent(events *[]Event, path string) (*Event, error)

TODO change path to type EventPath?

func (*Event) String

func (event *Event) String() string

func (*Event) Write

func (event *Event) Write(instance *Instance) error

Write writes the event to the appropriate location in 'instance'.

type EventPath

type EventPath interface {
	Calendar() string
	Name() string

	String() string
	Filepath(*Instance) string
}

EventPath specifies where to find an event, and its calendar.

func NewEventPath

func NewEventPath(calendar, name string) (EventPath, error)

NewEventPath safely constructs an EventPath from a calendar and name, which can be easily used for determining paths and filenames. name may be modified.

func NewFreeEventPath

func NewFreeEventPath(instance *Instance, calendar, name string) (EventPath, error)

NewFreeEventPath is like NewEventPath, but ensures that the filename is available, possibly by changing it.

func ParseEventPath

func ParseEventPath(input string) (EventPath, error)

type EventProperties

type EventProperties struct {
	Uid string

	Summary     string
	Description string
	Location    string
	Url         string

	// Start is an inclusive datetime representing when the event begins.
	Start time.Time
	// End is a non-inclusive datetime representing when the event ends.
	End time.Time

	Recurrence Recurrence

	Created  time.Time
	Modified time.Time
}

func FromIcal

func FromIcal(cal *ical.Calendar) ([]EventProperties, error)

func FromIcalEvent

func FromIcalEvent(icalEvent ical.Event) (EventProperties, error)

func (*EventProperties) FormatName

func (props *EventProperties) FormatName() string

func (*EventProperties) GetRruleSet

func (props *EventProperties) GetRruleSet() (rrule.Set, error)

func (*EventProperties) GetTimeRange

func (props *EventProperties) GetTimeRange() TimeRange

func (*EventProperties) IsAllDay

func (props *EventProperties) IsAllDay() bool

func (*EventProperties) Validate

func (p *EventProperties) Validate() error

func (*EventProperties) Write

func (props *EventProperties) Write(file string) error

type EventType

type EventType int
const (
	EventTypeNormal EventType = 1 << iota
	EventTypeCache
	EventTypeRecurrence
)

type Hook

type Hook struct {
	// PreCommand is run as a shell command BEFORE an event is updated, in the instance directory.
	// PreCommand has the same environment variables as PostCommand.
	PreCommand string
	// PostCommand is run as a shell command AFTER an event is updated, in the instance directory.
	//
	// Use $MESSAGE in the command to embed the message describing the event change.
	//
	// Use $FILES for a space-separated string with the affected file(s).
	//
	// Use $TYPE for the event type ID.
	//
	// Any stderr output from the command is printed to the user in the form of a warning.
	//
	// Example: 'git add . && git commit -m "$MESSAGE" && (git pull; git push)'
	PostCommand string
	// Type is a bitmask that represents the event(s) to listen to.
	Type SyncEventType
	// Cooldown is parsed as a time.Duration, and is the duration that has to pass before the command is executed again, to prevent fast-paced command execution.
	Cooldown  string
	Cooldown_ time.Duration
}

type Instance

type Instance struct {
	Root   string
	Config Config
}

func CreateInstance

func CreateInstance(root string) (*Instance, error)

func (*Instance) CacheEvent

func (instance *Instance) CacheEvent(subDir string, props EventProperties) error

func (*Instance) CacheEvents

func (instance *Instance) CacheEvents(name string, eventsProps []EventProperties) error

CacheEvents collectively caches a list of events under a certain directory.

func (*Instance) CleanSources

func (instance *Instance) CleanSources() error

func (*Instance) DeleteCache

func (instance *Instance) DeleteCache() error

func (*Instance) NewEvent

func (instance *Instance) NewEvent(props EventProperties, calendar string) (Event, error)

NewEvent constructs a standard event based on properties, as a part of calendar. NewEvent does not write anything.

func (*Instance) ReadCachedEvents

func (instance *Instance) ReadCachedEvents() ([]Event, error)

func (*Instance) ReadEvents

func (instance *Instance) ReadEvents(timeRange TimeRange) ([]Event, []*Event, error)

ReadEvents reads all events in the instance that appear during the time range, and parses their recurrences. If the time range is empty (From.IsZero() && To.IsZero()), then all events are shown, and recurrences are shown within the range of the normal events.

func (*Instance) Sync

func (instance *Instance) Sync(action func() error, eventInfo SyncEvent, ignoreCooldowns bool, stdouterr io.Writer) error

Sync is called whenever changes are made to event(s), with the changes occuring in action, and calls any configured commands.

func (*Instance) UpdateSources

func (instance *Instance) UpdateSources() error

UpdateSources updates the configured sources according to their lifetimes.

func (*Instance) Work

func (instance *Instance) Work() error

Work performs maintenance work and is run on every instance creation. It is used to e.g. update sources.

func (*Instance) WriteNewEvent

func (instance *Instance) WriteNewEvent(props EventProperties, calendar string) (*Event, error)

WriteNewEvent creates an event in the instance by writing it.

type Recurrence

type Recurrence struct {
	RRule  string
	RDate  string
	ExDate string
}

func (*Recurrence) IsThereRecurrence

func (rec *Recurrence) IsThereRecurrence() bool

type SyncCooldownInfo

type SyncCooldownInfo struct {
	Cooldowns map[string]time.Time
}

type SyncEvent

type SyncEvent struct {
	Type    SyncEventType
	Files   []string
	Message string
}

type SyncEventType

type SyncEventType int
const (
	SyncEventPing SyncEventType = 1 << iota
	SyncEventCreate
	SyncEventUpdate
	SyncEventDelete
)

type TimeRange

type TimeRange struct {
	From, To time.Time
}

func (*TimeRange) IsZero

func (tr *TimeRange) IsZero() bool

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL