Skip to content

Commit

Permalink
Implementing reading from ics folders
Browse files Browse the repository at this point in the history
  • Loading branch information
anufrievroman committed Mar 2, 2023
1 parent 6cd24c0 commit bd8e9ae
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 111 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Modern TUI calendar and task manager with customizable interface. Manages your e
## Features

- Vim keys
- View tasks and events from .ics file synced with clouds
- View tasks and events from .ics files synced with clouds
- Operation with fewest key presses possible
- Todo list with subtasks, deadlines, and timers
- Birthdays of your abook contacts
Expand Down
9 changes: 5 additions & 4 deletions calcure/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@

"""This is the main module that contains views and the main logic"""

# Libraries:
import curses
import time
import getopt
import sys
import importlib
import logging

# Modules:
from calcure.calendars import Calendar
from calcure.configuration import cf
from calcure.weather import Weather
Expand Down Expand Up @@ -41,7 +39,7 @@
from calcure.translations.en import *


__version__ = "2.7.6"
__version__ = "2.8.0"


def read_items_from_user_arguments(screen, user_tasks, user_events, task_saver_csv, event_saver_csv):
Expand Down Expand Up @@ -247,7 +245,7 @@ def icon(self):

@property
def color(self):
"""Select the color depending on the status and type"""
"""Assign color depending on the status or calendar number if it's from .ics file"""
if self.event.calendar_number is None:
if self.event.status == Status.IMPORTANT:
return Color.IMPORTANT
Expand Down Expand Up @@ -916,20 +914,23 @@ def main(stdscr) -> None:
weather.load_from_wttr()
screen = Screen(stdscr, cf)

# Initialise loaders:
event_loader_csv = EventLoaderCSV(cf)
task_loader_csv = TaskLoaderCSV(cf)
event_loader_ics = EventLoaderICS(cf)
task_loader_ics = TaskLoaderICS(cf)
birthday_loader = BirthdayLoader(cf)
holiday_loader = HolidayLoader(cf)

# Load the data:
user_events = event_loader_csv.load()
user_tasks = task_loader_csv.load()
user_ics_events = event_loader_ics.load()
user_ics_tasks = task_loader_ics.load()
holidays = holiday_loader.load()
birthdays = birthday_loader.load()

# Initialise savers and importers:
event_saver_csv = EventSaverCSV(user_events, cf)
task_saver_csv = TaskSaverCSV(user_tasks, cf)
importer = Importer(user_tasks, user_events, cf)
Expand Down
3 changes: 1 addition & 2 deletions calcure/calendars.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

import enum
import datetime
from itertools import repeat

import jdatetime
from itertools import repeat


def convert_to_persian_date(year, month, day):
Expand Down
6 changes: 3 additions & 3 deletions calcure/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ def initialize_colors(cf):
if not cf.MINIMAL_DAYS_INDICATOR:
curses.init_pair(Color.DAYS.value, curses.COLOR_BLACK, cf.COLOR_DAYS)

# Assign color pair for each ics resourse:
if cf.ICS_EVENT_FILES is None:
return

# Assign color pair for each ics file:
for index in range(len(cf.ICS_EVENT_FILES)):
if index < len(cf.COLOR_ICS_CALENDARS):
color = cf.COLOR_ICS_CALENDARS[index]
color = cf.COLOR_ICS_CALENDARS[index] # Take colors from config
else:
color = cf.COLOR_EVENTS
color = cf.COLOR_ICS_CALENDARS[-1] # Remaining resourses assume the last assigned color
curses.init_pair(Color.ICS_CALENDARS0.value + index, color, cf.COLOR_BACKGROUND)
219 changes: 118 additions & 101 deletions calcure/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,26 +233,48 @@ def read_lines(self, file):
previous_line = line
return text

def read_file(self, filename):
"""Parse the file or url from user config"""
def read_file(self, path):
"""Parse an ics file if it exists"""
if not os.path.exists(path):
logging.error("Failed to load %s. Probably path is incorrect.", path)
return ""
with open(path, 'r', encoding="utf-8") as file:
return self.read_lines(file)

def read_url(self, path):
"""Parse an ics URL if it exists and networks works"""
try:
with urllib.request.urlopen(path) as response:
file = io.TextIOWrapper(response, 'utf-8')
return self.read_lines(file)
except urllib.error.HTTPError:
logging.error("Failed to load %s. Probably url is wrong.", path)
return ""
except urllib.error.URLError:
logging.error("Failed to load %s. Probably no internet connection.", path)
return ""

def read_resource(self, path):
"""Determine type of the resourse, parse it, and return list of strings for each file"""
ics_files = []

# If it's a URL, try to load it:
if filename.startswith('http'):
try:
with urllib.request.urlopen(filename) as response:
file = io.TextIOWrapper(response, 'utf-8')
return self.read_lines(file)

except urllib.error.HTTPError:
logging.error("Failed to load %s. Probably url is wrong.", filename)
return ""
except urllib.error.URLError:
logging.error("Failed to load %s. Probably no internet connection.", filename)
return ""
if path.startswith('http'):
ics_files.append(self.read_url(path))
return ics_files

# If it's a local file, read it:
with open(filename, 'r', encoding="utf-8") as file:
return self.read_lines(file)
if path.endswith('.ics'):
ics_files.append(self.read_file(path))
return ics_files

# Otherwise, assume it's a folder, and read every file inside:
for root, directories, files in os.walk(path):
for filename in files:
# Get the full path to the file
file_path = os.path.join(root, filename)
ics_files.append(self.read_file(file_path))
return ics_files


class TaskLoaderICS(LoaderICS):
Expand All @@ -266,59 +288,56 @@ def __init__(self, cf):
def load(self):
"""Load tasks from each of the ics files"""

# Quit if the file is not specified:
# Quit if the files are not specified in config:
if self.ics_task_files is None:
return self.user_ics_tasks

for calendar_number, filename in enumerate(self.ics_task_files):

# Quit if file does not exists:
if not os.path.exists(filename) and not filename.startswith('http'):
logging.error("Failed to load %s as it does not seem to exist.", filename)
return self.user_ics_tasks

ics_text = self.read_file(filename)

# Try parcing ics file:
try:
cal = ics.Calendar(ics_text)
except NotImplementedError: # More than one calendar in the file
logging.error("Failed to load %s.", filename)
return self.user_ics_tasks


for task in cal.todos:
if task.status != "CANCELLED":
task_id = self.user_ics_tasks.generate_id()

# Assign status from priority:
status = Status.NORMAL
if task.priority is not None:
if task.priority > 5:
status = Status.UNIMPORTANT
if task.priority < 5:
status = Status.IMPORTANT

# Correct according to status:
if task.status == "COMPLETED":
status = Status.DONE

name = task.name

# Try reading task due date:
try:
year = task.due.year
month = task.due.month
day = task.due.day
except AttributeError:
year, month, day = 0, 0, 0

timer = Timer([])
is_private = False

# Add task:
new_task = Task(task_id, name, status, timer, is_private, year, month, day, calendar_number)
self.user_ics_tasks.add_item(new_task)
# For each resourse from config, load a list that has one or more ics files:
ics_files = self.read_resource(filename)
for ics_file in ics_files:

# Try parcing content of the ics file:
try:
cal = ics.Calendar(ics_file)
except NotImplementedError: # More than one calendar in the file
logging.error("Failed to load %s.", filename)
return self.user_ics_tasks

for task in cal.todos:
if task.status != "CANCELLED":
task_id = self.user_ics_tasks.generate_id()

# Assign status from priority:
status = Status.NORMAL
if task.priority is not None:
if task.priority > 5:
status = Status.UNIMPORTANT
if task.priority < 5:
status = Status.IMPORTANT

# Correct according to status:
if task.status == "COMPLETED":
status = Status.DONE

name = task.name

# Try reading task due date:
try:
year = task.due.year
month = task.due.month
day = task.due.day
except AttributeError:
year, month, day = 0, 0, 0

timer = Timer([])
is_private = False

# Add task:
new_task = Task(task_id, name, status, timer, is_private,
year, month, day, calendar_number)
self.user_ics_tasks.add_item(new_task)

return self.user_ics_tasks

Expand All @@ -334,54 +353,52 @@ def __init__(self, cf):
def load(self):
"""Load events from each of the ics files"""

# Quit if the file is not specified:
# Quit if the files are not specified in config:
if self.ics_event_files is None:
return self.user_ics_events

for calendar_number, filename in enumerate(self.ics_event_files):

# Quit if file does not exists:
if not os.path.exists(filename) and not filename.startswith('http'):
logging.error("Failed to load %s as it does not seem to exist.", filename)
return self.user_ics_events
# For each resourse from config, load a list that has one or more ics files:
ics_files = self.read_resource(filename)
for ics_file in ics_files:

ics_text = self.read_file(filename)
# Try parcing content of the ics file:
try:
cal = ics.Calendar(ics_file)
except NotImplementedError: # More than one calendar in the file
logging.error("Failed to load %s.", filename)
return self.user_ics_events

# Try parcing ics file:
try:
cal = ics.Calendar(ics_text)
except NotImplementedError: # More than one calendar in the file
logging.error("Failed to load %s.", filename)
return self.user_ics_events
for index, event in enumerate(cal.events):

for index, event in enumerate(cal.events):

# Default parameters:
event_id = index
repetition = '1'
frequency = Frequency.ONCE
status = Status.NORMAL
is_private = False
# Default parameters:
event_id = index
repetition = '1'
frequency = Frequency.ONCE
status = Status.NORMAL
is_private = False

# Parameters of the event from ics if they exist:
name = event.name if event.name is not None else ""
all_day = event.all_day if event.all_day is not None else True
year = event.begin.year if event.begin else 0
month = event.begin.month if event.begin else 1
day = event.begin.day if event.begin else 1
# Parameters of the event from ics if they exist:
name = event.name if event.name is not None else ""
all_day = event.all_day if event.all_day is not None else True
year = event.begin.year if event.begin else 0
month = event.begin.month if event.begin else 1
day = event.begin.day if event.begin else 1

# Add start time to name of non-all-day events:
if not all_day:
hour = event.begin.hour if event.begin else 0
minute = event.begin.minute if event.begin else 0
name = f"{hour:0=2}:{minute:0=2} {name}"
# Add start time to name of non-all-day events:
if not all_day:
hour = event.begin.hour if event.begin else 0
minute = event.begin.minute if event.begin else 0
name = f"{hour:0=2}:{minute:0=2} {name}"

# Convert to persian date if needed:
if self.use_persian_calendar:
year, month, day = convert_to_persian_date(year, month, day)
# Convert to persian date if needed:
if self.use_persian_calendar:
year, month, day = convert_to_persian_date(year, month, day)

# Add event:
new_event = UserEvent(event_id, year, month, day, name, repetition, frequency, status, is_private, calendar_number)
self.user_ics_events.add_item(new_event)
# Add event:
new_event = UserEvent(event_id, year, month, day, name, repetition,
frequency, status, is_private, calendar_number)
self.user_ics_events.add_item(new_event)

return self.user_ics_events

0 comments on commit bd8e9ae

Please sign in to comment.