From 201a60832aef4cb3983776c49aab8cf1434201f5 Mon Sep 17 00:00:00 2001 From: Anmol Kakkar Date: Wed, 16 Apr 2025 17:04:54 +0530 Subject: [PATCH 01/11] feat: add app.tcss Signed-off-by: Anmol Kakkar --- src/gitx/css/app.tcss | 172 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 156 insertions(+), 16 deletions(-) diff --git a/src/gitx/css/app.tcss b/src/gitx/css/app.tcss index 27b8f3c..4847b4c 100644 --- a/src/gitx/css/app.tcss +++ b/src/gitx/css/app.tcss @@ -1,39 +1,179 @@ -/* Base application styles */ +/* Base application styles inspired by LazyGit */ Screen { - background: $surface; - color: $text; - layout: vertical; + background: #0d1117; + color: #c9d1d9; } Header { dock: top; height: 1; + background: #161b22; + color: #c9d1d9; } Footer { dock: bottom; height: 1; + background: #161b22; + color: #58a6ff; } -/* Common styles that apply to the whole app */ -.welcome { - width: 100%; +/* Grid layout */ +#app-layout { + layout: grid; + grid-size: 2; + grid-rows: 1fr 3fr 1fr 1fr; + grid-columns: 1fr 2fr; +} + +#left-panels { + row-span: 4; + column-span: 1; + layout: vertical; + background: #0d1117; + border-right: solid #30363d; +} + +#right-panels { + row-span: 4; + column-span: 1; + layout: vertical; + background: #0d1117; +} + +#bottom-panel { + row-span: 1; + column-span: 2; height: 100%; - align: center middle; - background: $surface; + background: #0d1117; + border-top: solid #30363d; } -.welcome-title { +/* Panel styles */ +.panel { + border: none; + padding: 0 0 0 0; + height: 1fr; + overflow: auto; +} + +.section-title { text-style: bold; - text-align: center; - margin-bottom: 1; - color: $accent; + background: #21262d; + color: #89b4fa; + padding: 0 1; width: 100%; + text-align: left; height: 1; } -.welcome-text { - text-align: center; - margin-bottom: 1; +/* Status panel */ +#status-panel { + height: auto; + margin-bottom: 0; + border-bottom: solid #30363d; +} + +.status-row { + height: 1; + margin-bottom: 0; + padding: 0 1; +} + +.status-label { + width: 30%; + text-style: bold; + color: #c9d1d9; +} + +.status-value { + width: 70%; + color: #7ee787; +} + +/* Command panel */ +#command-panel { + width: 100%; + height: 100%; +} + +#command-row { width: 100%; + height: auto; + background: #21262d; +} + +#command-input { + width: 4fr; + background: #0d1117; + color: #c9d1d9; + border: none; + height: 1; + padding: 0 1; + margin: 0; +} + +#run-command-btn { + width: 1fr; + background: #388bfd; + color: #ffffff; + height: 1; + margin: 0; +} + +#command-output { + width: 100%; + height: 1fr; + background: #0d1117; + color: #8b949e; + padding: 1; +} + +/* Trees and tables */ +Tree { + padding: 0; +} + +Tree > .tree--cursor { + background: #21262d; + color: #e6edf3; +} + +Tree > .tree--highlight { + background: #388bfd; + color: #ffffff; +} + +/* Rich log styling */ +RichLog { + background: #0d1117; + color: #c9d1d9; + border: none; + padding: 0 1; +} + +Button { + background: #21262d; + color: #c9d1d9; + border: none; +} + +Button:hover { + background: #30363d; +} + +Button.active { + background: #388bfd; + color: #ffffff; +} + +/* Input field styling */ +Input { + background: #0d1117; + color: #c9d1d9; + border: solid #30363d; +} + +Input:focus { + border: solid #388bfd; } \ No newline at end of file From 277e5eb130054c4438ae1b0847e55f602438cd6d Mon Sep 17 00:00:00 2001 From: Anmol Kakkar Date: Wed, 16 Apr 2025 17:05:57 +0530 Subject: [PATCH 02/11] chore: update .gitignore Signed-off-by: Anmol Kakkar --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0a19790..689f282 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Byte-compiled / optimized / DLL files +.DS_Store __pycache__/ *.py[cod] *$py.class From 90bf5f7c98567bc866449a8c3e92f2c3901fb06d Mon Sep 17 00:00:00 2001 From: Anmol Kakkar Date: Wed, 16 Apr 2025 17:06:14 +0530 Subject: [PATCH 03/11] update app.py Signed-off-by: Anmol Kakkar --- src/gitx/app.py | 104 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 12 deletions(-) diff --git a/src/gitx/app.py b/src/gitx/app.py index 2e275f0..8c0fe0c 100644 --- a/src/gitx/app.py +++ b/src/gitx/app.py @@ -1,8 +1,16 @@ from textual.app import App, ComposeResult from textual.binding import Binding -from textual.containers import Container +from textual.containers import Container, Grid from textual.widgets import Header, Footer, Static +from gitx.widgets.status_panel import StatusPanel +from gitx.widgets.file_tree import FileTree +from gitx.widgets.commit_log import CommitLog +from gitx.widgets.branches_panel import BranchesPanel +from gitx.widgets.command_panel import CommandPanel +from gitx.widgets.main_panel import MainPanel +from gitx.git.handler import GitHandler + class GitxApp(App): """A TUI Git client built with Textual.""" @@ -11,27 +19,99 @@ class GitxApp(App): BINDINGS = [ Binding(key="q", action="quit", description="Quit"), - Binding(key="t", action="toggle_dark", description="Toggle dark mode"), + Binding(key="t", action="toggle_theme", description="Toggle theme"), + Binding(key="s", action="stage_file", description="Stage file"), + Binding(key="u", action="unstage_file", description="Unstage file"), + Binding(key="c", action="commit", description="Commit"), + Binding(key="p", action="push", description="Push"), + Binding(key="f", action="pull", description="Pull (fetch)"), + Binding(key="b", action="new_branch", description="New branch"), + Binding(key="?", action="toggle_help", description="Help"), + Binding(key="^p", action="palette", description="Command palette"), ] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.git = GitHandler() + def compose(self) -> ComposeResult: - """Compose the app layout using the welcome screen.""" - self.theme = "flexoki" # Default theme + """Compose the app layout.""" yield Header(show_clock=True) - yield Container( - Static("Welcome to gitx!", classes="welcome-title"), - Static("A Terminal User Interface for Git", classes="welcome-text"), - Static("Press 't' to toggle theme or 'q' to quit", classes="welcome-text"), - classes="welcome" + + # Left side - Status and Files + yield Grid( + # Left column - Status and file changes + Container( + StatusPanel(), + FileTree(), + BranchesPanel(), + id="left-panels" + ), + + # Right side - Main content area and logs + Container( + # Top right - Commit log + CommitLog(), + # Bottom right - Main content + MainPanel(), + id="right-panels" + ), + + # Command panel at bottom + Container( + CommandPanel(), + id="bottom-panel" + ), + + id="app-layout" ) + yield Footer() - def action_toggle_dark(self) -> None: - """Toggle between light and dark theme.""" + def on_mount(self) -> None: + """Initial setup when app is mounted.""" + self.title = "GitXApp" + self.dark = True + + # Show welcome message in main panel + main_panel = self.query_one(MainPanel) + main_panel.show_welcome() + + def action_toggle_theme(self) -> None: + """Toggle dark mode.""" self.theme = ( - "flexoki" if self.theme == "catppuccin-latte" else "catppuccin-latte" + "textual-dark" if self.theme == "textual-light" else "textual-light" ) + def action_stage_file(self) -> None: + """Stage the selected file.""" + self.notify("Action: Stage file (not implemented yet)") + + def action_unstage_file(self) -> None: + """Unstage the selected file.""" + self.notify("Action: Unstage file (not implemented yet)") + + def action_commit(self) -> None: + """Commit staged changes.""" + self.notify("Action: Commit (not implemented yet)") + + def action_push(self) -> None: + """Push changes to remote.""" + self.notify("Action: Push (not implemented yet)") + + def action_pull(self) -> None: + """Pull changes from remote.""" + self.notify("Action: Pull (not implemented yet)") + + def action_new_branch(self) -> None: + """Create a new branch.""" + self.notify("Action: New branch (not implemented yet)") + + def action_toggle_help(self) -> None: + """Toggle help screen.""" + self.notify("Action: Help (not implemented yet)") + + def main() -> None: """Run the app.""" app = GitxApp() From 815e63e343aeb1a759ad323d2c6e57616b48acfd Mon Sep 17 00:00:00 2001 From: Anmol Kakkar Date: Wed, 16 Apr 2025 17:06:22 +0530 Subject: [PATCH 04/11] feat: init handler.py Signed-off-by: Anmol Kakkar --- src/gitx/git/handler.py | 115 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/gitx/git/handler.py diff --git a/src/gitx/git/handler.py b/src/gitx/git/handler.py new file mode 100644 index 0000000..eafb073 --- /dev/null +++ b/src/gitx/git/handler.py @@ -0,0 +1,115 @@ +import os +from pathlib import Path +from typing import List, Dict, Optional, Tuple + + +class GitHandler: + """Handles Git operations.""" + + def __init__(self, repo_path: Optional[str] = None): + """Initialize the Git handler. + + Args: + repo_path: Path to the Git repository. Uses current directory if None. + """ + self.repo_path = repo_path or os.getcwd() + # For now we'll use dummy implementations + + def get_status(self) -> Dict[str, List[str]]: + """Get the status of the repository.""" + # Dummy implementation + return { + "untracked": ["file1.txt", "file2.py"], + "modified": ["app.py", "README.md"], + "staged": ["utils.py"], + } + + def get_branches(self) -> List[Dict[str, str]]: + """Get all branches in the repository.""" + # Dummy implementation + return [ + {"name": "main", "current": False, "remote": "origin/main"}, + {"name": "feat/init-widgets", "current": True, "remote": "origin/feat/init-widgets"}, + {"name": "feat/init-gitx-app", "current": False, "remote": "origin/feat/init-gitx-app"}, + {"name": "doc/tutorial-update", "current": False, "remote": "origin/doc/tutorial-update"}, + ] + + def get_current_branch(self) -> str: + """Get the name of the current branch.""" + # Dummy implementation + return "feat/init-widgets" + + def get_commit_history(self, count: int = 10) -> List[Dict[str, str]]: + """Get commit history.""" + # Dummy implementation + return [ + {"hash": "71d5e14", "author": "Anmol", "date": "4 days ago", "message": "Merge pull request #12: init base app"}, + {"hash": "6b16804", "author": "Ayush", "date": "4 days ago", "message": "feat+doc: init base app, change doc styling"}, + {"hash": "35d7366", "author": "Ashmit9955", "date": "7 days ago", "message": "Merge pull request #10: Doc/tutorial update"}, + {"hash": "86f8adc", "author": "Ayush", "date": "7 days ago", "message": "Doc/tutorial update"}, + {"hash": "d82ec34", "author": "Ayush", "date": "8 days ago", "message": "doc: use material-emojis"}, + {"hash": "74b4f182", "author": "Ashmit9955", "date": "8 days ago", "message": "doc: add real-life-analogy"}, + ] + + def get_repo_status_summary(self) -> Dict[str, str]: + """Get a summary of the repository status.""" + # Dummy implementation + return { + "branch": "feat/init-widgets", + "status": "✓ clean", # Or "! modified" or "+ untracked" + "remote": "origin (ahead:0, behind:0)" + } + + def stage_file(self, file_path: str) -> bool: + """Stage a file.""" + # Dummy implementation + print(f"Staging {file_path}") + return True + + def unstage_file(self, file_path: str) -> bool: + """Unstage a file.""" + # Dummy implementation + print(f"Unstaging {file_path}") + return True + + def commit(self, message: str) -> bool: + """Commit staged changes.""" + # Dummy implementation + print(f"Committing with message: {message}") + return True + + def checkout_branch(self, branch_name: str) -> bool: + """Checkout a branch.""" + # Dummy implementation + print(f"Checking out branch: {branch_name}") + return True + + def create_branch(self, branch_name: str) -> bool: + """Create a new branch.""" + # Dummy implementation + print(f"Creating branch: {branch_name}") + return True + + def merge_branch(self, branch_name: str) -> Tuple[bool, Optional[str]]: + """Merge a branch into the current branch.""" + # Dummy implementation + print(f"Merging branch {branch_name} into current branch") + return True, None + + def pull(self) -> Tuple[bool, Optional[str]]: + """Pull changes from remote.""" + # Dummy implementation + print("Pulling changes from remote") + return True, None + + def push(self) -> Tuple[bool, Optional[str]]: + """Push changes to remote.""" + # Dummy implementation + print("Pushing changes to remote") + return True, None + + def execute_command(self, command: str) -> Tuple[bool, str]: + """Execute a custom git command.""" + # Dummy implementation + print(f"Executing command: git {command}") + return True, f"Executed: git {command}" From b1127347e82913e1f09c9c6e3df172cf3680d9e5 Mon Sep 17 00:00:00 2001 From: Anmol Kakkar Date: Wed, 16 Apr 2025 17:06:42 +0530 Subject: [PATCH 05/11] feat: init branches_panel.py Signed-off-by: Anmol Kakkar --- src/gitx/widgets/branches_panel.py | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/gitx/widgets/branches_panel.py diff --git a/src/gitx/widgets/branches_panel.py b/src/gitx/widgets/branches_panel.py new file mode 100644 index 0000000..8431104 --- /dev/null +++ b/src/gitx/widgets/branches_panel.py @@ -0,0 +1,48 @@ +from textual.widgets import Static, Tree +from textual.app import ComposeResult +from textual.containers import Vertical +from textual.widgets import Label +from rich.text import Text + + +class BranchesPanel(Static): + """Panel that displays and manages branches.""" + + def compose(self) -> ComposeResult: + """Compose the branches panel.""" + yield Vertical( + Label("[bold]3-Local branches[/bold]", classes="section-title"), + Tree("Branches", id="branches-tree"), + classes="panel" + ) + + def on_mount(self) -> None: + """Set up the tree when mounted.""" + tree = self.query_one(Tree) + + # Add local branches + current_branch = "feat/init-git-commands" + branches = [ + "master", + "feat/init-git-commands", + "feat/init-gitx-app", + "doc/tutorial-update", + "feat/lint-ci", + "origin/feat/lint-ci", + "feat/ghactions-docs", + "feat/mkdocs-integrate" + ] + + # Add branches to tree with proper styling + for branch in branches: + node = tree.root.add_leaf(branch) + if branch == current_branch: + # Current branch in green with check mark + node.label.stylize("green bold") + node.label = Text("✓ ") + node.label + elif branch.startswith("origin/"): + # Remote branches in blue + node.label.stylize("blue") + + # Expand the tree by default + tree.root.expand() From 5bc29123194df8776f204d74a13480b150b1ac9c Mon Sep 17 00:00:00 2001 From: Anmol Kakkar Date: Wed, 16 Apr 2025 17:06:56 +0530 Subject: [PATCH 06/11] feat: init command_panel.py Signed-off-by: Anmol Kakkar --- src/gitx/widgets/command_panel.py | 51 +++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/gitx/widgets/command_panel.py diff --git a/src/gitx/widgets/command_panel.py b/src/gitx/widgets/command_panel.py new file mode 100644 index 0000000..cc32b52 --- /dev/null +++ b/src/gitx/widgets/command_panel.py @@ -0,0 +1,51 @@ +from textual.widgets import Static, Input, Button +from textual.app import ComposeResult +from textual.containers import Vertical, Horizontal +from textual.widgets import Label +from rich.text import Text + + +class CommandPanel(Static): + """Panel for executing custom Git commands.""" + + def compose(self) -> ComposeResult: + """Compose the command panel.""" + yield Vertical( + Label("[bold]5-Command log[/bold]", classes="section-title"), + Horizontal( + Input(placeholder="Enter git command...", id="command-input"), + Button("Run", id="run-command-btn", variant="primary"), + id="command-row" + ), + Static("You can hide/focus this panel by pressing '@'", id="command-output"), + id="command-panel", + classes="panel" + ) + + def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle button press event.""" + if event.button.id == "run-command-btn": + command_input = self.query_one("#command-input", Input) + command = command_input.value + + if command.strip(): + output = self.query_one("#command-output", Static) + output_text = Text(f"$ git {command}\n") + output_text.stylize("yellow") + + # Add dummy response + response = Text("Executing command...\n") + response.stylize("green") + + output.update(output_text + response) + + # Clear the input field after execution + command_input.value = "" + + # Focus back on input for next command + self.app.set_focus(command_input) + + # Notify user about command execution + self.app.notify(f"Executed: git {command}") + else: + self.app.notify("Please enter a command", severity="warning") From 4c3f0a834c0cc547a4e3ffabe4a5cf27f90c82d6 Mon Sep 17 00:00:00 2001 From: Anmol Kakkar Date: Wed, 16 Apr 2025 17:07:07 +0530 Subject: [PATCH 07/11] feat: init commit_log.py Signed-off-by: Anmol Kakkar --- src/gitx/widgets/commit_log.py | 74 ++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/gitx/widgets/commit_log.py diff --git a/src/gitx/widgets/commit_log.py b/src/gitx/widgets/commit_log.py new file mode 100644 index 0000000..67666b1 --- /dev/null +++ b/src/gitx/widgets/commit_log.py @@ -0,0 +1,74 @@ +from textual.widgets import Static, RichLog +from textual.app import ComposeResult +from textual.containers import Vertical +from textual.widgets import Label + + +class CommitLog(Static): + """Widget to display commit history.""" + + def compose(self) -> ComposeResult: + """Compose the commit log.""" + yield Vertical( + Label("[bold]2-Log[/bold]", classes="section-title"), + RichLog(id="commit-log", wrap=False, highlight=True, markup=True), + id="commit-log-panel", + classes="panel" + ) + + def on_mount(self) -> None: + """Set up the commit log when mounted.""" + log = self.query_one(RichLog) + + # Make sure we start with an empty log + log.clear() + + # Add dummy data that mimics LazyGit commit log styling + log.write("[green]✱[/green] [yellow]commit[/yellow] [green]a215ea6[/green] ([red]HEAD → feat/init-git-commands[/red], [red]feat/init-widgets[/red])") + log.write("│ [blue]Author:[/blue] Ayush ") + log.write("│ [blue]Date:[/blue] 3 minutes ago") + log.write("│ ") + log.write("│ widgets cn 1") + log.write("│ ") + log.write("│ [blue]Signed-off-by:[/blue] Ayush ") + log.write("│") + log.write("[green]✱[/green] [yellow]commit[/yellow] [green]71d3e14[/green] ([red]origin/master[/red], [red]origin/HEAD[/red], [red]master[/red])") + log.write("│\\ [blue]Merge:[/blue] 35d7366 6b16804") + log.write("│ │ [blue]Author:[/blue] Anmol ") + log.write("│ │ [blue]Date:[/blue] 4 days ago") + log.write("│ │ ") + log.write("│ │ Merge pull request #12 from gitxtui/feat/init-gitx-app") + log.write("│ │ ") + log.write("│ │ feat+doc: init base app, change doc site styling") + log.write("│ │") + + # Add more commits to simulate a longer history + self._add_more_sample_commits(log) + + def _add_more_sample_commits(self, log): + """Add more sample commits to fill the log.""" + log.write("[green]✱[/green] [yellow]commit[/yellow] [green]6b16804[/green] ([red]origin/feat/init-gitx-app[/red], [red]feat/init-gitx-app[/red])") + log.write("│ [blue]Author:[/blue] Ayush ") + log.write("│ [blue]Date:[/blue] 4 days ago") + log.write("│ ") + log.write("│ feat+doc: init base app, change doc site styling") + log.write("│ ") + log.write("│ [blue]Signed-off-by:[/blue] Ayush ") + log.write("│") + log.write("[green]✱[/green] [yellow]commit[/yellow] [green]35d7366[/green]") + log.write("│\\ [blue]Merge:[/blue] 5796a72 86f8adc") + log.write("│ │ [blue]Author:[/blue] Ashmit9955 ") + log.write("│ │ [blue]Date:[/blue] 7 days ago") + log.write("│ │ ") + log.write("│ │ Merge pull request #10 from gitxtui/doc/tutorial-update") + log.write("│ │ ") + log.write("│ │ Doc/tutorial update") + + def update_log(self, commits): + """Update the commit log with real commit data.""" + log = self.query_one(RichLog) + log.clear() + + # This would be implemented to display actual commit data + # For now we'll just display the dummy data + self.on_mount() From 78a76fbb816f3539a0d6623ce2f2abae96aff5ce Mon Sep 17 00:00:00 2001 From: Anmol Kakkar Date: Wed, 16 Apr 2025 17:07:20 +0530 Subject: [PATCH 08/11] feat: init file_tree.py Signed-off-by: Anmol Kakkar --- src/gitx/widgets/file_tree.py | 58 +++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/gitx/widgets/file_tree.py diff --git a/src/gitx/widgets/file_tree.py b/src/gitx/widgets/file_tree.py new file mode 100644 index 0000000..00791a8 --- /dev/null +++ b/src/gitx/widgets/file_tree.py @@ -0,0 +1,58 @@ +from textual.widgets import Tree, Static +from textual.app import ComposeResult +from textual.containers import Vertical +from textual.widgets import Label + + +class FileTree(Static): + """Tree view for displaying unstaged/untracked files.""" + + def compose(self) -> ComposeResult: + """Compose the file tree.""" + yield Vertical( + Label("[bold]2-Files[/bold]", classes="section-title"), + Tree("Files", id="file-tree"), + classes="panel" + ) + + def on_mount(self) -> None: + """Set up the tree when mounted.""" + tree = self.query_one(Tree) + + # Add sections for different file statuses with LazyGit-like styling + unstaged = tree.root.add("Unstaged Changes", expand=True) + # Add files to the unstaged section + for file in ["app.py", "README.md"]: + node = unstaged.add_leaf(file) + node.data = {"status": "modified"} + + untracked = tree.root.add("Untracked Files", expand=True) + # Add files to the untracked section + for file in ["file1.txt", "file2.py"]: + node = untracked.add_leaf(file) + node.data = {"status": "untracked"} + + staged = tree.root.add("Staged Changes", expand=True) + # Add files to the staged section + node = staged.add_leaf("utils.py") + node.data = {"status": "staged"} + + # Add styling to tree nodes + self.apply_tree_styling() + + def apply_tree_styling(self) -> None: + """Apply appropriate CSS classes to tree nodes based on their status.""" + tree = self.query_one(Tree) + # Walk all children and manually skip the root node + for node in tree.walk_children(): + # Skip the root node + if node is tree.root: + continue + + if hasattr(node, 'data') and node.data: + if node.data.get("status") == "modified": + node.label.stylize("red") + elif node.data.get("status") == "untracked": + node.label.stylize("magenta") + elif node.data.get("status") == "staged": + node.label.stylize("green") From 6702723816a6e840bb94aeb4a02372b80e868c1b Mon Sep 17 00:00:00 2001 From: Anmol Kakkar Date: Wed, 16 Apr 2025 17:07:32 +0530 Subject: [PATCH 09/11] feat: init main_panel.py Signed-off-by: Anmol Kakkar --- src/gitx/widgets/main_panel.py | 93 ++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/gitx/widgets/main_panel.py diff --git a/src/gitx/widgets/main_panel.py b/src/gitx/widgets/main_panel.py new file mode 100644 index 0000000..adb6f52 --- /dev/null +++ b/src/gitx/widgets/main_panel.py @@ -0,0 +1,93 @@ +from textual.widgets import Static, RichLog +from textual.app import ComposeResult +from textual.containers import Vertical +from textual.widgets import Label + + +class MainPanel(Static): + """Main panel that changes based on context.""" + + def compose(self) -> ComposeResult: + """Compose the main panel.""" + yield Vertical( + Label("[bold]4-Main[/bold]", classes="section-title"), + RichLog( + markup=True, + highlight=True, + id="main-content", + ), + id="main-panel", + classes="panel" + ) + + def on_mount(self) -> None: + """Set up the main panel when mounted.""" + content = self.query_one("#main-content", RichLog) + content.clear() + content.write("Select a file to view its contents or a commit to view its details.") + + def show_file_diff(self, file_path: str) -> None: + """Show the diff content of a file.""" + content = self.query_one("#main-content", RichLog) + content.clear() + + # Update the title + self.query_one(".section-title", Label).update(f"[bold]4-Diff: {file_path}[/bold]") + + # Add some dummy diff content + content.write("[green]diff --git a/app.py b/app.py[/green]") + content.write("[green]index 1234567..abcdefg 100644[/green]") + content.write("[green]--- a/app.py[/green]") + content.write("[green]+++ b/app.py[/green]") + content.write("[cyan]@@ -20,7 +20,7 @@[/cyan] def some_function():") + content.write(" # Some comment") + content.write(" print('Hello')") + content.write("[red]- return False[/red]") + content.write("[green]+ return True[/green]") + content.write(" ") + content.write(" # Another comment") + content.write("[cyan]@@ -35,6 +35,9 @@[/cyan] def another_function():") + content.write(" # Process data") + content.write("[green]+ # New functionality[/green]") + content.write("[green]+ result = process_data()[/green]") + content.write("[green]+ return result[/green]") + + def show_commit_details(self, commit_hash: str) -> None: + """Show the details of a commit.""" + content = self.query_one("#main-content", RichLog) + content.clear() + + # Update the title + self.query_one(".section-title", Label).update(f"[bold]4-Commit: {commit_hash}[/bold]") + + # Add commit details + content.write(f"[yellow]commit[/yellow] [green]{commit_hash}[/green]") + content.write("[blue]Author:[/blue] Ayush ") + content.write("[blue]Date:[/blue] 3 minutes ago") + content.write("") + content.write(" widgets cn 1") + content.write(" ") + content.write(" [blue]Signed-off-by:[/blue] Ayush ") + content.write("") + content.write("[bold]Changed files:[/bold]") + content.write("[red]- app.py[/red]") + content.write("[red]- README.md[/red]") + content.write("[green]+ utils.py[/green]") + + def show_welcome(self) -> None: + """Show welcome message.""" + content = self.query_one("#main-content", RichLog) + content.clear() + + # Update the title + self.query_one(".section-title", Label).update("[bold]4-Welcome[/bold]") + + # Add welcome content + content.write("[bold]Welcome to GitX - A beginner-friendly Git TUI[/bold]") + content.write("") + content.write("[green]•[/green] Select files to view and manage them") + content.write("[green]•[/green] View commit history and branch information") + content.write("[green]•[/green] Use keyboard shortcuts shown at the bottom") + content.write("[green]•[/green] Run custom Git commands in the command panel") + content.write("") + content.write("Press [bold]?[/bold] for help and available commands") From b53512818302fae7eb9faba032abb8b23fc30f05 Mon Sep 17 00:00:00 2001 From: Anmol Kakkar Date: Wed, 16 Apr 2025 17:07:41 +0530 Subject: [PATCH 10/11] feat: init status_panel.py Signed-off-by: Anmol Kakkar --- src/gitx/widgets/status_panel.py | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/gitx/widgets/status_panel.py diff --git a/src/gitx/widgets/status_panel.py b/src/gitx/widgets/status_panel.py new file mode 100644 index 0000000..5e617d8 --- /dev/null +++ b/src/gitx/widgets/status_panel.py @@ -0,0 +1,50 @@ +from textual.widgets import Static +from textual.app import ComposeResult +from textual.containers import Vertical, Horizontal +from textual.widgets import Label +from rich.text import Text + + +class StatusPanel(Static): + """Panel that shows the current status of the repository.""" + + def compose(self) -> ComposeResult: + """Compose the status panel.""" + yield Vertical( + Label("[bold]1-Status[/bold]", classes="section-title"), + Horizontal( + Static("gitx ➜", classes="status-label"), + Static("feat/init-git-commands", id="current-branch", classes="status-value"), + classes="status-row" + ), + id="status-panel", + classes="panel" + ) + + def on_mount(self) -> None: + """Set up the status panel when mounted.""" + # Set the branch text with proper styling - green for clean status + branch_label = self.query_one("#current-branch", Static) + branch_text = Text("feat/init-git-commands") + branch_text.stylize("green") + branch_label.update(branch_text) + + def update_status(self, branch: str, status: str = "clean") -> None: + """Update the status information. + + Args: + branch: The name of the current branch + status: Status string ('clean', 'modified', 'untracked') + """ + branch_label = self.query_one("#current-branch", Static) + branch_text = Text(branch) + + # Color based on status + if status == "clean": + branch_text.stylize("green") + elif status == "modified": + branch_text.stylize("red") + elif status == "untracked": + branch_text.stylize("magenta") + + branch_label.update(branch_text) From 06bebd72125a5f4866d439944605c86ac6378b54 Mon Sep 17 00:00:00 2001 From: Anmol Kakkar Date: Wed, 16 Apr 2025 17:13:51 +0530 Subject: [PATCH 11/11] fix ci errors Signed-off-by: Anmol Kakkar --- src/gitx/app.py | 2 +- src/gitx/git/handler.py | 115 ---------------------------------------- 2 files changed, 1 insertion(+), 116 deletions(-) diff --git a/src/gitx/app.py b/src/gitx/app.py index 8c0fe0c..337ad2b 100644 --- a/src/gitx/app.py +++ b/src/gitx/app.py @@ -1,7 +1,7 @@ from textual.app import App, ComposeResult from textual.binding import Binding from textual.containers import Container, Grid -from textual.widgets import Header, Footer, Static +from textual.widgets import Header, Footer from gitx.widgets.status_panel import StatusPanel from gitx.widgets.file_tree import FileTree diff --git a/src/gitx/git/handler.py b/src/gitx/git/handler.py index eafb073..e69de29 100644 --- a/src/gitx/git/handler.py +++ b/src/gitx/git/handler.py @@ -1,115 +0,0 @@ -import os -from pathlib import Path -from typing import List, Dict, Optional, Tuple - - -class GitHandler: - """Handles Git operations.""" - - def __init__(self, repo_path: Optional[str] = None): - """Initialize the Git handler. - - Args: - repo_path: Path to the Git repository. Uses current directory if None. - """ - self.repo_path = repo_path or os.getcwd() - # For now we'll use dummy implementations - - def get_status(self) -> Dict[str, List[str]]: - """Get the status of the repository.""" - # Dummy implementation - return { - "untracked": ["file1.txt", "file2.py"], - "modified": ["app.py", "README.md"], - "staged": ["utils.py"], - } - - def get_branches(self) -> List[Dict[str, str]]: - """Get all branches in the repository.""" - # Dummy implementation - return [ - {"name": "main", "current": False, "remote": "origin/main"}, - {"name": "feat/init-widgets", "current": True, "remote": "origin/feat/init-widgets"}, - {"name": "feat/init-gitx-app", "current": False, "remote": "origin/feat/init-gitx-app"}, - {"name": "doc/tutorial-update", "current": False, "remote": "origin/doc/tutorial-update"}, - ] - - def get_current_branch(self) -> str: - """Get the name of the current branch.""" - # Dummy implementation - return "feat/init-widgets" - - def get_commit_history(self, count: int = 10) -> List[Dict[str, str]]: - """Get commit history.""" - # Dummy implementation - return [ - {"hash": "71d5e14", "author": "Anmol", "date": "4 days ago", "message": "Merge pull request #12: init base app"}, - {"hash": "6b16804", "author": "Ayush", "date": "4 days ago", "message": "feat+doc: init base app, change doc styling"}, - {"hash": "35d7366", "author": "Ashmit9955", "date": "7 days ago", "message": "Merge pull request #10: Doc/tutorial update"}, - {"hash": "86f8adc", "author": "Ayush", "date": "7 days ago", "message": "Doc/tutorial update"}, - {"hash": "d82ec34", "author": "Ayush", "date": "8 days ago", "message": "doc: use material-emojis"}, - {"hash": "74b4f182", "author": "Ashmit9955", "date": "8 days ago", "message": "doc: add real-life-analogy"}, - ] - - def get_repo_status_summary(self) -> Dict[str, str]: - """Get a summary of the repository status.""" - # Dummy implementation - return { - "branch": "feat/init-widgets", - "status": "✓ clean", # Or "! modified" or "+ untracked" - "remote": "origin (ahead:0, behind:0)" - } - - def stage_file(self, file_path: str) -> bool: - """Stage a file.""" - # Dummy implementation - print(f"Staging {file_path}") - return True - - def unstage_file(self, file_path: str) -> bool: - """Unstage a file.""" - # Dummy implementation - print(f"Unstaging {file_path}") - return True - - def commit(self, message: str) -> bool: - """Commit staged changes.""" - # Dummy implementation - print(f"Committing with message: {message}") - return True - - def checkout_branch(self, branch_name: str) -> bool: - """Checkout a branch.""" - # Dummy implementation - print(f"Checking out branch: {branch_name}") - return True - - def create_branch(self, branch_name: str) -> bool: - """Create a new branch.""" - # Dummy implementation - print(f"Creating branch: {branch_name}") - return True - - def merge_branch(self, branch_name: str) -> Tuple[bool, Optional[str]]: - """Merge a branch into the current branch.""" - # Dummy implementation - print(f"Merging branch {branch_name} into current branch") - return True, None - - def pull(self) -> Tuple[bool, Optional[str]]: - """Pull changes from remote.""" - # Dummy implementation - print("Pulling changes from remote") - return True, None - - def push(self) -> Tuple[bool, Optional[str]]: - """Push changes to remote.""" - # Dummy implementation - print("Pushing changes to remote") - return True, None - - def execute_command(self, command: str) -> Tuple[bool, str]: - """Execute a custom git command.""" - # Dummy implementation - print(f"Executing command: git {command}") - return True, f"Executed: git {command}"