Skip to content

Commit 72e590d

Browse files
committed
Add smart-case support
Fixes #17
1 parent eecb553 commit 72e590d

File tree

4 files changed

+97
-17
lines changed

4 files changed

+97
-17
lines changed

cli.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const cli = meow(`
1212
--verbose -v Show process arguments
1313
--silent -s Silently kill and always exit with code 0
1414
--force-after-timeout <N>, -t <N> Force kill processes which didn't exit after N seconds
15+
--smart-case Case-insensitive unless pattern contains uppercase
16+
--case-sensitive Force case-sensitive matching
1517
1618
Examples
1719
$ fkill 1337
@@ -26,7 +28,7 @@ const cli = meow(`
2628
In interactive mode, 🚦n% indicates high CPU usage and 🐏n% indicates high memory usage.
2729
Supports fuzzy search in the interactive mode.
2830
29-
The process name is case insensitive.
31+
The process name is case-insensitive by default.
3032
`, {
3133
importMeta: import.meta,
3234
inferType: true,
@@ -47,15 +49,39 @@ const cli = meow(`
4749
type: 'number',
4850
shortFlag: 't',
4951
},
52+
smartCase: {
53+
type: 'boolean',
54+
},
55+
caseSensitive: {
56+
type: 'boolean',
57+
},
5058
},
5159
});
5260

61+
const shouldIgnoreCase = (inputs, flags) => {
62+
// Explicit case-sensitive flag takes precedence over smart-case
63+
if (flags.caseSensitive) {
64+
return false;
65+
}
66+
67+
// Smart-case: ignore case unless ANY input contains uppercase
68+
// Note: With multiple inputs, if ANY has uppercase, ALL are matched case-sensitively
69+
if (flags.smartCase) {
70+
const hasUpperCase = inputs.some(input => /[A-Z]/.test(String(input)));
71+
return !hasUpperCase;
72+
}
73+
74+
// Default: always ignore case (maintains backward compatibility)
75+
return true;
76+
};
77+
5378
if (cli.input.length === 0) {
5479
const interactiveInterface = await import('./interactive.js');
5580
interactiveInterface.init(cli.flags);
5681
} else {
5782
const forceAfterTimeout = cli.flags.forceAfterTimeout === undefined ? undefined : cli.flags.forceAfterTimeout * 1000;
58-
const promise = fkill(cli.input, {...cli.flags, forceAfterTimeout, ignoreCase: true});
83+
const ignoreCase = shouldIgnoreCase(cli.input, cli.flags);
84+
const promise = fkill(cli.input, {...cli.flags, forceAfterTimeout, ignoreCase});
5985

6086
if (!cli.flags.force) {
6187
try {
@@ -68,7 +94,7 @@ if (cli.input.length === 0) {
6894
}
6995

7096
const interactiveInterface = await import('./interactive.js');
71-
interactiveInterface.handleFkillError(cli.input);
97+
interactiveInterface.handleFkillError(cli.input, cli.flags);
7298
}
7399
}
74100
}

interactive.js

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -121,32 +121,37 @@ const searchProcessesByPort = (processes, port) => processes.filter(process_ =>
121121

122122
const searchProcessByPid = (processes, pid) => processes.find(process_ => String(process_.pid) === pid);
123123

124-
const searchProcessesByName = (processes, term, searcher) => {
125-
const lowerTerm = term.toLowerCase();
124+
const searchProcessesByName = (processes, term, searcher, flags = {}) => {
125+
// Determine if we should match case-sensitively
126+
const hasUpperCase = /[A-Z]/.test(term);
127+
const shouldMatchCase = flags.caseSensitive || (flags.smartCase && hasUpperCase);
128+
129+
const normalizedTerm = shouldMatchCase ? term : term.toLowerCase();
126130
const exactMatches = [];
127131
const startsWithMatches = [];
128132
const containsMatches = [];
129133

130134
for (const process_ of processes) {
131-
const lowerName = process_.name.toLowerCase();
132-
if (lowerName === lowerTerm) {
135+
const normalizedName = shouldMatchCase ? process_.name : process_.name.toLowerCase();
136+
if (normalizedName === normalizedTerm) {
133137
exactMatches.push(process_);
134-
} else if (lowerName.startsWith(lowerTerm)) {
138+
} else if (normalizedName.startsWith(normalizedTerm)) {
135139
startsWithMatches.push(process_);
136-
} else if (lowerName.includes(lowerTerm)) {
140+
} else if (normalizedName.includes(normalizedTerm)) {
137141
containsMatches.push(process_);
138142
}
139143
}
140144

141145
// Fuzzy matches (excluding all exact/starts/contains matches)
146+
// Keep fuzzy search case-insensitive for better UX
142147
const matchedPids = new Set([...exactMatches, ...startsWithMatches, ...containsMatches].map(process_ => process_.pid));
143148
const fuzzyResults = searcher.search(term).filter(process_ => !matchedPids.has(process_.pid));
144149

145150
// Combine in priority order
146151
return [...exactMatches, ...startsWithMatches, ...containsMatches, ...fuzzyResults];
147152
};
148153

149-
const filterAndSortProcesses = (processes, term, searcher) => {
154+
const filterAndSortProcesses = (processes, term, searcher, flags) => {
150155
const filtered = processes.filter(process_ => !isHelperProcess(process_));
151156

152157
// No search term: show all sorted by performance
@@ -167,16 +172,21 @@ const filterAndSortProcesses = (processes, term, searcher) => {
167172
}
168173

169174
// Search by name
170-
return searchProcessesByName(filtered, term, searcher);
175+
return searchProcessesByName(filtered, term, searcher, flags);
171176
};
172177

173-
const handleFkillError = async processes => {
174-
const shouldForceKill = await promptForceKill(processes, 'Error killing process.');
178+
const handleFkillError = async (inputs, flags = {}) => {
179+
const shouldForceKill = await promptForceKill(inputs, 'Error killing process.');
175180

176181
if (shouldForceKill) {
177-
await fkill(processes, {
182+
// Determine case sensitivity based on flags and inputs
183+
// If ANY input has uppercase letter, match case-sensitively with --smart-case
184+
const hasUpperCase = inputs.some(input => /[A-Z]/.test(String(input)));
185+
const ignoreCase = flags.caseSensitive ? false : (flags.smartCase ? !hasUpperCase : true);
186+
187+
await fkill(inputs, {
178188
force: true,
179-
ignoreCase: true,
189+
ignoreCase,
180190
});
181191
}
182192
};
@@ -250,7 +260,7 @@ const listProcesses = async (processes, flags) => {
250260
message: 'Running processes:',
251261
pageSize: 10,
252262
async source(term = '') {
253-
const matchingProcesses = filterAndSortProcesses(processes, term, searcher);
263+
const matchingProcesses = filterAndSortProcesses(processes, term, searcher, flags);
254264
return matchingProcesses.map(process_ => renderProcessForDisplay(process_, flags, memoryThreshold, cpuThreshold));
255265
},
256266
});

readme.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ $ fkill --help
2929
--verbose, -v Show process arguments
3030
--silent, -s Silently kill and always exit with code 0
3131
--force-timeout <N>, -t <N> Force kill processes which didn't exit after N seconds
32+
--smart-case Case-insensitive unless pattern contains uppercase
33+
--case-sensitive Force case-sensitive matching
3234
3335
Examples
3436
$ fkill 1337
@@ -43,7 +45,7 @@ $ fkill --help
4345
In interactive mode, 🚦n% indicates high CPU usage and 🐏n% indicates high memory usage.
4446
Supports fuzzy search in the interactive mode.
4547
46-
The process name is case insensitive.
48+
The process name is case-insensitive by default.
4749
```
4850

4951
## Interactive UI

test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import process from 'node:process';
12
import childProcess from 'node:child_process';
23
import test from 'ava';
34
import {execa} from 'execa';
@@ -55,3 +56,44 @@ test('silently force killing process at unused port exits with code 0', async t
5556
const {exitCode} = await execa('./cli.js', ['--force', '--silent', ':1337']);
5657
t.is(exitCode, 0);
5758
});
59+
60+
// Case-sensitivity tests only work on Unix-like systems
61+
// Windows process names work differently and don't support custom titles via noopProcess
62+
if (process.platform !== 'win32') {
63+
test('default case-insensitive behavior', async t => {
64+
const pid = await noopProcess({title: 'DefaultCase'});
65+
await execa('./cli.js', ['--force', 'defaultcase']);
66+
await noopProcessKilled(t, pid);
67+
});
68+
69+
test('case-sensitive flag makes matching case-sensitive', async t => {
70+
const pid = await noopProcess({title: 'CaseSensitive'});
71+
await t.throwsAsync(
72+
execa('./cli.js', ['--case-sensitive', '--force', 'casesensitive']),
73+
{message: /Killing process casesensitive failed/},
74+
);
75+
// Clean up the process
76+
await execa('./cli.js', ['--force', pid]);
77+
});
78+
79+
test('smart-case with lowercase is case-insensitive', async t => {
80+
const pid = await noopProcess({title: 'SmartLower'});
81+
await execa('./cli.js', ['--smart-case', '--force', 'smartlower']);
82+
await noopProcessKilled(t, pid);
83+
});
84+
85+
test('smart-case with uppercase is case-sensitive', async t => {
86+
const pid = await noopProcess({title: 'smartupper'});
87+
await t.throwsAsync(
88+
execa('./cli.js', ['--smart-case', '--force', 'SmartUpper']),
89+
{message: /Killing process SmartUpper failed/},
90+
);
91+
// Clean up the process
92+
await execa('./cli.js', ['--force', pid]);
93+
});
94+
}
95+
96+
test('silent flag with -s shortflag works', async t => {
97+
const {exitCode} = await execa('./cli.js', ['-s', '--force', ':1337']);
98+
t.is(exitCode, 0);
99+
});

0 commit comments

Comments
 (0)