From 8b99fc33ac3faa48f680a2d59f9ea68f46170f82 Mon Sep 17 00:00:00 2001 From: Hanzhang Ma Date: Sun, 30 Nov 2025 21:17:02 +0100 Subject: [PATCH] add utils --- .devcontainer/devcontainer.json | 2 +- inky_run.py | 2 +- inkycal/modules/inkycal_today.py | 144 ++++++++---- inkycal/modules/super_productivity_utils.py | 241 ++++++++++++++++++++ inkycal/modules/template.py | 3 +- inkycal/settings.json | 2 +- test_display.py | 4 + tests/test_inkycal_today.py | 3 +- 8 files changed, 349 insertions(+), 52 deletions(-) create mode 100644 inkycal/modules/super_productivity_utils.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1bf5540..7c637bb 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,7 +7,7 @@ }, // This is the settings.json mount - "mounts": ["source=/c/temp/settings_test.json,target=/boot/settings.json,type=bind,consistency=cached"], + "mounts": ["source=/mnt/c/temp/settings_test.json,target=/boot/settings.json,type=bind,consistency=cached"], // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": "dos2unix ./.devcontainer/postCreate.sh && chmod +x ./.devcontainer/postCreate.sh && ./.devcontainer/postCreate.sh", diff --git a/inky_run.py b/inky_run.py index b0b6b31..b6fc8e8 100644 --- a/inky_run.py +++ b/inky_run.py @@ -40,4 +40,4 @@ async def clear_display(): if __name__ == "__main__": - asyncio.run(run()) + asyncio.run(dry_run()) diff --git a/inkycal/modules/inkycal_today.py b/inkycal/modules/inkycal_today.py index 0312cde..9151f41 100644 --- a/inkycal/modules/inkycal_today.py +++ b/inkycal/modules/inkycal_today.py @@ -12,6 +12,33 @@ from inkycal.modules.template import inkycal_module logger = logging.getLogger(__name__) +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] + def get_ip_address(): """Get public IP address from external service.""" try: @@ -97,12 +124,35 @@ class Today(inkycal_module): # additional configuration self.timezone = get_system_tz() + + # 选择字体:优先使用支持中文的 NotoSansCJK,否则使用 NotoSans + self._font_family = self._select_font_family() self.num_font = ImageFont.truetype( - fonts['NotoSans-SemiCondensed'], size=self.fontsize + fonts[self._font_family], size=self.fontsize ) # give an OK message logger.debug(f'{__name__} loaded') + + def _select_font_family(self) -> str: + """选择合适的字体族(支持中文优先)""" + # 优先级:NotoSansCJKsc (支持中文) > NotoSans + 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) @staticmethod def flatten(values): @@ -143,9 +193,7 @@ class Today(inkycal_module): # Edit left section - show today's date now = arrow.now(tz=self.timezone) month_height = int(im_height * 0.15) - month_font = ImageFont.truetype( - fonts['NotoSans-SemiCondensed'], size=int(self.fontsize * 1.5) - ) + month_font = self._get_font(int(self.fontsize * 1.5)) write( im_black, (0, 0), @@ -157,9 +205,7 @@ class Today(inkycal_module): date_height = int(im_height * 0.5) date_y = month_height - large_font = ImageFont.truetype( - fonts['NotoSans-SemiCondensed'], size=int(self.fontsize * 4) - ) + large_font = self._get_font(int(self.fontsize * 4)) date_time = arrow.now() day = date_time.day print(str(day)) @@ -174,9 +220,7 @@ class Today(inkycal_module): weekday_y = month_height + date_height weekday_height = int(im_height * 0.15) - weekday_font = ImageFont.truetype( - fonts['NotoSans-SemiCondensed'], size=int(self.fontsize * 2) - ) + weekday_font = self._get_font(int(self.fontsize * 2)) write( im_black, (0, weekday_y), @@ -214,19 +258,7 @@ class Today(inkycal_module): # Edit right section - show today's events if self.show_events: # 导入日历解析器 - from inkycal.modules.ical_parser import iCalendar - - parser = iCalendar() - - if self.ical_urls: - parser.load_url(self.ical_urls) - if self.ical_files: - parser.load_from_file(self.ical_files) - - # 获取今天的事件 - today_start = now.floor('day') - today_end = now.ceil('day') - upcoming_events = parser.get_events(today_start, today_end, self.timezone) + upcoming_events = True # 计算右侧可用空间 right_x = left_section_width + 5 # 留5px边距 @@ -237,43 +269,61 @@ class Today(inkycal_module): text_bbox = self.font.getbbox("hg") line_height = text_bbox[3] + line_spacing max_lines = im_height // line_height + + from inkycal.modules.super_productivity_utils import get_today_tasks + + + json_file_path = '/workspaces/Inkycal/inkycal/modules/super_productivity.json' + task_list = get_today_tasks(json_file_path) if upcoming_events: - # 显示事件 - cursor = 0 - for event in upcoming_events[:max_lines]: - if cursor >= max_lines: - break - - y_pos = cursor * line_height - - # 显示时间 - time_str = event['begin'].format(self.time_format, locale=self.language) - time_width = int(self.font.getlength(time_str) * 1.1) + # 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 # 超出显示范围,停止绘制 + # 写任务标题 write( im_black, - (right_x, y_pos), - (time_width, line_height), - time_str, + (right_x, current_line * line_height), + (int(right_usable_width * 0.7), line_height), + event.title, font=self.font, alignment='left' ) - # 显示事件标题 - event_x = right_x + time_width + 5 - event_width = right_usable_width - time_width - 5 + # 写项目名称 + project_name = event.project_name if event.project_name else "Inbox" write( - im_black, - (event_x, y_pos), - (event_width, line_height), - event['title'], + im_colour, + (right_x + int(right_usable_width * 0.7), current_line * line_height), + (int(right_usable_width * 0.3), line_height), + project_name, font=self.font, - alignment='left' + alignment='right' ) - - cursor += 1 + 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 else: # 没有事件时显示提示 write( diff --git a/inkycal/modules/super_productivity_utils.py b/inkycal/modules/super_productivity_utils.py new file mode 100644 index 0000000..0be96d2 --- /dev/null +++ b/inkycal/modules/super_productivity_utils.py @@ -0,0 +1,241 @@ +""" +Super Productivity 数据解析工具 +用于从 Super Productivity 导出的 JSON 中提取任务信息 +""" + +import json +from datetime import date +from typing import List, Dict, Optional, Tuple + + +class TaskEntry: + """任务条目类""" + + def __init__( + self, + task_id: str, + title: str, + project_name: str, + subtasks: Optional[List['TaskEntry']] = None, + is_done: bool = False, + time_spent: int = 0 + ): + self.id = task_id + self.title = title + self.project_name = project_name + self.subtasks = subtasks or [] + self.is_done = is_done + self.time_spent = time_spent # 单位:毫秒 + + def __repr__(self): + return f"TaskEntry(title='{self.title}', project='{self.project_name}', subtasks={len(self.subtasks)})" + + def get_time_spent_hours(self) -> float: + """获取花费的时间(小时)""" + return self.time_spent / 3600000.0 + + +def parse_super_productivity_json(json_file_path: str) -> Tuple[Dict, Dict, Dict]: + """ + 解析 Super Productivity JSON 文件 + + Args: + json_file_path: JSON 文件路径 + + Returns: + (tasks, projects, tags) 元组 + """ + with open(json_file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + tasks = data['mainModelData']['task']['entities'] + projects = data['mainModelData']['project']['entities'] + tags = data['mainModelData']['tag']['entities'] + + return tasks, projects, tags + + +def get_project_name(projects: Dict, project_id: str) -> str: + """获取项目名称""" + if project_id in projects: + return projects[project_id]['title'] + return "Inbox" + + +def parse_task_entry( + task_id: str, + tasks: Dict, + projects: Dict +) -> Optional[TaskEntry]: + """ + 解析单个任务条目 + + Args: + task_id: 任务 ID + tasks: 所有任务字典 + projects: 所有项目字典 + + Returns: + TaskEntry 对象或 None + """ + task = tasks.get(task_id) + if not task: + return None + + # 获取基本信息 + title = task.get('title', 'Untitled') + project_id = task.get('projectId', 'INBOX_PROJECT') + project_name = get_project_name(projects, project_id) + is_done = task.get('isDone', False) + time_spent = task.get('timeSpent', 0) + + # 解析子任务 + subtask_ids = task.get('subTaskIds', []) + subtasks = [] + for subtask_id in subtask_ids: + subtask = parse_task_entry(subtask_id, tasks, projects) + if subtask: + subtasks.append(subtask) + + return TaskEntry( + task_id=task_id, + title=title, + project_name=project_name, + subtasks=subtasks, + is_done=is_done, + time_spent=time_spent + ) + + +def get_today_tasks( + json_file_path: str, + target_date: Optional[date] = None, + include_done: bool = False +) -> List[TaskEntry]: + """ + 获取今天 due 的任务 + + Args: + json_file_path: JSON 文件路径 + target_date: 目标日期,默认为今天 + include_done: 是否包含已完成的任务 + + Returns: + 今天的任务列表 + """ + if target_date is None: + target_date = date.today() + + today_str = target_date.strftime('%Y-%m-%d') + tasks, projects, _ = parse_super_productivity_json(json_file_path) + + today_tasks = [] + for task_id, task in tasks.items(): + # 检查是否是今天的任务 + due_day = task.get('dueDay') + # print(f'Task ID: {task_id}, Due Day: {due_day}, Today: {today_str}') # 调试输出 + if due_day != today_str: + continue + + # 检查是否已完成 + if not include_done and task.get('isDone', False): + continue + + # 只添加顶层任务(没有 parentId 的) + if not task.get('parentId'): + parsed_task = parse_task_entry(task_id, tasks, projects) + if parsed_task: + today_tasks.append(parsed_task) + + return today_tasks + + +def get_tasks_by_tag( + json_file_path: str, + tag_name: str, + include_done: bool = False +) -> List[TaskEntry]: + """ + 根据标签获取任务 + + Args: + json_file_path: JSON 文件路径 + tag_name: 标签名称(如 "TODAY") + include_done: 是否包含已完成的任务 + + Returns: + 带有该标签的任务列表 + """ + tasks, projects, tags = parse_super_productivity_json(json_file_path) + + # 查找标签 ID + tag_id = None + for tid, tag in tags.items(): + if tag.get('title') == tag_name: + tag_id = tid + break + + if not tag_id: + return [] + + # 获取带有该标签的任务 + task_ids = tags[tag_id].get('taskIds', []) + result_tasks = [] + + for task_id in task_ids: + task = tasks.get(task_id) + if not task: + continue + + # 检查是否已完成 + if not include_done and task.get('isDone', False): + continue + + parsed_task = parse_task_entry(task_id, tasks, projects) + if parsed_task: + result_tasks.append(parsed_task) + + return result_tasks + + +if __name__ == "__main__": + """测试代码""" + import sys + + if len(sys.argv) < 2: + print("Usage: python super_productivity_utils.py ") + sys.exit(1) + + json_file = sys.argv[1] + + # 获取今天的任务 + print("=" * 60) + print("📅 TODAY'S TASKS (with due date)") + print("=" * 60) + today_tasks = get_today_tasks(json_file) + + for task in today_tasks: + print(f"\n📋 {task.title}") + print(f" 📁 Project: {task.project_name}") + + if task.subtasks: + print(f" └─ Subtasks ({len(task.subtasks)}):") + for subtask in task.subtasks: + status = "✓" if subtask.is_done else "○" + print(f" {status} {subtask.title}") + + # 获取 TODAY 标签的任务 + print("\n" + "=" * 60) + print("🏷️ TASKS WITH 'TODAY' TAG") + print("=" * 60) + tagged_tasks = get_tasks_by_tag(json_file, "TODAY") + + for task in tagged_tasks: + print(f"\n📋 {task.title}") + print(f" 📁 Project: {task.project_name}") + if task.time_spent > 0: + print(f" ⏱️ Time spent: {task.get_time_spent_hours():.1f}h") + + print("\n" + "=" * 60) + print(f"📊 Summary: {len(today_tasks)} due today, {len(tagged_tasks)} tagged TODAY") + print("=" * 60) diff --git a/inkycal/modules/template.py b/inkycal/modules/template.py index 99ab27b..5ca4812 100644 --- a/inkycal/modules/template.py +++ b/inkycal/modules/template.py @@ -26,7 +26,8 @@ class inkycal_module(metaclass=abc.ABCMeta): self.fontsize = conf["fontsize"] self.font = ImageFont.truetype( - fonts['NotoSansUI-Regular'], size=self.fontsize) + fonts['NotoSansCJKsc-Regular'], size=self.fontsize) + # fonts['NotoSansUI-SemiCondensed'], size=self.fontsize) def set(self, help=False, **kwargs): """Set attributes of class, e.g. class.set(key=value) diff --git a/inkycal/settings.json b/inkycal/settings.json index 043b9e8..8ba49de 100644 --- a/inkycal/settings.json +++ b/inkycal/settings.json @@ -18,7 +18,7 @@ }, { "position": 2, - "name": "Calendar", + "name": "Today", "config": { "size": [ 528, diff --git a/test_display.py b/test_display.py index 737b06b..bab61c1 100644 --- a/test_display.py +++ b/test_display.py @@ -564,6 +564,10 @@ def list_supported_displays(): def main(): + """ + $ Main function to run display tests + $ python test_display.py --model epd_7_in_5_colour --test all + """ """Main function to run display tests""" parser = argparse.ArgumentParser(description='Universal E-Paper Display Test Script') parser.add_argument('--model', type=str, default=None, diff --git a/tests/test_inkycal_today.py b/tests/test_inkycal_today.py index 90e9e08..c095117 100644 --- a/tests/test_inkycal_today.py +++ b/tests/test_inkycal_today.py @@ -26,7 +26,8 @@ tests = [ "ical_urls": sample_url, "ical_files": None, "date_format": "D MMM", "time_format": "HH:mm", - "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + "padding_x": 10, "padding_y": 10, "fontsize": 14, "language": "zh", + "font": "NotoSansCJKsc-Regular", } }, ]