shellwise 0.1.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 +141 -0
- package/bin/sw.js +16 -0
- package/package.json +46 -0
- package/scripts/setup.sh +92 -0
- package/scripts/uninstall.sh +36 -0
- package/src/cli/add.ts +40 -0
- package/src/cli/import.ts +76 -0
- package/src/cli/init.ts +325 -0
- package/src/cli/prune.ts +6 -0
- package/src/cli/search.ts +291 -0
- package/src/cli/stats.ts +42 -0
- package/src/cli/suggest.ts +60 -0
- package/src/daemon/client.ts +41 -0
- package/src/daemon/protocol.ts +89 -0
- package/src/daemon/server.ts +222 -0
- package/src/data/common-commands.ts +276 -0
- package/src/db/connection.ts +29 -0
- package/src/db/queries.ts +181 -0
- package/src/db/schema.ts +58 -0
- package/src/index.ts +207 -0
- package/src/search/fuzzy.ts +77 -0
- package/src/search/index.ts +59 -0
- package/src/search/scorer.ts +71 -0
- package/src/tui/components/result-list.ts +106 -0
- package/src/tui/components/search-box.ts +20 -0
- package/src/tui/components/status-bar.ts +10 -0
- package/src/tui/input.ts +71 -0
- package/src/tui/renderer.ts +45 -0
- package/src/tui/theme.ts +34 -0
- package/src/utils/paths.ts +27 -0
- package/src/utils/platform.ts +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# shellwise
|
|
2
|
+
|
|
3
|
+
Smart command history with inline auto-suggest and fuzzy search for your terminal.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Auto-save** — commands are recorded automatically after successful execution (exit code 0)
|
|
11
|
+
- **Auto-suggest** — inline dropdown appears as you type, no `Ctrl+R` needed
|
|
12
|
+
- **Fuzzy search** — `Ctrl+R` opens a full interactive search with real-time filtering
|
|
13
|
+
- **Smart ranking** — frecency scoring (frequency x recency) for relevant results
|
|
14
|
+
- **Common commands** — suggests popular commands (git, npm, docker, etc.) even with empty history
|
|
15
|
+
- **Daemon mode** — persistent background process for sub-millisecond suggest latency (~1-3ms)
|
|
16
|
+
- **Zero-fork** — uses `zsh/net/tcp` persistent connection, never spawns a process while typing
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
You type: git s
|
|
22
|
+
┌─────────────────────────────┐
|
|
23
|
+
│ › git status (history)
|
|
24
|
+
│ git stash (history)
|
|
25
|
+
│ git switch main (history)
|
|
26
|
+
│ git stash pop (common)
|
|
27
|
+
│ git stash list (common)
|
|
28
|
+
└─────────────────────────────┘
|
|
29
|
+
Tab/Shift+Tab to navigate, Enter to select, Esc to dismiss
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bun install -g shellwise
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That's it. Shell integration is auto-injected into your `~/.zshrc` or `~/.bashrc` on install. Restart your terminal to activate.
|
|
39
|
+
|
|
40
|
+
### Manual setup
|
|
41
|
+
|
|
42
|
+
If auto-setup didn't work, add to your shell config:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# ~/.zshrc
|
|
46
|
+
eval "$(sw init zsh)"
|
|
47
|
+
|
|
48
|
+
# ~/.bashrc
|
|
49
|
+
eval "$(sw init bash)"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Auto-suggest (while typing)
|
|
55
|
+
|
|
56
|
+
Just start typing — suggestions appear automatically after 2+ characters.
|
|
57
|
+
|
|
58
|
+
| Key | Action |
|
|
59
|
+
|-----|--------|
|
|
60
|
+
| `Tab` | Next suggestion |
|
|
61
|
+
| `Shift+Tab` | Previous suggestion |
|
|
62
|
+
| `Enter` | Accept selected suggestion |
|
|
63
|
+
| `→` (Right arrow) | Accept suggestion inline |
|
|
64
|
+
| `Esc` | Dismiss suggestions |
|
|
65
|
+
|
|
66
|
+
### Interactive search
|
|
67
|
+
|
|
68
|
+
Press `Ctrl+R` to open full fuzzy search:
|
|
69
|
+
|
|
70
|
+
| Key | Action |
|
|
71
|
+
|-----|--------|
|
|
72
|
+
| Type | Filter results in real-time |
|
|
73
|
+
| `↑` / `↓` | Navigate results |
|
|
74
|
+
| `Enter` | Accept and paste to command line |
|
|
75
|
+
| `Esc` | Cancel |
|
|
76
|
+
|
|
77
|
+
### Commands
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
sw search [--query <text>] # Interactive fuzzy search (Ctrl+R)
|
|
81
|
+
sw suggest --query <text> # Get top suggestion (used by shell hook)
|
|
82
|
+
sw add --command <cmd> # Save a command to history
|
|
83
|
+
sw init <zsh|bash> # Output shell integration script
|
|
84
|
+
sw import [zsh|bash] # Import existing shell history
|
|
85
|
+
sw stats # Show usage statistics
|
|
86
|
+
sw prune --days <n> # Remove entries older than n days
|
|
87
|
+
sw daemon start|stop|status # Manage background daemon
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Import existing history
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
sw import zsh # Import from ~/.zsh_history
|
|
94
|
+
sw import bash # Import from ~/.bash_history
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Architecture
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
┌──────────────┐ TCP (persistent) ┌──────────────────┐
|
|
101
|
+
│ Zsh/Bash │◄────────────────────────►│ sw daemon │
|
|
102
|
+
│ (shell) │ ~1-3ms round-trip │ (Bun process) │
|
|
103
|
+
└──────────────┘ └────────┬─────────┘
|
|
104
|
+
│
|
|
105
|
+
┌────────▼─────────┐
|
|
106
|
+
│ SQLite (WAL) │
|
|
107
|
+
│ history.db │
|
|
108
|
+
└──────────────────┘
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- **Shell hooks** (`preexec`/`precmd`) capture commands after execution
|
|
112
|
+
- **Persistent TCP connection** opened once at shell init, reused for all queries
|
|
113
|
+
- **Prepared SQLite statements** pre-warmed at daemon start for instant queries
|
|
114
|
+
- **Frecency scoring** = frequency × recency_weight — recent commands rank higher
|
|
115
|
+
- **Auto-idle shutdown** — daemon stops after 30 min of inactivity
|
|
116
|
+
|
|
117
|
+
## Data storage
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
~/.local/share/shellwise/history.db # SQLite database
|
|
121
|
+
~/.config/shellwise/ # Config (future use)
|
|
122
|
+
/tmp/shellwise-<uid>.sock # Unix socket
|
|
123
|
+
/tmp/shellwise-<uid>.pid # Daemon PID + port
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Requirements
|
|
127
|
+
|
|
128
|
+
- [Bun](https://bun.sh) >= 1.0.0
|
|
129
|
+
- Zsh or Bash
|
|
130
|
+
|
|
131
|
+
## Uninstall
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
bun remove -g shellwise
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Shell integration is automatically removed from your config on uninstall.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
package/bin/sw.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require("child_process");
|
|
4
|
+
const { resolve } = require("path");
|
|
5
|
+
|
|
6
|
+
const entry = resolve(__dirname, "..", "src", "index.ts");
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
execFileSync("bun", [entry, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
10
|
+
} catch (err) {
|
|
11
|
+
if (err.code === "ENOENT") {
|
|
12
|
+
console.error("shellwise requires Bun runtime. Install it: https://bun.sh");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
process.exit(err.status ?? 1);
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shellwise",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Smart command history with inline auto-suggest and fuzzy search for your terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sw": "./bin/sw.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/sw.js",
|
|
11
|
+
"src/**/*.ts",
|
|
12
|
+
"scripts/*.sh"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "bun run src/index.ts",
|
|
16
|
+
"build": "bun build src/index.ts --compile --outfile dist/sw",
|
|
17
|
+
"test": "bun test",
|
|
18
|
+
"postinstall": "bash scripts/setup.sh || true",
|
|
19
|
+
"preuninstall": "bash scripts/uninstall.sh || true"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"shell",
|
|
23
|
+
"history",
|
|
24
|
+
"fuzzy-search",
|
|
25
|
+
"terminal",
|
|
26
|
+
"cli",
|
|
27
|
+
"autosuggest",
|
|
28
|
+
"zsh",
|
|
29
|
+
"bash",
|
|
30
|
+
"command-history",
|
|
31
|
+
"shellwise"
|
|
32
|
+
],
|
|
33
|
+
"author": "Vu Duc Tuan",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"engines": {
|
|
36
|
+
"bun": ">=1.0.0"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/kurovu146/shellwise"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/kurovu146/shellwise#readme",
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/bun": "latest"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/scripts/setup.sh
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Post-install: auto-add shell integration
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
# Colors
|
|
7
|
+
GREEN='\033[0;32m'
|
|
8
|
+
YELLOW='\033[0;33m'
|
|
9
|
+
DIM='\033[2m'
|
|
10
|
+
BOLD='\033[1m'
|
|
11
|
+
RESET='\033[0m'
|
|
12
|
+
|
|
13
|
+
MARKER="# shellwise shell integration"
|
|
14
|
+
|
|
15
|
+
# Detect sw binary path
|
|
16
|
+
SW_BIN=""
|
|
17
|
+
if command -v sw &>/dev/null; then
|
|
18
|
+
SW_BIN="sw"
|
|
19
|
+
else
|
|
20
|
+
# Try common global bin paths
|
|
21
|
+
for p in "$HOME/.bun/bin/sw" "$HOME/.local/bin/sw" "$(npm prefix -g 2>/dev/null)/bin/sw"; do
|
|
22
|
+
if [[ -x "$p" ]]; then
|
|
23
|
+
SW_BIN="$p"
|
|
24
|
+
break
|
|
25
|
+
fi
|
|
26
|
+
done
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [[ -z "$SW_BIN" ]]; then
|
|
30
|
+
echo -e "${YELLOW}[shellwise]${RESET} Could not find sw binary. Add manually:"
|
|
31
|
+
echo ' eval "$(sw init zsh)" # add to ~/.zshrc'
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
inject_to_rc() {
|
|
36
|
+
local rc_file="$1"
|
|
37
|
+
local shell_name="$2"
|
|
38
|
+
|
|
39
|
+
# Skip if already injected
|
|
40
|
+
if grep -qF "$MARKER" "$rc_file" 2>/dev/null; then
|
|
41
|
+
echo -e "${DIM}[shellwise] Already configured in $rc_file${RESET}"
|
|
42
|
+
return
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Backup
|
|
46
|
+
if [[ -f "$rc_file" ]]; then
|
|
47
|
+
cp "$rc_file" "${rc_file}.shellwise-backup"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Append integration
|
|
51
|
+
cat >> "$rc_file" << EOF
|
|
52
|
+
|
|
53
|
+
$MARKER
|
|
54
|
+
eval "\$($SW_BIN init $shell_name)"
|
|
55
|
+
EOF
|
|
56
|
+
|
|
57
|
+
echo -e "${GREEN}${BOLD}[shellwise]${RESET}${GREEN} Added to $rc_file${RESET}"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Detect user's shell and inject
|
|
61
|
+
SHELL_NAME=$(basename "${SHELL:-/bin/zsh}")
|
|
62
|
+
|
|
63
|
+
case "$SHELL_NAME" in
|
|
64
|
+
zsh)
|
|
65
|
+
inject_to_rc "$HOME/.zshrc" "zsh"
|
|
66
|
+
;;
|
|
67
|
+
bash)
|
|
68
|
+
# Prefer .bashrc, fallback to .bash_profile on macOS
|
|
69
|
+
if [[ -f "$HOME/.bashrc" ]]; then
|
|
70
|
+
inject_to_rc "$HOME/.bashrc" "bash"
|
|
71
|
+
else
|
|
72
|
+
inject_to_rc "$HOME/.bash_profile" "bash"
|
|
73
|
+
fi
|
|
74
|
+
;;
|
|
75
|
+
*)
|
|
76
|
+
echo -e "${YELLOW}[shellwise]${RESET} Unsupported shell: $SHELL_NAME"
|
|
77
|
+
echo ' Supported: zsh, bash'
|
|
78
|
+
echo ' Add manually: eval "$(sw init zsh)"'
|
|
79
|
+
exit 0
|
|
80
|
+
;;
|
|
81
|
+
esac
|
|
82
|
+
|
|
83
|
+
# Start daemon for fast suggest
|
|
84
|
+
if command -v sw &>/dev/null; then
|
|
85
|
+
sw daemon start &>/dev/null || true
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
echo -e "${GREEN}${BOLD}[shellwise]${RESET} Restart your terminal or run: ${BOLD}source ~/.${SHELL_NAME}rc${RESET}"
|
|
89
|
+
echo -e "${DIM} Auto-save: commands are recorded automatically"
|
|
90
|
+
echo -e " Auto-suggest: inline dropdown as you type (Tab/S-Tab to navigate)"
|
|
91
|
+
echo -e " Ctrl+R: full interactive fuzzy search"
|
|
92
|
+
echo -e " Daemon: auto-started for ~1-3ms suggest speed${RESET}"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Pre-uninstall: remove shell integration
|
|
3
|
+
|
|
4
|
+
MARKER="# shellwise shell integration"
|
|
5
|
+
|
|
6
|
+
remove_from_rc() {
|
|
7
|
+
local rc_file="$1"
|
|
8
|
+
|
|
9
|
+
[[ ! -f "$rc_file" ]] && return
|
|
10
|
+
|
|
11
|
+
if grep -qF "$MARKER" "$rc_file" 2>/dev/null; then
|
|
12
|
+
# Remove the marker line and the eval line after it
|
|
13
|
+
sed -i.shellwise-uninstall '
|
|
14
|
+
/^'"$MARKER"'$/,/^eval /d
|
|
15
|
+
' "$rc_file" 2>/dev/null || {
|
|
16
|
+
# macOS sed requires different syntax
|
|
17
|
+
sed -i '.shellwise-uninstall' '
|
|
18
|
+
/^'"$MARKER"'$/,/^eval /d
|
|
19
|
+
' "$rc_file"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Remove empty trailing lines left behind
|
|
23
|
+
sed -i'' -e :a -e '/^\n*$/{$d;N;ba' -e '}' "$rc_file" 2>/dev/null
|
|
24
|
+
|
|
25
|
+
# Cleanup backup
|
|
26
|
+
rm -f "${rc_file}.shellwise-uninstall"
|
|
27
|
+
|
|
28
|
+
echo "[shellwise] Removed from $rc_file"
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
remove_from_rc "$HOME/.zshrc"
|
|
33
|
+
remove_from_rc "$HOME/.bashrc"
|
|
34
|
+
remove_from_rc "$HOME/.bash_profile"
|
|
35
|
+
|
|
36
|
+
echo "[shellwise] Uninstalled. Restart your terminal to complete."
|
package/src/cli/add.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { insertCommand } from "../db/queries";
|
|
2
|
+
import { getHostname } from "../utils/platform";
|
|
3
|
+
|
|
4
|
+
interface AddOptions {
|
|
5
|
+
command: string;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
exitCode?: number;
|
|
8
|
+
duration?: number;
|
|
9
|
+
session?: string;
|
|
10
|
+
shell?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const IGNORED_COMMANDS = new Set(["ls", "cd", "pwd", "exit", "clear", "sw"]);
|
|
14
|
+
|
|
15
|
+
export function runAdd(opts: AddOptions): void {
|
|
16
|
+
const cmd = opts.command.trim();
|
|
17
|
+
|
|
18
|
+
// Skip empty, very short, or ignored commands
|
|
19
|
+
if (!cmd || cmd.length < 2) return;
|
|
20
|
+
|
|
21
|
+
// Skip commands starting with space (convention)
|
|
22
|
+
if (opts.command.startsWith(" ")) return;
|
|
23
|
+
|
|
24
|
+
// Only save successful commands (exit code 0)
|
|
25
|
+
if (opts.exitCode !== undefined && opts.exitCode !== 0) return;
|
|
26
|
+
|
|
27
|
+
// Skip ignored commands (only the base command, not arguments)
|
|
28
|
+
const baseCmd = cmd.split(/\s+/)[0];
|
|
29
|
+
if (IGNORED_COMMANDS.has(baseCmd)) return;
|
|
30
|
+
|
|
31
|
+
insertCommand({
|
|
32
|
+
command: cmd,
|
|
33
|
+
cwd: opts.cwd,
|
|
34
|
+
exit_code: opts.exitCode,
|
|
35
|
+
duration_ms: opts.duration,
|
|
36
|
+
hostname: getHostname(),
|
|
37
|
+
session_id: opts.session,
|
|
38
|
+
shell: opts.shell,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { insertCommand } from "../db/queries";
|
|
5
|
+
import { getHostname } from "../utils/platform";
|
|
6
|
+
|
|
7
|
+
export function runImport(shell?: string): void {
|
|
8
|
+
const home = homedir();
|
|
9
|
+
let imported = 0;
|
|
10
|
+
|
|
11
|
+
if (!shell || shell === "zsh") {
|
|
12
|
+
const zshPath = join(home, ".zsh_history");
|
|
13
|
+
if (existsSync(zshPath)) {
|
|
14
|
+
imported += importZshHistory(zshPath);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!shell || shell === "bash") {
|
|
19
|
+
const bashPath = join(home, ".bash_history");
|
|
20
|
+
if (existsSync(bashPath)) {
|
|
21
|
+
imported += importBashHistory(bashPath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`Imported ${imported} commands.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function importZshHistory(path: string): number {
|
|
29
|
+
const content = readFileSync(path, "utf-8");
|
|
30
|
+
const lines = content.split("\n");
|
|
31
|
+
let count = 0;
|
|
32
|
+
const hostname = getHostname();
|
|
33
|
+
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
// zsh extended format: : timestamp:0;command
|
|
36
|
+
const extMatch = line.match(/^: (\d+):\d+;(.+)$/);
|
|
37
|
+
if (extMatch) {
|
|
38
|
+
const cmd = extMatch[2].trim();
|
|
39
|
+
if (cmd && cmd.length >= 2) {
|
|
40
|
+
insertCommand({
|
|
41
|
+
command: cmd,
|
|
42
|
+
hostname,
|
|
43
|
+
shell: "zsh",
|
|
44
|
+
});
|
|
45
|
+
count++;
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Plain format
|
|
51
|
+
const cmd = line.trim();
|
|
52
|
+
if (cmd && cmd.length >= 2 && !cmd.startsWith("#")) {
|
|
53
|
+
insertCommand({ command: cmd, hostname, shell: "zsh" });
|
|
54
|
+
count++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return count;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function importBashHistory(path: string): number {
|
|
62
|
+
const content = readFileSync(path, "utf-8");
|
|
63
|
+
const lines = content.split("\n");
|
|
64
|
+
let count = 0;
|
|
65
|
+
const hostname = getHostname();
|
|
66
|
+
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const cmd = line.trim();
|
|
69
|
+
if (cmd && cmd.length >= 2 && !cmd.startsWith("#")) {
|
|
70
|
+
insertCommand({ command: cmd, hostname, shell: "bash" });
|
|
71
|
+
count++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return count;
|
|
76
|
+
}
|