2025-11-30 18:01:30 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Inkycal Calendar Module
|
|
|
|
|
|
Copyright by aceinnolab
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# pylint: disable=logging-fstring-interpolation
|
|
|
|
|
|
|
|
|
|
|
|
import calendar as cal
|
|
|
|
|
|
|
|
|
|
|
|
from inkycal.custom import *
|
|
|
|
|
|
from inkycal.modules.template import inkycal_module
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
2025-11-30 21:17:02 +01:00
|
|
|
|
class TaskEntry:
|
|
|
|
|
|
"""Class representing a task entry."""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, title, project=None, parent_project=None, subtasks=None):
|
|
|
|
|
|
self.title = title
|
|
|
|
|
|
self.project = project
|
|
|
|
|
|
self.consumed_time = 0 # in minutes
|
|
|
|
|
|
self.subtasks = []
|
|
|
|
|
|
def add_subtask(self, subtask):
|
|
|
|
|
|
"""Add a subtask to the task entry."""
|
|
|
|
|
|
self.subtasks.append(subtask)
|
|
|
|
|
|
def mock_task_list():
|
|
|
|
|
|
"""Generate a mock task list for testing purposes."""
|
|
|
|
|
|
finetune = TaskEntry("3 new models finetune work", project="AISentry")
|
|
|
|
|
|
generate_data = TaskEntry("Generate training data", project="AISentry")
|
|
|
|
|
|
function_development = TaskEntry("Function development", project="AISentry")
|
|
|
|
|
|
finetune.add_subtask(generate_data)
|
|
|
|
|
|
finetune.add_subtask(function_development)
|
|
|
|
|
|
|
|
|
|
|
|
check_llama = TaskEntry("Check Llama model performance", project="llama.cpp")
|
|
|
|
|
|
transform = TaskEntry("Transformers library exploration", project="llama.cpp")
|
|
|
|
|
|
check_llama.add_subtask(transform)
|
|
|
|
|
|
|
|
|
|
|
|
research_work = TaskEntry("Research new AI techniques", project="AISentry")
|
|
|
|
|
|
meeting = TaskEntry("Team meeting", project="General")
|
|
|
|
|
|
return [finetune, check_llama, research_work, meeting]
|
|
|
|
|
|
|
2025-11-30 18:01:30 +01:00
|
|
|
|
def get_ip_address():
|
|
|
|
|
|
"""Get public IP address from external service."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 方法1: 使用 ipify.org
|
|
|
|
|
|
response = requests.get('https://api.ipify.org?format=json', timeout=5)
|
|
|
|
|
|
return response.json()['ip']
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 方法2: 使用 icanhazip.com (备用)
|
|
|
|
|
|
response = requests.get('https://icanhazip.com', timeout=5)
|
|
|
|
|
|
return response.text.strip()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 方法3: 使用 ifconfig.me (备用)
|
|
|
|
|
|
response = requests.get('https://ifconfig.me/ip', timeout=5)
|
|
|
|
|
|
return response.text.strip()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return "N/A"
|
|
|
|
|
|
|
|
|
|
|
|
class Today(inkycal_module):
|
|
|
|
|
|
"""today class
|
|
|
|
|
|
Show today's date and events from given iCalendars
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
name = "Today - Show today's date and events from iCalendars"
|
|
|
|
|
|
|
|
|
|
|
|
optional = {
|
|
|
|
|
|
"date_format": {
|
|
|
|
|
|
"label": "Use an arrow-supported token for custom date formatting "
|
|
|
|
|
|
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM",
|
|
|
|
|
|
"default": "D MMM",
|
|
|
|
|
|
},
|
|
|
|
|
|
"time_format": {
|
|
|
|
|
|
"label": "Use an arrow-supported token for custom time formatting "
|
|
|
|
|
|
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
|
|
|
|
|
|
"default": "HH:mm",
|
|
|
|
|
|
},
|
2025-11-30 21:32:21 +01:00
|
|
|
|
"webdav_hostname": {
|
|
|
|
|
|
"label": "WebDAV Hostname (e.g. https://webdav.server.com)",
|
|
|
|
|
|
"default": "",
|
|
|
|
|
|
},
|
|
|
|
|
|
"webdav_login": {
|
|
|
|
|
|
"label": "WebDAV Login Username",
|
|
|
|
|
|
"default": "",
|
|
|
|
|
|
},
|
|
|
|
|
|
"webdav_password": {
|
|
|
|
|
|
"label": "WebDAV Login Password",
|
|
|
|
|
|
"default": "",
|
|
|
|
|
|
},
|
|
|
|
|
|
"webdav_file_path": {
|
|
|
|
|
|
"label": "WebDAV File Path to Super Productivity JSON file",
|
|
|
|
|
|
"default": "",
|
|
|
|
|
|
},
|
2025-11-30 18:01:30 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, config):
|
|
|
|
|
|
"""Initialize inkycal_calendar module"""
|
|
|
|
|
|
|
|
|
|
|
|
super().__init__(config)
|
|
|
|
|
|
config = config['config']
|
|
|
|
|
|
|
|
|
|
|
|
self.ical = None
|
|
|
|
|
|
self.month_events = None
|
|
|
|
|
|
self._upcoming_events = None
|
|
|
|
|
|
self._days_with_events = None
|
|
|
|
|
|
|
|
|
|
|
|
# optional parameters
|
|
|
|
|
|
self.week_start = config['week_starts_on']
|
|
|
|
|
|
self.show_events = config['show_events']
|
|
|
|
|
|
self.date_format = config["date_format"]
|
|
|
|
|
|
self.time_format = config['time_format']
|
|
|
|
|
|
self.language = config['language']
|
|
|
|
|
|
|
2025-11-30 21:32:21 +01:00
|
|
|
|
# webdav configuration
|
|
|
|
|
|
self.webdav_options = {
|
|
|
|
|
|
'webdav_hostname': config.get('webdav_hostname', ''),
|
|
|
|
|
|
'webdav_login': config.get('webdav_login', ''),
|
|
|
|
|
|
'webdav_password': config.get('webdav_password', ''),
|
|
|
|
|
|
'webdav_file_path': config.get('webdav_file_path', ''),
|
|
|
|
|
|
}
|
2025-11-30 18:01:30 +01:00
|
|
|
|
|
|
|
|
|
|
# additional configuration
|
|
|
|
|
|
self.timezone = get_system_tz()
|
2025-11-30 21:17:02 +01:00
|
|
|
|
|
|
|
|
|
|
# 选择字体:优先使用支持中文的 NotoSansCJK,否则使用 NotoSans
|
|
|
|
|
|
self._font_family = self._select_font_family()
|
2025-11-30 18:01:30 +01:00
|
|
|
|
|
|
|
|
|
|
# give an OK message
|
|
|
|
|
|
logger.debug(f'{__name__} loaded')
|
2025-11-30 21:17:02 +01:00
|
|
|
|
|
|
|
|
|
|
def _select_font_family(self) -> str:
|
|
|
|
|
|
preferred_fonts = [
|
|
|
|
|
|
'NotoSansCJKsc-Regular',
|
|
|
|
|
|
'NotoSans-SemiCondensed'
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
for font_name in preferred_fonts:
|
|
|
|
|
|
if font_name in fonts:
|
|
|
|
|
|
logger.debug(f'Selected font: {font_name}')
|
|
|
|
|
|
return font_name
|
|
|
|
|
|
|
|
|
|
|
|
return list(fonts.keys())[0]
|
|
|
|
|
|
|
|
|
|
|
|
def _get_font(self, size: int) -> ImageFont.FreeTypeFont:
|
|
|
|
|
|
return ImageFont.truetype(fonts[self._font_family], size=size)
|
2025-11-30 18:01:30 +01:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def flatten(values):
|
|
|
|
|
|
"""Flatten the values."""
|
|
|
|
|
|
return [x for y in values for x in y]
|
|
|
|
|
|
|
|
|
|
|
|
def generate_image(self):
|
|
|
|
|
|
"""Generate the image for today's date and events. """
|
|
|
|
|
|
|
|
|
|
|
|
# ****************************************************************************************************************
|
|
|
|
|
|
# Create base image
|
|
|
|
|
|
|
|
|
|
|
|
# Define new image size with respect to padding
|
|
|
|
|
|
im_width = self.width - 2 * self.padding_left
|
|
|
|
|
|
im_height = self.height - 2 * self.padding_top
|
|
|
|
|
|
im_size = (im_width, im_height)
|
|
|
|
|
|
|
|
|
|
|
|
event_height = 0
|
|
|
|
|
|
|
|
|
|
|
|
logger.debug(f'Generating Today module image of size {im_size}')
|
|
|
|
|
|
|
|
|
|
|
|
# Create an iamge for black and colour Inky displays
|
|
|
|
|
|
im_black = Image.new('RGB', im_size, color='white')
|
|
|
|
|
|
im_colour = Image.new('RGB', im_size, color='white')
|
|
|
|
|
|
|
|
|
|
|
|
# Split the image into two sections: date section and events section
|
|
|
|
|
|
left_section_width = int(im_width * 0.2)
|
|
|
|
|
|
right_section_width = im_width - left_section_width
|
|
|
|
|
|
|
|
|
|
|
|
# 5% bottom space will be reserved for show the day progress bar
|
|
|
|
|
|
left_section = (0, 0, left_section_width, im_height - int(im_height * 0.05))
|
|
|
|
|
|
right_section = (left_section_width, 0, im_width, im_height - int(im_height * 0.05))
|
|
|
|
|
|
|
|
|
|
|
|
section_height = left_section[3]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ****************************************************************************************************************
|
|
|
|
|
|
# Edit left section - show today's date
|
|
|
|
|
|
now = arrow.now(tz=self.timezone)
|
|
|
|
|
|
month_height = int(im_height * 0.15)
|
2025-11-30 21:17:02 +01:00
|
|
|
|
month_font = self._get_font(int(self.fontsize * 1.5))
|
2025-11-30 18:01:30 +01:00
|
|
|
|
write(
|
|
|
|
|
|
im_black,
|
|
|
|
|
|
(0, 0),
|
|
|
|
|
|
(left_section_width, month_height),
|
|
|
|
|
|
now.format('MMMM', locale=self.language),
|
|
|
|
|
|
font=month_font,
|
|
|
|
|
|
autofit=False
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
date_height = int(im_height * 0.5)
|
|
|
|
|
|
date_y = month_height
|
2025-11-30 21:17:02 +01:00
|
|
|
|
large_font = self._get_font(int(self.fontsize * 4))
|
2025-11-30 18:01:30 +01:00
|
|
|
|
date_time = arrow.now()
|
|
|
|
|
|
day = date_time.day
|
|
|
|
|
|
print(str(day))
|
|
|
|
|
|
write(
|
|
|
|
|
|
im_colour,
|
|
|
|
|
|
(0, date_y),
|
|
|
|
|
|
(left_section_width, date_height),
|
|
|
|
|
|
str(day),
|
|
|
|
|
|
font=large_font,
|
|
|
|
|
|
autofit=False
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
weekday_y = month_height + date_height
|
|
|
|
|
|
weekday_height = int(im_height * 0.15)
|
2025-11-30 21:17:02 +01:00
|
|
|
|
weekday_font = self._get_font(int(self.fontsize * 2))
|
2025-11-30 18:01:30 +01:00
|
|
|
|
write(
|
|
|
|
|
|
im_black,
|
|
|
|
|
|
(0, weekday_y),
|
|
|
|
|
|
(left_section_width, weekday_height),
|
|
|
|
|
|
now.format('dddd', locale=self.language),
|
|
|
|
|
|
font=weekday_font,
|
|
|
|
|
|
autofit=False
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# show IP address at the bottom left
|
|
|
|
|
|
ip_y = weekday_y + weekday_height
|
|
|
|
|
|
ip_height = im_height - ip_y - 5
|
|
|
|
|
|
ip_address = get_ip_address()
|
|
|
|
|
|
write(
|
|
|
|
|
|
im_black,
|
|
|
|
|
|
(0, ip_y),
|
|
|
|
|
|
(left_section_width, ip_height),
|
|
|
|
|
|
ip_address,
|
|
|
|
|
|
font=self.font,
|
|
|
|
|
|
alignment='center',
|
|
|
|
|
|
autofit=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ****************************************************************************************************************
|
|
|
|
|
|
# Draw a dash line to separate left and right sections
|
|
|
|
|
|
for _y in range(0, section_height, 8):
|
|
|
|
|
|
|
|
|
|
|
|
ImageDraw.Draw(im_black).line(
|
|
|
|
|
|
[(left_section_width, _y), (left_section_width, _y + 4)],
|
|
|
|
|
|
fill='black',
|
|
|
|
|
|
width=2,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ****************************************************************************************************************
|
|
|
|
|
|
# Edit right section - show today's events
|
|
|
|
|
|
if self.show_events:
|
|
|
|
|
|
# 导入日历解析器
|
2025-11-30 21:17:02 +01:00
|
|
|
|
upcoming_events = True
|
2025-11-30 18:01:30 +01:00
|
|
|
|
|
|
|
|
|
|
# 计算右侧可用空间
|
|
|
|
|
|
right_x = left_section_width + 5 # 留5px边距
|
|
|
|
|
|
right_usable_width = right_section_width - 10 # 左右各留5px
|
|
|
|
|
|
|
|
|
|
|
|
# 计算行高
|
|
|
|
|
|
line_spacing = 2
|
|
|
|
|
|
text_bbox = self.font.getbbox("hg")
|
|
|
|
|
|
line_height = text_bbox[3] + line_spacing
|
|
|
|
|
|
max_lines = im_height // line_height
|
2025-11-30 21:17:02 +01:00
|
|
|
|
|
|
|
|
|
|
from inkycal.modules.super_productivity_utils import get_today_tasks
|
|
|
|
|
|
|
2025-11-30 21:32:21 +01:00
|
|
|
|
import requests
|
|
|
|
|
|
url = self.webdav_options['webdav_hostname'] + self.webdav_options['webdav_file_path']
|
|
|
|
|
|
response = requests.get(url, auth=(
|
|
|
|
|
|
self.webdav_options['webdav_login'],
|
|
|
|
|
|
self.webdav_options['webdav_password']
|
|
|
|
|
|
))
|
|
|
|
|
|
content = response.content
|
|
|
|
|
|
content = content[8:]
|
|
|
|
|
|
with open('/workspaces/Inkycal/inkycal/modules/super_productivity.json', 'wb') as f:
|
|
|
|
|
|
f.write(content)
|
2025-11-30 21:17:02 +01:00
|
|
|
|
|
|
|
|
|
|
json_file_path = '/workspaces/Inkycal/inkycal/modules/super_productivity.json'
|
|
|
|
|
|
task_list = get_today_tasks(json_file_path)
|
2025-11-30 18:01:30 +01:00
|
|
|
|
|
|
|
|
|
|
if upcoming_events:
|
2025-11-30 21:17:02 +01:00
|
|
|
|
# Split to 2 parts
|
|
|
|
|
|
# Left part: title
|
|
|
|
|
|
# Right part: Project name
|
|
|
|
|
|
current_line = 0
|
|
|
|
|
|
for idx, event in enumerate(task_list):
|
|
|
|
|
|
if current_line >= max_lines:
|
|
|
|
|
|
break # 超出显示范围,停止绘制
|
2025-11-30 18:01:30 +01:00
|
|
|
|
|
2025-11-30 21:17:02 +01:00
|
|
|
|
# 写任务标题
|
2025-11-30 18:01:30 +01:00
|
|
|
|
write(
|
|
|
|
|
|
im_black,
|
2025-11-30 21:17:02 +01:00
|
|
|
|
(right_x, current_line * line_height),
|
|
|
|
|
|
(int(right_usable_width * 0.7), line_height),
|
|
|
|
|
|
event.title,
|
2025-11-30 18:01:30 +01:00
|
|
|
|
font=self.font,
|
|
|
|
|
|
alignment='left'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-30 21:17:02 +01:00
|
|
|
|
# 写项目名称
|
|
|
|
|
|
project_name = event.project_name if event.project_name else "Inbox"
|
2025-11-30 18:01:30 +01:00
|
|
|
|
|
|
|
|
|
|
write(
|
2025-11-30 21:17:02 +01:00
|
|
|
|
im_colour,
|
|
|
|
|
|
(right_x + int(right_usable_width * 0.7), current_line * line_height),
|
|
|
|
|
|
(int(right_usable_width * 0.3), line_height),
|
|
|
|
|
|
project_name,
|
2025-11-30 18:01:30 +01:00
|
|
|
|
font=self.font,
|
2025-11-30 21:17:02 +01:00
|
|
|
|
alignment='right'
|
2025-11-30 18:01:30 +01:00
|
|
|
|
)
|
2025-11-30 21:17:02 +01:00
|
|
|
|
current_line += 1
|
|
|
|
|
|
if event.subtasks:
|
|
|
|
|
|
for sub_idx, subtask in enumerate(event.subtasks):
|
|
|
|
|
|
if subtask.is_done:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if current_line + sub_idx + 1 >= max_lines:
|
|
|
|
|
|
break # 超出显示范围,停止绘制
|
|
|
|
|
|
# 写子任务标题,缩进显示
|
|
|
|
|
|
write(
|
|
|
|
|
|
im_black,
|
|
|
|
|
|
(right_x + 10, current_line * line_height),
|
|
|
|
|
|
(int(right_usable_width * 0.7) - 10, line_height),
|
|
|
|
|
|
f"- {subtask.title}",
|
|
|
|
|
|
font=self.font,
|
|
|
|
|
|
alignment='left'
|
|
|
|
|
|
)
|
|
|
|
|
|
current_line += 1 # 更新主循环的索引
|
|
|
|
|
|
pass
|
2025-11-30 18:01:30 +01:00
|
|
|
|
else:
|
|
|
|
|
|
# 没有事件时显示提示
|
|
|
|
|
|
write(
|
2025-11-30 21:32:21 +01:00
|
|
|
|
im_colour,
|
2025-11-30 18:01:30 +01:00
|
|
|
|
(right_x, int(im_height / 2)),
|
|
|
|
|
|
(right_usable_width, line_height),
|
|
|
|
|
|
"No events today",
|
|
|
|
|
|
font=self.font,
|
|
|
|
|
|
alignment='center'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ****************************************************************************************************************
|
|
|
|
|
|
# Draw progress bar at the bottom (24 segments for 24 hours)
|
|
|
|
|
|
progress_bar_height = int(im_height * 0.05)
|
|
|
|
|
|
progress_bar_y = im_height - progress_bar_height
|
|
|
|
|
|
|
|
|
|
|
|
# 计算当前小时进度
|
|
|
|
|
|
current_hour = now.hour
|
|
|
|
|
|
current_minute = now.minute
|
|
|
|
|
|
current_progress = current_hour + (current_minute / 60.0) # 0-24 的浮点数
|
|
|
|
|
|
|
|
|
|
|
|
# 绘制24个格子
|
|
|
|
|
|
num_segments = 24
|
|
|
|
|
|
segment_spacing = 2 # 格子之间的间距
|
|
|
|
|
|
total_spacing = segment_spacing * (num_segments - 1)
|
|
|
|
|
|
segment_width = (im_width - total_spacing) / num_segments
|
|
|
|
|
|
|
|
|
|
|
|
draw = ImageDraw.Draw(im_black)
|
|
|
|
|
|
|
|
|
|
|
|
for i in range(num_segments):
|
|
|
|
|
|
# 计算每个格子的位置
|
|
|
|
|
|
x_start = int(i * (segment_width + segment_spacing))
|
|
|
|
|
|
x_end = int(x_start + segment_width)
|
|
|
|
|
|
|
|
|
|
|
|
# 判断该格子是否已完成
|
|
|
|
|
|
if i < current_progress:
|
|
|
|
|
|
# 已完成的格子填充黑色(在 im_colour 上会显示为红色)
|
|
|
|
|
|
draw.rectangle(
|
|
|
|
|
|
[(x_start, progress_bar_y), (x_end, im_height)],
|
|
|
|
|
|
fill='black',
|
|
|
|
|
|
outline='black'
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 未完成的格子只画边框(在 im_black 上画)
|
|
|
|
|
|
ImageDraw.Draw(im_black).rectangle(
|
|
|
|
|
|
[(x_start, progress_bar_y), (x_end, im_height)],
|
|
|
|
|
|
fill='white',
|
|
|
|
|
|
outline='black',
|
|
|
|
|
|
width=1
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return im_black, im_colour
|