diff --git a/pyproject.toml b/pyproject.toml index 4aba2fe..3941777 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ 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" ] diff --git a/src/pykban/board.py b/src/pykban/board.py index 6607f93..609a711 100644 --- a/src/pykban/board.py +++ b/src/pykban/board.py @@ -2,22 +2,24 @@ 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. + 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 @@ -33,49 +35,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 +92,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 diff --git a/src/pykban/layout.tcss b/src/pykban/layout.tcss index 8346e01..e214a1d 100644 --- a/src/pykban/layout.tcss +++ b/src/pykban/layout.tcss @@ -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%; diff --git a/src/pykban/tui.py b/src/pykban/tui.py index f0b9bf8..097ff5b 100644 --- a/src/pykban/tui.py +++ b/src/pykban/tui.py @@ -3,24 +3,27 @@ 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 +from .board import Board, Task, BoardList 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 BINDINGS = [ Binding("k", "cursor_up", "Cursor Up", show=False, priority=True), Binding("j", "cursor_down", "Cursor Down", show=False, priority=True), ] + 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{ width:50%; background: #282828; @@ -47,10 +50,11 @@ class EditTaskScreen(Screen): } """ BINDINGS = [ - Binding('ctrl+s', 'save', 'Save Changes', priority=True), - Binding('escape', 'exit', 'Exit Without Changes', priority=True), + Binding("ctrl+s", "save", "Save Changes", priority=True), + Binding("escape", "exit", "Exit Without Changes", priority=True), ] - def __init__(self,text): + + def __init__(self, text): """ Initialize the screen """ @@ -61,19 +65,18 @@ class EditTaskScreen(Screen): """ 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 Label('Score:') + yield Label("Score:") if self.text.score: yield Input(value=self.text.score) else: yield Input(value="") - yield Label('Description:') + yield Label("Description:") if self.text.description: - yield TextArea(self.text.description, language='markdown') + yield TextArea(self.text.description, language="markdown") else: - yield TextArea(language='markdown') - + yield TextArea(language="markdown") def action_save(self): query = self.query(selector=Input) @@ -86,11 +89,13 @@ class EditTaskScreen(Screen): def action_exit(self): self.dismiss(None) + 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{ width:50%; background: #282828; @@ -104,10 +109,11 @@ class EditColScreen(Screen): } """ BINDINGS = [ - Binding('ctrl+s', 'save', 'Save Changes', priority=True), - Binding('enter', 'save', 'Save Changes', priority=True), + Binding("ctrl+s", "save", "Save Changes", priority=True), + Binding("enter", "save", "Save Changes", priority=True), ] - def __init__(self,text): + + def __init__(self, text): """ Initialize the screen """ @@ -118,67 +124,166 @@ class EditColScreen(Screen): """ 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) - def action_save(self): query = self.query(selector=Input) self.dismiss(query.nodes[0].value) -class KanbanForm(App): - CSS_PATH = 'layout.tcss' +class SelectBoardScreen(Screen): + """This is a screen used to select a board""" + 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("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( + "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("H", "move_down", "Focus Prev", show=False), - Binding("e", "edit_task", "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") - ] + Binding( + "e", + "edit_task", + "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): """ Initialization function for form """ # Initialize our board class - self.board = Board(file = '.board.yaml') + self.board = Board(file=".board.yaml") self.cols = list() self.col_widgets = list() - - + with Horizontal(): - for i,col in enumerate(self.board.get_columns()): - if i < len(self.board.get_columns())-1: - col_class = 'column' + for i, col in enumerate(self.board.get_columns()): + if i < len(self.board.get_columns()) - 1: + col_class = "column" else: - col_class = 'last-column' + col_class = "last-column" with Vertical(classes=col_class): if i == 0: - yield Static(col, classes='header-focused') + yield Static(col, classes="header-focused") else: - yield Static(col, classes='header') + 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): - """ Focus next column""" + """Focus next column""" 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() - query[icol].classes="header" + query[icol].classes = "header" self.children[0].focus_next() try: - query[icol+1].classes="header-focused" + query[icol + 1].classes = "header-focused" except IndexError: - query[0].classes="header-focused" - - + query[0].classes = "header-focused" def action_move_up(self): icol, itask = self.get_col_task() @@ -187,23 +292,22 @@ class KanbanForm(App): if moved: query = self.query(selector=TaskList) 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.action_fnext() self.focused.action_cursor_down() - def action_fprev(self): - """ Focus previous column """ + """Focus previous column""" 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() - query[icol].classes="header" + query[icol].classes = "header" self.children[0].focus_previous() try: - query[icol-1].classes="header-focused" + query[icol - 1].classes = "header-focused" except IndexError: - query[-1].classes="header-focused" + query[-1].classes = "header-focused" def action_move_down(self): icol, itask = self.get_col_task() @@ -212,7 +316,7 @@ class KanbanForm(App): if moved: query = self.query(selector=TaskList) 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.action_fprev() self.focused.action_cursor_down() @@ -221,9 +325,9 @@ class KanbanForm(App): icol, itask = self.get_col_task() task = self.board.get_task(icol, itask) self.push_screen(EditTaskScreen(task), self.update_task) - + 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): icol, itask = self.get_col_task() @@ -233,25 +337,23 @@ class KanbanForm(App): def action_delete_task(self): icol, itask = self.get_col_task() self.focused.highlighted_child.remove() - self.board.del_task(icol,itask) + self.board.del_task(icol, itask) def update_col(self, text): - """ Update the column - """ + """Update the column""" icol, itask = self.get_col_task() 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) self.board.get_columns()[icol] = text - def action_exit(self): - """ Exit the application """ - self.board.write_yaml(file='.board.yaml') + """Exit the application""" + self.board.write_yaml(file=".board.yaml") self.exit() def get_col_task(self): - """ + """ This function gets the relevant column and task from the Board object for the current selected item in the tui. """ @@ -267,15 +369,15 @@ class KanbanForm(App): to_move = focused_col.highlighted_child task_index = None for i, child in enumerate(focused_col.children): - if to_move == child: + if to_move == child: task_index = i return col_index, task_index - + def update_task(self, task): - """ This function gets the text inputted in the edit screen and updates the underlying - task and the board class - + """This function gets the text inputted in the edit screen and updates the underlying + task and the board class + """ if task: icol, itask = self.get_col_task() @@ -283,20 +385,33 @@ class KanbanForm(App): self.board.update_task(icol, itask, 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: - icol,_ = self.get_col_task() + icol, _ = self.get_col_task() 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)) -if __name__ == "__main__": + +def run_tui(): kb = KanbanForm() kb.run() +if __name__ == "__main__": + run_tui()