leftoff 0.1.0__tar.gz
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-0.1.0/PKG-INFO +11 -0
- leftoff-0.1.0/README.md +0 -0
- leftoff-0.1.0/pyproject.toml +27 -0
- leftoff-0.1.0/pyproject.toml.orig +26 -0
- leftoff-0.1.0/src/leftoff/__init__.py +2 -0
- leftoff-0.1.0/src/leftoff/__main__.py +115 -0
- leftoff-0.1.0/src/leftoff/core/cli.py +45 -0
- leftoff-0.1.0/src/leftoff/core/dtypes.py +53 -0
- leftoff-0.1.0/src/leftoff/core/input_val.py +4 -0
- leftoff-0.1.0/src/leftoff/core/parser.py +78 -0
- leftoff-0.1.0/src/leftoff/core/stats.py +26 -0
- leftoff-0.1.0/src/leftoff/core/type_val.py +43 -0
- leftoff-0.1.0/src/leftoff/tui/core.py +62 -0
- leftoff-0.1.0/src/leftoff/tui/table.py +44 -0
- leftoff-0.1.0/src/leftoff/tui/tcss/core_tui.tcss +150 -0
leftoff-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
|
leftoff-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "leftoff"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Cli tool for managing project works."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"pytest>=9.1.1",
|
|
9
|
+
"textual>=8.2.8",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[[project.authors]]
|
|
13
|
+
name = "shaheen-coder"
|
|
14
|
+
email = "shaheenvsa@gmail.com"
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
leftoff = "leftoff.__main__:main"
|
|
18
|
+
|
|
19
|
+
[tool.pyrefly]
|
|
20
|
+
project-includes = [
|
|
21
|
+
"**/*.py*",
|
|
22
|
+
"**/*.ipynb",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
27
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "leftoff"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Cli tool for managing project works."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "shaheen-coder", email = "shaheenvsa@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pytest>=9.1.1",
|
|
12
|
+
"textual>=8.2.8",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.pyrefly]
|
|
16
|
+
project-includes = [
|
|
17
|
+
"**/*.py*",
|
|
18
|
+
"**/*.ipynb",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
leftoff = "leftoff.__main__:main"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
26
|
+
build-backend = "uv_build"
|
|
@@ -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()
|
|
@@ -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
|
+
|
|
@@ -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,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
|
+
|
|
@@ -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
|
|
@@ -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
|
+
|
|
@@ -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
|
+
}
|