Skip to content

Commit 4dac6c6

Browse files
super3claude
andcommitted
Refactor to use testable module architecture
- Refactored index.js to use lib/mdtail.js module - Added comprehensive test coverage for index.js - Fixed Jest hanging issue with proper timer cleanup - Achieved 100% code coverage for all files - Added 29 new tests for full functionality coverage - Tests now validate the actual running code 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 0f4dd8a commit 4dac6c6

6 files changed

Lines changed: 696 additions & 205 deletions

File tree

index.js

Lines changed: 4 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -1,207 +1,9 @@
11
#!/usr/bin/env node
22

3-
const fs = require('fs');
4-
const path = require('path');
5-
const readline = require('readline');
3+
const MdTail = require('./lib/mdtail');
64

7-
// Parse command-line arguments
5+
// Create instance and run
6+
const mdtail = new MdTail();
87
const args = process.argv.slice(2);
9-
let files = [];
10-
let currentTabIndex = 0;
118

12-
// Handle help flag
13-
if (args.includes('-h') || args.includes('--help')) {
14-
console.log(`
15-
mdtail - Terminal markdown viewer with live refresh
16-
17-
Usage:
18-
mdtail [file1.md] [file2.md] ... Watch specific markdown files
19-
mdtail Watch TODO.md (default)
20-
mdtail -h, --help Show this help message
21-
22-
Navigation:
23-
← / → Arrow Keys Switch between tabs
24-
Ctrl+C Exit
25-
26-
Examples:
27-
mdtail README.md Watch README.md
28-
mdtail todo.md notes.md Watch multiple files with tabs
29-
mdtail *.md Watch all markdown files in tabs
30-
`);
31-
process.exit(0);
32-
}
33-
34-
// Get markdown files from arguments or default to todo.md
35-
if (args.length > 0) {
36-
// Expand wildcards and resolve paths
37-
args.forEach(arg => {
38-
if (arg.includes('*')) {
39-
// Handle wildcards
40-
const glob = require('fs').readdirSync(process.cwd())
41-
.filter(file => file.endsWith('.md'))
42-
.sort(); // Sort files alphabetically
43-
files.push(...glob.map(f => path.resolve(f)));
44-
} else {
45-
// Resolve individual file paths
46-
const filePath = path.resolve(arg);
47-
if (fs.existsSync(filePath) && filePath.endsWith('.md')) {
48-
files.push(filePath);
49-
} else if (!filePath.endsWith('.md')) {
50-
console.error(`Warning: ${arg} is not a markdown file`);
51-
} else {
52-
console.error(`Warning: ${arg} not found`);
53-
}
54-
}
55-
});
56-
} else {
57-
// Default to TODO.md in current directory
58-
const defaultFile = path.join(process.cwd(), 'TODO.md');
59-
if (fs.existsSync(defaultFile)) {
60-
files.push(defaultFile);
61-
}
62-
}
63-
64-
// Remove duplicates
65-
files = [...new Set(files)];
66-
67-
if (files.length === 0) {
68-
console.error('Error: No markdown files found to watch');
69-
console.log('Run "mdtail --help" for usage information');
70-
process.exit(1);
71-
}
72-
73-
function clearScreen() {
74-
console.clear();
75-
}
76-
77-
function renderTabs(width) {
78-
if (files.length === 1) return '';
79-
80-
const tabs = files.map((file, index) => {
81-
const filename = path.basename(file);
82-
const isActive = index === currentTabIndex;
83-
84-
if (isActive) {
85-
return `[${filename}]`;
86-
} else {
87-
return ` ${filename} `;
88-
}
89-
});
90-
91-
const tabLine = tabs.join(' │ ');
92-
const navigation = ' ← → Navigate tabs';
93-
94-
return `${tabLine}\n${'─'.repeat(width)}\n`;
95-
}
96-
97-
function displayCurrentFile() {
98-
clearScreen();
99-
process.stdout.write('\x1B[?25l'); // Hide cursor
100-
101-
const width = process.stdout.columns || 80;
102-
const currentFile = files[currentTabIndex];
103-
104-
try {
105-
const content = fs.readFileSync(currentFile, 'utf8');
106-
const filename = path.basename(currentFile);
107-
108-
// Header
109-
console.log('\n' + '═'.repeat(width));
110-
111-
// Tabs (if multiple files)
112-
if (files.length > 1) {
113-
console.log(renderTabs(width));
114-
} else {
115-
console.log(filename.toUpperCase());
116-
console.log('═'.repeat(width) + '\n');
117-
}
118-
119-
// Content
120-
console.log(content);
121-
122-
// Footer
123-
console.log('\n' + '═'.repeat(width));
124-
125-
if (files.length > 1) {
126-
console.log(`Tab ${currentTabIndex + 1} of ${files.length} │ ← → Navigate │ Ctrl+C Exit`);
127-
} else {
128-
console.log('Watching for changes... (Ctrl+C to exit)');
129-
}
130-
} catch (error) {
131-
console.error(`Error reading ${currentFile}:`, error.message);
132-
}
133-
}
134-
135-
function setupKeyboardNavigation() {
136-
// Skip keyboard setup if not in TTY mode
137-
if (!process.stdin.isTTY) {
138-
return;
139-
}
140-
141-
readline.emitKeypressEvents(process.stdin);
142-
process.stdin.setRawMode(true);
143-
process.stdin.resume(); // Start reading from stdin
144-
145-
process.stdin.on('keypress', (str, key) => {
146-
if (key && key.ctrl && key.name === 'c') {
147-
// Exit
148-
process.stdin.setRawMode(false);
149-
process.stdout.write('\x1B[?25h'); // Show cursor again
150-
console.log('\n\nStopping mdtail...');
151-
process.exit(0);
152-
}
153-
154-
if (files.length > 1) {
155-
if (key && key.name === 'left') {
156-
// Previous tab
157-
currentTabIndex = (currentTabIndex - 1 + files.length) % files.length;
158-
displayCurrentFile();
159-
} else if (key && key.name === 'right') {
160-
// Next tab
161-
currentTabIndex = (currentTabIndex + 1) % files.length;
162-
displayCurrentFile();
163-
}
164-
}
165-
});
166-
}
167-
168-
function startWatching() {
169-
// Setup keyboard navigation
170-
setupKeyboardNavigation();
171-
172-
// Initial display
173-
displayCurrentFile();
174-
175-
// Watch each file
176-
files.forEach((file, index) => {
177-
fs.watchFile(file, { interval: 100 }, (curr, prev) => {
178-
if (curr.mtime !== prev.mtime) {
179-
// Only redraw if we're viewing the changed file or if there's only one file
180-
if (index === currentTabIndex || files.length === 1) {
181-
displayCurrentFile();
182-
}
183-
}
184-
});
185-
});
186-
187-
// Show initial file list
188-
if (files.length > 1) {
189-
console.log(`\nWatching ${files.length} files. Use arrow keys to navigate.`);
190-
setTimeout(() => {
191-
displayCurrentFile();
192-
}, 1500);
193-
}
194-
}
195-
196-
// Handle exit gracefully
197-
process.on('SIGINT', () => {
198-
if (process.stdin.isTTY) {
199-
process.stdin.setRawMode(false);
200-
}
201-
process.stdout.write('\x1B[?25h'); // Show cursor again
202-
console.log('\n\nStopping mdtail...');
203-
process.exit(0);
204-
});
205-
206-
// Start watching the files
207-
startWatching();
9+
mdtail.run(args);

jest.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ module.exports = {
22
testEnvironment: 'node',
33
collectCoverageFrom: [
44
'lib/**/*.js',
5-
'!lib/**/*.test.js'
5+
'index.js',
6+
'!lib/**/*.test.js',
7+
'!test/**/*.js'
68
],
79
coverageDirectory: 'coverage',
810
coverageReporters: ['text', 'lcov', 'html'],

0 commit comments

Comments
 (0)