claudial 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -16
- package/dist/api/backend.js +7 -2
- package/dist/index.js +4 -3
- package/dist/setup/Wizard.js +5 -5
- package/dist/setup/apply.js +11 -1
- package/dist/setup/cursor.js +48 -0
- package/dist/setup/detect.js +5 -0
- package/dist/setup/flags.js +3 -1
- package/dist/setup/index.js +15 -2
- package/dist/snapshot.js +9 -7
- package/dist/statusline/cache.js +5 -3
- package/dist/statusline/line.js +5 -5
- package/dist/ui/MatchRow.js +17 -9
- package/dist/ui/TakeoverView.js +3 -3
- package/dist/ui/Ticker.js +4 -4
- package/dist/ui/UpcomingSection.js +7 -14
- package/dist/ui/flags.js +10 -0
- package/dist/ui/kickoff.js +24 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
# ⚽ claudial
|
|
4
4
|
|
|
5
|
-
### Live football inside Claude Code
|
|
5
|
+
### Live football inside Claude Code, Cursor and your editor
|
|
6
6
|
|
|
7
|
-
Your teams' scores sit
|
|
8
|
-
When one of them scores,
|
|
7
|
+
Your teams' scores sit in your status bar while you code: Claude Code, Cursor,
|
|
8
|
+
VS Code, Antigravity, Windsurf. When one of them scores, it celebrates.
|
|
9
9
|
|
|
10
10
|
```
|
|
11
11
|
npm install -g claudial && claudial setup
|
|
@@ -23,7 +23,7 @@ npm install -g claudial && claudial setup
|
|
|
23
23
|
|
|
24
24
|
<br/>
|
|
25
25
|
|
|
26
|
-

|
|
27
27
|
|
|
28
28
|
**Premier League · La Liga · Serie A · Bundesliga · Ligue 1 · Champions League ·
|
|
29
29
|
Europa League · Conference League · Super League Greece · World Cup**
|
|
@@ -32,6 +32,43 @@ Europa League · Conference League · Super League Greece · World Cup**
|
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
|
35
|
+
## Get started
|
|
36
|
+
|
|
37
|
+
| You are… | Run |
|
|
38
|
+
|---|---|
|
|
39
|
+
| **New to claudial** | `npm install -g claudial`<br>`claudial setup` |
|
|
40
|
+
| **Back from the World Cup** (you have 0.6.x) | `npm install -g claudial@latest`<br>`claudial --version` (should print 0.8.0 or newer)<br>`claudial follow` |
|
|
41
|
+
|
|
42
|
+
Then restart Claude Code. World Cup users keep their status line as it is; `claudial follow`
|
|
43
|
+
picks your leagues and teams, because the new version starts following nothing.
|
|
44
|
+
|
|
45
|
+
Still seeing "WORLD CUP 2026" after updating? An old copy comes first on your PATH.
|
|
46
|
+
`which -a claudial` lists every copy; remove the old one with
|
|
47
|
+
`npm uninstall -g --prefix <its folder> claudial` (e.g. `--prefix ~/.npm-global`).
|
|
48
|
+
|
|
49
|
+
## Works where you code
|
|
50
|
+
|
|
51
|
+
| Where you code | Run |
|
|
52
|
+
|---|---|
|
|
53
|
+
| **Claude Code** | `npm install -g claudial && claudial setup` |
|
|
54
|
+
| **Cursor CLI** (`cursor-agent`) | `npm install -g claudial && claudial setup --cursor --yes` |
|
|
55
|
+
| **VS Code** | `code --install-extension lefprog.claudial` |
|
|
56
|
+
| **Cursor** | `cursor --install-extension lefprog.claudial` |
|
|
57
|
+
| **Antigravity** | `antigravity --install-extension lefprog.claudial` |
|
|
58
|
+
| **Windsurf** | `windsurf --install-extension lefprog.claudial` |
|
|
59
|
+
| **Codex CLI** | not yet: Codex's status line can only show its own built-in items |
|
|
60
|
+
|
|
61
|
+
In the editors you can also search **claudial** in the Extensions panel. Reload the
|
|
62
|
+
window after installing; the score sits at the bottom right. Click it to pick your
|
|
63
|
+
teams. The editor extension and the CLI share your device, so you follow the same
|
|
64
|
+
teams everywhere.
|
|
65
|
+
|
|
66
|
+
<div align="center">
|
|
67
|
+
|
|
68
|
+

|
|
69
|
+
|
|
70
|
+
</div>
|
|
71
|
+
|
|
35
72
|
## The score, right under your prompt
|
|
36
73
|
|
|
37
74
|
One command puts a live score line into Claude Code's status bar. It stays out
|
|
@@ -39,15 +76,15 @@ of the way while Claude works, and lights up the moment anyone scores:
|
|
|
39
76
|
|
|
40
77
|
<div align="center">
|
|
41
78
|
|
|
42
|
-

|
|
43
80
|
|
|
44
81
|
</div>
|
|
45
82
|
|
|
46
83
|
```
|
|
47
|
-
⚽
|
|
48
|
-
⚽ G O O O L ·
|
|
49
|
-
🟥 R E D ·
|
|
50
|
-
○
|
|
84
|
+
⚽ Liverpool 6—0 Manchester United 87' · main ← every match you follow
|
|
85
|
+
⚽ G O O O L · Liverpool 7—0 Manchester United · main ← for 15 s after a goal
|
|
86
|
+
🟥 R E D · CASEMIRO · Manchester United — Southampton · main ← for 15 s after a red card
|
|
87
|
+
○ Liverpool — Arsenal Sat 05:30 PM · main ← between matches: next kickoff
|
|
51
88
|
```
|
|
52
89
|
|
|
53
90
|
Your git branch stays at the end. **Already run a custom status line?** Setup
|
|
@@ -62,11 +99,11 @@ esc to save:
|
|
|
62
99
|
```
|
|
63
100
|
claudial · what do you follow?
|
|
64
101
|
|
|
65
|
-
search ›
|
|
102
|
+
search › liver▏
|
|
66
103
|
|
|
67
|
-
› ◉
|
|
104
|
+
› ◉ Liverpool LIV · Premier League
|
|
68
105
|
|
|
69
|
-
Following
|
|
106
|
+
Following Liverpool
|
|
70
107
|
↑↓ move · space follow · type to search · esc clear search · ctrl+c cancel
|
|
71
108
|
```
|
|
72
109
|
|
|
@@ -100,10 +137,11 @@ claudial follow # pick leagues and teams
|
|
|
100
137
|
|
|
101
138
|
Requires Node ≥ 18. No account, no sign-up, no API key.
|
|
102
139
|
|
|
103
|
-
Scripting it? `claudial setup --statusline --yes`
|
|
104
|
-
|
|
105
|
-
`claudial
|
|
106
|
-
|
|
140
|
+
Scripting it? `claudial setup --statusline --yes` (Claude Code) and
|
|
141
|
+
`claudial setup --cursor --yes` (Cursor CLI) install without prompts, and
|
|
142
|
+
`claudial leagues`, `claudial teams <league>`, `claudial follow <id>...`,
|
|
143
|
+
`claudial unfollow <id>...` and `claudial following` work without the UI.
|
|
144
|
+
`claudial forget` deletes your device from the server.
|
|
107
145
|
|
|
108
146
|
## Status
|
|
109
147
|
|
|
@@ -159,4 +197,8 @@ npm test # vitest
|
|
|
159
197
|
[Request a feature](https://github.com/lefProg/claudial/issues) ·
|
|
160
198
|
[npm](https://www.npmjs.com/package/claudial)
|
|
161
199
|
|
|
200
|
+
<br/>
|
|
201
|
+
|
|
202
|
+
**YNWA** 🔴
|
|
203
|
+
|
|
162
204
|
</div>
|
package/dist/api/backend.js
CHANGED
|
@@ -3,8 +3,13 @@ import { deviceId } from './device.js';
|
|
|
3
3
|
// TLS; scripts/check-release.mjs blocks an npm publish until this is https://.
|
|
4
4
|
// CLAUDIAL_API_URL always wins (local dev: http://localhost:8080).
|
|
5
5
|
export const DEFAULT_API_URL = 'http://161.97.67.12:30083';
|
|
6
|
+
let serverOverride = null;
|
|
7
|
+
/** For embedders (the editor extension): a server chosen in settings. The env var still wins. */
|
|
8
|
+
export function setServerUrl(url) {
|
|
9
|
+
serverOverride = url && url.trim() ? url.trim() : null;
|
|
10
|
+
}
|
|
6
11
|
export function apiUrl(env = process.env) {
|
|
7
|
-
return (env.CLAUDIAL_API_URL || DEFAULT_API_URL).replace(/\/+$/, '');
|
|
12
|
+
return (env.CLAUDIAL_API_URL || serverOverride || DEFAULT_API_URL).replace(/\/+$/, '');
|
|
8
13
|
}
|
|
9
14
|
/** Thrown for any non-2xx answer, carrying the server's own error message. */
|
|
10
15
|
export class ApiError extends Error {
|
|
@@ -110,7 +115,7 @@ export async function fetchLiveRedCards() {
|
|
|
110
115
|
continue;
|
|
111
116
|
for (const inc of sm.incidents) {
|
|
112
117
|
if (inc.kind === 'redCard') {
|
|
113
|
-
out.push({ id: inc.id, homeCode: m.home.code, awayCode: m.away.code, player: inc.playerShort ?? inc.player ?? '?', minute: inc.minute, national: m.home.national !== false });
|
|
118
|
+
out.push({ id: inc.id, homeCode: m.home.code, awayCode: m.away.code, homeName: m.home.name, awayName: m.away.name, player: inc.playerShort ?? inc.player ?? '?', minute: inc.minute, national: m.home.national !== false });
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
121
|
}
|
package/dist/index.js
CHANGED
|
@@ -29,8 +29,9 @@ const HELP = `claudial: live football in your terminal and Claude Code's status
|
|
|
29
29
|
claudial the live dashboard (keys: f follow · r refresh · q quit)
|
|
30
30
|
claudial --ticker a 4-line strip for slim split panes
|
|
31
31
|
claudial | cat plain snapshot for pipes and scripts
|
|
32
|
-
claudial setup install the Claude Code
|
|
33
|
-
claudial setup --statusline --yes
|
|
32
|
+
claudial setup install the status line (Claude Code, Cursor CLI) and pick your teams
|
|
33
|
+
claudial setup --statusline --yes Claude Code status line only, no prompts
|
|
34
|
+
claudial setup --cursor --yes Cursor CLI status line only, no prompts
|
|
34
35
|
${USAGE.replace(/^usage:\n/, '')}
|
|
35
36
|
|
|
36
37
|
CLAUDIAL_API_URL=<url> use another claudial server`;
|
|
@@ -79,7 +80,7 @@ async function main() {
|
|
|
79
80
|
}
|
|
80
81
|
if (args.includes('--mock-redcard')) {
|
|
81
82
|
const { makeCache } = await import('./statusline/cache.js');
|
|
82
|
-
makeCache().armRedCard({ player: 'Otamendi', homeCode: 'ARG', awayCode: 'ENG' });
|
|
83
|
+
makeCache().armRedCard({ player: 'Otamendi', homeCode: 'ARG', awayCode: 'ENG', homeName: 'Argentina', awayName: 'England' });
|
|
83
84
|
process.stdout.write('🟥 RED armed — the Claude Code status bar flashes a red card (ARG—ENG) for 15s.\n');
|
|
84
85
|
process.exit(0);
|
|
85
86
|
}
|
package/dist/setup/Wizard.js
CHANGED
|
@@ -2,10 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text, useInput, useApp } from 'ink';
|
|
3
3
|
import { ACCENT } from '../ui/Header.js';
|
|
4
4
|
/**
|
|
5
|
-
* A single confirmation: install the live-score statusline
|
|
6
|
-
*
|
|
5
|
+
* A single confirmation: install the live-score statusline into Claude Code,
|
|
6
|
+
* and into the Cursor CLI too when Cursor is on this machine.
|
|
7
7
|
*/
|
|
8
|
-
export function Wizard({ defaultShell, onDone }) {
|
|
8
|
+
export function Wizard({ defaultShell, cursorFound, onDone }) {
|
|
9
9
|
const { exit } = useApp();
|
|
10
10
|
useInput((input, key) => {
|
|
11
11
|
if (input === 'q' || input.toLowerCase() === 'n') {
|
|
@@ -13,9 +13,9 @@ export function Wizard({ defaultShell, onDone }) {
|
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
if (key.return || input.toLowerCase() === 'y') {
|
|
16
|
-
onDone({ statusline: true, scope: 'global', tmux: false, shell: defaultShell, yes: true, interactive: false });
|
|
16
|
+
onDone({ statusline: true, cursor: cursorFound, scope: 'global', tmux: false, shell: defaultShell, yes: true, interactive: false });
|
|
17
17
|
exit();
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: _jsx(Text, { bold: true, color: ACCENT, children: "claudial setup" }) }), _jsx(Text, { children: "Install the live-score statusline
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: _jsx(Text, { bold: true, color: ACCENT, children: "claudial setup" }) }), _jsx(Text, { children: "Install the live-score statusline? (Y/n)" }), _jsx(Text, { dimColor: true, children: " \u2022 Claude Code \u2192 ~/.claude/settings.json" }), cursorFound ? _jsx(Text, { dimColor: true, children: " \u2022 Cursor CLI \u2192 ~/.cursor/cli-config.json" }) : null, _jsx(Text, { dimColor: true, children: "q to quit" })] }));
|
|
21
21
|
}
|
package/dist/setup/apply.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { installStatusline } from './settings.js';
|
|
2
|
+
import { installCursorStatusline } from './cursor.js';
|
|
2
3
|
import { appendAlias } from './shellrc.js';
|
|
3
4
|
export function applySetup(opts, ctx) {
|
|
4
|
-
const report = {
|
|
5
|
+
const report = {
|
|
6
|
+
statuslineWritten: false, statuslineBackedUp: false, statuslineWrapped: false,
|
|
7
|
+
cursorWritten: false, cursorBackedUp: false, cursorWrapped: false, aliasAppended: false,
|
|
8
|
+
};
|
|
5
9
|
if (opts.statusline) {
|
|
6
10
|
const r = installStatusline(ctx.settingsPath, ctx.command, ctx.conflict);
|
|
7
11
|
report.statuslineWritten = r.written;
|
|
8
12
|
report.statuslineBackedUp = r.backedUp;
|
|
9
13
|
report.statuslineWrapped = r.wrapped;
|
|
10
14
|
}
|
|
15
|
+
if (opts.cursor) {
|
|
16
|
+
const r = installCursorStatusline(ctx.cursorPath, ctx.command);
|
|
17
|
+
report.cursorWritten = r.written;
|
|
18
|
+
report.cursorBackedUp = r.backedUp;
|
|
19
|
+
report.cursorWrapped = r.wrapped;
|
|
20
|
+
}
|
|
11
21
|
if (opts.tmux) {
|
|
12
22
|
report.aliasAppended = appendAlias(ctx.rcPath, ctx.shell).appended;
|
|
13
23
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
// The Cursor CLI (cursor-agent) status line: the same `claudial --statusline`
|
|
4
|
+
// command as Claude Code, in ~/.cursor/cli-config.json with Cursor's own keys.
|
|
5
|
+
export function cursorConfigPath(home) {
|
|
6
|
+
return join(home, '.cursor', 'cli-config.json');
|
|
7
|
+
}
|
|
8
|
+
function block(command) {
|
|
9
|
+
return { type: 'command', command, padding: 0, updateIntervalMs: 1000, timeoutMs: 3000 };
|
|
10
|
+
}
|
|
11
|
+
/** Read → (wrap a foreign command) → merge → (backup) → write. Same rules as the Claude Code writer. */
|
|
12
|
+
export function installCursorStatusline(path, command) {
|
|
13
|
+
const fileExists = existsSync(path);
|
|
14
|
+
let existing = {};
|
|
15
|
+
if (fileExists) {
|
|
16
|
+
try {
|
|
17
|
+
existing = JSON.parse(readFileSync(path, 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
existing = {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const theirs = typeof existing?.statusLine?.command === 'string' ? existing.statusLine.command : '';
|
|
24
|
+
let final = command;
|
|
25
|
+
let wrapped = false;
|
|
26
|
+
if (theirs.includes('claudial --statusline')) {
|
|
27
|
+
const w = theirs.indexOf(' --wrap ');
|
|
28
|
+
if (w >= 0) {
|
|
29
|
+
final = command + theirs.slice(w);
|
|
30
|
+
wrapped = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (theirs) {
|
|
34
|
+
final = `${command} --wrap '${theirs.replace(/'/g, `'\\''`)}'`;
|
|
35
|
+
wrapped = true;
|
|
36
|
+
}
|
|
37
|
+
const next = block(final);
|
|
38
|
+
if (JSON.stringify(existing.statusLine) === JSON.stringify(next))
|
|
39
|
+
return { written: false, backedUp: false, wrapped };
|
|
40
|
+
let backedUp = false;
|
|
41
|
+
if (fileExists) {
|
|
42
|
+
copyFileSync(path, `${path}.bak`);
|
|
43
|
+
backedUp = true;
|
|
44
|
+
}
|
|
45
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
46
|
+
writeFileSync(path, JSON.stringify({ ...existing, statusLine: next }, null, 2) + '\n');
|
|
47
|
+
return { written: true, backedUp, wrapped };
|
|
48
|
+
}
|
package/dist/setup/detect.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
export function detectShell(env = process.env) {
|
|
4
5
|
const s = env.SHELL ?? '';
|
|
@@ -29,6 +30,10 @@ export function isOnPath(bin) {
|
|
|
29
30
|
return false;
|
|
30
31
|
}
|
|
31
32
|
}
|
|
33
|
+
/** Cursor (editor or CLI) has been used on this machine. */
|
|
34
|
+
export function hasCursor(home) {
|
|
35
|
+
return existsSync(join(home, '.cursor'));
|
|
36
|
+
}
|
|
32
37
|
export function hasTmux() {
|
|
33
38
|
return isOnPath('tmux');
|
|
34
39
|
}
|
package/dist/setup/flags.js
CHANGED
|
@@ -5,10 +5,12 @@ export function parseFlags(args) {
|
|
|
5
5
|
const shell = SHELLS.includes(shellArg) ? shellArg : null;
|
|
6
6
|
const statusline = has('--statusline');
|
|
7
7
|
const tmux = has('--tmux');
|
|
8
|
+
const cursor = has('--cursor');
|
|
8
9
|
// any actionable flag (or --yes) means run without prompting
|
|
9
|
-
const interactive = !(statusline || tmux || has('--yes'));
|
|
10
|
+
const interactive = !(statusline || tmux || cursor || has('--yes'));
|
|
10
11
|
return {
|
|
11
12
|
statusline,
|
|
13
|
+
cursor,
|
|
12
14
|
scope: has('--project') ? 'project' : 'global',
|
|
13
15
|
tmux,
|
|
14
16
|
shell,
|
package/dist/setup/index.js
CHANGED
|
@@ -3,7 +3,8 @@ import { join } from 'node:path';
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { render } from 'ink';
|
|
5
5
|
import { parseFlags } from './flags.js';
|
|
6
|
-
import { detectShell, rcPathFor, isOnPath } from './detect.js';
|
|
6
|
+
import { detectShell, rcPathFor, isOnPath, hasCursor } from './detect.js';
|
|
7
|
+
import { cursorConfigPath } from './cursor.js';
|
|
7
8
|
import { applySetup } from './apply.js';
|
|
8
9
|
import { Wizard } from './Wizard.js';
|
|
9
10
|
function commandFor() {
|
|
@@ -15,6 +16,7 @@ function contextFor(opts) {
|
|
|
15
16
|
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
16
17
|
return {
|
|
17
18
|
settingsPath,
|
|
19
|
+
cursorPath: cursorConfigPath(homedir()),
|
|
18
20
|
rcPath: rcPathFor(shell, homedir()),
|
|
19
21
|
shell,
|
|
20
22
|
command: commandFor(),
|
|
@@ -33,6 +35,16 @@ function printReport(opts, ctx, report) {
|
|
|
33
35
|
else if (opts.statusline) {
|
|
34
36
|
console.log('• statusline unchanged (an existing statusLine was kept).');
|
|
35
37
|
}
|
|
38
|
+
if (report.cursorWritten) {
|
|
39
|
+
console.log(`✓ Cursor CLI statusline installed → ${ctx.cursorPath}`);
|
|
40
|
+
if (report.cursorWrapped)
|
|
41
|
+
console.log(' (kept your existing status line and appended the score)');
|
|
42
|
+
if (report.cursorBackedUp)
|
|
43
|
+
console.log(` (previous config backed up to ${ctx.cursorPath}.bak)`);
|
|
44
|
+
}
|
|
45
|
+
else if (opts.cursor) {
|
|
46
|
+
console.log('• Cursor CLI statusline already installed.');
|
|
47
|
+
}
|
|
36
48
|
if (report.aliasAppended) {
|
|
37
49
|
console.log(`✓ tmux alias added → ${ctx.rcPath}`);
|
|
38
50
|
console.log(` Open a new shell, then run: claude-mundial`);
|
|
@@ -40,7 +52,7 @@ function printReport(opts, ctx, report) {
|
|
|
40
52
|
else if (opts.tmux) {
|
|
41
53
|
console.log('• tmux alias already present — left as-is.');
|
|
42
54
|
}
|
|
43
|
-
if (!opts.statusline && !opts.tmux)
|
|
55
|
+
if (!opts.statusline && !opts.cursor && !opts.tmux)
|
|
44
56
|
console.log('Nothing selected. Run `claudial setup` again to choose options.');
|
|
45
57
|
}
|
|
46
58
|
export async function runSetup(args) {
|
|
@@ -56,6 +68,7 @@ export async function runSetup(args) {
|
|
|
56
68
|
await new Promise((resolve) => {
|
|
57
69
|
const { waitUntilExit } = render(React.createElement(Wizard, {
|
|
58
70
|
defaultShell: detectShell(),
|
|
71
|
+
cursorFound: hasCursor(homedir()),
|
|
59
72
|
onDone: (final) => run(final),
|
|
60
73
|
}));
|
|
61
74
|
waitUntilExit().then(() => resolve());
|
package/dist/snapshot.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { fetchLive, fetchRecent, fetchUpcoming, followsNothing } from './api/backend.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
function line(m) {
|
|
2
|
+
import { kickoffColumn, kickoffWidth } from './ui/kickoff.js';
|
|
3
|
+
import { homeName, awayName } from './ui/flags.js';
|
|
4
|
+
function line(m, width = 22) {
|
|
5
5
|
if (m.status === 'upcoming')
|
|
6
|
-
return `o ${
|
|
6
|
+
return `o ${kickoffColumn(m.startTimestamp, Date.now(), width)}${homeName(m.home)} - ${awayName(m.away)}`;
|
|
7
7
|
const label = m.status === 'finished' ? 'FT' : m.status === 'halftime' ? 'HT' : `${m.minute ?? '?'}'`;
|
|
8
|
-
return `* ${label.padEnd(4)} ${
|
|
8
|
+
return `* ${label.padEnd(4)} ${homeName(m.home)} ${m.homeScore ?? '-'} - ${m.awayScore ?? '-'} ${awayName(m.away)}`;
|
|
9
9
|
}
|
|
10
10
|
export async function printSnapshot(seasonId) {
|
|
11
11
|
const [live, upcoming, recent] = await Promise.all([
|
|
@@ -21,6 +21,8 @@ export async function printSnapshot(seasonId) {
|
|
|
21
21
|
console.log(line(m));
|
|
22
22
|
if (upcoming.length)
|
|
23
23
|
console.log('UPCOMING');
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const next = upcoming.slice(0, 8);
|
|
25
|
+
const width = kickoffWidth(next.map((m) => m.startTimestamp));
|
|
26
|
+
for (const m of next)
|
|
27
|
+
console.log(line(m, width));
|
|
26
28
|
}
|
package/dist/statusline/cache.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, openSync, closeSync, writeFileSync, readFileSync, renameSync, statSync, existsSync, unlinkSync, utimesSync, futimesSync, } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { homeName, awayName } from '../ui/flags.js';
|
|
5
5
|
export const TTL_MS = 10_000;
|
|
6
6
|
export const GOAL_WINDOW_MS = 15_000;
|
|
7
7
|
const LOCK_STALE_MS = 30_000;
|
|
@@ -11,10 +11,12 @@ const GOAL_CLOSE = '\x1b[0m';
|
|
|
11
11
|
// RED #E5484D — the red-card flash.
|
|
12
12
|
const RED_OPEN = '\x1b[1;38;2;229;72;77m';
|
|
13
13
|
function goalText(m) {
|
|
14
|
-
return `${GOAL_OPEN}⚽ G O O O L · ${
|
|
14
|
+
return `${GOAL_OPEN}⚽ G O O O L · ${homeName(m.home)} ${m.homeScore ?? 0}—${m.awayScore ?? 0} ${awayName(m.away)}${GOAL_CLOSE}`;
|
|
15
15
|
}
|
|
16
16
|
function redCardText(rc) {
|
|
17
|
-
|
|
17
|
+
const home = homeName({ name: rc.homeName ?? rc.homeCode, code: rc.homeCode, national: rc.national });
|
|
18
|
+
const away = awayName({ name: rc.awayName ?? rc.awayCode, code: rc.awayCode, national: rc.national });
|
|
19
|
+
return `${RED_OPEN}🟥 R E D · ${rc.player.toUpperCase()} · ${home} — ${away}${GOAL_CLOSE}`;
|
|
18
20
|
}
|
|
19
21
|
export function makeCache(dir = join(tmpdir(), 'claudial-statusline')) {
|
|
20
22
|
mkdirSync(dir, { recursive: true });
|
package/dist/statusline/line.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { formatKickoff } from '../ui/
|
|
2
|
-
import {
|
|
1
|
+
import { formatKickoff } from '../ui/kickoff.js';
|
|
2
|
+
import { homeName, awayName } from '../ui/flags.js';
|
|
3
3
|
import { matchPrediction } from '../predictions/match.js';
|
|
4
4
|
import { headline } from '../predictions/format.js';
|
|
5
5
|
// Assumed wall-clock length of a match (kickoff → ~full time). ESPN exposes no
|
|
@@ -10,18 +10,18 @@ export function liveLine(match) {
|
|
|
10
10
|
const clock = match.status === 'halftime' ? ' HT'
|
|
11
11
|
: match.minute != null ? ` ${match.minute}'`
|
|
12
12
|
: '';
|
|
13
|
-
return `⚽ ${
|
|
13
|
+
return `⚽ ${homeName(match.home)} ${match.homeScore ?? 0}—${match.awayScore ?? 0} ${awayName(match.away)}${clock}`;
|
|
14
14
|
}
|
|
15
15
|
/** A finished match with its result: "⚽ QAT 🇶🇦 2—1 🇨🇭 SUI FT". */
|
|
16
16
|
export function finishedLine(match) {
|
|
17
|
-
return `⚽ ${
|
|
17
|
+
return `⚽ ${homeName(match.home)} ${match.homeScore ?? 0}—${match.awayScore ?? 0} ${awayName(match.away)} FT`;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* The next kickoff: "○ BRA 🇧🇷 — 🇲🇦 MAR Sun 01:00", with a prediction headline
|
|
21
21
|
* appended when one is available: "… · 🔮 BRA win 64% · 2-1".
|
|
22
22
|
*/
|
|
23
23
|
function upcomingLine(match, now, preds) {
|
|
24
|
-
const base = `○ ${
|
|
24
|
+
const base = `○ ${homeName(match.home)} — ${awayName(match.away)} ${formatKickoff(match.startTimestamp, now)}`;
|
|
25
25
|
const pred = matchPrediction(match, preds);
|
|
26
26
|
return pred ? `${base} · ${headline(pred)}` : base;
|
|
27
27
|
}
|
package/dist/ui/MatchRow.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { ACCENT } from './Header.js';
|
|
4
|
-
import { formatKickoff } from './
|
|
5
|
-
import {
|
|
4
|
+
import { formatKickoff, kickoffColumn } from './kickoff.js';
|
|
5
|
+
import { homeName, awayName } from './flags.js';
|
|
6
|
+
/** One side's goals grouped by scorer, in order of their first goal: "43' 50' C. Gakpo · 47' D. Núñez". */
|
|
7
|
+
export function scorers(goals) {
|
|
8
|
+
const byPlayer = new Map();
|
|
9
|
+
for (const g of goals) {
|
|
10
|
+
const who = g.playerShort ?? '?';
|
|
11
|
+
byPlayer.set(who, [...(byPlayer.get(who) ?? []), `${g.minute ?? '?'}'`]);
|
|
12
|
+
}
|
|
13
|
+
return [...byPlayer].map(([who, mins]) => `${mins.join(' ')} ${who}`).join(' · ');
|
|
14
|
+
}
|
|
6
15
|
export function ScorerLine({ incidents }) {
|
|
7
16
|
const goals = incidents.filter((i) => i.kind === 'goal');
|
|
8
17
|
if (goals.length === 0)
|
|
9
18
|
return null;
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const away = goals.filter((g) => !g.isHome).map(fmt).join(' · ');
|
|
19
|
+
const home = scorers(goals.filter((g) => g.isHome));
|
|
20
|
+
const away = scorers(goals.filter((g) => !g.isHome));
|
|
13
21
|
return _jsxs(Text, { dimColor: true, children: [' ', "\u26BD ", [home, away].filter(Boolean).join(' | ')] });
|
|
14
22
|
}
|
|
15
23
|
export function FeedLine({ incidents }) {
|
|
@@ -32,12 +40,12 @@ function statusLabel(m) {
|
|
|
32
40
|
export function MatchRow({ m, incidents, compact }) {
|
|
33
41
|
if (m.status === 'upcoming') {
|
|
34
42
|
if (compact) {
|
|
35
|
-
return _jsxs(Text, { dimColor: true, children: ["\u25CB ",
|
|
43
|
+
return _jsxs(Text, { dimColor: true, children: ["\u25CB ", homeName(m.home), " \u2014 ", awayName(m.away), " ", formatKickoff(m.startTimestamp)] });
|
|
36
44
|
}
|
|
37
|
-
return (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["\u25CB ",
|
|
45
|
+
return (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["\u25CB ", kickoffColumn(m.startTimestamp)] }), homeName(m.home), " \u2014 ", awayName(m.away), m.group ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", m.group] }) : null] }));
|
|
38
46
|
}
|
|
39
47
|
if (compact) {
|
|
40
|
-
return (_jsxs(Text, { children: [_jsx(Text, { color: m.status === 'finished' ? undefined : ACCENT, children: "\u23FA" }), _jsxs(Text, { dimColor: true, children: [" ", statusLabel(m), " "] }),
|
|
48
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: m.status === 'finished' ? undefined : ACCENT, children: "\u23FA" }), _jsxs(Text, { dimColor: true, children: [" ", statusLabel(m), " "] }), homeName(m.home), " ", _jsxs(Text, { bold: true, color: ACCENT, children: [m.homeScore ?? '–', "\u2014", m.awayScore ?? '–'] }), " ", awayName(m.away), m.varInProgress ? _jsx(Text, { dimColor: true, children: " \u2696" }) : null] }));
|
|
41
49
|
}
|
|
42
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: m.status === 'finished' ? undefined : ACCENT, children: "\u23FA" }), _jsxs(Text, { bold: m.status !== 'finished', dimColor: m.status === 'finished', children: [" ", statusLabel(m)] }), m.group ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", m.group] }) : null, m.varInProgress ? _jsx(Text, { dimColor: true, children: " \u00B7 \u2696 VAR" }) : null] }), _jsxs(Text, { children: [' ',
|
|
50
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: m.status === 'finished' ? undefined : ACCENT, children: "\u23FA" }), _jsxs(Text, { bold: m.status !== 'finished', dimColor: m.status === 'finished', children: [" ", statusLabel(m)] }), m.group ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", m.group] }) : null, m.varInProgress ? _jsx(Text, { dimColor: true, children: " \u00B7 \u2696 VAR" }) : null] }), _jsxs(Text, { children: [' ', homeName(m.home), " ", _jsxs(Text, { bold: true, color: ACCENT, children: [m.homeScore ?? '–', " \u2014 ", m.awayScore ?? '–'] }), " ", awayName(m.away)] }), _jsx(ScorerLine, { incidents: incidents }), _jsx(FeedLine, { incidents: incidents })] }));
|
|
43
51
|
}
|
package/dist/ui/TakeoverView.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { GOAL_ART, VAR_ART, RED_CARD_ART, spacedCaps } from '../banner.js';
|
|
4
4
|
import { ACCENT, RED } from './Header.js';
|
|
5
|
-
import {
|
|
5
|
+
import { homeName, awayName } from './flags.js';
|
|
6
6
|
const ART = {
|
|
7
7
|
goal: { art: GOAL_ART, color: ACCENT, caption: null },
|
|
8
8
|
redcard: { art: RED_CARD_ART, color: RED, caption: 'R E D C A R D' },
|
|
@@ -17,7 +17,7 @@ export function TakeoverView({ takeover }) {
|
|
|
17
17
|
const rows = process.stdout.rows ?? 24;
|
|
18
18
|
if (rows < 14) {
|
|
19
19
|
const label = kind === 'goal' ? 'G O A L' : kind === 'redcard' ? 'R E D C A R D' : 'V A R';
|
|
20
|
-
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", height: rows - 1, children: [_jsx(Text, { bold: true, color: color, wrap: "truncate", children: label }), _jsxs(Text, { bold: true, wrap: "truncate", children: [spacedCaps(fallback), takeover.minute != null ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", takeover.minute, "'"] }) : null] }), takeover.detail ? _jsx(Text, { dimColor: true, wrap: "truncate", children: takeover.detail }) : null, _jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, {
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", height: rows - 1, children: [_jsx(Text, { bold: true, color: color, wrap: "truncate", children: label }), _jsxs(Text, { bold: true, wrap: "truncate", children: [spacedCaps(fallback), takeover.minute != null ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", takeover.minute, "'"] }) : null] }), takeover.detail ? _jsx(Text, { dimColor: true, wrap: "truncate", children: takeover.detail }) : null, _jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: "gray", children: [homeName(match.home), " "] }), _jsxs(Text, { bold: true, color: ACCENT, children: [takeover.homeScore, " \u2014 ", takeover.awayScore] }), _jsxs(Text, { color: "gray", children: [" ", awayName(match.away)] })] })] }));
|
|
21
21
|
}
|
|
22
|
-
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", height: rows - 1, children: [art.map((line, i) => (_jsx(Text, { bold: true, color: color, wrap: "truncate", children: line }, i))), caption ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, color: color, wrap: "truncate", children: caption }) })) : null, _jsx(Box, { marginTop: 1, children: _jsxs(Text, { bold: true, children: [spacedCaps(fallback), takeover.minute != null ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", takeover.minute, "'"] }) : null] }) }), takeover.detail ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: takeover.detail }) })) : null, _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, {
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", height: rows - 1, children: [art.map((line, i) => (_jsx(Text, { bold: true, color: color, wrap: "truncate", children: line }, i))), caption ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, color: color, wrap: "truncate", children: caption }) })) : null, _jsx(Box, { marginTop: 1, children: _jsxs(Text, { bold: true, children: [spacedCaps(fallback), takeover.minute != null ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", takeover.minute, "'"] }) : null] }) }), takeover.detail ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: takeover.detail }) })) : null, _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "gray", children: [homeName(match.home), " "] }), _jsxs(Text, { bold: true, color: ACCENT, children: [takeover.homeScore, " \u2014 ", takeover.awayScore] }), _jsxs(Text, { color: "gray", children: [" ", awayName(match.away)] })] })] }));
|
|
23
23
|
}
|
package/dist/ui/Ticker.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import { formatKickoff } from './
|
|
3
|
+
import { formatKickoff } from './kickoff.js';
|
|
4
4
|
import { ACCENT, RED } from './Header.js';
|
|
5
5
|
import { spacedCaps } from '../banner.js';
|
|
6
|
-
import {
|
|
6
|
+
import { homeName, awayName } from './flags.js';
|
|
7
7
|
function TickerTakeover({ t }) {
|
|
8
8
|
const color = t.kind === 'redcard' ? RED : ACCENT;
|
|
9
9
|
const label = t.kind === 'goal' ? 'G O A L' : t.kind === 'redcard' ? 'R E D' : 'V A R';
|
|
10
|
-
return (_jsxs(Text, { bold: true, color: color, wrap: "truncate", children: [label, " \u00B7 ", t.who ? spacedCaps(t.who) : t.detail ?? '', " \u00B7 ",
|
|
10
|
+
return (_jsxs(Text, { bold: true, color: color, wrap: "truncate", children: [label, " \u00B7 ", t.who ? spacedCaps(t.who) : t.detail ?? '', " \u00B7 ", homeName(t.match.home), " ", t.homeScore, "\u2014", t.awayScore, " ", awayName(t.match.away)] }));
|
|
11
11
|
}
|
|
12
12
|
export function Ticker({ live, upcoming, takeover }) {
|
|
13
13
|
if (takeover)
|
|
14
14
|
return _jsx(TickerTakeover, { t: takeover });
|
|
15
|
-
return (_jsxs(Box, { flexDirection: "column", children: [live.slice(0, 3).map((m) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: ACCENT, children: "\u23FA" }), _jsxs(Text, { dimColor: true, children: [" ", m.minute != null ? `${m.minute}'` : m.status === 'halftime' ? 'HT' : m.status === 'finished' ? 'FT' : '', " "] }),
|
|
15
|
+
return (_jsxs(Box, { flexDirection: "column", children: [live.slice(0, 3).map((m) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: ACCENT, children: "\u23FA" }), _jsxs(Text, { dimColor: true, children: [" ", m.minute != null ? `${m.minute}'` : m.status === 'halftime' ? 'HT' : m.status === 'finished' ? 'FT' : '', " "] }), homeName(m.home), " ", _jsxs(Text, { bold: true, color: ACCENT, children: [m.homeScore ?? '–', "\u2014", m.awayScore ?? '–'] }), " ", awayName(m.away), m.varInProgress ? _jsx(Text, { dimColor: true, children: " \u2696" }) : null] }, m.id))), upcoming[0] ? (_jsxs(Text, { dimColor: true, wrap: "truncate", children: ["\u25CB ", homeName(upcoming[0].home), " \u2014 ", awayName(upcoming[0].away), " ", formatKickoff(upcoming[0].startTimestamp)] })) : null] }));
|
|
16
16
|
}
|
|
@@ -1,32 +1,25 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import {
|
|
3
|
+
import { homeName, awayName } from './flags.js';
|
|
4
4
|
import { matchPrediction } from '../predictions/match.js';
|
|
5
5
|
import { fullSlate, headline } from '../predictions/format.js';
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
const time = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
|
9
|
-
const sameDay = d.toDateString() === new Date(now).toDateString();
|
|
10
|
-
if (sameDay)
|
|
11
|
-
return time;
|
|
12
|
-
// Within the week a weekday is enough; further out (fixtures reach ~3 weeks) add the date.
|
|
13
|
-
const soon = ts * 1000 - now < 6 * 86_400_000;
|
|
14
|
-
const day = d.toLocaleDateString(undefined, soon ? { weekday: 'short' } : { weekday: 'short', day: 'numeric', month: 'short' });
|
|
15
|
-
return `${day} ${time}`;
|
|
16
|
-
}
|
|
6
|
+
export { formatKickoff, kickoffColumn } from './kickoff.js';
|
|
7
|
+
import { formatKickoff, kickoffColumn, kickoffWidth } from './kickoff.js';
|
|
17
8
|
export function UpcomingSection({ matches, compact, predictions = [], }) {
|
|
18
9
|
const shown = matches.slice(0, compact ? 1 : 8);
|
|
19
10
|
if (shown.length === 0)
|
|
20
11
|
return null;
|
|
12
|
+
// One column for the whole list: as wide as its longest kickoff, plus a gap.
|
|
13
|
+
const width = kickoffWidth(shown.map((m) => m.startTimestamp));
|
|
21
14
|
if (compact) {
|
|
22
15
|
const m = shown[0];
|
|
23
16
|
const pred = matchPrediction(m, predictions);
|
|
24
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["\u25CB ",
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["\u25CB ", homeName(m.home), " \u2014 ", awayName(m.away), " ", formatKickoff(m.startTimestamp)] }), pred ? _jsx(Text, { color: "yellow", children: headline(pred) }) : null] }));
|
|
25
18
|
}
|
|
26
19
|
// Every fixture shows its pick; the NEXT one (shown[0]) gets the full slate.
|
|
27
20
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "UPCOMING" }), shown.map((m, i) => {
|
|
28
21
|
const pred = matchPrediction(m, predictions);
|
|
29
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["\u25CB ",
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["\u25CB ", kickoffColumn(m.startTimestamp, Date.now(), width)] }), homeName(m.home), " \u2014 ", awayName(m.away), m.group ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", m.group] }) : null] }), pred && i === 0
|
|
30
23
|
? fullSlate(pred).map((ln, j) => _jsx(Text, { color: "yellow", children: ln }, j))
|
|
31
24
|
: pred
|
|
32
25
|
? _jsxs(Text, { color: "yellow", children: [" ", headline(pred)] })
|
package/dist/ui/flags.js
CHANGED
|
@@ -45,3 +45,13 @@ export function awayTag(code, national = true) {
|
|
|
45
45
|
const f = national ? flagFor(code) : '';
|
|
46
46
|
return f ? `${f} ${code}` : code;
|
|
47
47
|
}
|
|
48
|
+
/** Home side by full name, for the roomy views: "Qatar 🇶🇦", or "Arsenal" for a club. */
|
|
49
|
+
export function homeName(t) {
|
|
50
|
+
const f = t.national !== false ? flagFor(t.code) : '';
|
|
51
|
+
return f ? `${t.name} ${f}` : t.name;
|
|
52
|
+
}
|
|
53
|
+
/** Away side by full name: "🇨🇭 Switzerland", or "Liverpool" for a club. */
|
|
54
|
+
export function awayName(t) {
|
|
55
|
+
const f = t.national !== false ? flagFor(t.code) : '';
|
|
56
|
+
return f ? `${f} ${t.name}` : t.name;
|
|
57
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Kickoff times as text. Plain functions with no UI imports, so the status line
|
|
2
|
+
// and the editor extension can use them without pulling in Ink.
|
|
3
|
+
export function formatKickoff(ts, now = Date.now()) {
|
|
4
|
+
const d = new Date(ts * 1000);
|
|
5
|
+
const time = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
|
6
|
+
const sameDay = d.toDateString() === new Date(now).toDateString();
|
|
7
|
+
if (sameDay)
|
|
8
|
+
return time;
|
|
9
|
+
// Within the week a weekday is enough; further out (fixtures reach ~3 weeks) add the date.
|
|
10
|
+
const soon = ts * 1000 - now < 6 * 86_400_000;
|
|
11
|
+
const day = d.toLocaleDateString(undefined, soon ? { weekday: 'short' } : { weekday: 'short', day: 'numeric', month: 'short' });
|
|
12
|
+
return `${day} ${time}`;
|
|
13
|
+
}
|
|
14
|
+
/** The kickoff padded to `width` (default: fits the longest date format), always followed by a space. */
|
|
15
|
+
export function kickoffColumn(ts, now = Date.now(), width = 22) {
|
|
16
|
+
const k = formatKickoff(ts, now);
|
|
17
|
+
return k.length >= width - 1 ? `${k} ` : k.padEnd(width);
|
|
18
|
+
}
|
|
19
|
+
/** One column width for a list of kickoffs: its longest date plus a two-space gap. */
|
|
20
|
+
export function kickoffWidth(timestamps, now = Date.now()) {
|
|
21
|
+
if (timestamps.length === 0)
|
|
22
|
+
return 22;
|
|
23
|
+
return Math.max(...timestamps.map((ts) => formatKickoff(ts, now).length)) + 2;
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudial",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Live football
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Live football for your teams in the status bar of Claude Code and Cursor CLI, plus a terminal dashboard with goal and red-card takeovers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claudial": "dist/index.js"
|
|
@@ -35,10 +35,13 @@
|
|
|
35
35
|
"worldcup",
|
|
36
36
|
"tui",
|
|
37
37
|
"cli",
|
|
38
|
-
"football",
|
|
39
38
|
"soccer",
|
|
40
39
|
"live-scores",
|
|
41
|
-
"ink"
|
|
40
|
+
"ink",
|
|
41
|
+
"cursor",
|
|
42
|
+
"vscode",
|
|
43
|
+
"antigravity",
|
|
44
|
+
"statusline"
|
|
42
45
|
],
|
|
43
46
|
"license": "MIT",
|
|
44
47
|
"repository": {
|