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 chalk from 'chalk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { AppState, Issue, Comment } from './types.js';
|
||||
import { fetchIssues, fetchIssue, fetchIssueComments, validateConnection, normalizeUrl, authenticateAndFetchRepos, createIssue, createIssueComment, editIssue, addIssueTime, changeIssueState, setIssueAssignees } from './api.js';
|
||||
import { saveGlobalConfig } from './config.js';
|
||||
|
|
@ -191,6 +194,11 @@ export class TuiEngine {
|
|||
private searchInputBuffer: string = '';
|
||||
private animationFrame: number = 0;
|
||||
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) {
|
||||
this.state = initialState;
|
||||
|
|
@ -217,13 +225,15 @@ export class TuiEngine {
|
|||
// Enter alternate screen buffer and hide standard cursor
|
||||
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) {
|
||||
this.state.screen = 'list';
|
||||
this.loadIssues();
|
||||
this.launchDestScreen = 'list';
|
||||
} else {
|
||||
this.render();
|
||||
this.launchDestScreen = 'setup';
|
||||
}
|
||||
|
||||
this.state.screen = 'launch';
|
||||
this.startLaunchAnimation();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -235,6 +245,10 @@ export class TuiEngine {
|
|||
clearInterval(this.animationInterval);
|
||||
this.animationInterval = null;
|
||||
}
|
||||
if (this.launchInterval) {
|
||||
clearInterval(this.launchInterval);
|
||||
this.launchInterval = null;
|
||||
}
|
||||
// Exit alternate screen buffer and show standard cursor
|
||||
process.stdout.write('\x1B[?1049l\x1B[?25h');
|
||||
if (process.stdin.isTTY) {
|
||||
|
|
@ -361,6 +375,11 @@ export class TuiEngine {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.state.screen === 'launch') {
|
||||
this.skipLaunchAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.screen === 'setup') {
|
||||
this.handleSetupKeypress(str, key);
|
||||
} else if (this.state.screen === 'repo-picker') {
|
||||
|
|
@ -878,7 +897,9 @@ export class TuiEngine {
|
|||
// Clear terminal screen, clear scrollback buffer, and reset cursor position
|
||||
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);
|
||||
} else if (this.state.screen === 'repo-picker') {
|
||||
this.renderRepoPickerScreen(cols, rows);
|
||||
|
|
@ -2204,4 +2225,178 @@ export class TuiEngine {
|
|||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue