Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions gittask/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ def link_branch_to_task(self, branch_name: str, repo_path: str, task_gid: str, t
'workspace_gid': workspace_gid
}, (Branch.branch_name == branch_name) & (Branch.repo_path == repo_path))

def remove_branch_link(self, branch_name: str, repo_path: str):
Branch = Query()
self.branch_map.remove(
(Branch.branch_name == branch_name) & (Branch.repo_path == repo_path)
)

# Time Session Operations
def start_session(self, branch_name: str, repo_path: str, task_gid: str):
# Enforce single active session: Stop ANY other active session first
Expand Down
10 changes: 10 additions & 0 deletions gittask/git_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,13 @@ def get_remote_url(self, remote_name: str = "origin") -> Optional[str]:
return self.repo.remote(remote_name).url
except ValueError:
return None

def push_branch(self, branch_name: str, remote_name: str = "origin"):
try:
remote = self.repo.remote(remote_name)
# Push and set upstream
remote.push(refspec=f"{branch_name}:{branch_name}", set_upstream=True)
except ValueError:
raise Exception(f"Remote '{remote_name}' not found")
except git.exc.GitCommandError as e:
raise Exception(f"Failed to push branch: {e}")
21 changes: 21 additions & 0 deletions gittask/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@
app.command(name="start", help="Start time tracking")(session.start)
app.command(name="track", help="Track time on a global task")(track.track)

@app.command(name="gui", help="Launch the Graphical User Interface (TUI)")
def gui():
from .tui.app import GitTaskApp
from .config import ConfigManager
from .asana_client import AsanaClient

# Warm up AsanaClient to initialize multiprocessing pool before Textual starts
# This prevents "bad value(s) in fds_to_keep" error on macOS
try:
config = ConfigManager()
token = config.get_api_token()
if token:
client = AsanaClient(token)
client.close()
except Exception:
# Ignore errors here, let the app handle them or fail later
pass

app = GitTaskApp()
app.run()

@app.callback()
def main(ctx: typer.Context):
"""
Expand Down
48 changes: 48 additions & 0 deletions gittask/tui/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer
from .screens.dashboard import Dashboard
from .screens.task_search import TaskSearch
from .screens.progress import ProgressScreen
from .screens.status import StatusScreen

class GitTaskApp(App):
"""A Textual app for GitTask."""

CSS_PATH = "css/styles.tcss"
BINDINGS = [
("d", "navigate('dashboard')", "Dashboard"),
("s", "navigate('search')", "Search Tasks"),
("p", "navigate('progress')", "Progress"),
("ctrl+c", "request_quit", "Quit"),
]

SCREENS = {
"dashboard": Dashboard,
"search": TaskSearch,
"progress": ProgressScreen,
"status": StatusScreen,
}

def compose(self) -> ComposeResult:
yield Header()
yield Footer()

def on_mount(self) -> None:
self.push_screen("dashboard")
self.last_quit_request = 0

def action_navigate(self, screen: str) -> None:
self.switch_screen(screen)

def action_request_quit(self) -> None:
import time
now = time.time()
if now - self.last_quit_request < 1.0:
self.exit()
else:
self.last_quit_request = now
self.notify("Press Ctrl+C again to quit")

if __name__ == "__main__":
app = GitTaskApp()
app.run()
259 changes: 259 additions & 0 deletions gittask/tui/css/styles.tcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
Screen {
layout: vertical;
}

Header {
dock: top;
}

Footer {
dock: bottom;
}

/* Dashboard Layout */
.dashboard-container {
layout: vertical;
height: 100%;
}

.task-grid {
layout: grid;
grid-size: 3;
grid-rows: auto;
grid-gutter: 1;
padding: 1;
height: 1fr;
overflow-y: auto;
}



.bottom-bar {
dock: bottom;
height: auto;
border-top: solid $secondary;
padding: 1;
layout: horizontal;
align: center middle;
}

/* Task Card Styles */
TaskCard {
height: auto;
min-height: 14;
border: solid $primary-muted;
padding: 1;
background: $surface;
layout: vertical;
margin-bottom: 1;
}

.trash-btn {
dock: right;
width: 4;
min-width: 4;
height: 1;
background: transparent;
border: none;
color: $text-muted;
padding: 0;
margin: 0;
}

.trash-btn:hover {
color: $error;
background: transparent;
}

TaskCard.active {
border: solid $success;
background: $surface-lighten-1;
}

.task-name {
text-align: center;
text-style: bold;
height: auto;
width: 100%;
margin-bottom: 1;
}

.branch-name {
text-align: center;
color: $text-muted;
height: 1;
width: 100%;
}

.timer {
text-align: center;
text-style: bold;
color: $warning;
height: 3;
content-align: center middle;
width: 100%;
text-opacity: 0%; /* Hidden by default if not active? Or just 00:00:00 */
}

TaskCard.active .timer {
text-opacity: 100%;
}

.card-actions {
align: center bottom;
height: auto;
dock: bottom;
}

TaskCard Button {
min-width: 10;
margin: 0 1;
}

/* General Buttons */
Button {
margin: 0 1;
}

/* Status Screen Styles */
.screen-container {
layout: vertical;
height: 100%;
}

.page-title {
dock: top;
width: 100%;
text-align: center;
background: $primary;
color: $text;
padding: 1;
text-style: bold;
}

.content-scroll {
height: 1fr;
padding: 1 2;
}

.status-section {
margin-bottom: 2;
height: auto;
}

.section-title {
color: $secondary;
text-style: bold;
margin-bottom: 1;
}

.status-card {
border: solid $primary-muted;
padding: 1 2;
background: $surface;
height: auto;
}

.status-card.active {
border: solid $success;
background: $surface-lighten-1;
}

DataTable {
height: auto;
max-height: 20;
border: solid $primary-muted;
}

/* Log Screen */
.log-modal {
width: 80%;
height: 80%;
border: solid $error;
background: $surface;
padding: 1;
align: center middle;
}

.log-title {
text-align: center;
text-style: bold;
color: $error;
margin-bottom: 1;
}

RichLog {
height: 1fr;
border: solid $secondary;
background: $surface-darken-1;
margin-bottom: 1;
}

/* Options Modal */
.options-modal {
width: 60;
height: auto;
border: solid $primary;
background: $surface;
padding: 1 2;
align: center middle;
}

.modal-header {
text-align: center;
text-style: bold;
margin-bottom: 1;
color: $accent;
}

.hidden {
display: none;
}

#select-mode Button {
width: 100%;
margin-bottom: 1;
}

#input-mode {
height: auto;
}

#branch-input {
margin: 1 0;
}

#branch-suggestions {
height: auto;
max-height: 10;
border: solid $secondary;
background: $surface-lighten-1;
margin-bottom: 1;
}

#tag-list {
height: 10;
border: solid $secondary;
margin: 1 0;
}

.modal-subtitle {
text-align: center;
color: $text-muted;
margin-bottom: 1;
}

.modal-actions {
align: center middle;
height: auto;
margin-top: 1;
}

.experimental-notice {
width: 100%;
text-align: center;
background: $warning;
color: $text;
text-style: bold;
padding: 0 1;
}
Loading