Compare commits
2 Commits
master
...
feature/ma
Author | SHA1 | Date | |
---|---|---|---|
78931145ac | |||
8a67085d5b |
@ -1,6 +1,5 @@
|
|||||||
# PyKanban
|
# PyKanban
|
||||||
|
|
||||||
**Note this project has been abandoned. I ended up not liking Textual as a TUI library. I plan on creating another project to accomplish a similar goal using rust.**
|
|
||||||
A python implementation of a simple kanban board for managing personal projects.
|
A python implementation of a simple kanban board for managing personal projects.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
@ -15,13 +15,11 @@ requires-python = ">=3.8"
|
|||||||
classifiers = [
|
classifiers = [
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
|
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: Unix",
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"Environment :: Console"
|
"Environment :: Console"
|
||||||
]
|
]
|
||||||
dependencies = [
|
|
||||||
"textual"
|
|
||||||
]
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://alexselimov.com/git/aselimov/pykanban"
|
Homepage = "https://alexselimov.com/git/aselimov/pykanban"
|
||||||
Issues = "https://github.com/aselimov/PyKanban/issues"
|
Issues = "https://github.com/aselimov/PyKanban/issues"
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
columns:
|
|
||||||
- To Do
|
|
||||||
- In Progress
|
|
||||||
- Review
|
|
||||||
- Done
|
|
||||||
tasks:
|
|
||||||
- column: To Do
|
|
||||||
description: "We want to be able to retire some done tickets \nwithout having to\
|
|
||||||
\ delete them."
|
|
||||||
score: '5'
|
|
||||||
summary: Work out some way to handle sprints
|
|
||||||
- column: To Do
|
|
||||||
description: 'I want to add footers which describe the key shortcuts,
|
|
||||||
|
|
||||||
Additionally this should be disabledable via a command line argument
|
|
||||||
|
|
||||||
'
|
|
||||||
score: '5'
|
|
||||||
summary: 'Add some footers for the key shortcuts '
|
|
@ -1,14 +1,17 @@
|
|||||||
""" This module contains classes and functions to contain the kanban board information """
|
""" This module contains classes and functions to contain the kanban board information """
|
||||||
|
|
||||||
|
import sys
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import yaml
|
import yaml
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
|
||||||
|
|
||||||
class Task:
|
class Task:
|
||||||
""" This class represents each task,
|
"""This class represents each task,"""
|
||||||
"""
|
|
||||||
def __init__(self, summary, score, description):
|
def __init__(self, summary, score, description):
|
||||||
""" Initialize the task class
|
"""Initialize the task class"""
|
||||||
"""
|
|
||||||
# Each task has the following properties
|
# Each task has the following properties
|
||||||
self.summary = summary # Summary of the task
|
self.summary = summary # Summary of the task
|
||||||
self.score = score # Score for ticket
|
self.score = score # Score for ticket
|
||||||
@ -18,11 +21,9 @@ class Task:
|
|||||||
class Board:
|
class Board:
|
||||||
def __init__(self, file=None):
|
def __init__(self, file=None):
|
||||||
"""Initialize the Board class, this class has three important class variables.
|
"""Initialize the Board class, this class has three important class variables.
|
||||||
These are:
|
These are: self.sprint | str - name of the current sprint
|
||||||
self.sprint | str - name of the current sprint
|
|
||||||
self.columns | list(str) - columns in kanban board
|
self.columns | list(str) - columns in kanban board
|
||||||
self.tasks | list(list()) - tasks in each column
|
self.tasks | list(list()) - tasks in each column
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.sprint = None
|
self.sprint = None
|
||||||
self.columns = list()
|
self.columns = list()
|
||||||
@ -33,7 +34,6 @@ class Board:
|
|||||||
if file:
|
if file:
|
||||||
self.read_yaml(file)
|
self.read_yaml(file)
|
||||||
|
|
||||||
|
|
||||||
def read_yaml(self, file):
|
def read_yaml(self, file):
|
||||||
"""Read the yaml file in and set up the data
|
"""Read the yaml file in and set up the data
|
||||||
|
|
||||||
@ -42,15 +42,16 @@ class Board:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Read in the data
|
# Read in the data
|
||||||
with open(file, 'r') as f:
|
with open(file, "r") as f:
|
||||||
data = yaml.safe_load(f)
|
data = yaml.safe_load(f)
|
||||||
|
|
||||||
# Assign the data to board variables
|
# Assign the data to board variables
|
||||||
self.columns = data['columns']
|
self.columns = data["columns"]
|
||||||
self.tasks = [[] for col in self.columns]
|
self.tasks = [[] for col in self.columns]
|
||||||
for task in data['tasks']:
|
for task in data["tasks"]:
|
||||||
self.tasks[self.columns.index(task['column'])].append(
|
self.tasks[self.columns.index(task["column"])].append(
|
||||||
Task(task['summary'], task['score'], task['description']))
|
Task(task["summary"], task["score"], task["description"])
|
||||||
|
)
|
||||||
|
|
||||||
def write_yaml(self, file):
|
def write_yaml(self, file):
|
||||||
"""Write the yaml file
|
"""Write the yaml file
|
||||||
@ -61,17 +62,22 @@ class Board:
|
|||||||
|
|
||||||
# Set up data to write out
|
# Set up data to write out
|
||||||
data = dict()
|
data = dict()
|
||||||
data['columns'] = self.columns
|
data["columns"] = self.columns
|
||||||
data['tasks'] = list()
|
data["tasks"] = list()
|
||||||
for col, task_list in zip(self.columns, self.tasks):
|
for col, task_list in zip(self.columns, self.tasks):
|
||||||
for task in task_list:
|
for task in task_list:
|
||||||
data['tasks'].append({'column':col, 'summary':task.summary, 'score':task.score,
|
data["tasks"].append(
|
||||||
'description':task.description})
|
{
|
||||||
|
"column": col,
|
||||||
|
"summary": task.summary,
|
||||||
|
"score": task.score,
|
||||||
|
"description": task.description,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
with open(file, 'w') as f:
|
with open(file, "w") as f:
|
||||||
yaml.dump(data, f)
|
yaml.dump(data, f)
|
||||||
|
|
||||||
|
|
||||||
def move_task(self, col_index, task_index, direction):
|
def move_task(self, col_index, task_index, direction):
|
||||||
"""This class method moves tasks between columns by incrementing/decrementing the column
|
"""This class method moves tasks between columns by incrementing/decrementing the column
|
||||||
index
|
index
|
||||||
@ -93,8 +99,6 @@ class Board:
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def print_board_items(self):
|
def print_board_items(self):
|
||||||
for i, col in enumerate(self.columns):
|
for i, col in enumerate(self.columns):
|
||||||
print(col)
|
print(col)
|
||||||
@ -123,3 +127,30 @@ class Board:
|
|||||||
def del_task(self, icol, itask):
|
def del_task(self, icol, itask):
|
||||||
del self.tasks[icol][itask]
|
del self.tasks[icol][itask]
|
||||||
|
|
||||||
|
|
||||||
|
class BoardList:
|
||||||
|
"""This class is used to process the full list of boards"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.boards = self.get_boards()
|
||||||
|
|
||||||
|
def get_boards(self):
|
||||||
|
"""This function returns the boards that have been created"""
|
||||||
|
configpath = os.path.join(
|
||||||
|
os.environ.get("APPDATA")
|
||||||
|
or os.environ.get("XDG_CONFIG_HOME")
|
||||||
|
or os.path.join(os.environ["HOME"], ".config"),
|
||||||
|
"pykban",
|
||||||
|
)
|
||||||
|
|
||||||
|
boards = list()
|
||||||
|
for board in glob.glob(os.path.join(configpath, "(*.yaml)")):
|
||||||
|
with open(board, "r") as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
try:
|
||||||
|
boards.append((data["name"], board))
|
||||||
|
except KeyError:
|
||||||
|
print("Board yaml file is missing the name attribute")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
return boards
|
||||||
|
@ -19,6 +19,14 @@ EditColScreen {
|
|||||||
layout: vertical;
|
layout: vertical;
|
||||||
background: #000000 25%;
|
background: #000000 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SelectBoardScreen{
|
||||||
|
align: center middle;
|
||||||
|
overflow-x: hidden;
|
||||||
|
layout: vertical;
|
||||||
|
background: #000000 25%;
|
||||||
|
}
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
width: 1fr;
|
width: 1fr;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -3,12 +3,7 @@ from textual.widgets import Static, Label, ListItem, ListView, TextArea, Input
|
|||||||
from textual.containers import Horizontal, Vertical
|
from textual.containers import Horizontal, Vertical
|
||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
from .board import Board, Task
|
from .board import Board, Task, BoardList
|
||||||
|
|
||||||
|
|
||||||
def run_tui():
|
|
||||||
kb = KanbanForm()
|
|
||||||
kb.run()
|
|
||||||
|
|
||||||
|
|
||||||
class TaskList(ListView):
|
class TaskList(ListView):
|
||||||
@ -137,7 +132,72 @@ class EditColScreen(Screen):
|
|||||||
self.dismiss(query.nodes[0].value)
|
self.dismiss(query.nodes[0].value)
|
||||||
|
|
||||||
|
|
||||||
class KanbanForm(App):
|
class SelectBoardScreen(Screen):
|
||||||
|
"""This is a screen used to select a board"""
|
||||||
|
|
||||||
|
BINDINGS = [
|
||||||
|
Binding("enter", "pick_option", "Save Changes", priority=True),
|
||||||
|
Binding("q", "exit", "Exit"),
|
||||||
|
]
|
||||||
|
CSS = """
|
||||||
|
$bg: #282828;
|
||||||
|
Label{
|
||||||
|
width:50%;
|
||||||
|
background: #282828;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
TaskList{
|
||||||
|
width:50%;
|
||||||
|
background: #282828;
|
||||||
|
padding: 0 0;
|
||||||
|
border: #ebdbb9;
|
||||||
|
}
|
||||||
|
ListView{
|
||||||
|
width:50%;
|
||||||
|
background: #282828;
|
||||||
|
}
|
||||||
|
ListItem{
|
||||||
|
border: solid #ebdbb2 100%;
|
||||||
|
background: $bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView > ListItem.--highlight {
|
||||||
|
background: $bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView:focus > ListItem.--highlight {
|
||||||
|
background: #458588;
|
||||||
|
}
|
||||||
|
|
||||||
|
Label:focus{
|
||||||
|
background: #458588;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, logger):
|
||||||
|
"""
|
||||||
|
Initialize the screen
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self.board_list = BoardList()
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
def compose(self):
|
||||||
|
"""
|
||||||
|
Compose the widgets on the screen, this screen doesn't need dynamic layout changes
|
||||||
|
"""
|
||||||
|
yield Label("Select a board:")
|
||||||
|
yield TaskList(
|
||||||
|
*[ListItem(Label(board)) for board in self.board_list.get_boards()],
|
||||||
|
ListItem(Label("Add a new board")),
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_pick_option(self):
|
||||||
|
"""Pick a board from the ListItem"""
|
||||||
|
self.focused.highlighted_child
|
||||||
|
|
||||||
|
|
||||||
|
class MainBoardScreen(Screen):
|
||||||
CSS_PATH = "layout.tcss"
|
CSS_PATH = "layout.tcss"
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
Binding(
|
Binding(
|
||||||
@ -173,7 +233,7 @@ class KanbanForm(App):
|
|||||||
show=False,
|
show=False,
|
||||||
),
|
),
|
||||||
Binding(
|
Binding(
|
||||||
"x",
|
"d",
|
||||||
"delete_task",
|
"delete_task",
|
||||||
"Delete Task",
|
"Delete Task",
|
||||||
show=False,
|
show=False,
|
||||||
@ -181,6 +241,10 @@ class KanbanForm(App):
|
|||||||
Binding("q", "exit", "Exit"),
|
Binding("q", "exit", "Exit"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the Kanban Form App"""
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
def compose(self):
|
def compose(self):
|
||||||
"""
|
"""
|
||||||
Initialization function for form
|
Initialization function for form
|
||||||
@ -203,7 +267,10 @@ class KanbanForm(App):
|
|||||||
else:
|
else:
|
||||||
yield Static(col, classes="header")
|
yield Static(col, classes="header")
|
||||||
yield TaskList(
|
yield TaskList(
|
||||||
*[ListItem(Label(task.summary)) for task in self.board.get_tasks()[i]]
|
*[
|
||||||
|
ListItem(Label(task.summary))
|
||||||
|
for task in self.board.get_tasks()[i]
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def action_fnext(self):
|
def action_fnext(self):
|
||||||
@ -324,3 +391,27 @@ class KanbanForm(App):
|
|||||||
self.focused.mount(ListItem(Label(task.summary)))
|
self.focused.mount(ListItem(Label(task.summary)))
|
||||||
self.board.add_task(icol, task)
|
self.board.add_task(icol, task)
|
||||||
self.focused.action_cursor_down()
|
self.focused.action_cursor_down()
|
||||||
|
|
||||||
|
|
||||||
|
class KanbanForm(App):
|
||||||
|
"""Main Kanban app"""
|
||||||
|
|
||||||
|
CSS_PATH = "layout.tcss"
|
||||||
|
SCREENS = {"main": SelectBoardScreen()}
|
||||||
|
|
||||||
|
def on_mount(self):
|
||||||
|
self.push_screen("main")
|
||||||
|
|
||||||
|
|
||||||
|
# def on_key(self):
|
||||||
|
# with open('log','a') as f:
|
||||||
|
# f.write("{}".format(self.children[0].focus_next))
|
||||||
|
|
||||||
|
|
||||||
|
def run_tui():
|
||||||
|
kb = KanbanForm()
|
||||||
|
kb.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_tui()
|
||||||
|
Reference in New Issue
Block a user