From 4f98ae2687a5fd9b330dc11a8416e83215273c7d Mon Sep 17 00:00:00 2001 From: changiinlee Date: Mon, 5 Feb 2024 18:00:11 +0900 Subject: =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refact:=20Refactor=20calendar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- girok/api/task.py | 22 ++ girok/calendar_cli/__init__.py | 0 girok/calendar_cli/calendar_app.py | 37 ++ girok/calendar_cli/calendar_container.py | 409 +++++++++++++++++++++ girok/calendar_cli/calendar_main.css | 222 +++++++++++ girok/calendar_cli/calendar_main.py | 194 ++++++++++ girok/calendar_cli/entity.py | 7 + girok/calendar_cli/sidebar.py | 192 ++++++++++ .../textual_demo_2023-03-11T22_49_21_681307.svg | 246 +++++++++++++ girok/calendar_cli/utils.py | 133 +++++++ girok/commands/calendar/command.py | 14 + girok/commands/task/display.py | 1 + girok/commands/task/entity.py | 62 +++- girok/constants.py | 21 +- girok/girok.py | 2 + girok/utils/time.py | 13 + 16 files changed, 1566 insertions(+), 9 deletions(-) create mode 100644 girok/calendar_cli/__init__.py create mode 100644 girok/calendar_cli/calendar_app.py create mode 100644 girok/calendar_cli/calendar_container.py create mode 100644 girok/calendar_cli/calendar_main.css create mode 100644 girok/calendar_cli/calendar_main.py create mode 100644 girok/calendar_cli/entity.py create mode 100644 girok/calendar_cli/sidebar.py create mode 100644 girok/calendar_cli/textual_demo_2023-03-11T22_49_21_681307.svg create mode 100644 girok/calendar_cli/utils.py create mode 100644 girok/commands/calendar/command.py 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 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__() + self.category = category + + class CustomTestMessage(Message): + def __init__(self): + super().__init__() + + def on_mount(self): + self.highlighted_node = self.root + self.selected_node = self.root + calendar_utils.add_left_arrow_tree(self.highlighted_node) + + self.line = 0 + resp = category_api.get_all_categories() + if not resp.is_success: + exit(0) + self.categories = resp.body['rootCategories'] + self.root.expand() + + for category in self.categories: + top_cat = self.root.add( + category['name'], expand=True, data={"color": category['color'], "id": category['id']} + ) + top_cat.allow_expand = True + calendar_utils.build_category_tree(top_cat, category['children']) + + def on_key(self, evt): + if evt.key == "j": + self.action_cursor_down() + elif evt.key == "k": + self.action_cursor_up() + elif evt.key == "o": + self.action_select_cursor() + + def render_label(self, node, base_style: Style, style: Style): + node_label = node._label.copy() + icon = "" + if node.parent is None: + icon = "📖 " + elif node.parent.parent is None: + icon = Text("● ", style=CATEGORY_COLOR_PALETTE[node.data["color"]]) + text = Text() + text.append(icon) + text.append(node_label) + return text + + def on_tree_node_selected(self, event: Tree.NodeSelected): + event.stop() + full_cat_path = calendar_utils.get_full_path_from_node(event.node) + self.selected_node = event.node + if full_cat_path == "": + category = Category(id=None, path="") + else: + category = Category(id=event.node.data['id'], path=full_cat_path) + self.post_message(self.CategoryChanged(category)) + + def on_tree_node_highlighted(self, event: Tree.NodeHighlighted): + event.stop() + prev_highlighted_node = self.highlighted_node + calendar_utils.remove_left_arrow_tree(prev_highlighted_node) + calendar_utils.remove_highlight(prev_highlighted_node) + calendar_utils.add_left_arrow_tree(event.node) + calendar_utils.add_highlight(event.node) + self.highlighted_node = event.node + + def on_focus(self, evt): + calendar_utils.add_left_arrow_tree(self.highlighted_node) + + +class TagTree(Tree): + CSS_PATH = "./demo_dock.css" + tags = [] + can_focus = True + can_focus_children = True + auto_expand = False + highlighted_node = None + selected_node = None + + class TagChanged(Message): + def __init__(self, tag: str): + super().__init__() + self.tag = tag + + class CustomTestMessage(Message): + def __init__(self): + super().__init__() + + def on_mount(self): + self.select_node(self.root) + self.action_select_cursor() + self.highlighted_node = self.root + self.selected_node = self.root + + # tags = task_api.get_tags() + resp = task_api.get_all_tags() + if not resp.is_success: + exit(0) + self.tags = resp.body['tags'] + self.root.expand() + + for tag in self.tags: + self.root.add(tag, expand=True) + + def on_key(self, evt): + if evt.key == "j": + self.action_cursor_down() + elif evt.key == "k": + self.action_cursor_up() + elif evt.key == "o": + self.action_select_cursor() + + def on_focus(self, evt): + calendar_utils.add_left_arrow_tree(self.highlighted_node) + + def render_label(self, node, base_style: Style, style: Style): + node_label = node._label.copy() + + icon = "" + if node.parent is None: + icon = "📖 " + elif node.parent.parent is None: + icon = Text("● ", style="white") + + text = Text() + text.append(icon) + text.append(node_label) + return text + + def on_tree_node_selected(self, event: Tree.NodeSelected): + event.stop() + tag = str(event.node._label) + if tag.endswith(" " + Emoji.LEFT_ARROW): + tag = tag[:-2] + + self.post_message(self.TagChanged(tag)) + self.selected_node = event.node + + def on_tree_node_highlighted(self, event: Tree.NodeHighlighted): + event.stop() + prev_highlighted_node = self.highlighted_node + calendar_utils.remove_highlight(prev_highlighted_node) + calendar_utils.remove_left_arrow_tree(prev_highlighted_node) + calendar_utils.add_left_arrow_tree(event.node) + calendar_utils.add_highlight(event.node) + self.highlighted_node = event.node + + +class SidebarMainContainer(Vertical): + CSS_PATH = "./demo_dock.css" + + def compose(self): + yield CategoryTree("All Categories", id="sidebar") + yield TagTree("All Tags", id="tag-tree") + + +class SidebarContainer(Vertical): + CSS_PATH = "./demo_dock.css" + + def compose(self): + yield SidebarMainContainer(id="sidebar-main-container") diff --git a/girok/calendar_cli/textual_demo_2023-03-11T22_49_21_681307.svg b/girok/calendar_cli/textual_demo_2023-03-11T22_49_21_681307.svg new file mode 100644 index 0000000..816c090 --- /dev/null +++ b/girok/calendar_cli/textual_demo_2023-03-11T22_49_21_681307.svg @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Textual Demo + + + + + + + +