login and settings
This commit is contained in:
parent
e35f9ad95e
commit
506700caf9
80
src/api.ts
80
src/api.ts
|
|
@ -1,5 +1,5 @@
|
|||
import axios from 'axios';
|
||||
import { Config, Issue, Comment } from './types.js';
|
||||
import { Config, Issue, Comment, RepoItem } from './types.js';
|
||||
|
||||
/**
|
||||
* Normalizes the Gitea/Forgejo URL by stripping trailing slashes
|
||||
|
|
@ -170,3 +170,81 @@ export async function fetchIssueComments(
|
|||
throw new Error(`Network Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates connection and fetches all repositories accessible by the user.
|
||||
*/
|
||||
export async function authenticateAndFetchRepos(
|
||||
url: string,
|
||||
userid: string,
|
||||
token: string
|
||||
): Promise<RepoItem[]> {
|
||||
const normalized = normalizeUrl(url);
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (token.trim()) {
|
||||
headers['Authorization'] = `token ${token.trim()}`;
|
||||
}
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: `${normalized}/api/v1`,
|
||||
headers,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// 1. Verify credentials / user existence
|
||||
try {
|
||||
if (token.trim()) {
|
||||
await client.get('/user');
|
||||
} else if (userid.trim()) {
|
||||
await client.get(`/users/${userid.trim()}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
if (error.response.status === 401) {
|
||||
throw new Error('Unauthorized: Invalid API Token.');
|
||||
}
|
||||
if (error.response.status === 404) {
|
||||
throw new Error(`User "${userid}" not found.`);
|
||||
}
|
||||
throw new Error(`Authentication failed: ${error.response.data?.message || error.message}`);
|
||||
}
|
||||
throw new Error(`Failed to connect to ${normalized}: ${error.message}`);
|
||||
}
|
||||
|
||||
// 2. Fetch repositories
|
||||
try {
|
||||
let repos: any[] = [];
|
||||
|
||||
if (token.trim()) {
|
||||
// Fetch authenticated user's repos (lists all they have access to)
|
||||
const response = await client.get('/user/repos', { params: { limit: 100 } });
|
||||
repos = response.data;
|
||||
} else if (userid.trim()) {
|
||||
// Fetch public repos of the target username
|
||||
const response = await client.get(`/users/${userid.trim()}/repos`, { params: { limit: 100 } });
|
||||
repos = response.data;
|
||||
}
|
||||
|
||||
if (!Array.isArray(repos)) {
|
||||
throw new Error('Invalid response from server (expected list of repositories).');
|
||||
}
|
||||
|
||||
return repos.map((r: any) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
full_name: r.full_name || `${r.owner?.login}/${r.name}`,
|
||||
private: !!r.private,
|
||||
description: r.description || '',
|
||||
}));
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
throw new Error(`Failed to fetch repositories: ${error.response.data?.message || error.message}`);
|
||||
}
|
||||
throw new Error(`Failed to fetch repositories: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
13
src/index.ts
13
src/index.ts
|
|
@ -13,6 +13,7 @@ program
|
|||
.description('A premium CLI TUI Dashboard for exploring Gitea and Forgejo issues')
|
||||
.version('1.0.0')
|
||||
.option('-u, --url <url>', 'Forgejo/Gitea instance base URL (e.g. https://forgejo.freshbrewed.science)')
|
||||
.option('-i, --userid <userid>', 'Forgejo/Gitea user ID / username')
|
||||
.option('-r, --repo <owner/repo>', 'Repository path (e.g. owner/repo)')
|
||||
.option('-t, --token <token>', 'Personal Access Token (optional for public repositories)');
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ async function bootstrap() {
|
|||
config: {
|
||||
url: '',
|
||||
token: null,
|
||||
userid: '',
|
||||
owner: '',
|
||||
repo: '',
|
||||
},
|
||||
|
|
@ -46,11 +48,15 @@ async function bootstrap() {
|
|||
selectedIssueComments: [],
|
||||
commentsLoading: false,
|
||||
detailScrollOffset: 0,
|
||||
repos: [],
|
||||
selectedRepoIndex: 0,
|
||||
repoSearchQuery: '',
|
||||
repoPickerActiveSearch: false,
|
||||
setupForm: {
|
||||
url: options.url || 'https://forgejo.freshbrewed.science',
|
||||
repo: options.repo || '',
|
||||
userid: options.userid || '',
|
||||
token: options.token || '',
|
||||
activeField: options.url ? (options.repo ? 'token' : 'repo') : 'url',
|
||||
activeField: options.url ? (options.userid ? 'token' : 'userid') : 'url',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -62,6 +68,7 @@ async function bootstrap() {
|
|||
const config: Config = {
|
||||
url: normalized,
|
||||
token: options.token ? options.token.trim() : null,
|
||||
userid: options.userid || repoParts[0].trim(),
|
||||
owner: repoParts[0].trim(),
|
||||
repo: repoParts[1].trim(),
|
||||
};
|
||||
|
|
@ -79,7 +86,7 @@ async function bootstrap() {
|
|||
state.screen = 'setup';
|
||||
state.setupForm = {
|
||||
url: options.url,
|
||||
repo: options.repo,
|
||||
userid: options.userid || '',
|
||||
token: options.token || '',
|
||||
activeField: 'url',
|
||||
};
|
||||
|
|
|
|||
275
src/tui.ts
275
src/tui.ts
|
|
@ -1,7 +1,7 @@
|
|||
import readline from 'readline';
|
||||
import chalk from 'chalk';
|
||||
import { AppState, Issue, Comment } from './types.js';
|
||||
import { fetchIssues, fetchIssueComments, validateConnection, normalizeUrl } from './api.js';
|
||||
import { fetchIssues, fetchIssueComments, validateConnection, normalizeUrl, authenticateAndFetchRepos } from './api.js';
|
||||
|
||||
// Setup readline for stdin keypress events
|
||||
readline.emitKeypressEvents(process.stdin);
|
||||
|
|
@ -80,6 +80,14 @@ function truncate(str: string, length: number): string {
|
|||
return str.substring(0, length - 3) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to strip ANSI escape codes from a string for accurate length calculations.
|
||||
*/
|
||||
function stripAnsi(str: string): string {
|
||||
return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* TUI State Controller and Render Engine
|
||||
*/
|
||||
|
|
@ -227,6 +235,8 @@ export class TuiEngine {
|
|||
|
||||
if (this.state.screen === 'setup') {
|
||||
this.handleSetupKeypress(str, key);
|
||||
} else if (this.state.screen === 'repo-picker') {
|
||||
this.handleRepoPickerKeypress(str, key);
|
||||
} else if (this.state.screen === 'list') {
|
||||
if (this.activeSearchInput) {
|
||||
this.handleSearchKeypress(str, key);
|
||||
|
|
@ -243,7 +253,7 @@ export class TuiEngine {
|
|||
*/
|
||||
private async handleSetupKeypress(str: string, key: any) {
|
||||
const form = this.state.setupForm;
|
||||
const fields: Array<'url' | 'repo' | 'token'> = ['url', 'repo', 'token'];
|
||||
const fields: Array<'url' | 'userid' | 'token'> = ['url', 'userid', 'token'];
|
||||
const currentIdx = fields.indexOf(form.activeField);
|
||||
|
||||
if (key && key.name === 'down') {
|
||||
|
|
@ -271,15 +281,8 @@ export class TuiEngine {
|
|||
this.render();
|
||||
return;
|
||||
}
|
||||
if (!form.repo.trim()) {
|
||||
this.state.error = 'Repository path (owner/repo) is required.';
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
const repoParts = form.repo.split('/');
|
||||
if (repoParts.length !== 2 || !repoParts[0].trim() || !repoParts[1].trim()) {
|
||||
this.state.error = 'Repository must be in "owner/repo" format.';
|
||||
if (!form.userid.trim()) {
|
||||
this.state.error = 'User ID / Username is required.';
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
|
@ -288,22 +291,22 @@ export class TuiEngine {
|
|||
this.state.error = null;
|
||||
this.render();
|
||||
|
||||
const normalizedUrl = normalizeUrl(form.url);
|
||||
const testConfig = {
|
||||
url: normalizedUrl,
|
||||
token: form.token.trim() || null,
|
||||
owner: repoParts[0].trim(),
|
||||
repo: repoParts[1].trim(),
|
||||
};
|
||||
|
||||
try {
|
||||
await validateConnection(testConfig);
|
||||
// Save config
|
||||
this.state.config = testConfig;
|
||||
this.state.screen = 'list';
|
||||
this.state.currentPage = 1;
|
||||
this.state.selectedIssueIndex = 0;
|
||||
this.loadIssues();
|
||||
const repos = await authenticateAndFetchRepos(form.url, form.userid, form.token);
|
||||
|
||||
this.state.repos = repos;
|
||||
this.state.selectedRepoIndex = 0;
|
||||
this.state.repoSearchQuery = '';
|
||||
this.state.repoPickerActiveSearch = false;
|
||||
|
||||
if (repos.length === 0) {
|
||||
throw new Error('No repositories found for the provided credentials.');
|
||||
}
|
||||
|
||||
// Go to repo picker screen!
|
||||
this.state.screen = 'repo-picker';
|
||||
this.state.loading = false;
|
||||
this.render();
|
||||
} catch (err: any) {
|
||||
this.state.error = err.message;
|
||||
this.state.loading = false;
|
||||
|
|
@ -319,7 +322,7 @@ export class TuiEngine {
|
|||
|
||||
if (key && key.name === 'backspace') {
|
||||
if (form.activeField === 'url') form.url = form.url.slice(0, -1);
|
||||
if (form.activeField === 'repo') form.repo = form.repo.slice(0, -1);
|
||||
if (form.activeField === 'userid') form.userid = form.userid.slice(0, -1);
|
||||
if (form.activeField === 'token') form.token = form.token.slice(0, -1);
|
||||
this.render();
|
||||
return;
|
||||
|
|
@ -328,12 +331,107 @@ export class TuiEngine {
|
|||
// Type character
|
||||
if (str && !key.ctrl && !key.meta && str.length === 1 && str.charCodeAt(0) >= 32) {
|
||||
if (form.activeField === 'url') form.url += str;
|
||||
if (form.activeField === 'repo') form.repo += str;
|
||||
if (form.activeField === 'userid') form.userid += str;
|
||||
if (form.activeField === 'token') form.token += str;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Key handling for Repository Picker screen
|
||||
*/
|
||||
private handleRepoPickerKeypress(str: string, key: any) {
|
||||
if (this.state.loading) return;
|
||||
|
||||
const filteredRepos = this.state.repos.filter(r =>
|
||||
r.full_name.toLowerCase().includes(this.state.repoSearchQuery.toLowerCase()) ||
|
||||
(r.description && r.description.toLowerCase().includes(this.state.repoSearchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
if (this.state.repoPickerActiveSearch) {
|
||||
if ((key && key.name === 'escape') || str === '\u001b') {
|
||||
this.state.repoPickerActiveSearch = false;
|
||||
this.state.repoSearchQuery = '';
|
||||
this.state.selectedRepoIndex = 0;
|
||||
process.stdout.write('\x1B[?25l'); // Hide cursor
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((key && key.name === 'return') || str === '\r' || str === '\n') {
|
||||
this.state.repoPickerActiveSearch = false;
|
||||
process.stdout.write('\x1B[?25l'); // Hide cursor
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key && key.name === 'backspace') {
|
||||
this.state.repoSearchQuery = this.state.repoSearchQuery.slice(0, -1);
|
||||
this.state.selectedRepoIndex = 0;
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (str && !key.ctrl && !key.meta && str.length === 1 && str.charCodeAt(0) >= 32) {
|
||||
this.state.repoSearchQuery += str;
|
||||
this.state.selectedRepoIndex = 0;
|
||||
this.render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Search inactive keypresses
|
||||
if ((key && key.name === 'escape') || str === '\u001b') {
|
||||
this.state.screen = 'setup';
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key && key.name === 'up') {
|
||||
if (this.state.selectedRepoIndex > 0) {
|
||||
this.state.selectedRepoIndex--;
|
||||
this.render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (key && key.name === 'down') {
|
||||
if (this.state.selectedRepoIndex < filteredRepos.length - 1) {
|
||||
this.state.selectedRepoIndex++;
|
||||
this.render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (str === '/') {
|
||||
this.state.repoPickerActiveSearch = true;
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((key && key.name === 'return') || str === '\r' || str === '\n') {
|
||||
if (filteredRepos.length > 0 && this.state.selectedRepoIndex < filteredRepos.length) {
|
||||
const selectedRepo = filteredRepos[this.state.selectedRepoIndex];
|
||||
const parts = selectedRepo.full_name.split('/');
|
||||
|
||||
this.state.config = {
|
||||
url: normalizeUrl(this.state.setupForm.url),
|
||||
token: this.state.setupForm.token.trim() || null,
|
||||
userid: this.state.setupForm.userid.trim(),
|
||||
owner: parts[0],
|
||||
repo: parts[1],
|
||||
};
|
||||
|
||||
this.state.screen = 'list';
|
||||
this.state.currentPage = 1;
|
||||
this.state.selectedIssueIndex = 0;
|
||||
this.state.searchQuery = '';
|
||||
this.loadIssues();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Key handling for searching overlay
|
||||
*/
|
||||
|
|
@ -375,14 +473,14 @@ export class TuiEngine {
|
|||
if (this.state.loading) return;
|
||||
|
||||
if ((key && key.name === 'escape') || str === '\u001b') {
|
||||
// If we entered config via CLI, quit. If we came from setup form, go back to setup!
|
||||
if (this.state.setupForm.repo) {
|
||||
this.state.screen = 'setup';
|
||||
this.state.error = null;
|
||||
this.render();
|
||||
} else {
|
||||
this.stop();
|
||||
}
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (str === 'o' || str === 'O') {
|
||||
this.state.screen = 'setup';
|
||||
this.state.error = null;
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -524,6 +622,15 @@ export class TuiEngine {
|
|||
return;
|
||||
}
|
||||
|
||||
if (str === 'o' || str === 'O') {
|
||||
this.state.screen = 'setup';
|
||||
this.state.selectedIssue = null;
|
||||
this.state.selectedIssueComments = [];
|
||||
this.state.error = null;
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((key && (key.name === 'escape' || key.name === 'backspace')) || str === '\u001b' || str === 'q' || str === 'Q') {
|
||||
this.state.screen = 'list';
|
||||
this.state.selectedIssue = null;
|
||||
|
|
@ -544,6 +651,8 @@ export class TuiEngine {
|
|||
|
||||
if (this.state.screen === 'setup') {
|
||||
this.renderSetupScreen(cols, rows);
|
||||
} else if (this.state.screen === 'repo-picker') {
|
||||
this.renderRepoPickerScreen(cols, rows);
|
||||
} else if (this.state.screen === 'list') {
|
||||
this.renderListScreen(cols, rows);
|
||||
} else if (this.state.screen === 'details') {
|
||||
|
|
@ -591,8 +700,8 @@ export class TuiEngine {
|
|||
|
||||
lines.push(renderField('Gitea URL', form.url, form.activeField === 'url'));
|
||||
lines.push(border + ' '.repeat(width - 2) + border);
|
||||
lines.push(renderField('Repository', form.repo, form.activeField === 'repo'));
|
||||
lines.push(border + ' ' + chalk.gray('Format: owner/repo (e.g. gitea/tea)').padEnd(width - 5) + border);
|
||||
lines.push(renderField('User ID', form.userid, form.activeField === 'userid'));
|
||||
lines.push(border + ' ' + chalk.gray('Forgejo/Gitea username (e.g. gitea_admin)').padEnd(width - 5) + border);
|
||||
lines.push(border + ' '.repeat(width - 2) + border);
|
||||
lines.push(renderField('Access Token', form.token, form.activeField === 'token', true));
|
||||
lines.push(border + ' ' + chalk.gray('Optional. Required for private repos.').padEnd(width - 5) + border);
|
||||
|
|
@ -627,8 +736,8 @@ export class TuiEngine {
|
|||
if (!this.state.loading) {
|
||||
// Find current active field offset row
|
||||
let activeRowOffset = vertPadding + 6; // URL row
|
||||
if (form.activeField === 'repo') activeRowOffset += 2;
|
||||
if (form.activeField === 'token') activeRowOffset += 4;
|
||||
if (form.activeField === 'userid') activeRowOffset = vertPadding + 8;
|
||||
if (form.activeField === 'token') activeRowOffset = vertPadding + 11;
|
||||
|
||||
const activeValue = form.activeField === 'token' ? '*'.repeat(form.token.length) : form[form.activeField];
|
||||
const cursorCol = Math.max(0, Math.floor((cols - width) / 2)) + 23 + activeValue.length;
|
||||
|
|
@ -753,7 +862,7 @@ export class TuiEngine {
|
|||
console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘'));
|
||||
|
||||
// Keyboard controls help line
|
||||
const helpLine = chalk.gray(' [↑/↓] Navigate [Enter] View [/] Search [S] Sort [F] State [T] Type [N/P] Page [R] Reload [Esc] Back');
|
||||
const helpLine = chalk.gray(' [↑/↓] Navigate [Enter] View [/] Search [S] Sort [F] State [T] Type [N/P] Page [R] Reload [O] Settings [Esc] Quit');
|
||||
process.stdout.write(helpLine);
|
||||
|
||||
// If search active, position terminal cursor in search box
|
||||
|
|
@ -854,7 +963,89 @@ export class TuiEngine {
|
|||
scrollHelp += chalk.yellow(` (${pct}%)`);
|
||||
}
|
||||
|
||||
const helpLine = chalk.gray(` [Esc/Backspace] Back to List ${scrollHelp}`);
|
||||
const helpLine = chalk.gray(` [Esc/Backspace] Back to List [O] Settings ${scrollHelp}`);
|
||||
process.stdout.write(helpLine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw Repository Picker Screen
|
||||
*/
|
||||
private renderRepoPickerScreen(cols: number, rows: number) {
|
||||
const title = 'SELECT REPOSITORY';
|
||||
const subtitle = `Connected to ${normalizeUrl(this.state.setupForm.url).replace(/^https?:\/\//, '')} as ${this.state.setupForm.userid}`;
|
||||
|
||||
console.log(` ${chalk.bold.hex('#4A90E2')(title)} ─ ${chalk.gray(subtitle)}`);
|
||||
console.log(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(cols - 2) + '┐'));
|
||||
|
||||
const borderCh = chalk.bold.hex('#4A90E2')('│');
|
||||
|
||||
// Filter repositories based on query
|
||||
const filteredRepos = this.state.repos.filter(r =>
|
||||
r.full_name.toLowerCase().includes(this.state.repoSearchQuery.toLowerCase()) ||
|
||||
(r.description && r.description.toLowerCase().includes(this.state.repoSearchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
const listHeight = rows - 7; // Header, borders, search, footer
|
||||
|
||||
// Scroll window calculation
|
||||
let startIndex = 0;
|
||||
if (this.state.selectedRepoIndex >= listHeight) {
|
||||
startIndex = this.state.selectedRepoIndex - listHeight + 1;
|
||||
}
|
||||
|
||||
if (filteredRepos.length === 0) {
|
||||
const msg = this.state.repoSearchQuery ? 'No repositories match your filter.' : 'No repositories found.';
|
||||
const paddingRows = Math.floor((listHeight - 1) / 2);
|
||||
for (let i = 0; i < paddingRows; i++) console.log(borderCh + ' '.repeat(cols - 2) + borderCh);
|
||||
console.log(borderCh + (' '.repeat(Math.max(0, Math.floor((cols - 2 - msg.length) / 2))) + chalk.gray(msg)).padEnd(cols - 2) + borderCh);
|
||||
for (let i = 0; i < listHeight - paddingRows - 1; i++) console.log(borderCh + ' '.repeat(cols - 2) + borderCh);
|
||||
} else {
|
||||
for (let i = 0; i < listHeight; i++) {
|
||||
const repoIdx = i + startIndex;
|
||||
if (repoIdx < filteredRepos.length) {
|
||||
const repo = filteredRepos[repoIdx];
|
||||
const isSelected = repoIdx === this.state.selectedRepoIndex;
|
||||
|
||||
const typeStr = repo.private ? chalk.bold.yellow('[Private]') : chalk.bold.green('[Public] ');
|
||||
const nameStr = chalk.bold(repo.full_name);
|
||||
const descStr = repo.description ? chalk.gray(` ─ ${truncate(repo.description, cols - repo.full_name.length - 20)}`) : '';
|
||||
|
||||
let lineContent = ` ${typeStr} ${nameStr}${descStr}`;
|
||||
|
||||
// Pad the line content properly to match terminal width
|
||||
const visibleLen = stripAnsi(lineContent).length;
|
||||
const padding = ' '.repeat(Math.max(0, cols - 4 - visibleLen));
|
||||
lineContent = lineContent + padding;
|
||||
|
||||
if (isSelected) {
|
||||
lineContent = chalk.bgHex('#2E4E7E').white.bold(lineContent);
|
||||
}
|
||||
|
||||
console.log(borderCh + ' ' + lineContent + ' ' + borderCh);
|
||||
} else {
|
||||
console.log(borderCh + ' '.repeat(cols - 2) + borderCh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤'));
|
||||
|
||||
// Filter status line
|
||||
let filterLabel = this.state.repoSearchQuery ? chalk.yellow(`"${this.state.repoSearchQuery}"`) : chalk.gray('none');
|
||||
if (this.state.repoPickerActiveSearch) {
|
||||
filterLabel = chalk.inverse(this.state.repoSearchQuery + ' ');
|
||||
}
|
||||
const filterText = ` Filter: ${filterLabel}`;
|
||||
console.log(borderCh + filterText.padEnd(cols - 2) + borderCh);
|
||||
console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘'));
|
||||
|
||||
const helpLine = chalk.gray(' [↑/↓] Navigate [Enter] Select [/] Filter [Esc] Back to Setup');
|
||||
process.stdout.write(helpLine);
|
||||
|
||||
if (this.state.repoPickerActiveSearch) {
|
||||
const searchCursorCol = 10 + this.state.repoSearchQuery.length;
|
||||
process.stdout.write(`\x1B[${rows - 1};${searchCursorCol}H\x1B[?25h`); // Show cursor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
22
src/types.ts
22
src/types.ts
|
|
@ -1,6 +1,7 @@
|
|||
export interface Config {
|
||||
url: string;
|
||||
token: string | null;
|
||||
userid: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
}
|
||||
|
|
@ -41,12 +42,20 @@ export interface Comment {
|
|||
|
||||
export interface SetupForm {
|
||||
url: string;
|
||||
userid: string;
|
||||
token: string;
|
||||
repo: string;
|
||||
activeField: 'url' | 'token' | 'repo';
|
||||
activeField: 'url' | 'userid' | 'token';
|
||||
}
|
||||
|
||||
export type ScreenType = 'setup' | 'list' | 'details';
|
||||
export type ScreenType = 'setup' | 'repo-picker' | 'list' | 'details';
|
||||
|
||||
export interface RepoItem {
|
||||
id: number;
|
||||
name: string;
|
||||
full_name: string;
|
||||
private: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
screen: ScreenType;
|
||||
|
|
@ -72,6 +81,13 @@ export interface AppState {
|
|||
commentsLoading: boolean;
|
||||
detailScrollOffset: number;
|
||||
|
||||
// Repo Picker State
|
||||
repos: RepoItem[];
|
||||
selectedRepoIndex: number;
|
||||
repoSearchQuery: string;
|
||||
repoPickerActiveSearch: boolean;
|
||||
|
||||
// Setup Form State
|
||||
setupForm: SetupForm;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue