Compare commits

...
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

2 Commits

@ -15,7 +15,7 @@ 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"
] ]

@ -1,28 +1,29 @@
""" 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
self.description = description # Description of ticket self.description = description # Description of ticket
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,49 +34,54 @@ 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
Arguments: Arguments:
file - yaml file to read in file - yaml file to read in
""" """
# 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
Arguments: Arguments:
file - yaml file to write to file - yaml file to write to
""" """
# 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,
with open(file, 'w') as f: "summary": task.summary,
"score": task.score,
"description": task.description,
}
)
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
Arguments: Arguments:
col_index - index of the column we are in col_index - index of the column we are in
task_index - index of the task we are changing in the column 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 moved - True if a task was moved else false
""" """
task = self.tasks[col_index][task_index] task = self.tasks[col_index][task_index]
if col_index+direction >= 0 and col_index+direction < len(self.columns): if col_index + direction >= 0 and col_index + direction < len(self.columns):
self.tasks[col_index+direction].append(task) self.tasks[col_index + direction].append(task)
del self.tasks[col_index][task_index] del self.tasks[col_index][task_index]
return True return True
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)
print(self.tasks[i]) print(self.tasks[i])
def get_columns(self): def get_columns(self):
""" Return columns""" """Return columns"""
return self.columns return self.columns
def get_tasks(self): def get_tasks(self):
""" Return tasks""" """Return tasks"""
return self.tasks return self.tasks
def get_task(self, icol, itask): 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] return self.tasks[icol][itask]
def update_task( self, icol, itask, task): def update_task(self, icol, itask, task):
""" Update the task based on text """ """Update the task based on text"""
self.tasks[icol][itask] = task self.tasks[icol][itask] = task
def add_task( self, icol, task): def add_task(self, icol, task):
"""Add a task to icol""" """Add a task to icol"""
self.tasks[icol].append(task) self.tasks[icol].append(task)
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,24 +3,27 @@ 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
class TaskList(ListView): class TaskList(ListView):
""" """
Inherited widget from Listview to use as the kanban board columns Inherited widget from Listview to use as the kanban board columns
""" """
# Keybinds # Keybinds
BINDINGS = [ BINDINGS = [
Binding("k", "cursor_up", "Cursor Up", show=False, priority=True), Binding("k", "cursor_up", "Cursor Up", show=False, priority=True),
Binding("j", "cursor_down", "Cursor Down", show=False, priority=True), Binding("j", "cursor_down", "Cursor Down", show=False, priority=True),
] ]
class EditTaskScreen(Screen): class EditTaskScreen(Screen):
""" """
This is a screen used to edit the name of a task This is a screen used to edit the name of a task
""" """
CSS="""
CSS = """
Label{ Label{
width:50%; width:50%;
background: #282828; background: #282828;
@ -47,10 +50,11 @@ class EditTaskScreen(Screen):
} }
""" """
BINDINGS = [ BINDINGS = [
Binding('ctrl+s', 'save', 'Save Changes', priority=True), Binding("ctrl+s", "save", "Save Changes", priority=True),
Binding('escape', 'exit', 'Exit Without Changes', priority=True), Binding("escape", "exit", "Exit Without Changes", priority=True),
] ]
def __init__(self,text):
def __init__(self, text):
""" """
Initialize the screen Initialize the screen
""" """
@ -61,19 +65,18 @@ class EditTaskScreen(Screen):
""" """
Compose the widgets on the screen, this screen doesn't need dynamic layout changes Compose the widgets on the screen, this screen doesn't need dynamic layout changes
""" """
yield Label('Task Name:') yield Label("Task Name:")
yield Input(value=self.text.summary) yield Input(value=self.text.summary)
yield Label('Score:') yield Label("Score:")
if self.text.score: if self.text.score:
yield Input(value=self.text.score) yield Input(value=self.text.score)
else: else:
yield Input(value="") yield Input(value="")
yield Label('Description:') yield Label("Description:")
if self.text.description: if self.text.description:
yield TextArea(self.text.description, language='markdown') yield TextArea(self.text.description, language="markdown")
else: else:
yield TextArea(language='markdown') yield TextArea(language="markdown")
def action_save(self): def action_save(self):
query = self.query(selector=Input) query = self.query(selector=Input)
@ -86,11 +89,13 @@ class EditTaskScreen(Screen):
def action_exit(self): def action_exit(self):
self.dismiss(None) self.dismiss(None)
class EditColScreen(Screen): class EditColScreen(Screen):
""" """
This is a screen used to edit the name of a task This is a screen used to edit the name of a task
""" """
CSS="""
CSS = """
Label{ Label{
width:50%; width:50%;
background: #282828; background: #282828;
@ -104,10 +109,11 @@ class EditColScreen(Screen):
} }
""" """
BINDINGS = [ BINDINGS = [
Binding('ctrl+s', 'save', 'Save Changes', priority=True), Binding("ctrl+s", "save", "Save Changes", priority=True),
Binding('enter', 'save', 'Save Changes', priority=True), Binding("enter", "save", "Save Changes", priority=True),
] ]
def __init__(self,text):
def __init__(self, text):
""" """
Initialize the screen Initialize the screen
""" """
@ -118,67 +124,166 @@ class EditColScreen(Screen):
""" """
Compose the widgets on the screen, this screen doesn't need dynamic layout changes Compose the widgets on the screen, this screen doesn't need dynamic layout changes
""" """
yield Label('Column Name:') yield Label("Column Name:")
yield Input(value=self.text) yield Input(value=self.text)
def action_save(self): def action_save(self):
query = self.query(selector=Input) query = self.query(selector=Input)
self.dismiss(query.nodes[0].value) self.dismiss(query.nodes[0].value)
class KanbanForm(App): class SelectBoardScreen(Screen):
CSS_PATH = 'layout.tcss' """This is a screen used to select a board"""
BINDINGS = [ BINDINGS = [
Binding("a", "new_task", "Add New Task", show=False, ), Binding("enter", "pick_option", "Save Changes", priority=True),
Binding("l", "fnext", "Focus Next", show=False, ), Binding("q", "exit", "Exit"),
Binding("h", "fprev", "Focus Prev", show=False, ), ]
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(
"a",
"new_task",
"Add New Task",
show=False,
),
Binding(
"l",
"fnext",
"Focus Next",
show=False,
),
Binding(
"h",
"fprev",
"Focus Prev",
show=False,
),
Binding("L", "move_up", "Focus Next", show=False), Binding("L", "move_up", "Focus Next", show=False),
Binding("H", "move_down", "Focus Prev", show=False), Binding("H", "move_down", "Focus Prev", show=False),
Binding("e", "edit_task", "Edit Task", show=False,), Binding(
Binding("r", "edit_column", "Edit Column Name", show=False,), "e",
Binding("d", "delete_task", "Delete Task", show=False,), "edit_task",
Binding('q', 'exit', "Exit") "Edit Task",
] show=False,
),
Binding(
"r",
"edit_column",
"Edit Column Name",
show=False,
),
Binding(
"d",
"delete_task",
"Delete Task",
show=False,
),
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
""" """
# Initialize our board class # Initialize our board class
self.board = Board(file = '.board.yaml') self.board = Board(file=".board.yaml")
self.cols = list() self.cols = list()
self.col_widgets = list() self.col_widgets = list()
with Horizontal(): with Horizontal():
for i,col in enumerate(self.board.get_columns()): for i, col in enumerate(self.board.get_columns()):
if i < len(self.board.get_columns())-1: if i < len(self.board.get_columns()) - 1:
col_class = 'column' col_class = "column"
else: else:
col_class = 'last-column' col_class = "last-column"
with Vertical(classes=col_class): with Vertical(classes=col_class):
if i == 0: if i == 0:
yield Static(col, classes='header-focused') yield Static(col, classes="header-focused")
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):
""" Focus next column""" """Focus next column"""
query = self.query(selector=Static) query = self.query(selector=Static)
query = [node for node in query.nodes if str(node) == 'Static()'] query = [node for node in query.nodes if str(node) == "Static()"]
icol, _ = self.get_col_task() icol, _ = self.get_col_task()
query[icol].classes="header" query[icol].classes = "header"
self.children[0].focus_next() self.children[0].focus_next()
try: try:
query[icol+1].classes="header-focused" query[icol + 1].classes = "header-focused"
except IndexError: except IndexError:
query[0].classes="header-focused" query[0].classes = "header-focused"
def action_move_up(self): def action_move_up(self):
icol, itask = self.get_col_task() icol, itask = self.get_col_task()
@ -187,23 +292,22 @@ class KanbanForm(App):
if moved: if moved:
query = self.query(selector=TaskList) query = self.query(selector=TaskList)
self.focused.highlighted_child.remove() self.focused.highlighted_child.remove()
query.nodes[icol+1].append(ListItem(Label(text))) query.nodes[icol + 1].append(ListItem(Label(text)))
self.focused.action_cursor_down() self.focused.action_cursor_down()
self.action_fnext() self.action_fnext()
self.focused.action_cursor_down() self.focused.action_cursor_down()
def action_fprev(self): def action_fprev(self):
""" Focus previous column """ """Focus previous column"""
query = self.query(selector=Static) query = self.query(selector=Static)
query = [node for node in query.nodes if str(node) == 'Static()'] query = [node for node in query.nodes if str(node) == "Static()"]
icol, _ = self.get_col_task() icol, _ = self.get_col_task()
query[icol].classes="header" query[icol].classes = "header"
self.children[0].focus_previous() self.children[0].focus_previous()
try: try:
query[icol-1].classes="header-focused" query[icol - 1].classes = "header-focused"
except IndexError: except IndexError:
query[-1].classes="header-focused" query[-1].classes = "header-focused"
def action_move_down(self): def action_move_down(self):
icol, itask = self.get_col_task() icol, itask = self.get_col_task()
@ -212,7 +316,7 @@ class KanbanForm(App):
if moved: if moved:
query = self.query(selector=TaskList) query = self.query(selector=TaskList)
self.focused.highlighted_child.remove() self.focused.highlighted_child.remove()
query.nodes[icol-1].append(ListItem(Label(text))) query.nodes[icol - 1].append(ListItem(Label(text)))
self.focused.action_cursor_down() self.focused.action_cursor_down()
self.action_fprev() self.action_fprev()
self.focused.action_cursor_down() self.focused.action_cursor_down()
@ -221,9 +325,9 @@ class KanbanForm(App):
icol, itask = self.get_col_task() icol, itask = self.get_col_task()
task = self.board.get_task(icol, itask) task = self.board.get_task(icol, itask)
self.push_screen(EditTaskScreen(task), self.update_task) self.push_screen(EditTaskScreen(task), self.update_task)
def action_new_task(self): def action_new_task(self):
self.push_screen(EditTaskScreen(Task(None,None,None)), self.new_task) self.push_screen(EditTaskScreen(Task(None, None, None)), self.new_task)
def action_edit_column(self): def action_edit_column(self):
icol, itask = self.get_col_task() icol, itask = self.get_col_task()
@ -233,25 +337,23 @@ class KanbanForm(App):
def action_delete_task(self): def action_delete_task(self):
icol, itask = self.get_col_task() icol, itask = self.get_col_task()
self.focused.highlighted_child.remove() self.focused.highlighted_child.remove()
self.board.del_task(icol,itask) self.board.del_task(icol, itask)
def update_col(self, text): def update_col(self, text):
""" Update the column """Update the column"""
"""
icol, itask = self.get_col_task() icol, itask = self.get_col_task()
query = self.query(selector=Static) query = self.query(selector=Static)
query = [node for node in query.nodes if str(node) == 'Static()'] query = [node for node in query.nodes if str(node) == "Static()"]
query[icol].update(text) query[icol].update(text)
self.board.get_columns()[icol] = text self.board.get_columns()[icol] = text
def action_exit(self): def action_exit(self):
""" Exit the application """ """Exit the application"""
self.board.write_yaml(file='.board.yaml') self.board.write_yaml(file=".board.yaml")
self.exit() self.exit()
def get_col_task(self): def get_col_task(self):
""" """
This function gets the relevant column and task from the Board object for the current This function gets the relevant column and task from the Board object for the current
selected item in the tui. selected item in the tui.
""" """
@ -267,15 +369,15 @@ class KanbanForm(App):
to_move = focused_col.highlighted_child to_move = focused_col.highlighted_child
task_index = None task_index = None
for i, child in enumerate(focused_col.children): for i, child in enumerate(focused_col.children):
if to_move == child: if to_move == child:
task_index = i task_index = i
return col_index, task_index return col_index, task_index
def update_task(self, task): def update_task(self, task):
""" This function gets the text inputted in the edit screen and updates the underlying """This function gets the text inputted in the edit screen and updates the underlying
task and the board class task and the board class
""" """
if task: if task:
icol, itask = self.get_col_task() icol, itask = self.get_col_task()
@ -283,20 +385,33 @@ class KanbanForm(App):
self.board.update_task(icol, itask, task) self.board.update_task(icol, itask, task)
def new_task(self, task): def new_task(self, task):
""" This function adds a new task to our board """This function adds a new task to our board"""
"""
if task: if task:
icol,_ = self.get_col_task() icol, _ = self.get_col_task()
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): # def on_key(self):
# with open('log','a') as f: # with open('log','a') as f:
# f.write("{}".format(self.children[0].focus_next)) # f.write("{}".format(self.children[0].focus_next))
if __name__ == "__main__":
def run_tui():
kb = KanbanForm() kb = KanbanForm()
kb.run() kb.run()
if __name__ == "__main__":
run_tui()