Skip to content

Commit

Permalink
ready for initial release
Browse files Browse the repository at this point in the history
  • Loading branch information
jossmoff committed Oct 22, 2023
1 parent 9ec2f3f commit fd7e53b
Show file tree
Hide file tree
Showing 20 changed files with 350 additions and 106 deletions.
2 changes: 1 addition & 1 deletion bookshelf/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.3"
__version__ = "0.0.1"
133 changes: 80 additions & 53 deletions bookshelf/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
def entry_point():
try:
bookshelf()
except Exception as exception:
except (Exception, KeyboardInterrupt) as exception:
bookshelf_console.print(exception.terminal_message, style='red')
exit(0)

Expand All @@ -27,106 +27,133 @@ def bookshelf():

@bookshelf.command(name='create')
@click.argument('story_name')
@click.option('--force', is_flag=True, help='Usually, the command will not start a story that already exists. '
'This flag disables these checks, use it with care.')
@click.option('--tags', type=str, help='Comma-separated list of tags that you want to apply to the story')
@click.option('--start-chapter', is_flag=True, show_default=False, default=False,
help='Optionally start the first chapter right away')
@click.option('-f',
'--force',
type=str,
is_flag=True,
help='Usually, the command will not start a story that already exists. '
'This flag disables these checks, use it with care.')
@click.option('-t',
'--tags',
type=str,
help='Comma-separated list of tags that you want to apply to the story.')
@click.option('--start-chapter',
type=bool,
is_flag=True,
show_default=True,
default=False,
help='Optionally start the first chapter of the story you are creating right away.')
def create_story_entry(story_name: str, force: bool, tags: str, start_chapter: bool):
"""Create a new story for your bookshelf"""
try:
tags_list = [tag.strip() for tag in tags.split(',')] if tags is not None else []

tags_list = [tag.strip() for tag in tags.split(',')] if tags is not None else []

if bookshelf_storage.story_exists(story_name) and not force:
raise StoryAlreadyExistsException(story_name)
if bookshelf_storage.story_exists(story_name) and not force:
raise StoryAlreadyExistsException(story_name)

start_time = datetime.now()
story = Story(story_name, start_time, tags=tags_list)
if start_chapter:
story.add_chapter(Chapter(start_time))
start_time = datetime.now()
story = Story(story_name, start_time, tags=tags_list)
if start_chapter:
story.add_chapter(Chapter(start_time))

bookshelf_storage.save_story(story)
bookshelf_storage.save_story(story)

bookshelf_console.render_story_panel(story)
bookshelf_console.render_story_panel(story)
except KeyboardInterrupt:
pass


@bookshelf.command(name='start')
@click.argument('story_name')
def start_chapter_entry(story_name):
"""Start a new chapter for a story on your bookshelf"""
try:
if not bookshelf_storage.story_exists(story_name):
raise StoryNotFoundException(story_name)

if not bookshelf_storage.story_exists(story_name):
raise StoryNotFoundException(story_name)

story = bookshelf_storage.load_story(story_name)
story = bookshelf_storage.load_story(story_name)

if story.in_progress():
raise ChapterInProgressException(story_name)
if story.in_progress():
raise ChapterInProgressException(story_name)

if story.is_finished():
raise StoryAlreadyFinishedException(story_name)
if story.is_finished():
raise StoryAlreadyFinishedException(story_name)

start_time = datetime.now()
chapter = Chapter(start_time)
story.add_chapter(chapter)
start_time = datetime.now()
chapter = Chapter(start_time)
story.add_chapter(chapter)

bookshelf_storage.save_story(story)
bookshelf_console.render_story_panel(story)
bookshelf_storage.save_story(story)
bookshelf_console.render_story_panel(story)
except KeyboardInterrupt:
pass


@bookshelf.command(name='stop')
@click.argument('story_name')
def stop_chapter_entry(story_name):
"""Stop the current chapter of a story on your bookshelf"""

if not bookshelf_storage.story_exists(story_name):
raise StoryNotFoundException(story_name)
story = bookshelf_storage.load_story(story_name)
story.finish_chapter()
try:
if not bookshelf_storage.story_exists(story_name):
raise StoryNotFoundException(story_name)
story = bookshelf_storage.load_story(story_name)
story.finish_chapter()

bookshelf_storage.save_story(story)
bookshelf_storage.save_story(story)

bookshelf_console.render_story_panel(story)
bookshelf_console.render_story_panel(story)
except KeyboardInterrupt:
pass


@bookshelf.command(name='finish')
@click.argument('story_name')
def finish_story_entry(story_name):
"""Finish writing a story on your bookshelf"""
try:
if not bookshelf_storage.story_exists(story_name):
raise StoryNotFoundException(story_name)
story = bookshelf_storage.load_story(story_name)
story.finish_story()

if not bookshelf_storage.story_exists(story_name):
raise StoryNotFoundException(story_name)
story = bookshelf_storage.load_story(story_name)
story.finish_story()

bookshelf_storage.save_story(story)
bookshelf_storage.save_story(story)

bookshelf_console.render_story_panel(story)
bookshelf_console.render_story_panel(story)
except KeyboardInterrupt:
pass


@bookshelf.command(name='ls')
@click.option('--with-tags', type=str, help='Comma-separated list of tags that you wish to filter in')
@click.option('--with-tags', type=str, help='Comma-separated list of tags that you wish to filter in.')
def list_stories_entry(with_tags: str):
"""List all the current stories on your bookshelf"""

tags_to_filter_by = [tag.strip() for tag in with_tags.split(',')] if with_tags is not None else []
bookshelf_console.render_all_stories_table(tags_to_filter_by=tags_to_filter_by)
try:
tags_to_filter_by = [tag.strip() for tag in with_tags.split(',')] if with_tags is not None else []
bookshelf_console.render_all_stories_table(tags_to_filter_by=tags_to_filter_by)
except KeyboardInterrupt:
pass


@bookshelf.command(name='rm')
@click.argument('story_name')
def remove_story_entry(story_name):
"""Remove a story from your bookshelf"""
story = bookshelf_storage.load_story(story_name)
exit_live_message = 'Press [bold]Enter[/bold] to delete'
bookshelf_console.render_story_panel(story, exit_live_message=exit_live_message, should_clear=False)
bookshelf_storage.delete_story(story_name)
try:
story = bookshelf_storage.load_story(story_name)
exit_live_message = 'Press [bold]CTRL+C[/bold] to delete'
bookshelf_console.render_story_panel(story, exit_live_message=exit_live_message)
except KeyboardInterrupt:
bookshelf_storage.delete_story(story_name)
bookshelf_console.print(f'🗑️ Story \'{story_name}\' has been deleted!')


@bookshelf.command(name='info')
@click.argument('story_name')
def story_info_entry(story_name: str):
"""Displays the information for a given story on your bookshelf"""
story = bookshelf_storage.load_story(story_name)
bookshelf_console.render_story_panel(story)
bookshelf_console.print(f'🗑️ Story \'{story_name}\' has been deleted!')
try:
story = bookshelf_storage.load_story(story_name)
bookshelf_console.render_story_panel(story)
except KeyboardInterrupt:
pass
41 changes: 14 additions & 27 deletions bookshelf/console.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import threading
import time
from typing import Optional

from rich.live import Live
from rich.padding import Padding
from rich.panel import Panel
from rich.table import Table
from rich.console import Console
from rich import box

from bookshelf.storage import BookshelfStorage
from bookshelf.models import Story


class BookshelfConsole(Console):
Expand All @@ -31,33 +32,19 @@ def render_all_stories_table(self, tags_to_filter_by: Optional[list[str]] = None

def render_story_panel(self, story, exit_live_message: Optional[str] = None, should_clear: bool = True) -> None:
if exit_live_message is None:
exit_live_message = 'Press [bold]Enter[/bold] to exit'
exit_live_message = 'Press [bold]CTRL+C[/bold] to exit'

stop_flag = threading.Event()

def key_listener():
input()
stop_flag.set()

def update_dashboard():
with Live(Panel(story.to_renderable(exit_live_message=exit_live_message), title='📖 Story 📖', expand=True),
transient=should_clear,
refresh_per_second=1) as live_panel:
while not stop_flag.is_set():
live_panel.update(
Panel(story.to_renderable(exit_live_message=exit_live_message), title='📖 Story 📖', expand=True))
time.sleep(0.4)

update_thread = threading.Thread(target=update_dashboard)
key_thread = threading.Thread(target=key_listener)

# Start the threads
update_thread.start()
key_thread.start()

# Wait for the threads to finish
update_thread.join()
key_thread.join()
with Live(self._get_story_panel(story, exit_live_message),
transient=should_clear,
refresh_per_second=1) as live_panel:
while True:
live_panel.update(self._get_story_panel(story, exit_live_message))
time.sleep(0.4)

if should_clear:
self.clear()

@staticmethod
def _get_story_panel(story: Story, exit_live_message: str):
panel = Panel(story.to_renderable(exit_live_message=exit_live_message), title='📖 Story 📖', expand=True)
return Padding(panel, 1)
2 changes: 1 addition & 1 deletion bookshelf/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def __init__(self):
pass

def save_story(self, story: Story) -> None:
if not self.story_exists(story.name):
if not os.path.exists(self.BOOKSHELF_DIR):
os.makedirs(self.BOOKSHELF_TASK_DIR)
filename = os.path.join(self.BOOKSHELF_TASK_DIR, f'{story.name}.json')
with open(filename, 'w') as file:
Expand Down
Binary file removed docs/public/favicon.ico
Binary file not shown.
Binary file removed docs/public/favicon.png
Binary file not shown.
9 changes: 9 additions & 0 deletions docs/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/src/assets/click-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/src/assets/example-terminal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/src/assets/rich-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 13 additions & 11 deletions docs/src/content/docs/getting-started/glossary.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ description: A guide to install bookshelf
---
import { Tabs, TabItem } from '@astrojs/starlight/components';

## Prerequisites
## Why need a glossary?

## Using a Package Manager
You can easily add this project to your own by using your favorite project manager. Here are some examples for popular build tools:
There are a couple of terms thrown around in the bookshelf documentation that you might not be familiar with.
This glossary should help you understand what they mean.

<Tabs>
<TabItem label="pip">
```bash
foo@bar:~$ pip install bookshelf
```
</TabItem>
</Tabs>
## Bookshelf
A **bookshelf** is a representation of the whole collection of work that you have planned, are doing or have done.

For more information about the project and its usage, you can visit the PyPI page [here](https://pypi.org/manage/project/bookshelf-cli/releases/).
## Story
A **story** is a representation of a larger piece of work that you have to do.
In the agile development world, this is usually a user story, but could also represent a subtask or an epic.
It is simply a piece of work.

## Chapter
A **chapter** is the representation of the individual chunks of time you have dedicated to a story.
By measuring the chunks of time you are actively developing a story, you can get a better idea of how long it will take to complete.
16 changes: 13 additions & 3 deletions docs/src/content/docs/index.mdx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
title: Bookshelf
description: A CLI tool for tracking your stories in the SDLC.
description: 📚 A CLI tool for tracking your stories in the SDLC.
template: splash
hero:
tagline: A CLI tool for tracking your stories in the SDLC.
tagline: 📚 A CLI tool for tracking your stories in the SDLC.
image:
file: ../../assets/bookshelf.png
actions:
Expand All @@ -15,5 +15,15 @@ hero:
link: https://github.com/jossmoff/bookshelf
icon: github
---
import { Image } from "astro:assets"
import terminal from "../../assets/example-terminal.png"
import rich from "../../assets/rich-logo.svg"
import click from "../../assets/click-logo.png"


## 🔎 Tracking your productivity has never been easier
<Image src={terminal} alt="Terminal with bookshelf commands" style="max-width: 100%; max-height: 100%; display: block;"/>




## Tracking your productivity has never been easier
43 changes: 43 additions & 0 deletions docs/src/content/docs/reference/create.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,52 @@ description: Create a new story for your bookshelf

### Usage

```bash
bookshelf create [OPTIONS] STORY_NAME
```

### Description

Use `bookshelf create` to create a new story for your bookshelf. The story name must be unique between all stories in your bookshelf.

### Options

| Name | Shorthand | Default | Description |
|------- | --------- | ------- | ----------- |
| `--force` | `-f` | | Usually, the command will not start a story that already exists. This flag disables these checks, use it with care. |
| `--tags` | `-t` | | Comma-separated list of tags that you want to apply to the story. |
| `--start-chapter` | | `False` | Optionally start the first chapter of the story you are creating right away. |

### Examples

#### Create a new story
<pre><span style="background-color:#26384B"><font color="#93FF91"> root </font></span><font color="#26384B"></font> <font color="#49FF6D">bookshelf</font> create example-story --tags hello,world
╭───────────────── 📖 Story 📖 ──────────────────╮
│ <b>✏️ Name:</b> │
│ example-story │
│ <b>🗓️ Start Date:</b> │
│ 22/10/2023 │
│ <b>⏱️ Elapsed Time:</b> │
│ 0 hours, 0 minutes, 0 seconds │
│ <b>🏷️ Tags:</b> │
[]
│ │
[Press <b>CTRL+C</b> to exit]
╰────────────────────────────────────────────────╯</pre>

#### Create a new story with tags

<pre><span style="background-color:#26384B"><font color="#93FF91"> root </font></span><font color="#26384B"></font> <font color="#49FF6D">bookshelf</font> create example-story -t hello,world
╭───────────────── 📖 Story 📖 ──────────────────╮
│ <b>✏️ Name:</b> │
│ example-story │
│ <b>🗓️ Start Date:</b> │
│ 22/10/2023 │
│ <b>⏱️ Elapsed Time:</b> │
│ 0 hours, 0 minutes, 0 seconds │
│ <b>🏷️ Tags:</b> │
[&apos;hello&apos;, &apos;world&apos;]
│ │
[Press <b>CTRL+C</b> to exit]
╰────────────────────────────────────────────────╯</pre>

Loading

0 comments on commit fd7e53b

Please sign in to comment.