cool boot animation, done in WSL
This commit is contained in:
parent
7a1006508c
commit
616685c9b2
|
|
@ -0,0 +1,85 @@
|
||||||
|
# Gitea TUI (fjtui)
|
||||||
|
|
||||||
|
A premium interactive CLI TUI Dashboard for exploring Gitea and Forgejo issues directly from your terminal.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Interactive TUI:** A full-featured terminal user interface using Node.js.
|
||||||
|
- **Multiple Screens:**
|
||||||
|
- **Setup Wizard:** Easy configuration and connection testing.
|
||||||
|
- **Repository Picker:** Browse and select repositories seamlessly.
|
||||||
|
- **Issue List:** View, filter, sort, and search issues in a repository.
|
||||||
|
- **Issue Details:** Read full issue descriptions and comments in a dedicated view.
|
||||||
|
- **Create Issue:** Draft and submit new issues directly from the terminal.
|
||||||
|
- **Add Comment:** Participate in issue discussions seamlessly.
|
||||||
|
- **Configuration Management:** Save your connection settings and tokens securely for future sessions.
|
||||||
|
- **Support for Forgejo & Gitea:** Built to integrate smoothly with Forgejo and Gitea APIs.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Ensure you have Node.js (v20+) installed.
|
||||||
|
|
||||||
|
Clone the repository and install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url> fjtui
|
||||||
|
cd fjtui
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Build the project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Link globally (optional):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm link
|
||||||
|
```
|
||||||
|
You can then run the tool anywhere using `gitea-tui`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
You can start the interactive setup wizard by simply running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
Or, if installed globally:
|
||||||
|
```bash
|
||||||
|
gitea-tui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Direct Connection
|
||||||
|
|
||||||
|
You can also use command-line options to connect directly and bypass the setup wizard:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start -- --url "https://forgejo.example.com" --userid "myuser" --repo "myuser/myrepo" --token "my-access-token"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command-Line Options
|
||||||
|
|
||||||
|
- `-u, --url <url>`: Forgejo/Gitea instance base URL (e.g. `https://forgejo.freshbrewed.science`)
|
||||||
|
- `-i, --userid <userid>`: Forgejo/Gitea user ID / username
|
||||||
|
- `-r, --repo <owner/repo>`: Repository path (e.g. `owner/repo`)
|
||||||
|
- `-t, --token <token>`: Personal Access Token (optional for public repositories)
|
||||||
|
- `-s, --save`: Save the provided settings to the global config file (`~/.config/fjtui/fjtui.json`)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Settings can be saved interactively via the Setup Wizard or by passing the `-s` or `--save` flag in the CLI. The application configuration file is stored safely at `~/.config/fjtui/fjtui.json`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
- `npm run build`: Compiles the TypeScript code to JavaScript in the `dist/` directory.
|
||||||
|
- `npm start`: Runs the built `dist/index.js` file.
|
||||||
|
|
||||||
|
## Technologies Used
|
||||||
|
|
||||||
|
- **TypeScript** / **Node.js**
|
||||||
|
- **Commander:** Command-line argument parsing.
|
||||||
|
- **Chalk:** Terminal styling.
|
||||||
|
- **Axios:** API interactions.
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
# Gitea TUI (fjtui) System Architecture
|
||||||
|
|
||||||
|
This document provides a comprehensive overview of the Gitea/Forgejo TUI Dashboard's system architecture, component layout, and user flows. It uses **Mermaid.js** diagrams to detail the application structure and page lifecycle transitions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. High-Level Architecture
|
||||||
|
|
||||||
|
The Gitea TUI client is a modular CLI application written in TypeScript and Node.js. It operates as a local state machine that communicates with a Gitea or Forgejo instance REST API over HTTPS.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph Client ["Gitea TUI Application Context"]
|
||||||
|
EP["index.ts (CLI Entry Point)"]
|
||||||
|
ConfigManager["config.ts (Configuration Manager)"]
|
||||||
|
TuiEngine["tui.ts (TuiEngine Controller)"]
|
||||||
|
APILayer["api.ts (API Service Layer)"]
|
||||||
|
Types["types.ts (State & Entity Types)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph External ["External Resources"]
|
||||||
|
LocalConfig["Local Config (./fjtui.json)"]
|
||||||
|
GlobalConfig["Global Config (~/.config/fjtui/fjtui.json)"]
|
||||||
|
GiteaAPI["Gitea/Forgejo Instance REST API"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph IO ["Terminal User Interface"]
|
||||||
|
Stdin["stdin (Raw Keypress Listeners)"]
|
||||||
|
Stdout["stdout (Chalk Styled Rendering)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
%% Key Relationships
|
||||||
|
EP -->|Reads Config / Parses Args| ConfigManager
|
||||||
|
EP -->|Direct Connect Check| APILayer
|
||||||
|
EP -->|Initializes & Starts| TuiEngine
|
||||||
|
|
||||||
|
ConfigManager <-->|Read / Write| LocalConfig
|
||||||
|
ConfigManager <-->|Read / Write| GlobalConfig
|
||||||
|
|
||||||
|
TuiEngine -->|Subscribes to Events| Stdin
|
||||||
|
TuiEngine -->|Paints Screens| Stdout
|
||||||
|
TuiEngine -->|Fetches & Mutates Data| APILayer
|
||||||
|
TuiEngine -->|Saves Config| ConfigManager
|
||||||
|
|
||||||
|
APILayer -->|HTTP / REST Requests (Axios)| GiteaAPI
|
||||||
|
|
||||||
|
TuiEngine -.->|Operates on AppState| Types
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Screen & Page Transition Flow
|
||||||
|
|
||||||
|
The terminal application operates as a single-page terminal application (SPTA) with a central screen router state. Below is the transition flow (state machine) representing how keypress actions route the user between screens.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Launch : Run gitea-tui (Bootstrap or Direct Connect)
|
||||||
|
Launch --> Setup : No config / finished or skipped
|
||||||
|
Launch --> List : Config exists / direct connect (finished or skipped)
|
||||||
|
|
||||||
|
state Setup {
|
||||||
|
[*] --> EnteringCredentials
|
||||||
|
EnteringCredentials --> Validating : Enter/Submit Form
|
||||||
|
Validating --> EnteringCredentials : Connection Error
|
||||||
|
}
|
||||||
|
|
||||||
|
Setup --> RepoPicker : Validated & Repositories Loaded
|
||||||
|
|
||||||
|
state RepoPicker {
|
||||||
|
[*] --> BrowsingRepos
|
||||||
|
BrowsingRepos --> BrowsingRepos : Filter / Search
|
||||||
|
}
|
||||||
|
|
||||||
|
RepoPicker --> Setup : Escape (Go Back)
|
||||||
|
RepoPicker --> List : Select Repo (Enter) & Load Issues
|
||||||
|
|
||||||
|
state List {
|
||||||
|
[*] --> DisplayingIssues
|
||||||
|
DisplayingIssues --> DisplayingIssues : Toggle Filters / Sort / Paginate / Search
|
||||||
|
}
|
||||||
|
|
||||||
|
List --> Setup : Change Connection ('o' key)
|
||||||
|
List --> CreateIssue : Create Issue ('c' key)
|
||||||
|
List --> Details : View Issue Details (Enter)
|
||||||
|
|
||||||
|
state CreateIssue {
|
||||||
|
[*] --> FillingIssueForm
|
||||||
|
FillingIssueForm --> SavingIssue : Ctrl+S (Submit)
|
||||||
|
SavingIssue --> DisplayingIssues : Success (List reloads)
|
||||||
|
SavingIssue --> FillingIssueForm : Failure (Shows error)
|
||||||
|
}
|
||||||
|
CreateIssue --> List : Escape (Cancel)
|
||||||
|
|
||||||
|
state Details {
|
||||||
|
[*] --> ViewingDetails
|
||||||
|
ViewingDetails --> ViewingDetails : Scroll description & comments
|
||||||
|
}
|
||||||
|
|
||||||
|
Details --> List : Escape / Backspace / Q (Go Back)
|
||||||
|
Details --> Setup : Change Connection ('o' key)
|
||||||
|
Details --> AddComment : Add Comment ('c' key)
|
||||||
|
Details --> EditIssue : Edit Issue ('e' key)
|
||||||
|
Details --> AddTime : Add Tracked Time ('t' key)
|
||||||
|
Details --> SetAssignees : Set Assignees ('a' key)
|
||||||
|
Details --> ConfirmStateChange : Toggle Issue State ('x' key)
|
||||||
|
|
||||||
|
state AddComment {
|
||||||
|
[*] --> EnteringComment
|
||||||
|
EnteringComment --> SubmittingComment : Ctrl+S (Submit)
|
||||||
|
SubmittingComment --> ViewingDetails : Success (Comments reload)
|
||||||
|
SubmittingComment --> EnteringComment : Failure (Shows error)
|
||||||
|
}
|
||||||
|
AddComment --> Details : Escape (Cancel)
|
||||||
|
|
||||||
|
state EditIssue {
|
||||||
|
[*] --> EditingIssueForm
|
||||||
|
EditingIssueForm --> SubmittingEdit : Ctrl+S (Submit)
|
||||||
|
SubmittingEdit --> ViewingDetails : Success (Details reload)
|
||||||
|
SubmittingEdit --> EditingIssueForm : Failure (Shows error)
|
||||||
|
}
|
||||||
|
EditIssue --> Details : Escape (Cancel)
|
||||||
|
|
||||||
|
state AddTime {
|
||||||
|
[*] --> EnteringTime
|
||||||
|
EnteringTime --> SubmittingTime : Ctrl+S (Submit)
|
||||||
|
SubmittingTime --> ViewingDetails : Success (Time reloads)
|
||||||
|
SubmittingTime --> EnteringTime : Failure (Shows error)
|
||||||
|
}
|
||||||
|
AddTime --> Details : Escape (Cancel)
|
||||||
|
|
||||||
|
state SetAssignees {
|
||||||
|
[*] --> EnteringAssignees
|
||||||
|
EnteringAssignees --> SubmittingAssignees : Enter (Submit)
|
||||||
|
SubmittingAssignees --> ViewingDetails : Success (Assignees reload)
|
||||||
|
SubmittingAssignees --> EnteringAssignees : Failure (Shows error)
|
||||||
|
}
|
||||||
|
SetAssignees --> Details : Escape (Cancel)
|
||||||
|
|
||||||
|
state ConfirmStateChange {
|
||||||
|
[*] --> AwaitingConfirmation
|
||||||
|
AwaitingConfirmation --> Details : No / Escape (Cancel)
|
||||||
|
AwaitingConfirmation --> AnimatingChange : Yes ('y' key)
|
||||||
|
|
||||||
|
state AnimatingChange {
|
||||||
|
[*] --> AnimatingClose : If Closing (Gravestones)
|
||||||
|
[*] --> AnimatingReopen : If Reopening (Zombies)
|
||||||
|
AnimatingClose --> ExecutingStateChangeAPI : Finish Frames (1.5s)
|
||||||
|
AnimatingReopen --> ExecutingStateChangeAPI : Finish Frames (1.8s)
|
||||||
|
}
|
||||||
|
ExecutingStateChangeAPI --> ViewingDetails : Done & Reloaded
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Data Interaction & Rerender Lifecycle
|
||||||
|
|
||||||
|
When the user interacts with the interface, keyboard events are processed by the active screen key handler, state variables are updated, API calls are made if necessary, and a full screen redraw is triggered on the terminal buffer.
|
||||||
|
|
||||||
|
The following sequence diagram details the lifecyle of adding a comment to an issue:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
actor User as User Terminal
|
||||||
|
participant TUI as TuiEngine (tui.ts)
|
||||||
|
participant API as API Layer (api.ts)
|
||||||
|
participant Gitea as Gitea REST API
|
||||||
|
|
||||||
|
User->>TUI: Keypress (e.g. 'c' on Details screen)
|
||||||
|
TUI->>TUI: Update screen state to 'add-comment'
|
||||||
|
TUI->>TUI: Render text input area (addCommentForm)
|
||||||
|
TUI-->>User: Display empty comment field
|
||||||
|
|
||||||
|
User->>TUI: Type comment text + Ctrl+S
|
||||||
|
TUI->>TUI: Set state.loading = true & render spinner
|
||||||
|
TUI->>API: createIssueComment(config, issueNumber, commentBody)
|
||||||
|
API->>Gitea: POST /repos/{owner}/{repo}/issues/{index}/comments
|
||||||
|
Gitea-->>API: 201 Created (Comment entity JSON)
|
||||||
|
API-->>TUI: Comment Object
|
||||||
|
|
||||||
|
TUI->>TUI: Set screen state to 'details'
|
||||||
|
TUI->>TUI: Trigger loadComments(issue) (Async)
|
||||||
|
TUI->>API: fetchIssueComments(config, issueNumber)
|
||||||
|
API->>Gitea: GET /repos/{owner}/{repo}/issues/{index}/comments
|
||||||
|
Gitea-->>API: 200 OK (Comments list JSON)
|
||||||
|
API-->>TUI: Comment[] Array
|
||||||
|
TUI->>TUI: Set state.loading = false
|
||||||
|
TUI->>TUI: Render details screen + updated comments
|
||||||
|
TUI-->>User: Display issue details and new comment
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Module Directories & Code Structure
|
||||||
|
|
||||||
|
The project code is organized into a clean multi-file architecture located under [src](file:///wsl.localhost/Ubuntu/home/builder/Workspaces/fjtui/src):
|
||||||
|
|
||||||
|
1. **[index.ts](file:///wsl.localhost/Ubuntu/home/builder/Workspaces/fjtui/src/index.ts)**:
|
||||||
|
- Sets up Command-line parsing with the `commander` package.
|
||||||
|
- Automatically bootstraps the configuration and constructs the default `AppState`.
|
||||||
|
- Checks CLI credentials. If valid, validates them directly, bypassing the setup page and going straight to the issue dashboard.
|
||||||
|
- Instantiates the `TuiEngine` and starts the app loop.
|
||||||
|
|
||||||
|
2. **[tui.ts](file:///wsl.localhost/Ubuntu/home/builder/Workspaces/fjtui/src/tui.ts)**:
|
||||||
|
- Contains the core `TuiEngine` class which manages key listeners on `process.stdin` (in raw mode).
|
||||||
|
- Manages loading spinners and UI animations (Gravestones for closed issues and Zombies for reopened issues).
|
||||||
|
- Performs terminal buffer manipulation (e.g. entering alternate screen buffer `\x1B[?1049h`, hiding cursor `\x1B[?25l`).
|
||||||
|
- Delegates the rendering of specific screens using modular helper functions (e.g., `renderSetupScreen`, `renderListScreen`, `renderDetailsScreen`).
|
||||||
|
|
||||||
|
3. **[api.ts](file:///wsl.localhost/Ubuntu/home/builder/Workspaces/fjtui/src/api.ts)**:
|
||||||
|
- Hosts the REST API layer built on top of `axios`.
|
||||||
|
- Interacts with Forgejo/Gitea endpoints such as `/repos`, `/issues`, `/comments`, and `/times`.
|
||||||
|
- Handles network errors and formats them into user-friendly diagnostic messages.
|
||||||
|
|
||||||
|
4. **[config.ts](file:///wsl.localhost/Ubuntu/home/builder/Workspaces/fjtui/src/config.ts)**:
|
||||||
|
- Manages persistence of credentials.
|
||||||
|
- Reads configuration locally from `fjtui.json` and globally from `~/.config/fjtui/fjtui.json`. Local configurations take priority.
|
||||||
|
- Writes updated credentials back to the user's home configuration safely.
|
||||||
|
|
||||||
|
5. **[types.ts](file:///wsl.localhost/Ubuntu/home/builder/Workspaces/fjtui/src/types.ts)**:
|
||||||
|
- Declares the TypeScript contracts and interfaces representing the application state (`AppState`), target forms (`SetupForm`, `CreateIssueForm`, etc.), and returned entities (`User`, `Issue`, `Label`, `Comment`, `RepoItem`).
|
||||||
|
|
||||||
|
6. **ASCII Art Assets**:
|
||||||
|
- [ascii-art.txt](file:///wsl.localhost/Ubuntu/home/builder/Workspaces/fjtui/ascii-art.txt): ASCII art loaded at startup to perform the color-shifting launch animation.
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
..
|
||||||
|
-.
|
||||||
|
:+.
|
||||||
|
.:=+***#****++++-.*+++: .++
|
||||||
|
.-+********#%%#**++++++%***- +*:
|
||||||
|
:+###%%%%@@@@@%%%%#%%%%%@@@@@@= .+*=
|
||||||
|
..............:#@%%%%%%%@*:%@@@+ :**+.
|
||||||
|
#@%%%#@% .=***.
|
||||||
|
=@@%%%@+ .+****.
|
||||||
|
=%%@@+ .+*####+.
|
||||||
|
-##@@+ .-********-
|
||||||
|
-**@@+ .:-+**#********.
|
||||||
|
-**%#*********#******.
|
||||||
|
-**********#*******+
|
||||||
|
=***********#*####*:
|
||||||
|
.+***********####*=.
|
||||||
|
=******#***++=:.
|
||||||
|
.+: -**@@+
|
||||||
|
:. -**@@+
|
||||||
|
-**@@+
|
||||||
|
.......:::::.......................
|
||||||
|
.::::::::::::-+#*************####@@@@@@@@@@@@@@@@@@@@:
|
||||||
|
.%@%*********#%#######%%%%%%%%%%@@@@@@@@@@@@@%#*=-:.
|
||||||
|
.*@@@@%##**####%%%######@@@@@@@@@@@@@@@@*
|
||||||
|
.+@@@@@@%###%%#******@@@@@@@@%=.. ...
|
||||||
|
.-+%@@@%%#******@@@@@@@+
|
||||||
|
::-******@@@@@@=
|
||||||
|
.******@@@@@@.
|
||||||
|
:******@@@@@@=
|
||||||
|
-*****##@@@@@@@=
|
||||||
|
.-+**#@@@@@@@@%%#%%#=.
|
||||||
|
*%%%%%%@@@@@@@@@@%%#%@@@@@@#
|
||||||
|
:--+****************++++******+--:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.
|
||||||
|
=
|
||||||
|
.-==+*+===---: +--- +=
|
||||||
|
-++++++++*##*++-----:#=== =+
|
||||||
|
:+**##%%%@@@%####*%####@@%%%%: +*-
|
||||||
|
*%######%@+ %@@@- +*=
|
||||||
|
*%###*%# -**+
|
||||||
|
.%%##%@: =***+
|
||||||
|
.##@@- =*****=
|
||||||
|
.++@@- :********.
|
||||||
|
.++@@- .=+*********+
|
||||||
|
.++%*+++++**********+
|
||||||
|
.++++++++**********-
|
||||||
|
-++++++***********+
|
||||||
|
=++++++++*******+:
|
||||||
|
-+++++**+++=-:
|
||||||
|
+ ++@@-
|
||||||
|
.++@@-
|
||||||
|
.++@@-
|
||||||
|
|
||||||
|
.-+++++++++++*******@@@@@@@@@@@@@@@@@@@%
|
||||||
|
#@#+++++++++*****#***#####%%%%%@@@@@@@@@@@@%#*=:.
|
||||||
|
=@@@@#**++****###******@@@@@@@@@@@@@@@@=
|
||||||
|
-@@@@@@#***##***++++@@@@@@@@%:
|
||||||
|
.-#@@%##*++++++@@@@@@@:
|
||||||
|
.++++++@@@@@@.
|
||||||
|
++++++@@@@@@
|
||||||
|
++++++@@@@@@:
|
||||||
|
.++++***@@@@@@@:
|
||||||
|
.=++*%@@@@@@@%#*##*:
|
||||||
|
+%%%%%%@@@@@@@@@@%#+%%%%%%%*
|
||||||
|
-=================---======-
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
205
src/tui.ts
205
src/tui.ts
|
|
@ -1,5 +1,8 @@
|
||||||
import readline from 'readline';
|
import readline from 'readline';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
import { AppState, Issue, Comment } from './types.js';
|
import { AppState, Issue, Comment } from './types.js';
|
||||||
import { fetchIssues, fetchIssue, fetchIssueComments, validateConnection, normalizeUrl, authenticateAndFetchRepos, createIssue, createIssueComment, editIssue, addIssueTime, changeIssueState, setIssueAssignees } from './api.js';
|
import { fetchIssues, fetchIssue, fetchIssueComments, validateConnection, normalizeUrl, authenticateAndFetchRepos, createIssue, createIssueComment, editIssue, addIssueTime, changeIssueState, setIssueAssignees } from './api.js';
|
||||||
import { saveGlobalConfig } from './config.js';
|
import { saveGlobalConfig } from './config.js';
|
||||||
|
|
@ -191,6 +194,11 @@ export class TuiEngine {
|
||||||
private searchInputBuffer: string = '';
|
private searchInputBuffer: string = '';
|
||||||
private animationFrame: number = 0;
|
private animationFrame: number = 0;
|
||||||
private animationInterval: NodeJS.Timeout | null = null;
|
private animationInterval: NodeJS.Timeout | null = null;
|
||||||
|
private launchDestScreen: 'list' | 'setup' = 'setup';
|
||||||
|
private launchFrame: string[] = [];
|
||||||
|
private launchFrameIndex = 0;
|
||||||
|
private launchInterval: NodeJS.Timeout | null = null;
|
||||||
|
private maxLaunchFrames = 60;
|
||||||
|
|
||||||
constructor(initialState: AppState) {
|
constructor(initialState: AppState) {
|
||||||
this.state = initialState;
|
this.state = initialState;
|
||||||
|
|
@ -217,13 +225,15 @@ export class TuiEngine {
|
||||||
// Enter alternate screen buffer and hide standard cursor
|
// Enter alternate screen buffer and hide standard cursor
|
||||||
process.stdout.write('\x1B[?1049h\x1B[?25l');
|
process.stdout.write('\x1B[?1049h\x1B[?25l');
|
||||||
|
|
||||||
// Bootstrap if config exists
|
// Bootstrap destination
|
||||||
if (this.state.config.url && this.state.config.owner && this.state.config.repo) {
|
if (this.state.config.url && this.state.config.owner && this.state.config.repo) {
|
||||||
this.state.screen = 'list';
|
this.launchDestScreen = 'list';
|
||||||
this.loadIssues();
|
|
||||||
} else {
|
} else {
|
||||||
this.render();
|
this.launchDestScreen = 'setup';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.state.screen = 'launch';
|
||||||
|
this.startLaunchAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -235,6 +245,10 @@ export class TuiEngine {
|
||||||
clearInterval(this.animationInterval);
|
clearInterval(this.animationInterval);
|
||||||
this.animationInterval = null;
|
this.animationInterval = null;
|
||||||
}
|
}
|
||||||
|
if (this.launchInterval) {
|
||||||
|
clearInterval(this.launchInterval);
|
||||||
|
this.launchInterval = null;
|
||||||
|
}
|
||||||
// Exit alternate screen buffer and show standard cursor
|
// Exit alternate screen buffer and show standard cursor
|
||||||
process.stdout.write('\x1B[?1049l\x1B[?25h');
|
process.stdout.write('\x1B[?1049l\x1B[?25h');
|
||||||
if (process.stdin.isTTY) {
|
if (process.stdin.isTTY) {
|
||||||
|
|
@ -361,6 +375,11 @@ export class TuiEngine {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.screen === 'launch') {
|
||||||
|
this.skipLaunchAnimation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.screen === 'setup') {
|
if (this.state.screen === 'setup') {
|
||||||
this.handleSetupKeypress(str, key);
|
this.handleSetupKeypress(str, key);
|
||||||
} else if (this.state.screen === 'repo-picker') {
|
} else if (this.state.screen === 'repo-picker') {
|
||||||
|
|
@ -878,7 +897,9 @@ export class TuiEngine {
|
||||||
// Clear terminal screen, clear scrollback buffer, and reset cursor position
|
// Clear terminal screen, clear scrollback buffer, and reset cursor position
|
||||||
process.stdout.write('\x1B[2J\x1B[3J\x1B[H');
|
process.stdout.write('\x1B[2J\x1B[3J\x1B[H');
|
||||||
|
|
||||||
if (this.state.screen === 'setup') {
|
if (this.state.screen === 'launch') {
|
||||||
|
this.renderLaunchScreen(cols, rows);
|
||||||
|
} else if (this.state.screen === 'setup') {
|
||||||
this.renderSetupScreen(cols, rows);
|
this.renderSetupScreen(cols, rows);
|
||||||
} else if (this.state.screen === 'repo-picker') {
|
} else if (this.state.screen === 'repo-picker') {
|
||||||
this.renderRepoPickerScreen(cols, rows);
|
this.renderRepoPickerScreen(cols, rows);
|
||||||
|
|
@ -2204,4 +2225,178 @@ export class TuiEngine {
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LAUNCH ANIMATION IMPLEMENTATION
|
||||||
|
private startLaunchAnimation() {
|
||||||
|
this.launchFrameIndex = 0;
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
this.launchInterval = setInterval(() => {
|
||||||
|
this.launchFrameIndex++;
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
if (this.launchFrameIndex >= this.maxLaunchFrames - 1) {
|
||||||
|
this.skipLaunchAnimation();
|
||||||
|
}
|
||||||
|
}, 60); // 60ms interval for fast, smooth fluid color shifting
|
||||||
|
}
|
||||||
|
|
||||||
|
private skipLaunchAnimation() {
|
||||||
|
if (this.launchInterval) {
|
||||||
|
clearInterval(this.launchInterval);
|
||||||
|
this.launchInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transition to target destination screen
|
||||||
|
if (this.launchDestScreen === 'list') {
|
||||||
|
this.state.screen = 'list';
|
||||||
|
this.loadIssues();
|
||||||
|
} else {
|
||||||
|
this.state.screen = 'setup';
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadLaunchFrames() {
|
||||||
|
try {
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
// Resolve path to the root folder where ascii-art.txt is located
|
||||||
|
const artPath = path.resolve(__dirname, '..', 'ascii-art.txt');
|
||||||
|
|
||||||
|
if (fs.existsSync(artPath)) {
|
||||||
|
const rawArt = fs.readFileSync(artPath, 'utf8').split(/\r?\n/);
|
||||||
|
this.launchFrame = this.preprocessArt(rawArt);
|
||||||
|
} else {
|
||||||
|
// Fallback frames if file not found
|
||||||
|
this.launchFrame = [
|
||||||
|
" ( ( ",
|
||||||
|
" ) ) ",
|
||||||
|
" [===] ",
|
||||||
|
" \\___/ "
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.launchFrame = [
|
||||||
|
" ( ( ",
|
||||||
|
" ) ) ",
|
||||||
|
" [===] ",
|
||||||
|
" \\___/ "
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private preprocessArt(lines: string[]): string[] {
|
||||||
|
let startIdx = 0;
|
||||||
|
while (startIdx < lines.length && lines[startIdx].trim() === '') {
|
||||||
|
startIdx++;
|
||||||
|
}
|
||||||
|
let endIdx = lines.length - 1;
|
||||||
|
while (endIdx >= 0 && lines[endIdx].trim() === '') {
|
||||||
|
endIdx--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIdx > endIdx) return [];
|
||||||
|
const activeLines = lines.slice(startIdx, endIdx + 1);
|
||||||
|
|
||||||
|
let minPadding = Infinity;
|
||||||
|
for (const line of activeLines) {
|
||||||
|
if (line.trim() === '') continue;
|
||||||
|
const match = line.match(/^[ \t]*/);
|
||||||
|
const padding = match ? match[0].length : 0;
|
||||||
|
if (padding < minPadding) {
|
||||||
|
minPadding = padding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minPadding === Infinity || minPadding === 0) return activeLines;
|
||||||
|
return activeLines.map(line => line.length >= minPadding ? line.substring(minPadding) : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLaunchScreen(cols: number, rows: number) {
|
||||||
|
if (this.launchFrame.length === 0) {
|
||||||
|
this.loadLaunchFrames();
|
||||||
|
}
|
||||||
|
|
||||||
|
const frame = this.launchFrame;
|
||||||
|
if (frame.length === 0) return;
|
||||||
|
|
||||||
|
const W = Math.max(...frame.map(line => line.length));
|
||||||
|
const H = frame.length;
|
||||||
|
|
||||||
|
const padLeftCount = Math.max(0, Math.floor((cols - W) / 2));
|
||||||
|
const padTopCount = Math.max(0, Math.floor((rows - H - 5) / 2));
|
||||||
|
|
||||||
|
for (let i = 0; i < padTopCount; i++) {
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// HSL helper function to generate HSL-based hex colors
|
||||||
|
const hslToHex = (h: number, s: number, l: number): string => {
|
||||||
|
l /= 100;
|
||||||
|
const a = (s * Math.min(l, 1 - l)) / 100;
|
||||||
|
const f = (n: number) => {
|
||||||
|
const k = (n + h / 30) % 12;
|
||||||
|
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||||
|
return Math.round(255 * color).toString(16).padStart(2, '0');
|
||||||
|
};
|
||||||
|
return `#${f(0)}${f(8)}${f(4)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const step = this.launchFrameIndex;
|
||||||
|
|
||||||
|
for (let i = 0; i < frame.length; i++) {
|
||||||
|
const line = frame[i];
|
||||||
|
const leftPad = ' '.repeat(padLeftCount);
|
||||||
|
|
||||||
|
let coloredLine = '';
|
||||||
|
if (i < 20) {
|
||||||
|
// Steam gradient: waving light intensity using sine wave based on time and row index
|
||||||
|
const factor = i / 20; // 0 to 1
|
||||||
|
|
||||||
|
// Wave lightness of the steam
|
||||||
|
const wave = Math.sin(step * 0.4 - i * 0.3);
|
||||||
|
const lightness = Math.max(10, Math.min(80, Math.round(25 + wave * 15 + factor * 25)));
|
||||||
|
|
||||||
|
// Soft cyan/blue steam hue = 195
|
||||||
|
const hex = hslToHex(195, 30, lightness);
|
||||||
|
coloredLine = chalk.hex(hex)(line);
|
||||||
|
} else {
|
||||||
|
// Cup body gradient: shifting rainbow wave
|
||||||
|
const rowOffset = i - 20;
|
||||||
|
|
||||||
|
// Cycle the hue over time and rows
|
||||||
|
const hue = (step * 8 + rowOffset * 12) % 360;
|
||||||
|
const hex = hslToHex(hue, 95, 55);
|
||||||
|
coloredLine = chalk.hex(hex).bold(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(leftPad + coloredLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
const titlePlain = ' G I T E A & F O R G E J O T U I ';
|
||||||
|
const subtitlePlain = 'Interactive Issue & Pull Request Explorer';
|
||||||
|
|
||||||
|
// Cycle the title box background color too!
|
||||||
|
const titleHue = (step * 6) % 360;
|
||||||
|
const titleColor = hslToHex(titleHue, 95, 50);
|
||||||
|
|
||||||
|
const titleLeftPad = Math.max(0, Math.floor((cols - titlePlain.length) / 2));
|
||||||
|
const subtitleLeftPad = Math.max(0, Math.floor((cols - subtitlePlain.length) / 2));
|
||||||
|
|
||||||
|
console.log(' '.repeat(titleLeftPad) + chalk.bold.bgHex(titleColor).black(titlePlain));
|
||||||
|
console.log(' '.repeat(subtitleLeftPad) + chalk.gray(subtitlePlain));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const skipPlain = 'Press any key to skip animation';
|
||||||
|
const skipLeftPad = Math.max(0, Math.floor((cols - skipPlain.length) / 2));
|
||||||
|
console.log(' '.repeat(skipLeftPad) + chalk.gray.italic(skipPlain));
|
||||||
|
|
||||||
|
const printedRows = H + padTopCount + 5;
|
||||||
|
const remainingRows = Math.max(0, rows - printedRows - 1);
|
||||||
|
for (let i = 0; i < remainingRows; i++) {
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export interface AddTimeForm {
|
||||||
timeInput: string;
|
timeInput: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScreenType = 'setup' | 'repo-picker' | 'list' | 'details' | 'create-issue' | 'add-comment' | 'edit-issue' | 'add-time' | 'confirm-state-change' | 'animating-close' | 'animating-reopen' | 'set-assignees';
|
export type ScreenType = 'launch' | 'setup' | 'repo-picker' | 'list' | 'details' | 'create-issue' | 'add-comment' | 'edit-issue' | 'add-time' | 'confirm-state-change' | 'animating-close' | 'animating-reopen' | 'set-assignees';
|
||||||
|
|
||||||
|
|
||||||
export interface RepoItem {
|
export interface RepoItem {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue