Compare commits

..

2 Commits

@ -1,6 +1,5 @@
# 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.
## License

@ -15,13 +15,11 @@ requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
"Operating System :: OS Independent",
"Operating System :: Unix",
"Development Status :: 3 - Alpha",
"Environment :: Console"
]
dependencies = [
"textual"
]
[project.urls]
Homepage = "https://alexselimov.com/git/aselimov/pykanban"
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,28 +1,29 @@
""" This module contains classes and functions to contain the kanban board information """
import sys
import numpy as np
import yaml
import os
import glob
class Task:
""" This class represents each task,
"""
"""This class represents each task,"""
def __init__(self, summary, score, description):
""" Initialize the task class
"""
"""Initialize the task class"""
# Each task has the following properties
self.summary = summary # Summary of the task
self.score = score # Score for ticket
self.description = description # Description of ticket
self.summary = summary # Summary of the task
self.score = score # Score for ticket
self.description = description # Description of ticket
class Board:
def __init__(self, file = None):
""" Initialize the Board class, this class has three important class variables.
These are:
self.sprint | str - name of the current sprint
def __init__(self, file=None):
"""Initialize the Board class, this class has three important class variables.
These are: self.sprint | str - name of the current sprint
self.columns | list(str) - columns in kanban board
self.tasks | list(list()) - tasks in each column
"""
self.sprint = None
self.columns = list()
@ -33,49 +34,54 @@ class Board:
if file:
self.read_yaml(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
Arguments:
file - yaml file to read in
"""
# Read in the data
with open(file, 'r') as f:
with open(file, "r") as f:
data = yaml.safe_load(f)
# Assign the data to board variables
self.columns = data['columns']
self.columns = data["columns"]
self.tasks = [[] for col in self.columns]
for task in data['tasks']:
self.tasks[self.columns.index(task['column'])].append(
Task(task['summary'], task['score'], task['description']))
for task in data["tasks"]:
self.tasks[self.columns.index(task["column"])].append(
Task(task["summary"], task["score"], task["description"])
)
def write_yaml(self, file):
""" Write the yaml file
"""Write the yaml file
Arguments:
file - yaml file to write to
"""
# Set up data to write out
# Set up data to write out
data = dict()
data['columns'] = self.columns
data['tasks'] = list()
for col,task_list in zip(self.columns, self.tasks):
data["columns"] = self.columns
data["tasks"] = list()
for col, task_list in zip(self.columns, self.tasks):
for task in task_list:
data['tasks'].append({'column':col, 'summary':task.summary, 'score':task.score,
'description':task.description})
with open(file, 'w') as f:
data["tasks"].append(
{
"column": col,
"summary": task.summary,
"score": task.score,
"description": task.description,
}
)
with open(file, "w") as f:
yaml.dump(data, f)
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
Arguments:
col_index - index of the column we are in
task_index - index of the task we are changing in the column
@ -85,41 +91,66 @@ class Board:
moved - True if a task was moved else false
"""
task = self.tasks[col_index][task_index]
if col_index+direction >= 0 and col_index+direction < len(self.columns):
self.tasks[col_index+direction].append(task)
if col_index + direction >= 0 and col_index + direction < len(self.columns):
self.tasks[col_index + direction].append(task)
del self.tasks[col_index][task_index]
return True
else:
else:
return False
def print_board_items(self):
for i, col in enumerate(self.columns):
print(col)
print(self.tasks[i])
def get_columns(self):
""" Return columns"""
"""Return columns"""
return self.columns
def get_tasks(self):
""" Return tasks"""
"""Return tasks"""
return self.tasks
def get_task(self, icol, itask):
""" Return a task based on column and task index"""
"""Return a task based on column and task index"""
return self.tasks[icol][itask]
def update_task( self, icol, itask, task):
""" Update the task based on text """
def update_task(self, icol, itask, task):
"""Update the task based on text"""
self.tasks[icol][itask] = task
def add_task( self, icol, task):
def add_task(self, icol, task):
"""Add a task to icol"""
self.tasks[icol].append(task)
def del_task(self, 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;
background: #000000 25%;
}
SelectBoardScreen{
align: center middle;
overflow-x: hidden;
layout: vertical;
background: #000000 25%;
}
.column {
width: 1fr;
height: 100%;

@ -3,12 +3,7 @@ from textual.widgets import Static, Label, ListItem, ListView, TextArea, Input
from textual.containers import Horizontal, Vertical
from textual.screen import Screen
from textual.binding import Binding
from .board import Board, Task
def run_tui():
kb = KanbanForm()
kb.run()
from .board import Board, Task, BoardList
class TaskList(ListView):
@ -137,7 +132,72 @@ class EditColScreen(Screen):
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"
BINDINGS = [
Binding(
@ -173,7 +233,7 @@ class KanbanForm(App):
show=False,
),
Binding(
"x",
"d",
"delete_task",
"Delete Task",
show=False,
@ -181,6 +241,10 @@ class KanbanForm(App):
Binding("q", "exit", "Exit"),
]
def __init__(self):
"""Initialize the Kanban Form App"""
super().__init__()
def compose(self):
"""
Initialization function for form
@ -203,7 +267,10 @@ class KanbanForm(App):
else:
yield Static(col, classes="header")
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):
@ -324,3 +391,27 @@ class KanbanForm(App):
self.focused.mount(ListItem(Label(task.summary)))
self.board.add_task(icol, task)
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()