@typescript-calendar-lib/cli 0.2.2 → 0.2.5
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/package.json +2 -2
- package/src/ansi.ts +9 -2
- package/src/bin.ts +90 -115
- package/src/border.ts +22 -4
- package/src/calendar.ts +3 -42
- package/src/month.ts +64 -127
- package/src/theme.ts +6 -8
- package/src/year.ts +31 -33
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typescript-calendar-lib/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"typescript-calendar-lib": "src/bin.ts"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"!src/*.test.ts"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@typescript-calendar-lib/core": "0.2.
|
|
19
|
+
"@typescript-calendar-lib/core": "0.2.5",
|
|
20
20
|
"cli-table3": "^0.6.5"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
package/src/ansi.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// ─── ANSI 色付け ─────────────────────────────────────────
|
|
2
2
|
|
|
3
|
+
import { displayWidth } from "./align.ts";
|
|
4
|
+
|
|
3
5
|
const ANSI_PATTERN = new RegExp(`${"\u001b"}\\[[0-9;]*m`, "g");
|
|
4
6
|
|
|
5
7
|
/** ANSI コードを付与する(code が undefined ならそのまま) */
|
|
@@ -12,7 +14,12 @@ export function colorize(
|
|
|
12
14
|
return `\u001b[${code}m${text}\u001b[0m`;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
/** ANSI
|
|
17
|
+
/** ANSI エスケープシーケンスを除去した文字列を返す */
|
|
18
|
+
export function stripAnsi(text: string): string {
|
|
19
|
+
return text.replace(ANSI_PATTERN, "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** ANSI エスケープシーケンスを除去した表示幅を返す(全角文字は2列) */
|
|
16
23
|
export function visibleWidth(text: string): number {
|
|
17
|
-
return text
|
|
24
|
+
return displayWidth(stripAnsi(text));
|
|
18
25
|
}
|
package/src/bin.ts
CHANGED
|
@@ -4,21 +4,10 @@ import type {
|
|
|
4
4
|
Locale,
|
|
5
5
|
WeekStart,
|
|
6
6
|
} from "@typescript-calendar-lib/core";
|
|
7
|
+
import { createDate } from "@typescript-calendar-lib/core";
|
|
7
8
|
import { calendar } from "./calendar.ts";
|
|
8
9
|
import type { ColorSchemeName, ThemeName } from "./theme.ts";
|
|
9
10
|
|
|
10
|
-
const THEMES: readonly ThemeName[] = ["default", "modern"];
|
|
11
|
-
const COLOR_SCHEMES: readonly ColorSchemeName[] = [
|
|
12
|
-
"default",
|
|
13
|
-
"ocean",
|
|
14
|
-
"forest",
|
|
15
|
-
"sunset",
|
|
16
|
-
"mono",
|
|
17
|
-
];
|
|
18
|
-
const LOCALES: readonly Locale[] = ["en", "ja", "es", "de", "fr", "ko", "zh"];
|
|
19
|
-
const WEEK_STARTS: readonly WeekStart[] = ["sunday", "monday"];
|
|
20
|
-
const HIGHLIGHT_STYLES: readonly HighlightStyle[] = ["bracket", "reverse"];
|
|
21
|
-
|
|
22
11
|
export interface CliArgs {
|
|
23
12
|
year?: number;
|
|
24
13
|
month?: number;
|
|
@@ -37,6 +26,27 @@ export interface ParseResult {
|
|
|
37
26
|
help?: boolean;
|
|
38
27
|
}
|
|
39
28
|
|
|
29
|
+
const THEMES: readonly ThemeName[] = ["default", "modern"];
|
|
30
|
+
const COLOR_SCHEMES: readonly ColorSchemeName[] = [
|
|
31
|
+
"default",
|
|
32
|
+
"ocean",
|
|
33
|
+
"forest",
|
|
34
|
+
"sunset",
|
|
35
|
+
"mono",
|
|
36
|
+
];
|
|
37
|
+
const LOCALES: readonly Locale[] = ["en", "ja", "es", "de", "fr", "ko", "zh"];
|
|
38
|
+
const WEEK_STARTS: readonly WeekStart[] = ["sunday", "monday"];
|
|
39
|
+
const HIGHLIGHT_STYLES: readonly HighlightStyle[] = ["bracket", "reverse"];
|
|
40
|
+
|
|
41
|
+
/** 値を一つ取るオプションと、代入先のフィールド名 */
|
|
42
|
+
const VALUE_OPTIONS: Record<string, keyof CliArgs> = {
|
|
43
|
+
"--theme": "theme",
|
|
44
|
+
"--color-scheme": "colorScheme",
|
|
45
|
+
"--locale": "locale",
|
|
46
|
+
"--week-start": "weekStart",
|
|
47
|
+
"--highlight-style": "highlightStyle",
|
|
48
|
+
};
|
|
49
|
+
|
|
40
50
|
/** YYYY-MM-DD 形式の日付文字列をパースする */
|
|
41
51
|
export function parseDate(value: string): Date | null {
|
|
42
52
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
@@ -44,29 +54,18 @@ export function parseDate(value: string): Date | null {
|
|
|
44
54
|
const year = Number(match[1]);
|
|
45
55
|
const month = Number(match[2]);
|
|
46
56
|
const day = Number(match[3]);
|
|
47
|
-
if (month < 1 || month > 12)
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
if (year < 1 || month < 1 || month > 12 || day < 1 || day > 31) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const date = createDate(year, month - 1, day);
|
|
50
61
|
if (
|
|
51
62
|
date.getFullYear() !== year ||
|
|
52
63
|
date.getMonth() !== month - 1 ||
|
|
53
64
|
date.getDate() !== day
|
|
54
|
-
)
|
|
65
|
+
) {
|
|
55
66
|
return null;
|
|
56
|
-
return date;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** 指定した候補リストに値が含まれるか検証する */
|
|
60
|
-
function validateChoice<T extends string>(
|
|
61
|
-
value: T | undefined,
|
|
62
|
-
choices: readonly T[],
|
|
63
|
-
name: string,
|
|
64
|
-
): string | undefined {
|
|
65
|
-
if (value === undefined) return undefined;
|
|
66
|
-
if (!choices.includes(value)) {
|
|
67
|
-
return `Invalid ${name}: "${value}" (expected: ${choices.join(" | ")})`;
|
|
68
67
|
}
|
|
69
|
-
return
|
|
68
|
+
return date;
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
/**
|
|
@@ -74,109 +73,85 @@ function validateChoice<T extends string>(
|
|
|
74
73
|
*/
|
|
75
74
|
export function parseArgs(args: readonly string[]): ParseResult {
|
|
76
75
|
const result: CliArgs = {};
|
|
77
|
-
const
|
|
76
|
+
const error = (message: string): ParseResult => ({
|
|
78
77
|
args: result,
|
|
79
|
-
error:
|
|
78
|
+
error: message,
|
|
80
79
|
});
|
|
81
80
|
|
|
82
81
|
for (let i = 0; i < args.length; i++) {
|
|
83
82
|
const arg = args[i]!;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
case "--color-scheme": {
|
|
97
|
-
const value = next();
|
|
98
|
-
if (value === undefined) return missing("--color-scheme");
|
|
99
|
-
result.colorScheme = value as ColorSchemeName;
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
case "--color":
|
|
103
|
-
result.color = true;
|
|
104
|
-
break;
|
|
105
|
-
case "--locale": {
|
|
106
|
-
const value = next();
|
|
107
|
-
if (value === undefined) return missing("--locale");
|
|
108
|
-
result.locale = value as Locale;
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
case "--week-start": {
|
|
112
|
-
const value = next();
|
|
113
|
-
if (value === undefined) return missing("--week-start");
|
|
114
|
-
result.weekStart = value as WeekStart;
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
case "--highlight": {
|
|
118
|
-
const value = next();
|
|
119
|
-
if (value === undefined) return missing("--highlight");
|
|
120
|
-
const parsed = parseDate(value);
|
|
121
|
-
if (parsed === null) {
|
|
122
|
-
return {
|
|
123
|
-
args: result,
|
|
124
|
-
error: `Invalid --highlight date: "${value}" (expected YYYY-MM-DD, e.g. 2026-09-08)`,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
result.highlight = parsed;
|
|
128
|
-
break;
|
|
83
|
+
|
|
84
|
+
if (arg === "-h" || arg === "--help") {
|
|
85
|
+
return { args: result, help: true };
|
|
86
|
+
}
|
|
87
|
+
if (arg === "--color") {
|
|
88
|
+
result.color = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg === "--highlight") {
|
|
92
|
+
const value = args[++i];
|
|
93
|
+
if (value === undefined) {
|
|
94
|
+
return error("Missing value for option: --highlight");
|
|
129
95
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
96
|
+
const parsed = parseDate(value);
|
|
97
|
+
if (parsed === null) {
|
|
98
|
+
return error(
|
|
99
|
+
`Invalid --highlight date: "${value}" (expected YYYY-MM-DD, e.g. 2026-09-08)`,
|
|
100
|
+
);
|
|
135
101
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
if (result.year === undefined) {
|
|
147
|
-
result.year = Number(arg);
|
|
148
|
-
} else if (result.month === undefined) {
|
|
149
|
-
result.month = Number(arg);
|
|
150
|
-
} else {
|
|
151
|
-
return { args: result, error: `Too many arguments: "${arg}"` };
|
|
152
|
-
}
|
|
102
|
+
result.highlight = parsed;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const key = VALUE_OPTIONS[arg];
|
|
107
|
+
if (key !== undefined) {
|
|
108
|
+
const value = args[++i];
|
|
109
|
+
if (value === undefined) {
|
|
110
|
+
return error(`Missing value for option: ${arg}`);
|
|
153
111
|
}
|
|
112
|
+
(result as Record<string, unknown>)[key] = value;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const numeric = /^-?\d+$/.test(arg);
|
|
117
|
+
if (arg.startsWith("-") && !numeric) {
|
|
118
|
+
return error(`Unknown option: ${arg}`);
|
|
119
|
+
}
|
|
120
|
+
if (!numeric) {
|
|
121
|
+
return error(
|
|
122
|
+
`Invalid argument: "${arg}" (expected a year or month number)`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (result.year === undefined) {
|
|
126
|
+
result.year = Number(arg);
|
|
127
|
+
} else if (result.month === undefined) {
|
|
128
|
+
result.month = Number(arg);
|
|
129
|
+
} else {
|
|
130
|
+
return error(`Too many arguments: "${arg}"`);
|
|
154
131
|
}
|
|
155
132
|
}
|
|
156
133
|
|
|
157
134
|
// 値のバリデーション
|
|
158
135
|
if (result.year !== undefined && (result.year < 1 || result.year > 9999)) {
|
|
159
|
-
return {
|
|
160
|
-
args: result,
|
|
161
|
-
error: `Invalid year: ${result.year} (expected 1–9999)`,
|
|
162
|
-
};
|
|
136
|
+
return error(`Invalid year: ${result.year} (expected 1–9999)`);
|
|
163
137
|
}
|
|
164
138
|
if (result.month !== undefined && (result.month < 1 || result.month > 12)) {
|
|
165
|
-
return {
|
|
166
|
-
args: result,
|
|
167
|
-
error: `Invalid month: ${result.month} (expected 1–12)`,
|
|
168
|
-
};
|
|
139
|
+
return error(`Invalid month: ${result.month} (expected 1–12)`);
|
|
169
140
|
}
|
|
170
141
|
|
|
171
|
-
|
|
172
|
-
[result.theme, THEMES, "theme"]
|
|
173
|
-
[result.colorScheme, COLOR_SCHEMES, "color-scheme"]
|
|
174
|
-
[result.locale, LOCALES, "locale"]
|
|
175
|
-
[result.weekStart, WEEK_STARTS, "week-start"]
|
|
176
|
-
[result.highlightStyle, HIGHLIGHT_STYLES, "highlight-style"]
|
|
177
|
-
]
|
|
178
|
-
|
|
179
|
-
if (
|
|
142
|
+
const choiceTable: Array<[string | undefined, readonly string[], string]> = [
|
|
143
|
+
[result.theme, THEMES, "theme"],
|
|
144
|
+
[result.colorScheme, COLOR_SCHEMES, "color-scheme"],
|
|
145
|
+
[result.locale, LOCALES, "locale"],
|
|
146
|
+
[result.weekStart, WEEK_STARTS, "week-start"],
|
|
147
|
+
[result.highlightStyle, HIGHLIGHT_STYLES, "highlight-style"],
|
|
148
|
+
];
|
|
149
|
+
for (const [value, choices, name] of choiceTable) {
|
|
150
|
+
if (value !== undefined && !choices.includes(value)) {
|
|
151
|
+
return error(
|
|
152
|
+
`Invalid ${name}: "${value}" (expected: ${choices.join(" | ")})`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
180
155
|
}
|
|
181
156
|
|
|
182
157
|
return { args: result };
|
package/src/border.ts
CHANGED
|
@@ -7,6 +7,19 @@ export function innerWidth(cellWidth: number, cols: number): number {
|
|
|
7
7
|
return cols * cellWidth + (cols - 1);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/** セル幅の水平線を cols 個つなげた区切り線を生成する */
|
|
11
|
+
function divider(
|
|
12
|
+
frame: FrameChars,
|
|
13
|
+
cellWidth: number,
|
|
14
|
+
cols: number,
|
|
15
|
+
left: string,
|
|
16
|
+
right: string,
|
|
17
|
+
join: string,
|
|
18
|
+
): string {
|
|
19
|
+
const segments = Array<string>(cols).fill(frame.h.repeat(cellWidth));
|
|
20
|
+
return `${left}${segments.join(join)}${right}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
/** 上枠: ┌────┬────...────┐ */
|
|
11
24
|
export function topBorder(
|
|
12
25
|
frame: FrameChars,
|
|
@@ -22,8 +35,14 @@ export function bottomBorder(
|
|
|
22
35
|
cellWidth: number,
|
|
23
36
|
cols: number,
|
|
24
37
|
): string {
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
return divider(
|
|
39
|
+
frame,
|
|
40
|
+
cellWidth,
|
|
41
|
+
cols,
|
|
42
|
+
frame.bottomLeft,
|
|
43
|
+
frame.bottomRight,
|
|
44
|
+
frame.footJ,
|
|
45
|
+
);
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
/** 区切り行: ├────┬────...┬────┤ */
|
|
@@ -32,6 +51,5 @@ export function separatorRow(
|
|
|
32
51
|
cellWidth: number,
|
|
33
52
|
cols: number,
|
|
34
53
|
): string {
|
|
35
|
-
|
|
36
|
-
return `├${segments.join(frame.j)}┤`;
|
|
54
|
+
return divider(frame, cellWidth, cols, "├", "┤", frame.j);
|
|
37
55
|
}
|
package/src/calendar.ts
CHANGED
|
@@ -4,67 +4,28 @@ import type {
|
|
|
4
4
|
CalendarOptions,
|
|
5
5
|
CalendarRangeOptions,
|
|
6
6
|
CalendarYearOptions,
|
|
7
|
-
RenderMonthOptions,
|
|
8
7
|
} from "./types.ts";
|
|
9
8
|
|
|
10
|
-
// ─── Options 解決 ─────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 各 API に共通のオプションをデフォルト値込みの描画オプションに正規化する。
|
|
14
|
-
* core 由来のオプションと CLI 固有のオプション(theme 等)をまとめる。
|
|
15
|
-
*/
|
|
16
|
-
function toRenderOptions(
|
|
17
|
-
options: CalendarOptions | CalendarYearOptions | CalendarRangeOptions,
|
|
18
|
-
): RenderMonthOptions {
|
|
19
|
-
const {
|
|
20
|
-
locale = "en",
|
|
21
|
-
weekStart = "sunday",
|
|
22
|
-
highlight,
|
|
23
|
-
highlightStyle = "bracket",
|
|
24
|
-
range,
|
|
25
|
-
color = false,
|
|
26
|
-
theme,
|
|
27
|
-
colorScheme,
|
|
28
|
-
today,
|
|
29
|
-
} = options;
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
locale,
|
|
33
|
-
weekStart,
|
|
34
|
-
highlight,
|
|
35
|
-
highlightStyle,
|
|
36
|
-
range,
|
|
37
|
-
color,
|
|
38
|
-
theme,
|
|
39
|
-
colorScheme,
|
|
40
|
-
today,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ─── 公開API ─────────────────────────────────────────────
|
|
45
|
-
|
|
46
9
|
/**
|
|
47
10
|
* 月カレンダーをテキストで返す
|
|
48
11
|
*/
|
|
49
12
|
export function calendar(options: CalendarOptions): string {
|
|
50
|
-
return renderMonth(options.year, options.month,
|
|
13
|
+
return renderMonth(options.year, options.month, options);
|
|
51
14
|
}
|
|
52
15
|
|
|
53
16
|
/**
|
|
54
17
|
* 年間カレンダーを4列×3行でテキストで返す
|
|
55
18
|
*/
|
|
56
19
|
export function calendarYear(options: CalendarYearOptions): string {
|
|
57
|
-
return renderYear(options.year,
|
|
20
|
+
return renderYear(options.year, options);
|
|
58
21
|
}
|
|
59
22
|
|
|
60
23
|
/**
|
|
61
24
|
* 任意の日付範囲のカレンダーをテキストで返す
|
|
62
25
|
*/
|
|
63
26
|
export function calendarRange(options: CalendarRangeOptions): string {
|
|
64
|
-
const renderOptions = toRenderOptions(options);
|
|
65
27
|
const months = getMonthRange(options.from, options.to);
|
|
66
|
-
|
|
67
28
|
return months
|
|
68
|
-
.map(({ year, month }) => renderMonth(year, month,
|
|
29
|
+
.map(({ year, month }) => renderMonth(year, month, options))
|
|
69
30
|
.join("\n\n");
|
|
70
31
|
}
|
package/src/month.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildMonthGrid,
|
|
3
|
+
createDate,
|
|
3
4
|
getMonthName,
|
|
4
5
|
getWeekdayHeaders,
|
|
5
6
|
isDateInRange,
|
|
6
7
|
isSameDay,
|
|
7
8
|
} from "@typescript-calendar-lib/core";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
centerText,
|
|
11
|
+
centerTextFull,
|
|
12
|
+
displayWidth,
|
|
13
|
+
padStartWidth,
|
|
14
|
+
} from "./align.ts";
|
|
9
15
|
import { colorize } from "./ansi.ts";
|
|
10
16
|
import { bottomBorder, innerWidth, separatorRow, topBorder } from "./border.ts";
|
|
11
|
-
import type { CliPalette } from "./theme.ts";
|
|
12
17
|
import { resolveColorScheme, resolveTheme } from "./theme.ts";
|
|
13
18
|
import type { RenderMonthOptions } from "./types.ts";
|
|
14
19
|
|
|
15
|
-
const CELL_WIDTH = 3;
|
|
16
|
-
|
|
17
20
|
/**
|
|
18
21
|
* 1ヶ月分のカレンダーテキストを描画する
|
|
19
22
|
*/
|
|
@@ -36,28 +39,61 @@ export function renderMonth(
|
|
|
36
39
|
|
|
37
40
|
const theme = resolveTheme(themeOption);
|
|
38
41
|
const palette = resolveColorScheme(schemeOption);
|
|
42
|
+
const frame = theme.frame;
|
|
39
43
|
|
|
40
44
|
const title = `${getMonthName(locale, month)} ${year}`;
|
|
41
45
|
const weekdays = getWeekdayHeaders(locale, weekStart);
|
|
42
46
|
const grid = buildMonthGrid(year, month, weekStart);
|
|
47
|
+
const cols = weekdays.length;
|
|
48
|
+
|
|
49
|
+
// セル幅はテーマ指定を基本としつつ、以下を満たすように広げる:
|
|
50
|
+
// - 曜日ヘッダーの表示幅(fr の "dim." 等がセル幅を超えると列が崩れる)
|
|
51
|
+
// - bracket ハイライトは2桁の日付で `[10]` の4文字になるため
|
|
52
|
+
const cellWidth = Math.max(
|
|
53
|
+
theme.cellWidth,
|
|
54
|
+
...weekdays.map(displayWidth),
|
|
55
|
+
highlight !== undefined && highlightStyle === "bracket" ? 4 : 2,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// 枠なしテーマは separator、枠ありテーマは縦線でセルを繋ぐ
|
|
59
|
+
const sep = frame === null ? theme.separator : frame.v;
|
|
60
|
+
|
|
61
|
+
/** 1セルを描画する(day は日数または null=空欄) */
|
|
62
|
+
const renderCell = (day: number | null): string => {
|
|
63
|
+
if (day === null) return " ".repeat(cellWidth);
|
|
64
|
+
|
|
65
|
+
const date = createDate(year, month - 1, day);
|
|
66
|
+
const isHighlight = highlight !== undefined && isSameDay(date, highlight);
|
|
67
|
+
const isInRange = isDateInRange(date, range);
|
|
68
|
+
const isToday = isSameDay(date, today);
|
|
69
|
+
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
|
|
70
|
+
|
|
71
|
+
const text = (
|
|
72
|
+
isHighlight && highlightStyle === "bracket" ? `[${day}]` : String(day)
|
|
73
|
+
).padStart(cellWidth);
|
|
74
|
+
|
|
75
|
+
let code: number | undefined;
|
|
76
|
+
if (isHighlight && highlightStyle === "reverse") {
|
|
77
|
+
code = palette.highlight ?? 7;
|
|
78
|
+
} else if (isInRange && !isHighlight) {
|
|
79
|
+
code = palette.range ?? 33;
|
|
80
|
+
} else if (isToday && palette.today !== undefined) {
|
|
81
|
+
code = palette.today;
|
|
82
|
+
} else if (isWeekend && palette.weekend !== undefined) {
|
|
83
|
+
code = palette.weekend;
|
|
84
|
+
} else if (palette.day !== undefined) {
|
|
85
|
+
code = palette.day;
|
|
86
|
+
}
|
|
43
87
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const cellWidth =
|
|
47
|
-
highlight !== undefined && highlightStyle === "bracket"
|
|
48
|
-
? CELL_WIDTH + 1
|
|
49
|
-
: CELL_WIDTH;
|
|
88
|
+
return colorize(text, code, color);
|
|
89
|
+
};
|
|
50
90
|
|
|
51
91
|
const lines: string[] = [];
|
|
52
92
|
|
|
53
|
-
if (
|
|
93
|
+
if (frame === null) {
|
|
54
94
|
// ── 枠なし(default) ──
|
|
55
|
-
const
|
|
56
|
-
const totalWidth =
|
|
57
|
-
weekdays.length * cellWidth + (weekdays.length - 1) * sep.length;
|
|
58
|
-
|
|
95
|
+
const totalWidth = cols * cellWidth + (cols - 1) * sep.length;
|
|
59
96
|
lines.push(centerText(title, totalWidth));
|
|
60
|
-
|
|
61
97
|
lines.push(
|
|
62
98
|
weekdays
|
|
63
99
|
.map((d) =>
|
|
@@ -65,51 +101,20 @@ export function renderMonth(
|
|
|
65
101
|
)
|
|
66
102
|
.join(sep),
|
|
67
103
|
);
|
|
68
|
-
|
|
69
|
-
for (const row of grid) {
|
|
70
|
-
if (row.every((d) => d === null)) continue;
|
|
71
|
-
|
|
72
|
-
const cells = row.map((day) =>
|
|
73
|
-
renderCell(
|
|
74
|
-
year,
|
|
75
|
-
month,
|
|
76
|
-
day,
|
|
77
|
-
highlight,
|
|
78
|
-
highlightStyle,
|
|
79
|
-
range,
|
|
80
|
-
today,
|
|
81
|
-
color,
|
|
82
|
-
palette,
|
|
83
|
-
cellWidth,
|
|
84
|
-
),
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
lines.push(cells.join(sep));
|
|
88
|
-
}
|
|
89
104
|
} else {
|
|
90
105
|
// ── 枠あり(modern) ──
|
|
91
|
-
const frame = theme.frame;
|
|
92
|
-
|
|
93
106
|
lines.push(
|
|
94
|
-
colorize(
|
|
95
|
-
topBorder(frame, cellWidth, weekdays.length),
|
|
96
|
-
palette.frame,
|
|
97
|
-
color,
|
|
98
|
-
),
|
|
107
|
+
colorize(topBorder(frame, cellWidth, cols), palette.frame, color),
|
|
99
108
|
);
|
|
100
109
|
lines.push(
|
|
101
110
|
colorize(
|
|
102
|
-
`${frame.v}${centerTextFull(title, innerWidth(cellWidth,
|
|
111
|
+
`${frame.v}${centerTextFull(title, innerWidth(cellWidth, cols))}${frame.v}`,
|
|
103
112
|
palette.title,
|
|
104
113
|
color,
|
|
105
114
|
),
|
|
106
115
|
);
|
|
107
116
|
lines.push(
|
|
108
|
-
colorize(
|
|
109
|
-
separatorRow(frame, cellWidth, weekdays.length),
|
|
110
|
-
palette.frame,
|
|
111
|
-
color,
|
|
112
|
-
),
|
|
117
|
+
colorize(separatorRow(frame, cellWidth, cols), palette.frame, color),
|
|
113
118
|
);
|
|
114
119
|
lines.push(
|
|
115
120
|
colorize(
|
|
@@ -119,89 +124,21 @@ export function renderMonth(
|
|
|
119
124
|
),
|
|
120
125
|
);
|
|
121
126
|
lines.push(
|
|
122
|
-
colorize(
|
|
123
|
-
separatorRow(frame, cellWidth, weekdays.length),
|
|
124
|
-
palette.frame,
|
|
125
|
-
color,
|
|
126
|
-
),
|
|
127
|
+
colorize(separatorRow(frame, cellWidth, cols), palette.frame, color),
|
|
127
128
|
);
|
|
129
|
+
}
|
|
128
130
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
year,
|
|
135
|
-
month,
|
|
136
|
-
day,
|
|
137
|
-
highlight,
|
|
138
|
-
highlightStyle,
|
|
139
|
-
range,
|
|
140
|
-
today,
|
|
141
|
-
color,
|
|
142
|
-
palette,
|
|
143
|
-
cellWidth,
|
|
144
|
-
),
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
lines.push(`${frame.v}${cells.join(frame.v)}${frame.v}`);
|
|
148
|
-
}
|
|
131
|
+
for (const row of grid) {
|
|
132
|
+
if (row.every((d) => d === null)) continue;
|
|
133
|
+
const cells = row.map(renderCell).join(sep);
|
|
134
|
+
lines.push(frame === null ? cells : `${frame.v}${cells}${frame.v}`);
|
|
135
|
+
}
|
|
149
136
|
|
|
137
|
+
if (frame !== null) {
|
|
150
138
|
lines.push(
|
|
151
|
-
colorize(
|
|
152
|
-
bottomBorder(frame, cellWidth, weekdays.length),
|
|
153
|
-
palette.frame,
|
|
154
|
-
color,
|
|
155
|
-
),
|
|
139
|
+
colorize(bottomBorder(frame, cellWidth, cols), palette.frame, color),
|
|
156
140
|
);
|
|
157
141
|
}
|
|
158
142
|
|
|
159
143
|
return lines.join("\n");
|
|
160
144
|
}
|
|
161
|
-
|
|
162
|
-
// ─── セル ─────────────────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
function renderCell(
|
|
165
|
-
year: number,
|
|
166
|
-
month: number,
|
|
167
|
-
day: number | null,
|
|
168
|
-
highlight: Date | undefined,
|
|
169
|
-
highlightStyle: "bracket" | "reverse",
|
|
170
|
-
range: { from: Date; to: Date } | undefined,
|
|
171
|
-
today: Date,
|
|
172
|
-
color: boolean,
|
|
173
|
-
palette: CliPalette,
|
|
174
|
-
cellWidth: number,
|
|
175
|
-
): string {
|
|
176
|
-
if (day === null) {
|
|
177
|
-
return " ".repeat(cellWidth);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const date = new Date(year, month - 1, day);
|
|
181
|
-
const isHighlight = highlight !== undefined && isSameDay(date, highlight);
|
|
182
|
-
const isInRange = isDateInRange(date, range);
|
|
183
|
-
const isToday = isSameDay(date, today);
|
|
184
|
-
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
|
|
185
|
-
|
|
186
|
-
let text: string;
|
|
187
|
-
if (isHighlight && highlightStyle === "bracket") {
|
|
188
|
-
text = `[${day}]`.padStart(cellWidth);
|
|
189
|
-
} else {
|
|
190
|
-
text = String(day).padStart(cellWidth);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
let code: number | undefined;
|
|
194
|
-
if (isHighlight && highlightStyle === "reverse") {
|
|
195
|
-
code = palette.highlight ?? 7;
|
|
196
|
-
} else if (isInRange && !isHighlight) {
|
|
197
|
-
code = palette.range ?? 33;
|
|
198
|
-
} else if (isToday && palette.today !== undefined) {
|
|
199
|
-
code = palette.today;
|
|
200
|
-
} else if (isWeekend && palette.weekend !== undefined) {
|
|
201
|
-
code = palette.weekend;
|
|
202
|
-
} else if (palette.day !== undefined) {
|
|
203
|
-
code = palette.day;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return colorize(text, code, color);
|
|
207
|
-
}
|
package/src/theme.ts
CHANGED
|
@@ -52,9 +52,9 @@ export const THEMES: Record<ThemeName, CliTheme> = {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
export function resolveTheme(theme?: ThemeName | CliTheme): CliTheme {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
return typeof theme === "string"
|
|
56
|
+
? (THEMES[theme] ?? THEMES.default)
|
|
57
|
+
: (theme ?? THEMES.default);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// ─── カラースキーム ───────────────────────────────────────
|
|
@@ -140,9 +140,7 @@ export const COLOR_SCHEMES: Record<ColorSchemeName, CliPalette> = {
|
|
|
140
140
|
export function resolveColorScheme(
|
|
141
141
|
scheme?: ColorSchemeName | CliPalette,
|
|
142
142
|
): CliPalette {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
return scheme;
|
|
143
|
+
return typeof scheme === "string"
|
|
144
|
+
? (COLOR_SCHEMES[scheme] ?? COLOR_SCHEMES.default)
|
|
145
|
+
: (scheme ?? COLOR_SCHEMES.default);
|
|
148
146
|
}
|
package/src/year.ts
CHANGED
|
@@ -1,50 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { displayWidth } from "./align.ts";
|
|
2
|
+
import { stripAnsi } from "./ansi.ts";
|
|
2
3
|
import { renderMonth } from "./month.ts";
|
|
3
4
|
import type { RenderMonthOptions } from "./types.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 年間カレンダーを4列×3行でテキストで返す
|
|
8
|
+
*
|
|
9
|
+
* 列幅・パディングは「ANSI 除去後のターミナル表示幅」で計算する。
|
|
10
|
+
* 全角文字(日本語・韓国語・中国語の月名や曜日)も正しく揃う。
|
|
7
11
|
*/
|
|
8
12
|
export function renderYear(
|
|
9
13
|
year: number,
|
|
10
14
|
options: RenderMonthOptions = {},
|
|
11
15
|
): string {
|
|
12
|
-
const
|
|
13
|
-
for (let m = 1; m <= 12; m++) {
|
|
14
|
-
months.push(renderMonth(year, m, options));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const monthLines = months.map((m) => m.split("\n"));
|
|
18
|
-
const maxLines = Math.max(...monthLines.map((l) => l.length));
|
|
16
|
+
const widthOf = (line: string): number => displayWidth(stripAnsi(line));
|
|
19
17
|
|
|
20
|
-
const
|
|
21
|
-
|
|
18
|
+
const months = Array.from({ length: 12 }, (_, i) =>
|
|
19
|
+
renderMonth(year, i + 1, options).split("\n"),
|
|
22
20
|
);
|
|
21
|
+
const maxLines = Math.max(...months.map((lines) => lines.length));
|
|
22
|
+
const colWidths = months.map((lines) => Math.max(...lines.map(widthOf)));
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
parts.push("");
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const lines = monthLines[monthIdx]!;
|
|
36
|
-
const line = lines[lineIdx] ?? "";
|
|
37
|
-
const pad = Math.max(0, colWidths[monthIdx]! - visibleWidth(line));
|
|
38
|
-
parts.push(line + " ".repeat(pad));
|
|
39
|
-
}
|
|
40
|
-
rowLines.push(parts.join(" "));
|
|
41
|
-
}
|
|
42
|
-
result.push(rowLines.join("\n"));
|
|
24
|
+
/** 月の各行をその列幅まで右パディングする(行が足りなければ空行として埋める) */
|
|
25
|
+
const padMonth = (monthIdx: number): string[] =>
|
|
26
|
+
Array.from({ length: maxLines }, (_, li) => {
|
|
27
|
+
const line = months[monthIdx]![li] ?? "";
|
|
28
|
+
return (
|
|
29
|
+
line + " ".repeat(Math.max(0, colWidths[monthIdx]! - widthOf(line)))
|
|
30
|
+
);
|
|
31
|
+
});
|
|
43
32
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
const padded = months.map((_, mi) => padMonth(mi));
|
|
34
|
+
|
|
35
|
+
const rows: string[] = [];
|
|
36
|
+
for (let row = 0; row < 3; row++) {
|
|
37
|
+
rows.push(
|
|
38
|
+
Array.from({ length: maxLines }, (_, li) =>
|
|
39
|
+
Array.from({ length: 4 }, (_, col) => {
|
|
40
|
+
const monthIdx = row * 4 + col;
|
|
41
|
+
return monthIdx < 12 ? padded[monthIdx]![li]! : "";
|
|
42
|
+
}).join(" "),
|
|
43
|
+
).join("\n"),
|
|
44
|
+
);
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
return
|
|
47
|
+
return rows.join("\n\n");
|
|
50
48
|
}
|