summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorchangiinlee <changjin9792@gmail.com>2024-02-05 18:00:11 +0900
committerchangiinlee <changjin9792@gmail.com>2024-02-05 18:00:11 +0900
commit4f98ae2687a5fd9b330dc11a8416e83215273c7d (patch)
treec461f20b3595bc1c5fc0ed7921ce1cef8042092f
parent60066bf8db0c5a4ffc58461677e75ec982fc33c6 (diff)
♻️ refact: Refactor calendar
-rw-r--r--girok/api/task.py22
-rw-r--r--girok/calendar_cli/__init__.py0
-rw-r--r--girok/calendar_cli/calendar_app.py37
-rw-r--r--girok/calendar_cli/calendar_container.py409
-rw-r--r--girok/calendar_cli/calendar_main.css222
-rw-r--r--girok/calendar_cli/calendar_main.py194
-rw-r--r--girok/calendar_cli/entity.py7
-rw-r--r--girok/calendar_cli/sidebar.py192
-rw-r--r--girok/calendar_cli/textual_demo_2023-03-11T22_49_21_681307.svg246
-rw-r--r--girok/calendar_cli/utils.py133
-rw-r--r--girok/commands/calendar/command.py14
-rw-r--r--girok/commands/task/display.py1
-rw-r--r--girok/commands/task/entity.py62
-rw-r--r--girok/constants.py21
-rw-r--r--girok/girok.py2
-rw-r--r--girok/utils/time.py13
16 files changed, 1566 insertions, 9 deletions
diff --git a/girok/api/task.py b/girok/api/task.py
index 4ff4a0c..fdfd8a0 100644
--- a/girok/api/task.py
+++ b/girok/api/task.py
@@ -66,6 +66,7 @@ def get_all_tasks(
category_id: Optional[int] = None,
priority: Optional[str] = None,
tags: Optional[List[str]] = None,
+ fetch_children: bool = False
):
params = {
"startDate": start_date,
@@ -73,6 +74,7 @@ def get_all_tasks(
"categoryId": category_id,
"priority": priority,
"tags": tags,
+ "fetchCategoryChildren": fetch_children
}
access_token = AuthHandler.get_access_token()
@@ -110,3 +112,23 @@ def remove_event(event_id: int):
error_message = "Failed to retrieve tasks"
return APIResponse(is_success=False, error_message=error_message)
+
+
+def get_all_tags():
+ access_token = AuthHandler.get_access_token()
+ resp = requests.get(
+ url=urljoin(BASE_URL, "tags"),
+ headers={"Authorization": "Bearer " + access_token}
+ )
+
+ try:
+ resp.raise_for_status()
+ return APIResponse(is_success=True, body=resp.json())
+ except HTTPError:
+ try:
+ error_body = resp.json()
+ error_message = error_body["message"]
+ except:
+ error_message = "Failed to retrieve tags"
+
+ return APIResponse(is_success=False, error_message=error_message) \ No newline at end of file
diff --git a/girok/calendar_cli/__init__.py b/girok/calendar_cli/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/girok/calendar_cli/__init__.py
diff --git a/girok/calendar_cli/calendar_app.py b/girok/calendar_cli/calendar_app.py
new file mode 100644
index 0000000..fc1d27d
--- /dev/null
+++ b/girok/calendar_cli/calendar_app.py
@@ -0,0 +1,37 @@
+from rich.style import Style
+from rich.table import Table
+from textual import log
+from textual.app import App, ComposeResult
+from textual.containers import Container, Horizontal, Vertical
+from textual.messages import Message
+from textual.widget import Widget
+from textual.widgets import Button, Footer, Header, Label, Placeholder, Static, Tree
+
+import girok.api.category as category_api
+import girok.calendar_cli.utils as calendar_utils
+from girok.calendar_cli.calendar_container import CalendarContainer
+from girok.calendar_cli.sidebar import CategoryTree, SidebarContainer
+from girok.calendar_cli.entity import Category
+from girok.constants import Emoji
+
+class CalendarApp(Horizontal):
+ CSS_PATH = "./demo_dock.css"
+
+ def compose(self):
+ yield SidebarContainer(id="sidebar-container")
+ yield CalendarContainer(id="calendar-container")
+
+ def on_category_tree_category_changed(self, event: CategoryTree.CategoryChanged):
+ self.query_one(CalendarContainer).update_category(event.category)
+ cat_tree = self.query_one(CategoryTree)
+
+ def on_tag_tree_tag_changed(self, event):
+ tag = event.tag
+ if tag.endswith(" " + Emoji.LEFT_ARROW):
+ tag = tag[:-2]
+ if tag == "All Tags":
+ tag = None
+ self.query_one(CalendarContainer).update_tag(tag)
+
+ def on_category_tree_custom_test_message(self, event):
+ self.refresh()
diff --git a/girok/calendar_cli/calendar_container.py b/girok/calendar_cli/calendar_container.py
new file mode 100644
index 0000000..58c39fc
--- /dev/null
+++ b/girok/calendar_cli/calendar_container.py
@@ -0,0 +1,409 @@
+from collections import defaultdict
+from typing import List, Dict, Optional
+import asyncio
+import calendar
+from calendar import monthrange
+from datetime import datetime, timedelta
+
+from rich.markdown import Markdown
+from rich.panel import Panel
+from rich.segment import Segment
+from rich.style import Style
+from rich.text import Text
+from textual import log
+from textual.app import App, ComposeResult
+from textual.containers import Container, Horizontal, Vertical
+from textual.messages import Message
+from textual.reactive import var
+from textual.widget import Widget
+from textual.widgets import (
+ Button,
+ DataTable,
+ Footer,
+ Header,
+ Label,
+ Placeholder,
+ Static,
+ Tree,
+)
+
+import girok.api.category as category_api
+import girok.api.task as task_api
+import girok.calendar_cli.utils as calendar_utils
+from girok.calendar_cli.sidebar import CategoryTree
+from girok.utils.time import get_year_and_month_by_month_offset
+from girok.constants import CALENDAR_HEADER_DATE_COLOR, CALENDAR_TODAY_COLOR, CALENDAR_WEEKDAY_NAME_COLOR
+from girok.calendar_cli.entity import Category
+from girok.commands.task.command import map_to_event_entities
+from girok.commands.task.entity import Event
+from girok.utils.time import convert_iso_date_str_to_date_obj
+
+
+class WeekdayBarContainer(Horizontal):
+ pass
+
+
+class CalendarHeader(Vertical):
+ year = datetime.now().year
+ month = datetime.now().month
+ cat_path = ""
+ category: Category = None
+ tag = ""
+
+ def on_mount(self):
+ self.display_date()
+
+ def compose(self):
+ month_name = calendar.month_name[self.month]
+
+ with Horizontal():
+ with Container(id="calendar-header-category-container"):
+ yield Static(self.cat_path, id="calendar-header-category")
+ with Container(id="calendar-header-date-container"):
+ yield Static(
+ Text(
+ f"{month_name} {self.year}",
+ style=Style(
+ color=CALENDAR_HEADER_DATE_COLOR, bold=True
+ ),
+ ),
+ id="calendar-header-date",
+ )
+ with Container(id="calendar-header-tag-container"):
+ yield Static(f"{self.tag}", id="calendar-header-tag")
+ yield Horizontal()
+ with WeekdayBarContainer(id="weekday-bar"):
+ yield Static(
+ Text(
+ "Monday",
+ style=Style(color=CALENDAR_WEEKDAY_NAME_COLOR, bold=True),
+ ),
+ classes="calendar-weekday-name",
+ )
+ yield Static(
+ Text(
+ "Tuesday",
+ style=Style(color=CALENDAR_WEEKDAY_NAME_COLOR, bold=True),
+ ),
+ classes="calendar-weekday-name",
+ )
+ yield Static(
+ Text(
+ "Wednesday",
+ style=Style(color=CALENDAR_WEEKDAY_NAME_COLOR, bold=True),
+ ),
+ classes="calendar-weekday-name",
+ )
+ yield Static(
+ Text(
+ "Thursday",
+ style=Style(color=CALENDAR_WEEKDAY_NAME_COLOR, bold=True),
+ ),
+ classes="calendar-weekday-name",
+ )
+ yield Static(
+ Text(
+ "Friday",
+ style=Style(color=CALENDAR_WEEKDAY_NAME_COLOR, bold=True),
+ ),
+ classes="calendar-weekday-name",
+ )
+ yield Static(
+ Text("Saturday", style=Style(color="#87C5FA", bold=True)),
+ classes="calendar-weekday-name",
+ )
+ yield Static(
+ Text("Sunday", style=Style(color="#DB4455", bold=True)),
+ classes="calendar-weekday-name",
+ )
+
+ def update_year_and_month(self, year, month):
+ self.year, self.month = year, month
+ self.display_date()
+
+ def update_category(self, category: Category):
+ self.cat_path = category.path
+ self.display_date()
+
+ def update_tag(self, new_tag: Optional[str]):
+ self.tag = new_tag if new_tag else "All"
+ self.display_date()
+
+ def display_date(self):
+ month_name = calendar.month_name[self.month]
+ calendar_header_category = self.query_one("#calendar-header-category")
+ calendar_header_date = self.query_one("#calendar-header-date")
+ calendar_header_tag = self.query_one("#calendar-header-tag")
+ calendar_weekday_bar = self.query_one("#weekday-bar")
+
+ calendar_header_category.update(
+ Text(
+ f"Category: /{self.cat_path}",
+ style=Style(color=CALENDAR_HEADER_DATE_COLOR),
+ )
+ )
+ calendar_header_date.update(
+ Text(
+ f"{month_name} {self.year}",
+ style=Style(color=CALENDAR_HEADER_DATE_COLOR, bold=True),
+ )
+ )
+ calendar_header_tag.update(
+ Text(
+ f"Tag: {self.tag}",
+ style=Style(color=CALENDAR_HEADER_DATE_COLOR),
+ )
+ )
+
+
+class CalendarCell(Vertical):
+ pass
+
+
+class Calendar(Container):
+ year = datetime.now().year
+ month = datetime.now().month
+ cat_path = "" # If "", show all categories
+ cat_id: int = None
+ category: Category = None
+ tag = ""
+ tasks = []
+ events: List[Event] = []
+ can_focus = True
+ cur_month_first_day_cell_num = None
+ cur_focused_cell_cord = (None, None)
+ cur_focused_cell = None
+ day_to_events_map: Dict[str, List[Event]] = defaultdict(list)
+ m = 5
+ n = 7
+ grid = [[False for _ in range(7)] for _ in range(5)]
+ is_pop_up = False
+
+ class TaskCellSelected(Message):
+ def __init__(self, cell_events: List[Event], year: int, month: int, day: int):
+ super().__init__()
+ self.cell_events = cell_events
+ self.year = year
+ self.month = month
+ self.day = day
+
+ def on_mount(self):
+ self.update_calendar()
+
+ def compose(self):
+ for i in range(35):
+ yield CalendarCell(classes="calendar-cell", id=f"cell{i}")
+
+ def on_key(self, event):
+ if self.is_pop_up:
+ return
+ x, y = self.cur_focused_cell_cord
+ if event.key == "h": # left
+ next_cell_coord = (x, y - 1)
+ elif event.key == "j": # down
+ next_cell_coord = (x + 1, y)
+ elif event.key == "k": # up
+ next_cell_coord = (x - 1, y)
+ elif event.key == "l": # right
+ next_cell_coord = (x, y + 1)
+ elif event.key == "o":
+ pass
+ else:
+ return
+
+ if event.key in ["h", "j", "k", "l"]: # moving on cells
+ nx, ny = next_cell_coord
+ if nx < 0 or ny < 0 or nx >= self.m or ny >= self.n: # Out of matrix
+ return
+
+ if not self.grid[nx][ny]: # Out of boundary of the current month
+ return
+
+ prev_cell_num = calendar_utils.convert_coord_to_cell_num(
+ *self.cur_focused_cell_cord
+ )
+ prev_cell = self.query_one(f"#cell{prev_cell_num}")
+ calendar_utils.remove_left_arrow(prev_cell)
+
+ cur_cell_num = calendar_utils.convert_coord_to_cell_num(nx, ny)
+ next_cell = self.query_one(f"#cell{cur_cell_num}")
+ calendar_utils.add_left_arrow(next_cell)
+
+ self.cur_focused_cell_cord = (nx, ny)
+ self.cur_focused_cell = next_cell
+ elif event.key == "o": # select a cell
+ cur_cell_num = calendar_utils.convert_coord_to_cell_num(x, y)
+ cur_cell = self.query_one(f"#cell{cur_cell_num}")
+ # cell_events = cur_cell.children[1:] # task data
+
+ # Retrieve tasks for the selected day
+ selected_day = calendar_utils.convert_cell_num_to_day(
+ self.year, self.month, cur_cell_num
+ )
+
+ self.post_message(
+ self.TaskCellSelected(self.day_to_events_map[selected_day], self.year, self.month, selected_day)
+ )
+ self.is_pop_up = True
+
+ def on_focus(self):
+ x, y = calendar_utils.convert_cell_num_to_coord(
+ self.cur_month_first_day_cell_num
+ )
+ target_cell = self.query_one(f"#cell{self.cur_month_first_day_cell_num}")
+ calendar_utils.add_left_arrow(target_cell)
+ self.cur_focused_cell_cord = (x, y)
+ self.cur_focused_cell = target_cell
+
+ def update_year_and_month(self, year, month):
+ self.year, self.month = year, month
+ self.update_calendar()
+
+ def update_category(self, category: Category):
+ self.cat_path = category.path
+ self.cat_id = category.id
+ self.category = category
+ self.update_calendar(show_arrow=False)
+
+ def update_tag(self, new_tag: Optional[str]):
+ self.tag = new_tag
+ self.update_calendar(show_arrow=False)
+
+ def refresh_cell_days(self):
+ self.grid = [[False for _ in range(7)] for _ in range(5)]
+ first_weekday, total_days = calendar.monthrange(self.year, self.month)
+ self.cur_month_first_day_cell_num = first_weekday
+ now = datetime.now()
+ for i in range(35):
+ cell = self.query_one(f"#cell{i}")
+ for child in cell.walk_children():
+ child.remove()
+ if i >= first_weekday and i <= first_weekday + total_days - 1:
+ x, y = calendar_utils.convert_cell_num_to_coord(i)
+ self.grid[x][y] = True
+ day = calendar_utils.convert_cell_num_to_day(self.year, self.month, i)
+ day_text = Text()
+ day_text.append(f"{day}")
+ if self.year == now.year and self.month == now.month and day == now.day:
+ day_text = Text(
+ str(day_text),
+ style=Style(
+ bgcolor=CALENDAR_TODAY_COLOR, color="black"
+ ),
+ )
+ cell.mount(Label(day_text, id=f"cell-header-{i}"))
+
+ def update_calendar(self, show_arrow=True):
+ """
+ If val == "", then "root category" is selected
+ """
+
+ # Filter by category
+ category_id = None
+ if self.category:
+ category_id = self.category.id
+
+ # Filter by tag
+ tags = None if not self.tag else [self.tag]
+
+ # Filter by date
+ current_month_first_date = datetime(self.year, self.month, 1)
+ current_month_last_day = monthrange(self.year, self.month)[1]
+ current_month_last_date = datetime(self.year, self.month, current_month_last_day)
+
+ start_date = current_month_first_date.strftime("%Y-%m-%d")
+ end_date = current_month_last_date.strftime("%Y-%m-%d")
+
+ # Retrieve all events
+ resp = task_api.get_all_tasks(start_date=start_date, end_date=end_date, category_id=category_id, tags=tags, fetch_children=True)
+ if not resp.is_success:
+ exit(0)
+ self.events = map_to_event_entities(resp.body['events'])
+
+ # Empty out current calendar view
+ self.refresh_cell_days()
+
+ first_weekday, last_day = calendar.monthrange(self.year, self.month)
+
+ if self.cur_focused_cell:
+ if show_arrow:
+ calendar_utils.remove_left_arrow(self.cur_focused_cell)
+ self.cur_focused_cell_cord = calendar_utils.convert_cell_num_to_coord(
+ first_weekday
+ )
+ self.cur_focused_cell = self.query_one(f"#cell{first_weekday}") # update
+ if show_arrow:
+ calendar_utils.add_left_arrow(self.cur_focused_cell)
+
+ for idx, event in enumerate(self.events):
+ target_days = event.get_all_days_for_month(self.year, self.month)
+ for day in target_days:
+ self.day_to_events_map[day].append(event)
+ # event_date_obj = convert_iso_date_str_to_date_obj(event.event_date.start_date)
+ # day = event_date_obj.day
+ cell_num = calendar_utils.convert_day_to_cell_num(
+ self.year, self.month, day
+ )
+ cell = self.query_one(f"#cell{cell_num}")
+ color = event.color_hex
+ name = event.name
+ # if len(name) > 13:
+ # name = name[:13] + ".."
+ task_item_name = Text()
+ task_item_name.append("●", style=color)
+ task_item_name.append(" " + name)
+
+ task_item = Static(
+ task_item_name, id=f"task-cell{cell_num}-{idx}", classes="task-item"
+ )
+ cell.mount(task_item)
+
+ task_item = self.query_one(f"#task-cell{cell_num}-{idx}")
+ task_item.styles.overflow_x = "hidden"
+ task_item.styles.overflow_y = "hidden"
+
+
+class CalendarContainer(Vertical):
+ year = datetime.now().year
+ month = datetime.now().month
+ tag = None
+
+ def update_month_by_offset(self, offset: int):
+ new_year, new_month = get_year_and_month_by_month_offset(
+ month_offset=offset, year=self.year, month=self.month
+ )
+ self.year, self.month = new_year, new_month
+ calendar_header = self.query_one(CalendarHeader)
+ cal = self.query_one(Calendar)
+
+ calendar_header.update_year_and_month(self.year, self.month)
+ cal.update_year_and_month(self.year, self.month)
+
+ def update_year_and_month(self, year: int, month: int):
+ self.year, self.month = year, month
+ calendar_header = self.query_one(CalendarHeader)
+ cal = self.query_one(Calendar)
+
+ calendar_header.update_year_and_month(self.year, self.month)
+ cal.update_year_and_month(self.year, self.month)
+
+ def update_category(self, category: Category):
+ """
+ cat_path: ex) HKU/COMP3230 or ""
+ """
+ cal = self.query_one(Calendar)
+ cal.update_category(category)
+
+ cal_header = self.query_one(CalendarHeader)
+ cal_header.update_category(category)
+
+ def update_tag(self, tag: Optional[str]):
+ self.tag = tag
+ cal = self.query_one(Calendar)
+ cal.update_tag(new_tag=tag)
+ cal_header = self.query_one(CalendarHeader)
+ cal_header.update_tag(new_tag=tag)
+
+ def compose(self):
+ yield CalendarHeader(id="calendar-header")
+ yield Calendar(id="calendar")
diff --git a/girok/calendar_cli/calendar_main.css b/girok/calendar_cli/calendar_main.css
new file mode 100644
index 0000000..bb44e86
--- /dev/null
+++ b/girok/calendar_cli/calendar_main.css
@@ -0,0 +1,222 @@
+* {
+ padding: 0;
+ margin: 0;
+ color: #8f929b;
+ background: #171921;
+ overflow: hidden hidden;
+}
+
+Screen {
+ layers: below above;
+ layer: below;
+}
+
+#app-calendar {
+ layout: horizontal;
+}
+
+#sidebar-container {
+ height: 100%;
+ width: 2fr;
+ border: solid grey;
+ padding-left: 3;
+ padding-top: 1;
+}
+
+#sidebar-header {
+ content-align: center middle;
+}
+
+#sidebar-main-container {
+ layout: vertical;
+ margin-bottom: -10;
+}
+#sidebar {
+ box-sizing: content-box;
+ height: auto;
+}
+
+#tag-tree {
+ box-sizing: content-box;
+ height: auto;
+ margin-top: 2;
+}
+
+#calendar-container {
+ height: 100%;
+ width: 10fr;
+ border: solid grey;
+ align: center middle;
+}
+
+#calendar-header {
+ /* content-align: center middle; */
+ height: 3;
+ /* background: blue; */
+}
+
+#calendar {
+ /* background: yellow; */
+}
+
+#calendar-header-category-container {
+ /* content-align-horizontal: left; */
+ margin-left: 2;
+ width: 1fr;
+}
+
+#calendar-header-category {
+ content-align-horizontal: left;
+}
+
+#calendar-header-date-container {
+ width: 1fr;
+}
+
+#calendar-header-date {
+ content-align-horizontal: center;
+}
+
+#calendar-header-tag-container {
+ /* content-align-horizontal: left; */
+ margin-right: 2;
+ width: 1fr;
+}
+
+#calendar-header-tag {
+ margin-right: 3;
+ content-align-horizontal: right;
+ color: #9bdfbb;
+}
+
+#calendar-header-place-holder {
+ width: 1fr;
+}
+
+#weekday-bar {
+ width: 100%;
+ background: red;
+ margin-left: 2;
+}
+
+.calendar-weekday-name {
+ width: 1fr;
+ content-align-horizontal: center;
+}
+
+#calendar {
+ layout: grid;
+ grid-size: 7 5;
+ grid-columns: 1fr;
+ grid-rows: 1fr;
+ height: 95%;
+ margin-left: 2;
+ padding-top: 0;
+ /* outline: solid grey; */
+ /* background: blue; */
+}
+
+.vertical {
+ width: 1fr;
+ border: solid grey;
+}
+
+.calendar-cell {
+ height: 100%;
+ border-top: grey;
+ /* background: grey; */
+ /* border: grey; */
+}
+
+.weekday-name {
+ content-align: center middle;
+ color: antiquewhite;
+}
+
+.task-item-container {
+ box-sizing: content-box;
+ height: 1;
+ overflow: hidden hidden;
+ width: 95%;
+}
+
+.task-item {
+ /* color: rgb(168, 193, 209); */
+ /* color: #d1cec0; */
+ color: #b9b7ad;
+ /* width: 100%; */
+ height: 1;
+}
+
+#calendar-header-category {
+ color: #9bdfbb;
+}
+
+.focused-cell {
+ background: #222530;
+ /* layer: above; */
+}
+
+/* .pop-up {
+ layer: above;
+ background: red;
+ content-align: center middle;
+}
+
+.task-pop-up-container {
+ layer: above;
+ layout: grid;
+ grid-size: 3 3;
+ grid-columns: 1fr 10fr 1fr;
+ grid-rows: 1fr 10fr 1fr;
+ width: 50%;
+ height: 50%;
+ background: white;
+}
+
+.task-pop-up-table {
+ width: auto;
+ height: auto;
+ background: blue;
+} */
+
+Screen {
+ align: center middle;
+}
+
+.task-pop-up-container {
+ layer: above;
+ width: 90%;
+ height: auto;
+}
+
+.task-pop-up-header-container {
+ layer: above;
+ align: center middle;
+ height: 1;
+}
+
+.task-pop-up-header {
+ layer: above;
+ background: #3e4348;
+ content-align: center bottom;
+ content-align-vertical: bottom;
+}
+
+.task-pop-up-table-container {
+ layer: above;
+ align: center middle;
+ background: #222429;
+ height: auto;
+}
+
+.task-pop-up-table {
+ layer: above;
+ background: #222429;
+ content-align-horizontal: center;
+ width: 90%;
+}
+
+.calendar-weekday-name {
+ width: 1fr;
+}
diff --git a/girok/calendar_cli/calendar_main.py b/girok/calendar_cli/calendar_main.py
new file mode 100644
index 0000000..495deb4
--- /dev/null
+++ b/girok/calendar_cli/calendar_main.py
@@ -0,0 +1,194 @@
+import calendar
+from datetime import datetime
+
+from rich import box
+from rich.style import Style
+from rich.table import Column, Table
+from rich.text import Text
+from textual import log
+from textual.app import App, ComposeResult
+from textual.containers import Container, Horizontal, Vertical
+from textual.messages import Message
+from textual.reactive import reactive, var
+from textual.scroll_view import ScrollView
+from textual.widget import Widget
+from textual.widgets import Button, Footer, Header, Label, Placeholder, Static, Tree
+
+import girok.api.category as category_api
+import girok.calendar_cli.utils as calendar_utils
+
+from girok.calendar_cli.calendar_app import CalendarApp
+from girok.calendar_cli.calendar_container import Calendar, CalendarContainer
+from girok.calendar_cli.sidebar import CategoryTree, SidebarContainer, TagTree
+from girok.commands.task.display import display_events_by_list
+from girok.constants import TABLE_HEADER_DATE_COLOR
+
+
+class Entry(App):
+ CSS_PATH = "./calendar_main.css"
+ current_focused = "CategoryTree"
+ is_pop_up = False
+ BINDINGS = [
+ ("q", "quit", "Quit Nuro"),
+ ("u", "show_previous_month", "Show prev month"),
+ ("i", "show_next_month", "Show next month"),
+ ("y", "show_current_month", "Show current month"),
+ ("e", "focus_on_calendar", "Move to calendar"),
+ ("w", "focus_on_sidebar", "Move to sidebar"),
+ ("ctrl+j", "move_down_to_tag_tree", "Move down to tag tree"),
+ ("ctrl+k", "move_up_to_category_tree", "Move up to category tree"),
+ ("o", "close_pop_up", "Close pop up box"),
+ ("f", "toggle_files", "Toggle Files"),
+ ]
+ show_sidebar = reactive(True)
+ pilot = None
+
+ def on_mount(self):
+ self.set_focus(self.query_one(CategoryTree))
+
+ def compose(self):
+ yield CalendarApp()
+
+ # Display pop-up box when selecting a cell
+ def on_calendar_task_cell_selected(self, event: Calendar.TaskCellSelected):
+ cell_events = event.cell_events
+ year, month, day = event.year, event.month, event.day
+ table = display_events_by_list(cell_events)
+
+ self.query_one(CalendarContainer).mount(
+ Vertical(
+ Static(
+ Text(
+ f"{day} {calendar.month_name[month]} {year}",
+ style=Style(bold=True, color=TABLE_HEADER_DATE_COLOR),
+ ),
+ classes="task-pop-up-header",
+ ),
+ Container(
+ Static(table, classes="task-pop-up-table"),
+ classes="task-pop-up-table-container",
+ ),
+ classes="task-pop-up-container",
+ )
+ )
+ self.is_pop_up = True
+
+ def action_quit(self):
+ self.exit()
+
+ def action_show_next_month(self):
+ if self.is_pop_up:
+ return
+ calendar_container = self.query_one(CalendarContainer)
+ calendar_container.update_month_by_offset(1)
+
+ def action_show_previous_month(self):
+ if self.is_pop_up:
+ return
+ calendar_container = self.query_one(CalendarContainer)
+ calendar_container.update_month_by_offset(-1)
+
+ def action_show_current_month(self):
+ if self.is_pop_up:
+ return
+ calendar_container = self.query_one(CalendarContainer)
+ now = datetime.now()
+ cur_year, cur_month = now.year, now.month
+ calendar_container.update_year_and_month(cur_year, cur_month)
+
+ def action_focus_on_calendar(self):
+ if self.is_pop_up:
+ return
+ self.set_focus(self.query_one(Calendar))
+ self.current_focused = "Calendar"
+
+ cat_tree = self.query_one(CategoryTree)
+ tag_tree = self.query_one(TagTree)
+ calendar_utils.remove_left_arrow_tree(cat_tree.highlighted_node)
+ calendar_utils.remove_left_arrow_tree(tag_tree.highlighted_node)
+ calendar_utils.remove_highlight(cat_tree.highlighted_node)
+ calendar_utils.remove_highlight(tag_tree.highlighted_node)
+
+ def action_focus_on_sidebar(self):
+ if self.is_pop_up:
+ return
+ self.set_focus(self.query_one(CategoryTree))
+ self.current_focused = "CategoryTree"
+
+ cal = self.query_one(Calendar)
+
+ if self.is_pop_up:
+ return
+
+ cat_tree = self.query_one(CategoryTree)
+ calendar_utils.remove_left_arrow(cal.cur_focused_cell)
+ calendar_utils.add_highlight(cat_tree.highlighted_node)
+
+ ############# UNKNOWN ERROR - Temporary Fix ############
+ ngbrs = [
+ self.query_one("#cell0"),
+ self.query_one("#cell1"),
+ self.query_one("#cell2"),
+ self.query_one("#cell3"),
+ self.query_one("#cell4"),
+ self.query_one("#cell5"),
+ self.query_one("#cell6"),
+ ]
+ for ngbr_cell in ngbrs:
+ temp = []
+ while ngbr_cell.children:
+ child = ngbr_cell.children[0]
+ temp.append(child.render())
+ child.remove()
+
+ for child in temp:
+ ngbr_cell.mount(Static(child))
+ ######################################################
+
+ def action_move_down_to_tag_tree(self):
+ if self.is_pop_up:
+ return
+ if self.current_focused != "CategoryTree":
+ return
+ tag_tree = self.query_one(TagTree)
+ self.set_focus(tag_tree)
+ self.current_focused = "TagTree"
+ category_tree = self.query_one(CategoryTree)
+ calendar_utils.remove_highlight(category_tree.highlighted_node)
+ calendar_utils.remove_left_arrow_tree(category_tree.highlighted_node)
+ calendar_utils.add_left_arrow_tree(tag_tree.highlighted_node)
+ calendar_utils.add_highlight(tag_tree.highlighted_node)
+
+ def action_move_up_to_category_tree(self):
+ if self.is_pop_up:
+ return
+ if self.current_focused != "TagTree":
+ return
+ category_tree = self.query_one(CategoryTree)
+ self.set_focus(category_tree)
+ self.current_focused = "CategoryTree"
+ tag_tree = self.query_one(TagTree)
+ calendar_utils.remove_highlight(tag_tree.highlighted_node)
+ calendar_utils.remove_left_arrow_tree(tag_tree.highlighted_node)
+ calendar_utils.add_left_arrow_tree(category_tree.highlighted_node)
+ calendar_utils.add_highlight(category_tree.highlighted_node)
+
+ def action_close_pop_up(self):
+ if not self.is_pop_up:
+ return
+ self.query_one(".task-pop-up-container").remove()
+ self.query_one(Calendar).is_pop_up = False
+ self.is_pop_up = False
+
+ def action_toggle_files(self):
+ self.show_sidebar = not self.show_sidebar
+ sidebar_container = self.query_one(SidebarContainer)
+ if self.show_sidebar:
+ sidebar_container.styles.display = "block"
+ else:
+ sidebar_container.styles.display = "none"
+
+
+if __name__ == "__main__":
+ app = Entry()
+ app.run()
diff --git a/girok/calendar_cli/entity.py b/girok/calendar_cli/entity.py
new file mode 100644
index 0000000..b01015c
--- /dev/null
+++ b/girok/calendar_cli/entity.py
@@ -0,0 +1,7 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class Category:
+ id: int
+ path: str \ No newline at end of file
diff --git a/girok/calendar_cli/sidebar.py b/girok/calendar_cli/sidebar.py
new file mode 100644
index 0000000..e3be271
--- /dev/null
+++ b/girok/calendar_cli/sidebar.py
@@ -0,0 +1,192 @@
+from rich.style import Style
+from rich.text import Text
+from textual import events, log
+from textual.app import App, ComposeResult
+from textual.containers import Container, Horizontal, Vertical
+from textual.messages import Message
+from textual.reactive import reactive, var
+from textual.widget import Widget
+from textual.widgets import Button, Footer, Header, Label, Placeholder, Static, Tree
+from textual.widgets._tree import TreeNode
+
+import girok.calendar_cli.utils as calendar_utils
+import girok.api.category as category_api
+import girok.api.task as task_api
+from girok.constants import CATEGORY_COLOR_PALETTE, Emoji
+from girok.calendar_cli.entity import Category
+
+
+
+class CategoryTree(Tree):
+ CSS_PATH = "./demo_dock.css"
+ categories: list[dict] = None
+ can_focus = True
+ can_focus_children = True
+ auto_expand = False
+ highlighted_node = None
+ selected_node = None
+ init_select = False
+ init_highlight = False
+
+ class CategoryChanged(Message):
+ def __init__(self, category: Category):
+ super().__init__()
<