Added Todo

This commit is contained in:
tr1x_em committed 2025-08-14 13:26:24 +05:30
1 parent e735e722f6
commit feec2917f6
12 files changed
+1160 -1

No files matched your search

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="800px"
height="800px"
viewBox="0 0 24 24"
fill="none"
version="1.1"
id="svg1"
sodipodi:docname="checkbox-check.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.59658691"
inkscape:cx="273.22088"
inkscape:cy="432.46004"
inkscape:window-width="954"
inkscape:window-height="1045"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Interface / Checkbox_Check" />
<g
id="Interface / Checkbox_Check">
<path
id="Vector"
d="m 8,12 3,3 5,-6 M 4,16.8002 v -9.6 C 4,6.08009 4,5.51962 4.21799,5.0918 4.40973,4.71547 4.71547,4.40973 5.0918,4.21799 5.51962,4 6.08009,4 7.2002,4 h 9.6 c 1.1201,0 1.6794,0 2.1072,0.21799 0.3763,0.19174 0.6831,0.49748 0.8748,0.87381 C 20,5.5192 20,6.07899 20,7.19691 v 9.60669 c 0,1.1179 0,1.6769 -0.2178,2.1043 -0.1917,0.3763 -0.4985,0.6826 -0.8748,0.8743 C 18.48,20 17.921,20 16.8031,20 H 7.19691 C 6.07899,20 5.5192,20 5.0918,19.7822 4.71547,19.5905 4.40973,19.2842 4.21799,18.9079 4,18.4801 4,17.9203 4,16.8002 Z"
stroke="#000000"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
style="display:inline" />
<path
style="fill:#ffffff;stroke:#ffffff;stroke-width:0.838101;stroke-linejoin:round"
d="m 352.84046,528.88803 c -3.22669,-1.73681 -30.50855,-27.91507 -60.62636,-58.17393 -48.27401,-48.50006 -54.98796,-55.88217 -56.68725,-62.32871 -2.56679,-9.73752 -0.8264,-18.72921 5.24825,-27.11502 8.6315,-11.91544 26.48529,-16.82146 38.81477,-10.66587 2.95043,1.47303 23.25033,20.4845 45.1109,42.24773 21.86056,21.76322 40.03448,39.22671 40.3865,38.80776 0.35201,-0.41895 33.23427,-39.98485 73.0717,-87.92422 62.72092,-75.4768 73.59894,-87.80127 81.13812,-91.9271 7.39956,-4.04943 9.96286,-4.62014 17.07668,-3.80208 21.67955,2.49306 35.67131,26.45751 26.24377,44.9491 -1.70529,3.34483 -41.79351,52.51958 -89.08494,109.27721 -92.6727,111.22277 -91.5219,110.04524 -107.3927,109.88705 -4.088,-0.0407 -10.07275,-1.4951 -13.29944,-3.23192 z"
id="path2"
transform="scale(0.03)" />
<path
style="fill:#ffffff;stroke:#ffffff;stroke-width:0.838101;stroke-linejoin:round"
d="m 195.70868,698.11228 c -27.93637,-2.37404 -48.35167,-11.27596 -65.62867,-28.61684 -15.23491,-15.29123 -23.53294,-32.06515 -27.00859,-54.59607 -1.19256,-7.73077 -1.59794,-80.27817 -1.2442,-222.66817 0.46185,-185.91001 0.83864,-212.20504 3.14648,-219.58242 10.44456,-33.38776 37.76921,-59.88854 70.18938,-68.07309 13.71651,-3.46277 435.54403,-3.45635 449.22205,0.007 36.58829,9.26391 62.90519,36.49828 71.72045,74.22085 2.62875,11.24902 2.87053,29.86056 2.87053,220.97074 0,197.21061 -0.17077,209.42455 -3.10538,222.09672 -8.5147,36.76819 -35.67568,64.50713 -72.3237,73.8627 -10.9052,2.7839 -25.87217,3.02005 -212.03951,3.34533 -110.16836,0.1925 -207.27784,-0.24246 -215.79884,-0.96658 z m 418.64543,-67.1917 c 7.95592,-3.6232 16.27613,-13.45601 17.91031,-21.16637 0.73689,-3.47688 1.33982,-99.09936 1.33982,-212.49441 0,-202.8563 -0.0539,-206.27893 -3.3524,-212.76994 -1.84382,-3.62842 -6.16986,-8.84278 -9.61342,-11.58746 -6.18579,-4.93036 -6.60873,-5.0085 -35.20024,-6.50335 -15.91657,-0.83217 -110.77976,-1.27106 -210.8071,-0.97532 -180.66887,0.53417 -181.91761,0.56092 -189.41079,4.05708 -8.82098,4.11569 -12.17627,7.54515 -16.27904,16.63892 -2.8118,6.23235 -3.02931,18.36469 -3.51483,196.05426 -0.29566,108.20723 0.16058,198.57651 1.0642,210.79021 1.42971,19.32469 2.01374,21.94582 6.0768,27.27277 6.33669,8.30783 11.34079,11.12128 22.34305,12.56193 5.27238,0.69037 100.47817,1.15399 211.56844,1.03029 181.32528,-0.20195 202.58499,-0.49941 207.8752,-2.90861 z"
id="path3"
transform="scale(0.03)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="800px"
height="800px"
viewBox="0 0 24 24"
fill="none"
version="1.1"
id="svg1"
sodipodi:docname="checkbox-uncheck.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.549375"
inkscape:cx="334.01593"
inkscape:cy="498.74858"
inkscape:window-width="954"
inkscape:window-height="1045"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Interface / Checkbox_Check" />
<g
id="Interface / Checkbox_Check">
<path
id="Vector"
d="m 4,16.8002 v -9.6 C 4,6.08009 4,5.51962 4.21799,5.0918 4.40973,4.71547 4.71547,4.40973 5.0918,4.21799 5.51962,4 6.08009,4 7.2002,4 h 9.6 c 1.1201,0 1.6794,0 2.1072,0.21799 0.3763,0.19174 0.6831,0.49748 0.8748,0.87381 C 20,5.5192 20,6.07899 20,7.19691 v 9.60669 c 0,1.1179 0,1.6769 -0.2178,2.1043 -0.1917,0.3763 -0.4985,0.6826 -0.8748,0.8743 C 18.48,20 17.921,20 16.8031,20 H 7.19691 C 6.07899,20 5.5192,20 5.0918,19.7822 4.71547,19.5905 4.40973,19.2842 4.21799,18.9079 4,18.4801 4,17.9203 4,16.8002 Z"
stroke="#000000"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
sodipodi:nodetypes="ssccssccssccssccs" />
<path
style="fill:#ffffff;stroke:none;stroke-width:0;stroke-linejoin:round;stroke-dasharray:none"
d="m 197.49716,698.05867 c -9.51081,-0.69714 -20.97839,-2.28442 -25.48351,-3.52728 -23.70435,-6.53953 -49.53163,-28.08527 -60.0267,-50.07571 -11.25625,-23.58535 -11.13566,-20.69772 -10.54104,-252.42412 0.50777,-197.87636 0.73145,-210.5848 3.87255,-220.01791 10.98818,-32.99882 33.69637,-55.70701 66.69519,-66.69519 9.43311,-3.1411 22.14155,-3.36478 220.01791,-3.87255 231.59178,-0.59428 228.85346,-0.7082 252.33345,10.49777 15.3635,7.33232 34.26318,25.68875 42.34389,41.12675 12.42777,23.74297 11.88232,14.05756 12.69599,225.43391 0.78039,202.7352 -0.0884,235.68165 -6.71864,254.79581 -9.30619,26.82845 -32.58524,50.14014 -59.23915,59.32223 -6.58312,2.26783 -19.59829,4.44143 -32.76451,5.47182 -24.77577,1.93895 -376.67127,1.90795 -403.18543,-0.0355 z m 416.8785,-66.68447 c 7.41973,-3.34954 16.1894,-13.53942 17.68441,-20.54837 0.76286,-3.5765 1.38703,-99.88158 1.38703,-214.01127 0,-232.19509 1.09029,-213.46397 -13.05322,-224.25175 l -6.68616,-5.09978 H 400.59675 c -209.88362,0 -213.20246,0.0551 -219.15251,3.6405 -3.32285,2.00227 -7.82797,6.50739 -10.01138,10.01137 l -3.96983,6.37088 v 213.11097 213.11097 l 5.07967,6.65981 c 6.00435,7.87211 10.54552,10.61402 20.40383,12.3196 13.0969,2.2659 416.28392,1.0098 421.42913,-1.31293 z"
id="path2"
transform="scale(0.03)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
viewBox="0 0 16 16"
id="svg1"
sodipodi:docname="delete-symbolic.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="53.25"
inkscape:cx="8"
inkscape:cy="8"
inkscape:window-width="1912"
inkscape:window-height="1045"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<defs
id="defs1">
<style
id="current-color-scheme"
type="text/css">.ColorScheme-Text { color:#dedede; }</style>
</defs>
<path
d="m6 0c-0.554 0-1 0.446-1 1v1h-4.5c-0.277 0-0.5 0.223-0.5 0.5s0.223 0.5 0.5 0.5h0.49805l0.54688 11.061c0.0612 1.0155 0.90615 1.9395 1.998 1.9395h7.9141c1.0919 0 1.9368-0.92391 1.998-1.9395l0.54688-11.061h0.49805c0.277 0 0.5-0.223 0.5-0.5s-0.223-0.5-0.5-0.5h-4.5v-1c0-0.554-0.446-1-1-1h-3zm0 1h3v1h-3v-1zm-4 2h11l-0.54297 11c-0.03333 0.553-0.446 1-1 1h-7.9141c-0.554 0-0.96667-0.447-1-1l-0.54297-11zm2.5 2c-0.277 0-0.5 0.223-0.5 0.5v7c0 0.277 0.223 0.5 0.5 0.5s0.5-0.223 0.5-0.5v-7c0-0.277-0.223-0.5-0.5-0.5zm3 0c-0.277 0-0.5 0.223-0.5 0.5v7c0 0.277 0.223 0.5 0.5 0.5s0.5-0.223 0.5-0.5v-7c0-0.277-0.223-0.5-0.5-0.5zm3 0c-0.277 0-0.5 0.223-0.5 0.5v7c0 0.277 0.223 0.5 0.5 0.5s0.5-0.223 0.5-0.5v-7c0-0.277-0.223-0.5-0.5-0.5z"
style="fill:#ffffff"
class="ColorScheme-Text"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+59
View File
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
id="svg2"
sodipodi:docname="edit.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="18.826718"
inkscape:cx="4.1430482"
inkscape:cy="11.101244"
inkscape:window-width="1014"
inkscape:window-height="1045"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<metadata
id="metadata1">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<path
d="m4 1c-1.108 0-2 0.892-2 2v10c0 1.108 0.892 2 2 2h8c1.108 0 2-0.892 2-2v-5.0488l-1 1v4.0488c0 0.554-0.446 1-1 1h-8c-0.554 0-1-0.446-1-1v-10c0-0.554 0.446-1 1-1h6.0488l1-1z"
fill="#dedede"
id="path1"
style="fill:#ffffff" />
<path
class="ColorScheme-Text"
d="m14.607 1.6863c-0.78347-0.78347-2.045-0.78347-2.8284 0l3.5355 3.5355c0.78347-0.78347 0.78348-2.045 0-2.8284zm-3.5355 0.70711-7.0711 7.0711-6e-7 3.5355h3.5355l7.0711-7.0711zm0 1.4142 2.1213 2.1213-5.6569 5.6569-2.1213-2.1213z"
fill="#dedede"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
viewBox="0 0 16 16"
id="svg2"
sodipodi:docname="plus-symbolic.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="53.25"
inkscape:cx="7.9906103"
inkscape:cy="8"
inkscape:window-width="1912"
inkscape:window-height="1045"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<rect
x="7.5"
y="2"
width="1"
height="12"
fill="#dedede"
id="rect1"
style="fill:#ffffff;stroke:none;stroke-opacity:1" />
<rect
transform="rotate(90)"
x="7.5"
y="-14"
width="1"
height="12"
fill="#dedede"
id="rect2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M13.969 2.969L6.5 10.438l-4.469-4.47L.97 7.032l5.531 5.53 8.531-8.53z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1;marker:none" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="#dedede"/></svg>

After

Width:  |  Height:  |  Size: 711 B

+1
View File
@@ -14,6 +14,7 @@
@import url("./styles/tray.css");
@import url("./styles/battery-widget.css");
@import url("./styles/lock.css");
@import url("./styles/todo.css");
* {
all: unset;
+10 -1
View File
@@ -17,6 +17,7 @@ from modules.panel.components.indicators import (
from modules.panel.components.menubar import MenuBar
from modules.panel.components.recording_indicator import RecordingIndicator
from modules.panel.components.workspace import WorkspaceIndicator
from modules.todo.todo_widget import TodoListCapture
from utils.roam import modus_service
from widgets.mousecapture import MouseCapture
from widgets.wayland import WaylandWindow as Window
@@ -113,6 +114,9 @@ class Panel(Window):
layer="overlay", child_window=NotificationCenter()
)
# Todo List with MouseCapture
self.todo_list = TodoListCapture()
# Notification Center Icon
self.notification_icon = Svg(
size=22,
@@ -127,10 +131,11 @@ class Panel(Window):
on_clicked=self.on_notification_icon_clicked,
)
# Clickable DateTime for notification center
# Clickable DateTime for todo list
self.datetime_button = Button(
name="datetime-button",
child=DateTime(name="date-time", formatters=["%a %-d %b %I:%M %P"]),
on_clicked=self.on_datetime_clicked,
)
self.recording_indicator = RecordingIndicator()
@@ -213,6 +218,10 @@ class Panel(Window):
self.notification_center.toggle_mousecapture()
# Do nothing if no notifications
def on_datetime_clicked(self, *args):
"""Handle datetime button clicks - open todo list."""
self.todo_list.toggle_mousecapture()
def update_notification_icon(self):
"""Update the notification icon based on count and DND state."""
count = notification_service.count
+1
View File
@@ -0,0 +1 @@
# Todo module
+456
View File
@@ -0,0 +1,456 @@
# Standard library imports
from datetime import datetime
# Fabric imports
from fabric.utils import get_relative_path
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.entry import Entry
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.widgets.svg import Svg
from gi.repository import GLib
# Local imports
from services.todo import todo_service
from widgets.mousecapture import MouseCapture
from widgets.wayland import WaylandWindow as Window
class TodoItem(Box):
"""Individual todo item widget"""
def __init__(self, todo_data, todo_list_widget, **kwargs):
self.todo_data = todo_data
self.todo_list_widget = todo_list_widget
self.editing = False
super().__init__(
name="todo-item",
orientation="h",
spacing=8,
style_classes=["menu"],
**kwargs,
)
self._build_ui()
def _build_ui(self):
"""Build the todo item UI"""
# Checkbox for completion - using SVG icons
checkbox_icon = (
"checkbox-check.svg"
if self.todo_data["completed"]
else "checkbox-uncheck.svg"
)
self.checkbox_icon = Svg(
name="todo-checkbox-icon",
size=24,
svg_file=get_relative_path(
"../../config/assets/icons/todo/" + checkbox_icon
),
)
self.checkbox = Button(
name="todo-checkbox",
child=self.checkbox_icon,
on_clicked=self._toggle_completion,
)
# Todo text (can be converted to entry for editing)
text_content = self.todo_data["text"]
if self.todo_data["completed"]:
text_content = f"<s>{text_content}</s>"
self.text_label = Label(
markup=text_content,
name="todo-text",
h_align="start",
h_expand=True,
line_wrap="word-char",
style_classes=(
["title-widget"]
if not self.todo_data["completed"]
else ["status-label"]
),
)
# Date/time label
created_at = datetime.fromisoformat(self.todo_data["created_at"])
date_text = created_at.strftime("%b %d, %Y at %I:%M %p")
self.date_label = Label(
label=date_text,
name="todo-date",
h_align="start",
style_classes=["todo-date-text"],
)
self.text_entry = Entry(
name="todo-text-entry",
text=self.todo_data["text"],
h_expand=True,
visible=False,
)
self.text_entry.connect("activate", self._save_edit)
# Priority indicator
# priority_symbols = {"high": "🔴", "medium": "🟡", "low": "🟢"}
#
# self.priority_label = Label(
# label=priority_symbols.get(self.todo_data["priority"], "🟡"),
# name="todo-priority-label",
# )
# self.priority_button = Button(
# name="todo-priority",
# size=(20, 20),
# child=self.priority_label,
# on_clicked=self._cycle_priority,
# )
#
# Edit button - using SVG icon
self.edit_icon = Svg(
name="todo-edit-icon",
size=14,
svg_file=get_relative_path("../../config/assets/icons/todo/edit.svg"),
)
self.edit_button = Button(
name="todo-edit",
child=self.edit_icon,
on_clicked=self._start_edit,
)
# Delete button - using SVG icon
self.delete_icon = Svg(
name="todo-delete-icon",
size=14,
svg_file=get_relative_path(
"../../config/assets/icons/todo/delete-symbolic.svg"
),
)
self.delete_button = Button(
name="todo-delete",
child=self.delete_icon,
on_clicked=self._delete_todo,
)
# Text container that switches between label and entry
self.text_container = Box(
orientation="v",
h_expand=True,
children=[
Box(orientation="h", children=[self.text_label]),
self.date_label,
],
)
self.children = [
self.checkbox,
self.text_container,
# self.priority_button,
self.edit_button,
self.delete_button,
]
def _toggle_completion(self, *_):
"""Toggle todo completion status"""
todo_service.toggle_todo(self.todo_data["id"])
def _cycle_priority(self, *_):
"""Cycle through priority levels"""
priorities = ["low", "medium", "high"]
current_index = priorities.index(self.todo_data["priority"])
new_priority = priorities[(current_index + 1) % len(priorities)]
todo_service.set_priority(self.todo_data["id"], new_priority)
def _start_edit(self, *_):
"""Start editing the todo text"""
if self.editing:
return
self.editing = True
self.text_container.children = [
Box(orientation="h", children=[self.text_entry]),
self.date_label,
]
self.text_entry.set_visible(True)
self.text_entry.grab_focus()
self.text_entry.set_position(-1) # Move cursor to end
def _save_edit(self, *_):
"""Save the edited todo text"""
if not self.editing:
return
new_text = self.text_entry.get_text().strip()
if new_text:
todo_service.edit_todo(self.todo_data["id"], new_text)
self._cancel_edit()
def _cancel_edit(self):
"""Cancel editing and revert to label"""
self.editing = False
self.text_container.children = [
Box(orientation="h", children=[self.text_label]),
self.date_label,
]
self.text_entry.set_visible(False)
def _delete_todo(self, *_):
"""Delete this todo"""
todo_service.delete_todo(self.todo_data["id"])
def update_from_data(self, todo_data):
"""Update the widget from new todo data"""
self.todo_data = todo_data
# Update checkbox icon by recreating it
checkbox_icon = (
"checkbox-check.svg" if todo_data["completed"] else "checkbox-uncheck.svg"
)
new_checkbox_icon = Svg(
name="todo-checkbox-icon",
size=20,
svg_file=get_relative_path(
"../../config/assets/icons/todo/" + checkbox_icon
),
)
self.checkbox.set_child(new_checkbox_icon)
self.checkbox_icon = new_checkbox_icon
# Update text and styling with markup
text_content = todo_data["text"]
if todo_data["completed"]:
text_content = f"<s>{text_content}</s>"
self.text_label.set_markup(text_content)
self.text_label.style_classes = (
["title-widget"] if not todo_data["completed"] else ["status-label"]
)
# Update date/time
created_at = datetime.fromisoformat(todo_data["created_at"])
date_text = created_at.strftime("%b %d, %Y at %I:%M %p")
self.date_label.set_label(date_text)
class TodoListWidget(Window):
"""Main todo list widget window"""
def __init__(self, **kwargs):
super().__init__(
title="modus-todo",
anchor="top right",
margin="2px 10px 0px 0px",
exclusivity="auto",
keyboard_mode="on-demand",
name="todo-list-window",
visible=False, # Back to hidden by default
**kwargs,
)
self.todo_items = {} # Maps todo IDs to TodoItem widgets
# Register callback with todo service
todo_service.add_callback(self._on_todo_event)
self._build_ui()
self._refresh_todos()
# Add keybinding for escape
self.add_keybinding("Escape", self.hide_todo_list)
def _build_ui(self):
"""Build the main UI"""
# Header with title and stats
self.stats_label = Label(
label="",
name="todo-stats",
style_classes=["status-label"],
h_align="start",
)
self.header = Box(
name="todo-header",
orientation="v",
children=[
Label(
label="Todo List",
name="todo-title",
style_classes=["title"],
h_align="start",
),
self.stats_label,
],
)
# Add new todo section
self.new_todo_entry = Entry(
name="new-todo-entry",
placeholder_text="Add a new task...",
h_expand=True,
)
self.new_todo_entry.connect("activate", self._add_todo)
# Add button - using SVG icon
self.add_icon = Svg(
name="add-todo-icon",
size=12,
svg_file=get_relative_path(
"../../config/assets/icons/todo/plus-symbolic.svg"
),
)
self.add_button = Button(
name="add-todo-button",
child=self.add_icon,
on_clicked=self._add_todo,
)
self.add_section = Box(
name="todo-add-section",
orientation="h",
spacing=8,
style_classes=["menu"],
children=[
self.new_todo_entry,
self.add_button,
],
)
# Todo items container
self.todos_container = Box(
name="todos-container",
orientation="v",
spacing=4,
)
# Scrolled window for todos
self.scrolled = ScrolledWindow(
name="todos-scrolled",
min_content_height=300,
max_content_height=500,
min_content_width=400,
child=self.todos_container,
policy="automatic",
v_expand=True, # Allow vertical expansion
)
# Clear completed button
self.clear_button = Button(
name="clear-completed-button",
label="Clear Completed",
style_classes=["status-label"],
on_clicked=self._clear_completed,
)
# Main container
self.main_container = Box(
name="todo-main-container",
orientation="v",
spacing=8,
style_classes=["menu"],
children=[
self.header,
self.add_section,
self.scrolled,
self.clear_button,
],
)
self.children = [self.main_container]
def _add_todo(self, *_):
"""Add a new todo"""
text = self.new_todo_entry.get_text().strip()
if text:
todo_service.add_todo(text)
self.new_todo_entry.set_text("")
def _clear_completed(self, *_):
"""Clear all completed todos"""
todo_service.clear_completed()
def _on_todo_event(self, event_type, data=None):
"""Handle todo service events via callback"""
if event_type == "todos-changed":
GLib.idle_add(self._refresh_todos)
elif event_type == "todo-added":
GLib.idle_add(self._refresh_todos)
elif event_type == "todo-deleted":
GLib.idle_add(self._refresh_todos)
elif event_type in ["todo-toggled", "todo-edited", "todo-priority-changed"]:
if data and data["id"] in self.todo_items:
GLib.idle_add(
lambda: self.todo_items[data["id"]].update_from_data(data)
)
GLib.idle_add(self._update_stats)
def _refresh_todos(self, *_):
"""Refresh the entire todo list"""
# Clear existing items
self.todo_items.clear()
self.todos_container.children = []
# Get all todos
todos = todo_service.todos
# Sort todos: incomplete first, then by priority, then by creation date
def sort_key(todo):
priority_order = {"high": 0, "medium": 1, "low": 2}
return (
todo["completed"], # False (incomplete) comes before True (completed)
priority_order.get(todo["priority"], 1),
todo["created_at"],
)
sorted_todos = sorted(todos, key=sort_key)
# Create todo item widgets
for todo in sorted_todos:
todo_item = TodoItem(todo, self)
self.todo_items[todo["id"]] = todo_item
# Update container children
self.todos_container.children = list(self.todo_items.values())
# Update stats
self._update_stats()
def _update_stats(self):
"""Update the statistics display"""
stats = todo_service.get_stats()
stats_text = f"{stats['pending']} pending, {stats['completed']} completed"
self.stats_label.set_label(stats_text)
def set_visible(self, visible):
"""Override set_visible for debugging"""
super().set_visible(visible)
def hide_todo_list(self, *_):
"""Hide the todo list"""
if hasattr(self, "_mousecapture_parent"):
self._mousecapture_parent.toggle_mousecapture()
self.set_visible(False)
def _init_mousecapture(self, mousecapture):
"""Initialize mousecapture parent reference"""
self._mousecapture_parent = mousecapture
def destroy(self):
"""Clean up when destroyed"""
# Remove callback from todo service
todo_service.remove_callback(self._on_todo_event)
super().destroy()
class TodoListCapture(MouseCapture):
"""MouseCapture wrapper for the todo list"""
def __init__(self, **kwargs):
super().__init__(
layer="top",
child_window=TodoListWidget(),
**kwargs,
)
+169
View File
@@ -0,0 +1,169 @@
# Standard library imports
import json
import os
import uuid
from datetime import datetime
from pathlib import Path
# Fabric imports
from fabric.core.service import Property, Service, Signal
# Local imports
import config.data as data
class TodoService(Service):
"""Service for managing persistent todo list with JSON storage"""
def __init__(self):
super().__init__()
self._todos = []
self._file_path = self._get_todos_file_path()
self._load_todos()
self._callbacks = []
def add_callback(self, callback):
"""Add a callback function to be notified of changes"""
self._callbacks.append(callback)
def remove_callback(self, callback):
"""Remove a callback function"""
if callback in self._callbacks:
self._callbacks.remove(callback)
def _notify_callbacks(self, event_type, data=None):
"""Notify all registered callbacks of changes"""
for callback in self._callbacks:
try:
callback(event_type, data)
except Exception as e:
print(f"Error in todo callback: {e}")
def _get_todos_file_path(self):
"""Returns the path to the todos JSON file"""
cache_dir = Path(data.CACHE_DIR) / "todos"
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir / "todos.json"
def _load_todos(self):
"""Load todos from JSON file"""
try:
if self._file_path.exists():
with open(self._file_path, "r", encoding="utf-8") as f:
self._todos = json.load(f)
else:
self._todos = []
except Exception as e:
print(f"Error loading todos: {e}")
self._todos = []
def _save_todos(self):
"""Save todos to JSON file"""
try:
with open(self._file_path, "w", encoding="utf-8") as f:
json.dump(self._todos, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error saving todos: {e}")
@Property(list, "readable")
def todos(self):
"""Get all todos"""
return self._todos.copy()
def add_todo(self, text: str, priority: str = "medium") -> dict:
"""Add a new todo item"""
todo = {
"id": str(uuid.uuid4()),
"text": text,
"completed": False,
"priority": priority, # low, medium, high
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat()
}
self._todos.append(todo)
self._save_todos()
self._notify_callbacks("todo-added", todo)
self._notify_callbacks("todos-changed")
return todo
def delete_todo(self, todo_id: str) -> bool:
"""Delete a todo item by ID"""
for i, todo in enumerate(self._todos):
if todo["id"] == todo_id:
deleted_todo = self._todos.pop(i)
self._save_todos()
self._notify_callbacks("todo-deleted", deleted_todo)
self._notify_callbacks("todos-changed")
return True
return False
def toggle_todo(self, todo_id: str) -> bool:
"""Toggle completion status of a todo item"""
for todo in self._todos:
if todo["id"] == todo_id:
todo["completed"] = not todo["completed"]
todo["updated_at"] = datetime.now().isoformat()
self._save_todos()
self._notify_callbacks("todo-toggled", todo)
self._notify_callbacks("todos-changed")
return True
return False
def edit_todo(self, todo_id: str, new_text: str) -> bool:
"""Edit the text of a todo item"""
for todo in self._todos:
if todo["id"] == todo_id:
todo["text"] = new_text
todo["updated_at"] = datetime.now().isoformat()
self._save_todos()
self._notify_callbacks("todo-edited", todo)
self._notify_callbacks("todos-changed")
return True
return False
def set_priority(self, todo_id: str, priority: str) -> bool:
"""Set the priority of a todo item"""
if priority not in ["low", "medium", "high"]:
return False
for todo in self._todos:
if todo["id"] == todo_id:
todo["priority"] = priority
todo["updated_at"] = datetime.now().isoformat()
self._save_todos()
self._notify_callbacks("todo-priority-changed", todo)
self._notify_callbacks("todos-changed")
return True
return False
def get_todo(self, todo_id: str) -> dict | None:
"""Get a specific todo by ID"""
for todo in self._todos:
if todo["id"] == todo_id:
return todo.copy()
return None
def clear_completed(self):
"""Remove all completed todos"""
initial_count = len(self._todos)
self._todos = [todo for todo in self._todos if not todo["completed"]]
if len(self._todos) < initial_count:
self._save_todos()
self._notify_callbacks("todos-changed")
def get_stats(self) -> dict:
"""Get todo statistics"""
total = len(self._todos)
completed = sum(1 for todo in self._todos if todo["completed"])
pending = total - completed
return {
"total": total,
"completed": completed,
"pending": pending,
"completion_rate": (completed / total * 100) if total > 0 else 0
}
# Global service instance
todo_service = TodoService()
+258
View File
@@ -0,0 +1,258 @@
/* Todo List Styles - matching control center design */
#todo-list-window {
/* background-color: alpha(#fff, 0.05); */
border-radius: 12px;
box-shadow: none;
margin: 0;
}
#todo-main-container {
box-shadow: inset 0 0 0 1px alpha(#aaa, 0.4);
border-radius: 12px;
padding: 12px; /* Increased padding */
min-height: 500px; /* Ensure minimum height */
min-width: 350px; /* Ensure minimum width */
}
#todo-header {
margin-bottom: 8px;
}
#todo-title {
font-size: 16px;
font-family: "SF Pro Rounded";
font-weight: bold;
color: #ffffff;
}
#todo-stats {
font-size: 12px;
font-weight: 500;
color: #999;
margin-top: -2px;
}
/* Add new todo section */
#todo-add-section {
box-shadow: inset 0 0 0 0.5px alpha(#aaa, 0.4);
border: 1px solid alpha(#111, 0.4);
border-radius: 12px;
padding: 1rem;
margin-bottom: 8px;
min-height: 40px; /* Ensure minimum height */
}
#new-todo-entry {
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
padding: 8px 12px;
color: #ffffff;
font-size: 13px;
font-family: "SF Pro Rounded";
}
#new-todo-entry:focus {
border-color: #007aff;
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.3);
background-color: rgba(255, 255, 255, 0.15);
}
#add-todo-button {
/* background-color: #007aff; */
/* border: 1px solid #007aff; */
border-radius: 6px;
min-width: 20px;
min-height: 12px;
color: #ffffff;
font-size: 16px;
}
#add-todo-button:hover {
/* background-color: #0056cc; */
/* border-color: #0056cc; */
}
/* Todo items container */
#todos-scrolled {
background-color: transparent;
min-height: 200px; /* Minimum height for content */
}
#todos-container {
padding: 4px;
}
/* Individual todo items */
#todo-item {
background-color: alpha(#000, 0.2); /* More visible background */
border: 1px solid alpha(#111, 0.4);
border-radius: 12px;
padding: 12px;
margin: 4px 0;
min-height: 40px; /* Ensure minimum height */
transition: background-color 0.2s ease;
}
#todo-item:hover {
background-color: alpha(#fff, 0.12);
}
/* Todo item controls */
#todo-checkbox {
background-color: transparent;
border-radius: 50%; /* Circular appearance */
min-width: 20px;
min-height: 20px;
margin-right: 8px;
}
#todo-checkbox:hover {
border-color: #007aff;
}
/* SVG icon styling for checkboxes and buttons */
#todo-checkbox-icon {
color: #007aff;
}
#todo-edit-icon,
#todo-delete-icon,
#add-todo-icon {
color: #ffffff;
opacity: 0.8;
}
#todo-edit-icon:hover,
#todo-delete-icon:hover,
#add-todo-icon:hover {
opacity: 1;
}
#todo-text {
font-size: 13px;
font-weight: 400;
color: #ffffff;
font-family: "SF Pro Rounded";
}
#todo-date {
font-size: 10px;
font-weight: 400;
color: #999999;
font-family: "SF Pro Rounded";
margin-top: 2px;
opacity: 0.8;
}
.todo-date-text {
color: #999999;
font-size: 10px;
}
#todo-text-entry {
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
padding: 4px 8px;
color: #ffffff;
font-size: 14px;
font-family: "SF Pro Rounded";
}
#todo-text-entry:focus {
border-color: #007aff;
box-shadow: 0 0 0 1px rgba(0, 122, 255, 0.3);
background-color: rgba(255, 255, 255, 0.15);
}
/* Priority, edit, delete buttons */
#todo-priority,
#todo-edit,
#todo-delete {
background-color: transparent;
border: none;
border-radius: 4px;
min-width: 20px;
min-height: 20px;
margin: 0 2px;
transition: background-color 0.2s ease;
}
#todo-priority:hover,
#todo-edit:hover,
#todo-delete:hover {
background-color: alpha(#fff, 0.1);
}
#todo-priority-label,
#todo-edit-label,
#todo-delete-label {
font-size: 12px;
}
/* Clear completed button */
#clear-completed-button {
background-color: alpha(#fff, 0.1);
border: 1px solid alpha(#999, 0.3);
border-radius: 8px;
padding: 8px 16px;
color: #999;
font-size: 12px;
font-weight: 500;
font-family: "SF Pro Rounded";
margin-top: 8px;
transition: background-color 0.2s ease;
}
#clear-completed-button:hover {
background-color: alpha(#fff, 0.15);
color: #ffffff;
}
/* Completed todos styling */
#todo-item.completed {
opacity: 0.6;
}
#todo-item.completed #todo-text {
text-decoration: line-through;
color: #999;
}
/* Priority styling */
.priority-high #todo-priority-label {
color: #ff4444;
}
.priority-medium #todo-priority-label {
color: #ffaa00;
}
.priority-low #todo-priority-label {
color: #44ff44;
}
/* Scrollbar styling to match control center */
#todos-scrolled scrollbar {
background-color: transparent;
border-radius: 8px;
margin: 2px;
min-width: 8px;
}
#todos-scrolled scrollbar slider {
background-color: alpha(#fff, 0.3);
border-radius: 4px;
min-width: 6px;
margin: 1px;
}
#todos-scrolled scrollbar slider:hover {
background-color: alpha(#fff, 0.5);
}
#todos-scrolled scrollbar slider:active {
background-color: alpha(#fff, 0.7);
}