leftoff 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
leftoff/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ def main() -> None:
2
+ print("Hello from leftoff!")
leftoff/__main__.py ADDED
@@ -0,0 +1,115 @@
1
+
2
+ # engine
3
+ from leftoff.core.dtypes import StatsType # stats type dict
4
+ from leftoff.core.parser import LeftOffParser # main todo engine
5
+ from leftoff.core.stats import Stats # statics of task
6
+ from leftoff.core.cli import CLI # cli manger
7
+ # tui
8
+ from leftoff.tui.core import TuiApp # core tui app
9
+ # py libs
10
+ from pathlib import Path # py path lib
11
+ import argparse as cli # cli argument parser
12
+ # json
13
+ import json
14
+ # sys
15
+ import sys
16
+
17
+ def create_base_json(root_dir : Path) -> None :
18
+
19
+ title : str = input("Enter the Title : ")
20
+ version : float = float(input("Enter your current version : "))
21
+ data : dict = {
22
+ "title" : title,
23
+ "version": version,
24
+ "mode" : {
25
+ 'feat' : [],
26
+ 'issue' : []
27
+ }
28
+ }
29
+
30
+ file : Path = root_dir / "leftoff.json"
31
+ json.dump(data,file.open(mode='w'))
32
+
33
+ # tui
34
+ def show_tui(l_engine):
35
+ if l_engine.data['mode']['feat'] == [] and l_engine.data['mode']['issue'] == [] :
36
+ print("There is no task to show")
37
+ return
38
+ feat_stats = Stats(l_engine.get_feat_table())
39
+ issue_stats = Stats(l_engine.get_issue_table())
40
+ stats = StatsType(
41
+ feat_count=feat_stats.get_count(),
42
+ issue_count=issue_stats.get_count(),
43
+ active=(feat_stats.get_active_count()[0] + issue_stats.get_active_count()[0]),
44
+ compl=(feat_stats.get_active_count()[1] + issue_stats.get_active_count()[1])
45
+ )
46
+ app = TuiApp(
47
+ title=l_engine.get_title(),
48
+ version=l_engine.get_version(),
49
+ mode=None,
50
+ feat_tabel=l_engine.get_feat_table(),
51
+ issue_tabel=l_engine.get_issue_table(),
52
+ stats=stats
53
+ )
54
+ app.run()
55
+
56
+ def main() -> None :
57
+
58
+
59
+ root_dir : Path = Path(".")
60
+ # if leftover.json isnt in root everything falls apart
61
+ if not (root_dir / "leftoff.json").exists() :
62
+ create_base_json(root_dir)
63
+ print("\nGood Luck with project !!")
64
+ return
65
+
66
+ parser = cli.ArgumentParser(description="Todo cli also support tui")
67
+ parser.add_argument("--tui", action="store_true",help="show tui")
68
+ parser.add_argument("--mode",type=str,help="feature mode")
69
+ parser.add_argument("--add",type=str,help="add to task")
70
+ parser.add_argument("--mod",type=str,help="Change the task status")
71
+ parser.add_argument("--rm",type=int,help="Remove task")
72
+ args = parser.parse_args()
73
+
74
+ if len(sys.argv) == 1 :
75
+ print("To Know how to use it ? use ( --help )")
76
+ return
77
+
78
+ engine = LeftOffParser(root_dir)
79
+ cli_parser = CLI(engine)
80
+
81
+ if args.tui: show_tui(engine)
82
+
83
+ # cli
84
+
85
+ if args.mode == 'feat':
86
+ if args.add is not None :
87
+ cli_parser.add_task('feat',args.add)
88
+ print("New task is added on Feature !!")
89
+
90
+ if args.mod is not None :
91
+ mod_id = cli_parser.mod_task('feat',args.mod)
92
+ print(f"Task {mod_id} status is modifed")
93
+
94
+ if args.rm is not None :
95
+ cli_parser.rm_task('feat',task_id=args.rm)
96
+ print(f"Task {args.rm} is removed")
97
+
98
+ if args.mode == 'issue':
99
+ if args.add is not None :
100
+ cli_parser.add_task('issue',args.add)
101
+ print("New task is added on issue !!")
102
+
103
+ if args.mod is not None :
104
+
105
+ mod_id = cli_parser.mod_task('issue',args.mod)
106
+ print(f"Task {mod_id} status is modifed")
107
+
108
+ if args.rm is not None :
109
+ cli_parser.rm_task('issue',task_id=args.rm)
110
+ print(f"Task {args.rm} is removed")
111
+
112
+
113
+ if __name__ == "__main__":
114
+
115
+ main()
leftoff/core/cli.py ADDED
@@ -0,0 +1,45 @@
1
+
2
+ # engine
3
+ from leftoff.core.parser import LeftOffParser
4
+ from leftoff.core.input_val import clean_args
5
+ # typing
6
+ from typing import Literal
7
+ # py date and time lin
8
+ from datetime import datetime,timedelta
9
+
10
+
11
+ class CLI:
12
+
13
+ def __init__(self, parser_engine : LeftOffParser) -> None:
14
+
15
+ self.engine = parser_engine
16
+
17
+ def add_task(self, mode : Literal['feat', 'issue'] , add_input_text : str) -> None :
18
+ vals : list = clean_args(add_input_text)
19
+ today = datetime.now().date()
20
+ mode_id : int = self.engine.get_feat_id if mode == 'feat' else self.engine.get_issue_id
21
+ self.engine.add_task(mode,[{
22
+ "id" : mode_id + 1,
23
+ "task" : vals[0],
24
+ "status" : "TODO",
25
+ "due" : (today + timedelta(days=int(vals[1]))).strftime("%d-%m-%Y")
26
+ }])
27
+ self.engine.json_writer()
28
+
29
+ def mod_task(self, mode : Literal['feat','issue'], mod_input_text : str ) -> int:
30
+
31
+ vals : list = clean_args(mod_input_text)
32
+
33
+ self.engine.mod_status(mode,
34
+ task_id=int(vals[0]),
35
+ status=vals[1]
36
+ )
37
+ self.engine.json_writer()
38
+ return int(vals[0])
39
+
40
+ def rm_task(self, mode : Literal['feat', 'issue'], task_id : int ) -> None:
41
+
42
+ self.engine.rm_task(mode,task_id=task_id)
43
+
44
+ self.engine.json_writer()
45
+
leftoff/core/dtypes.py ADDED
@@ -0,0 +1,53 @@
1
+ from typing import TypedDict,Literal
2
+
3
+
4
+ class Table(TypedDict):
5
+
6
+ id : int
7
+ task : str
8
+ status : Literal["TODO","FIXING","FIXED"]
9
+ due : str # deadline date as str
10
+
11
+
12
+ class TodoJson(TypedDict):
13
+ title : str
14
+ version : float
15
+ mode : dict[Literal["feat","issue"],list[Table]]
16
+
17
+ '''
18
+ {
19
+ "title" : "shatokens",
20
+ "version" : 0.1,
21
+ "mode" : {
22
+ "feat" : [
23
+ {
24
+ "id" : 1,
25
+ "task" : "add mult file reading",
26
+ "status" : false,
27
+ "due" : "12-08-2026"
28
+ },
29
+ {...}
30
+ ],
31
+ "issue" : [
32
+ {
33
+ "id" : 1,
34
+ "task" : "fix the padding on encode function",
35
+ "status" : false,
36
+ "due" : "21-08-2026"
37
+ }
38
+ ]
39
+ }
40
+
41
+
42
+ }
43
+ '''
44
+
45
+ class StatsType(TypedDict):
46
+
47
+ feat_count : int
48
+
49
+ issue_count : int
50
+
51
+ active : int
52
+
53
+ compl : int # complete task
@@ -0,0 +1,4 @@
1
+
2
+ def clean_args(input_text : str ) -> list :
3
+
4
+ return [x.strip() for x in input_text.split(",")]
leftoff/core/parser.py ADDED
@@ -0,0 +1,78 @@
1
+ from typing import Literal
2
+ from pathlib import Path
3
+
4
+ import json
5
+
6
+ from leftoff.core.dtypes import TodoJson,Table,StatsType
7
+ from leftoff.core.type_val import validate_todo,InvalidTodo
8
+
9
+ class LeftOffParser:
10
+
11
+ def __init__(self, root_dir : Path ) -> None :
12
+
13
+ self.root_dir : Path = root_dir
14
+
15
+ self.todo_file : Path = root_dir / "leftoff.json"
16
+
17
+ if not self.todo_file.exists :
18
+ raise FileNotFoundError("todo file is not found")
19
+
20
+ self.data : TodoJson = self.json_reader()
21
+ if not validate_todo(self.data):
22
+ raise InvalidTodo("Invalid todo.json format")
23
+
24
+ def json_reader(self) -> TodoJson :
25
+
26
+ data = json.load(self.todo_file.open(mode='r'))
27
+
28
+ return data
29
+
30
+ def json_writer(self) -> None:
31
+ json.dump(self.data,self.todo_file.open(mode="w"))
32
+
33
+ # reader
34
+
35
+ def get_title(self) -> str : return self.data.get('title')
36
+
37
+ def get_version(self) -> float : return self.data.get("version")
38
+
39
+ def get_feat_table(self) -> list[Table] : return self.data['mode']['feat']
40
+
41
+ def get_issue_table(self) -> list[Table] : return self.data['mode']['issue']
42
+
43
+ # utils
44
+ @property
45
+ def get_feat_id(self) -> int : return self.get_feat_table()[-1].get('id') if self.get_feat_table() != [] else 0
46
+ @property
47
+ def get_issue_id(self) -> int : return self.get_issue_table()[-1].get('id') if self.get_issue_table() != [] else 0
48
+
49
+ # writer
50
+
51
+ def add_task(self,mode : Literal['feat', 'issue'], task : list[Table]) -> None :
52
+
53
+ self.data['mode'][mode].extend(task)
54
+
55
+ def mod_status(self, mode : Literal['feat', 'issue'], task_id : int, status : Literal['FIXING', 'FIXED'] ) -> None :
56
+
57
+ tasks = self.data['mode'][mode]
58
+
59
+ task = next((x for x in tasks if x['id'] == task_id),None)
60
+
61
+ if task is not None : task['status'] = status
62
+
63
+ def rm_task(self, mode : Literal['feat', 'issue'], task_id: int ) -> None :
64
+
65
+ tasks = self.data['mode'][mode]
66
+
67
+ if tasks is None:
68
+ print("There is No task to delete")
69
+ return
70
+
71
+ # re-build the tasks without detele one
72
+ self.data['mode'][mode] = [
73
+ task for task in tasks if task['id'] != task_id
74
+ ]
75
+
76
+ # re-set task ids
77
+ for i,task in enumerate(self.data['mode'][mode],start=1) : task['id'] = i
78
+
leftoff/core/stats.py ADDED
@@ -0,0 +1,26 @@
1
+
2
+ from leftoff.core.dtypes import StatsType,Table
3
+
4
+ class Stats:
5
+
6
+ def __init__(self, table_data : list[Table]) -> None :
7
+
8
+ self.table_data : list[Table] = table_data
9
+
10
+
11
+ def get_count(self) -> int : return len(self.table_data)
12
+
13
+ def get_active_count(self) -> tuple[int , int]:
14
+
15
+ ac_count : int = 0
16
+ comp_count : int = 0
17
+
18
+ for task in self.table_data:
19
+ status : str = task.get('status')
20
+ if status == 'TODO' or status == 'FIXING' : ac_count += 1
21
+ elif status == 'FIXED' : comp_count += 1
22
+
23
+ return (ac_count,comp_count)
24
+
25
+
26
+
@@ -0,0 +1,43 @@
1
+ from typing import Any
2
+
3
+
4
+ class InvalidTodo(Exception):
5
+ pass
6
+
7
+
8
+ def validate_task(task: Any) -> bool:
9
+ return (
10
+ isinstance(task, dict)
11
+ and isinstance(task.get("id"), int)
12
+ and isinstance(task.get("task"), str)
13
+ and isinstance(task.get("status"), str)
14
+ and isinstance(task.get("due"), str)
15
+ )
16
+
17
+
18
+ def validate_todo(data: Any) -> bool:
19
+ if not isinstance(data, dict):
20
+ return False
21
+
22
+ if not isinstance(data.get("title"), str):
23
+ return False
24
+
25
+ if not isinstance(data.get("version"), (int, float)):
26
+ return False
27
+
28
+ mode = data.get("mode")
29
+
30
+ if not isinstance(mode, dict):
31
+ return False
32
+
33
+ for category, tasks in mode.items():
34
+ if not isinstance(category, str):
35
+ return False
36
+
37
+ if not isinstance(tasks, list):
38
+ return False
39
+
40
+ if not all(validate_task(task) for task in tasks):
41
+ return False
42
+
43
+ return True
leftoff/tui/core.py ADDED
@@ -0,0 +1,62 @@
1
+ from textual.widgets import Header
2
+ from textual.containers import Vertical,Horizontal
3
+ from textual.app import App, ComposeResult
4
+ from textual.widgets import Static
5
+ from textual.containers import Container
6
+
7
+ from leftoff.core.dtypes import Table,StatsType
8
+ from leftoff.tui.table import GTable
9
+
10
+ class TuiApp(App):
11
+ CSS_PATH = "tcss/core_tui.tcss"
12
+
13
+ TITLE = "Todo CLI"
14
+
15
+ def __init__(self,title : str | None,
16
+ version : float | None,
17
+ mode : str | None,
18
+ feat_tabel : list[Table],
19
+ issue_tabel : list[Table],
20
+ stats : StatsType
21
+ ) -> None:
22
+
23
+ super().__init__()
24
+
25
+ if title : self.title = f"{title} | {version}V"
26
+ if mode : self.HEADER_MODE = mode
27
+
28
+ self.tabel_data_feat : list[Table] = feat_tabel
29
+ self.tabel_data_issue : list[Table] = issue_tabel
30
+ self.stats_data : StatsType = stats
31
+
32
+
33
+ def compose(self) -> ComposeResult:
34
+ # Header
35
+ yield Header(show_clock=False)
36
+
37
+ with Vertical(id="main") :
38
+ # --- STATS --
39
+ with Container(id="stats-box"):
40
+
41
+ yield Static("─ STATISTICS ─", id="stats-title")
42
+ with Horizontal(id="stats-row"):
43
+ yield Static(f"Active: [{self.stats_data.get('active')}]", classes="stat stat-green")
44
+ yield Static(f"Features: [{self.stats_data.get('feat_count')}]", classes="stat stat-blue")
45
+ yield Static(f"Issues: [{self.stats_data.get('issue_count')}]", classes="stat stat-red")
46
+ yield Static(f"Completed: [{self.stats_data.get('compl')}]", classes="stat stat-yellow")
47
+
48
+ # --- Tabel ----
49
+ with Container(id="tasks-box"):
50
+ yield Static("─ ACTIVE PROJECT TASKS ─", id="tasks-title")
51
+ with Horizontal(id="tables"):
52
+ with Vertical(id="features-panel"):
53
+ yield Static("[ FEATURES / WORKS ]", classes="panel-title")
54
+ yield GTable(id="features-table",table_data=self.tabel_data_feat)
55
+ with Vertical(id="issues-panel"):
56
+ yield Static("[ ISSUES / FIXES ]", classes="panel-title")
57
+ yield GTable(id="issues-table",table_data=self.tabel_data_issue)
58
+
59
+
60
+ def on_mount(self) -> None:
61
+ self.query_one("#features-table", GTable).focus()
62
+
leftoff/tui/table.py ADDED
@@ -0,0 +1,44 @@
1
+ from textual.widgets import DataTable
2
+ from rich.text import Text
3
+
4
+ from leftoff.core.dtypes import Table
5
+
6
+ class GTable(DataTable):
7
+ ''' Genral Tabel Format '''
8
+
9
+
10
+ STATUS_STYLES = {
11
+ "TODO": "bold green",
12
+ "FIXED": "bold yellow",
13
+ "FIXING": "bold magenta",
14
+ }
15
+
16
+ def __init__(self, table_data : list[Table], **kwargs) -> None :
17
+
18
+ super().__init__(**kwargs)
19
+
20
+ self.table_data : list[Table] = table_data
21
+
22
+
23
+ def on_mount(self) -> None :
24
+
25
+ self.cursor_type = "row"
26
+ self.zebra_stripes = False
27
+
28
+ # adding cols of table
29
+ self.add_column("ID",width=4)
30
+ self.add_column("TASK",width=30)
31
+ self.add_column("STATUS",width=10)
32
+ self.add_column("DUE",width=12)
33
+
34
+ # adding rows of table
35
+ for item in self.table_data:
36
+ style = self.STATUS_STYLES.get(item["status"],"White")
37
+ self.add_row(
38
+ str(item["id"]),
39
+ item["task"],
40
+ Text(f"[{item['status']}]", style=style),
41
+ Text(item["due"], style="yellow"),
42
+ key=str(item["id"]),
43
+ )
44
+ self.move_cursor(row=0)
@@ -0,0 +1,150 @@
1
+ Screen {
2
+ background: #0a0a0a;
3
+ color: #c8c8c8;
4
+ }
5
+
6
+ Header {
7
+ background: #0a0a0a;
8
+ color: #e0e0e0;
9
+ text-style: bold;
10
+ dock: top;
11
+ height: 1;
12
+ }
13
+
14
+ #main {
15
+ layout: vertical;
16
+ height: 1fr;
17
+ padding: 0 1;
18
+ }
19
+
20
+ /* ========== STATISTICS ========== */
21
+ #stats-box {
22
+ height: 4;
23
+ border: solid #3a3a3a;
24
+ background: #0a0a0a;
25
+ }
26
+
27
+ #stats-title {
28
+ text-align: center;
29
+ color: #909090;
30
+ height: 1;
31
+ width: 100%;
32
+ }
33
+
34
+ #stats-row {
35
+ height: 1;
36
+ layout: horizontal;
37
+ align: center middle;
38
+ padding: 0 1;
39
+ }
40
+
41
+ .stat {
42
+ width: 1fr;
43
+ text-align: center;
44
+ height: 1;
45
+ }
46
+
47
+ .stat-green { color: #00ff66; }
48
+ .stat-blue { color: #00bfff; }
49
+ .stat-red { color: #ff5555; }
50
+ .stat-yellow { color: #ffcc00; }
51
+
52
+ /* ========== ACTIVE PROJECT TASKS ========== */
53
+ #tasks-box {
54
+ height: 1fr;
55
+ border: solid #3a3a3a;
56
+ background: #0a0a0a;
57
+ margin-top: 0;
58
+ }
59
+
60
+ #tasks-title {
61
+ text-align: center;
62
+ color: #909090;
63
+ height: 1;
64
+ width: 100%;
65
+ }
66
+
67
+ #tables {
68
+ height: 1fr;
69
+ layout: horizontal;
70
+ }
71
+
72
+ #features-panel, #issues-panel {
73
+ width: 1fr;
74
+ height: 1fr;
75
+ border: solid #2a2a2a;
76
+ background: #0a0a0a;
77
+ }
78
+
79
+ .panel-title {
80
+ text-align: center;
81
+ color: #d8d8d8;
82
+ background: #141414;
83
+ height: 1;
84
+ width: 100%;
85
+ text-style: bold;
86
+ }
87
+
88
+ DataTable {
89
+ height: 1fr;
90
+ background: #0a0a0a;
91
+ color: #c8c8c8;
92
+ scrollbar-size-vertical: 0;
93
+ scrollbar-size-horizontal: 0;
94
+ }
95
+
96
+ DataTable > .datatable--header {
97
+ background: #1a3a7a;
98
+ color: #ffffff;
99
+ text-style: bold;
100
+ }
101
+
102
+ DataTable > .datatable--cursor {
103
+ background: #0d5a0d;
104
+ color: #ffffff;
105
+ text-style: bold;
106
+ }
107
+
108
+ DataTable > .datatable--hover {
109
+ background: #122212;
110
+ }
111
+
112
+ /* ========== COMMAND BAR ========== */
113
+ #cmd-box {
114
+ height: 3;
115
+ border: solid #00aa88;
116
+ background: #0a0a0a;
117
+ margin: 0 1;
118
+ layout: horizontal;
119
+ align: left middle;
120
+ padding: 0 1;
121
+ }
122
+
123
+ #cmd-label {
124
+ color: #00ffaa;
125
+ width: auto;
126
+ padding-right: 1;
127
+ }
128
+
129
+ #cmd-input {
130
+ background: #0a0a0a;
131
+ color: #ffffff;
132
+ border: none;
133
+ width: 1fr;
134
+ padding: 0;
135
+ }
136
+
137
+ #cmd-input:focus {
138
+ border: none;
139
+ background: #0a0a0a;
140
+ }
141
+
142
+ /* ========== FOOTER ========== */
143
+ #footer-bar {
144
+ dock: bottom;
145
+ height: 1;
146
+ background: #121212;
147
+ color: #808080;
148
+ text-align: center;
149
+ width: 100%;
150
+ }
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.3
2
+ Name: leftoff
3
+ Version: 0.1.0
4
+ Summary: Cli tool for managing project works.
5
+ Author: shaheen-coder
6
+ Author-email: shaheen-coder <shaheenvsa@gmail.com>
7
+ Requires-Dist: pytest>=9.1.1
8
+ Requires-Dist: textual>=8.2.8
9
+ Requires-Python: >=3.14
10
+ Description-Content-Type: text/markdown
11
+
@@ -0,0 +1,15 @@
1
+ leftoff/__init__.py,sha256=FdXJ8CrnXAClJ52Ns6_4EvQFrS3DNip-9xS67xS9BvA,53
2
+ leftoff/__main__.py,sha256=IQlp7whsrJMnXt4KsDTrnKjfn2UL_exCLJYHS_-t7cc,3712
3
+ leftoff/core/cli.py,sha256=A_yCaY0TzWdHWpWSGHtukwoAbih2owcG5932yZ5hhhc,1482
4
+ leftoff/core/dtypes.py,sha256=_I8ChdTJudJFp1xowMD7CgNRN7YLO1Uq-vTN1g2dy1o,974
5
+ leftoff/core/input_val.py,sha256=PfxmWUbqB9X8SYRCduUdwyTBVqfQVf5lLvFms73yino,100
6
+ leftoff/core/parser.py,sha256=FvxQvXDZ9tch31yYfmhdBIZrHs9N2MzDrlSK0NGNgCY,2369
7
+ leftoff/core/stats.py,sha256=0FhcCsgjwOuPN3mzdUdzvs89mD5M4BG3igPj__FqLUQ,623
8
+ leftoff/core/type_val.py,sha256=WJfXduq5M4as42-e0d88lg__xaJc5WYAaY9UiitSGOM,945
9
+ leftoff/tui/core.py,sha256=H0B7316gxrXqwH8mzTUV_ViJlZKdUaeSX90dk3QjUF4,2493
10
+ leftoff/tui/table.py,sha256=l6q0ZMQ4Bs3CepoXQsXfV41Ym9MwEP0olVt5x3mZ1pY,1180
11
+ leftoff/tui/tcss/core_tui.tcss,sha256=qvZ5ci7PwujZQN6zKAaNYUpXLVDcENJ7nQbA1o8Mp7g,2371
12
+ leftoff-0.1.0.dist-info/WHEEL,sha256=R1d3uUTbmXM1FHXH_itQashbrqrOSVj-hvBCpmkIIGE,81
13
+ leftoff-0.1.0.dist-info/entry_points.txt,sha256=TLHnjw5pWHe-xm10QVn3XdgZPtNCfAygBk2peaqN3h4,51
14
+ leftoff-0.1.0.dist-info/METADATA,sha256=eBZ-LlsGos5ISsR_Ialf0NNHrpWlEQ8ki7OL7xre_GI,294
15
+ leftoff-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.17
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ leftoff = leftoff.__main__:main
3
+