command-line-tool-task-manager 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ankit Wahane
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # cli-task-manager
2
+
3
+ A simple and powerful command-line task manager. Add, list, complete, edit, search, and track tasks right from your terminal — no account, no cloud, just a local JSON file in your home directory.
4
+
5
+ [![CI](https://github.com/ankitw497/Command-Line-Task-Manager/actions/workflows/ci.yml/badge.svg)](https://github.com/ankitw497/Command-Line-Task-Manager/actions/workflows/ci.yml)
6
+ [![npm version](https://badge.fury.io/js/command-line-tool-task-manager.svg)](https://www.npmjs.com/package/command-line-tool-task-manager)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install -g command-line-tool-task-manager
15
+ ```
16
+
17
+ > Requires Node.js >= 18
18
+
19
+ Once installed, the `task` command is available globally.
20
+
21
+ ---
22
+
23
+ ## Quick Start
24
+
25
+ ```bash
26
+ task add "Buy groceries"
27
+ task add "Submit report" --priority high
28
+ task list
29
+ task complete 1
30
+ task stats
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Commands
36
+
37
+ ### `task add <title>`
38
+
39
+ Add a new task.
40
+
41
+ | Option | Alias | Description | Default |
42
+ |--------|-------|-------------|---------|
43
+ | `--priority <level>` | `-p` | `high`, `medium`, or `low` | `medium` |
44
+
45
+ ```bash
46
+ task add "Read documentation"
47
+ task add "Fix critical bug" -p high
48
+ task add "Clean up comments" -p low
49
+ ```
50
+
51
+ ---
52
+
53
+ ### `task list` / `task ls`
54
+
55
+ List all tasks in a formatted table, sorted by status → priority → ID.
56
+
57
+ | Option | Alias | Description |
58
+ |--------|-------|-------------|
59
+ | `--filter <status>` | `-f` | `completed` or `pending` |
60
+ | `--priority <level>` | `-p` | `high`, `medium`, or `low` |
61
+
62
+ ```bash
63
+ task list
64
+ task ls
65
+ task list --filter pending
66
+ task list --filter completed
67
+ task list --priority high
68
+ ```
69
+
70
+ ---
71
+
72
+ ### `task complete <id>` / `task done <id>`
73
+
74
+ Mark a task as completed.
75
+
76
+ ```bash
77
+ task complete 3
78
+ task done 3
79
+ ```
80
+
81
+ ---
82
+
83
+ ### `task edit <id>`
84
+
85
+ Edit a task's title or priority.
86
+
87
+ | Option | Alias | Description |
88
+ |--------|-------|-------------|
89
+ | `--title <title>` | `-t` | New title |
90
+ | `--priority <level>` | `-p` | New priority |
91
+
92
+ ```bash
93
+ task edit 2 --title "Updated title"
94
+ task edit 2 --priority high
95
+ task edit 2 -t "New title" -p low
96
+ ```
97
+
98
+ ---
99
+
100
+ ### `task delete <id>` / `task rm <id>`
101
+
102
+ Permanently delete a task.
103
+
104
+ ```bash
105
+ task delete 4
106
+ task rm 4
107
+ ```
108
+
109
+ ---
110
+
111
+ ### `task search <keyword>`
112
+
113
+ Search tasks by keyword (case-insensitive). Matching text is highlighted.
114
+
115
+ ```bash
116
+ task search bug
117
+ task search "meeting notes"
118
+ ```
119
+
120
+ ---
121
+
122
+ ### `task stats`
123
+
124
+ Show completion statistics with a visual progress bar.
125
+
126
+ ```bash
127
+ task stats
128
+ ```
129
+
130
+ ```
131
+ Task Statistics
132
+ ─────────────────────────────────
133
+ Total : 8
134
+ Completed : 5
135
+ Pending : 3
136
+ Progress : [██████████████████░░░░░░░░░░░░] 63%
137
+ ```
138
+
139
+ ---
140
+
141
+ ### `task clear`
142
+
143
+ Remove all completed tasks in one go.
144
+
145
+ ```bash
146
+ task clear
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Data Storage
152
+
153
+ Tasks are saved to `~/.task-manager/tasks.json`. This file is created automatically on first use and persists across all directories and sessions.
154
+
155
+ To use a custom location (e.g. for per-project task lists):
156
+
157
+ ```bash
158
+ TASK_MANAGER_DATA_DIR=/path/to/dir task list
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Development
164
+
165
+ ```bash
166
+ git clone https://github.com/ankitw497/Command-Line-Task-Manager.git
167
+ cd Command-Line-Task-Manager
168
+ npm install
169
+
170
+ # Run the CLI locally
171
+ node src/index.js add "Test task"
172
+
173
+ # Or link it globally for development
174
+ npm link
175
+ task add "Test task"
176
+
177
+ # Run tests
178
+ npm test
179
+ ```
180
+
181
+ ### Project Structure
182
+
183
+ ```
184
+ src/
185
+ index.js — CLI entry point (commands + argument parsing)
186
+ tasks.js — Business logic (add, complete, edit, delete, search …)
187
+ storage.js — JSON persistence (read/write ~/.task-manager/tasks.json)
188
+ display.js — Table rendering and output formatting
189
+ tests/
190
+ tasks.test.js — Unit tests for task operations (35 tests)
191
+ storage.test.js — Unit tests for storage layer
192
+ ```
193
+
194
+ ---
195
+
196
+ ## Contributing
197
+
198
+ 1. Fork the repository
199
+ 2. Create a feature branch (`git checkout -b feat/my-feature`)
200
+ 3. Commit your changes
201
+ 4. Open a Pull Request
202
+
203
+ Please ensure all tests pass (`npm test`) before submitting.
204
+
205
+ ---
206
+
207
+ ## License
208
+
209
+ MIT © [Ankit Wahane](https://github.com/ankitwahane)
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "command-line-tool-task-manager",
3
+ "version": "1.0.0",
4
+ "description": "A simple and powerful command-line task manager — add, list, complete, edit, search and track tasks right from your terminal.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "task": "./src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js",
12
+ "test": "node --test tests/tasks.test.js tests/storage.test.js"
13
+ },
14
+ "keywords": [
15
+ "cli",
16
+ "task",
17
+ "manager",
18
+ "todo",
19
+ "productivity",
20
+ "command-line",
21
+ "terminal"
22
+ ],
23
+ "author": "Ankit Wahane",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/ankitw497/Command-Line-Task-Manager.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/ankitw497/Command-Line-Task-Manager/issues"
31
+ },
32
+ "homepage": "https://github.com/ankitw497/Command-Line-Task-Manager#readme",
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "files": [
37
+ "src/",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "dependencies": {
42
+ "chalk": "^5.3.0",
43
+ "cli-table3": "^0.6.3",
44
+ "commander": "^12.0.0"
45
+ }
46
+ }
package/src/display.js ADDED
@@ -0,0 +1,91 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+
4
+ export const PRIORITY_ORDER = { high: 0, medium: 1, low: 2 };
5
+
6
+ export const PRIORITY_LABEL = {
7
+ high: chalk.red.bold('HIGH'),
8
+ medium: chalk.yellow.bold('MED'),
9
+ low: chalk.green.bold('LOW'),
10
+ };
11
+
12
+ export const PRIORITY_DOT = {
13
+ high: chalk.red('●'),
14
+ medium: chalk.yellow('●'),
15
+ low: chalk.green('●'),
16
+ };
17
+
18
+ export function formatDate(iso) {
19
+ if (!iso) return chalk.gray('—');
20
+ const d = new Date(iso);
21
+ const pad = (n) => String(n).padStart(2, '0');
22
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
23
+ }
24
+
25
+ export function formatTitle(task) {
26
+ return task.completed ? chalk.gray.strikethrough(task.title) : task.title;
27
+ }
28
+
29
+ export function statusIcon(task) {
30
+ return task.completed ? chalk.green('✔') : chalk.gray('○');
31
+ }
32
+
33
+ export function highlightMatch(text, keyword) {
34
+ const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
35
+ return text.replace(new RegExp(escaped, 'gi'), (m) => chalk.cyan.bold(m));
36
+ }
37
+
38
+ /**
39
+ * Build a formatted table for the given tasks.
40
+ * @param {object[]} tasks
41
+ * @param {(task: object) => string} [titleFn] Optional override for title cell rendering
42
+ */
43
+ export function buildTable(tasks, titleFn = null) {
44
+ const table = new Table({
45
+ head: [
46
+ chalk.cyan.bold('ID'),
47
+ chalk.cyan.bold(''),
48
+ chalk.cyan.bold('Title'),
49
+ chalk.cyan.bold('Priority'),
50
+ chalk.cyan.bold('Created'),
51
+ chalk.cyan.bold('Completed'),
52
+ ],
53
+ colWidths: [5, 3, 38, 10, 20, 20],
54
+ style: { head: [], border: ['gray'] },
55
+ wordWrap: true,
56
+ });
57
+
58
+ for (const task of tasks) {
59
+ const title = titleFn ? titleFn(task) : formatTitle(task);
60
+ table.push([
61
+ chalk.dim(String(task.id)),
62
+ statusIcon(task),
63
+ title,
64
+ `${PRIORITY_DOT[task.priority]} ${PRIORITY_LABEL[task.priority]}`,
65
+ chalk.dim(formatDate(task.createdAt)),
66
+ chalk.dim(formatDate(task.completedAt)),
67
+ ]);
68
+ }
69
+
70
+ return table;
71
+ }
72
+
73
+ export function printStats(tasks) {
74
+ const total = tasks.length;
75
+ const completed = tasks.filter((t) => t.completed).length;
76
+ const pending = total - completed;
77
+ const pct = total === 0 ? 0 : Math.round((completed / total) * 100);
78
+
79
+ const BAR_WIDTH = 30;
80
+ const filled = Math.round((pct / 100) * BAR_WIDTH);
81
+ const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(BAR_WIDTH - filled));
82
+
83
+ console.log('');
84
+ console.log(chalk.bold(' Task Statistics'));
85
+ console.log(chalk.gray(' ─────────────────────────────────'));
86
+ console.log(` Total : ${chalk.bold(total)}`);
87
+ console.log(` Completed : ${chalk.green.bold(completed)}`);
88
+ console.log(` Pending : ${chalk.yellow.bold(pending)}`);
89
+ console.log(` Progress : [${bar}] ${chalk.bold(pct + '%')}`);
90
+ console.log('');
91
+ }
package/src/index.js ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander';
4
+ import chalk from 'chalk';
5
+ import { readFileSync } from 'fs';
6
+ import { join, dirname } from 'path';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ import {
10
+ addTask,
11
+ listTasks,
12
+ completeTask,
13
+ deleteTask,
14
+ editTask,
15
+ searchTasks,
16
+ clearCompleted,
17
+ getAllTasks,
18
+ VALID_PRIORITIES,
19
+ } from './tasks.js';
20
+ import { buildTable, printStats, highlightMatch } from './display.js';
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
24
+
25
+ function handleError(err) {
26
+ console.error(chalk.red('Error: ') + err.message);
27
+ process.exit(1);
28
+ }
29
+
30
+ program
31
+ .name('task')
32
+ .description('A simple and powerful command-line task manager')
33
+ .version(pkg.version, '-V, --version', 'output the current version');
34
+
35
+ // ── add ──────────────────────────────────────────────────────────────────────
36
+
37
+ program
38
+ .command('add <title>')
39
+ .description('Add a new task')
40
+ .option('-p, --priority <level>', `Priority: ${VALID_PRIORITIES.join(' | ')}`, 'medium')
41
+ .action((title, opts) => {
42
+ try {
43
+ const task = addTask(title, opts.priority.toLowerCase());
44
+ console.log(chalk.green(`✔ Added task #${task.id}:`) + ' ' + task.title);
45
+ } catch (err) {
46
+ handleError(err);
47
+ }
48
+ });
49
+
50
+ // ── list ─────────────────────────────────────────────────────────────────────
51
+
52
+ program
53
+ .command('list')
54
+ .alias('ls')
55
+ .description('List all tasks')
56
+ .option('-f, --filter <status>', 'Filter: completed | pending')
57
+ .option('-p, --priority <level>', `Filter by priority: ${VALID_PRIORITIES.join(' | ')}`)
58
+ .action((opts) => {
59
+ try {
60
+ const tasks = listTasks({ filter: opts.filter, priority: opts.priority });
61
+ if (tasks.length === 0) {
62
+ console.log(chalk.yellow('No tasks found.'));
63
+ return;
64
+ }
65
+ console.log(buildTable(tasks).toString());
66
+ console.log(chalk.dim(` ${tasks.length} task(s)`));
67
+ } catch (err) {
68
+ handleError(err);
69
+ }
70
+ });
71
+
72
+ // ── complete ──────────────────────────────────────────────────────────────────
73
+
74
+ program
75
+ .command('complete <id>')
76
+ .alias('done')
77
+ .description('Mark a task as completed')
78
+ .action((id) => {
79
+ try {
80
+ const task = completeTask(parseInt(id, 10));
81
+ console.log(chalk.green(`✔ Completed task #${task.id}:`) + ' ' + task.title);
82
+ } catch (err) {
83
+ handleError(err);
84
+ }
85
+ });
86
+
87
+ // ── delete ────────────────────────────────────────────────────────────────────
88
+
89
+ program
90
+ .command('delete <id>')
91
+ .alias('rm')
92
+ .description('Delete a task permanently')
93
+ .action((id) => {
94
+ try {
95
+ const task = deleteTask(parseInt(id, 10));
96
+ console.log(chalk.red(`✖ Deleted task #${task.id}:`) + ' ' + task.title);
97
+ } catch (err) {
98
+ handleError(err);
99
+ }
100
+ });
101
+
102
+ // ── edit ──────────────────────────────────────────────────────────────────────
103
+
104
+ program
105
+ .command('edit <id>')
106
+ .description('Edit a task title or priority')
107
+ .option('-t, --title <title>', 'New title')
108
+ .option('-p, --priority <level>', `New priority: ${VALID_PRIORITIES.join(' | ')}`)
109
+ .action((id, opts) => {
110
+ try {
111
+ const task = editTask(parseInt(id, 10), { title: opts.title, priority: opts.priority });
112
+ console.log(chalk.blue(`✎ Updated task #${task.id}:`) + ' ' + task.title);
113
+ } catch (err) {
114
+ handleError(err);
115
+ }
116
+ });
117
+
118
+ // ── search ────────────────────────────────────────────────────────────────────
119
+
120
+ program
121
+ .command('search <keyword>')
122
+ .description('Search tasks by keyword')
123
+ .action((keyword) => {
124
+ try {
125
+ const tasks = searchTasks(keyword);
126
+ if (tasks.length === 0) {
127
+ console.log(chalk.yellow(`No tasks match "${keyword}".`));
128
+ return;
129
+ }
130
+ const table = buildTable(tasks, (task) => {
131
+ const highlighted = highlightMatch(task.title, keyword);
132
+ return task.completed ? chalk.gray.strikethrough(highlighted) : highlighted;
133
+ });
134
+ console.log(table.toString());
135
+ console.log(chalk.dim(` ${tasks.length} match(es) for "${chalk.cyan(keyword)}"`));
136
+ } catch (err) {
137
+ handleError(err);
138
+ }
139
+ });
140
+
141
+ // ── stats ─────────────────────────────────────────────────────────────────────
142
+
143
+ program
144
+ .command('stats')
145
+ .description('Show task statistics and progress')
146
+ .action(() => {
147
+ try {
148
+ printStats(getAllTasks());
149
+ } catch (err) {
150
+ handleError(err);
151
+ }
152
+ });
153
+
154
+ // ── clear ─────────────────────────────────────────────────────────────────────
155
+
156
+ program
157
+ .command('clear')
158
+ .description('Remove all completed tasks')
159
+ .action(() => {
160
+ try {
161
+ const count = clearCompleted();
162
+ if (count === 0) {
163
+ console.log(chalk.yellow('No completed tasks to clear.'));
164
+ } else {
165
+ console.log(chalk.green(`✔ Cleared ${count} completed task(s).`));
166
+ }
167
+ } catch (err) {
168
+ handleError(err);
169
+ }
170
+ });
171
+
172
+ program.parse(process.argv);
package/src/storage.js ADDED
@@ -0,0 +1,28 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ // Read dynamically so tests can override via TASK_MANAGER_DATA_DIR
6
+ function getDataDir() {
7
+ return process.env.TASK_MANAGER_DATA_DIR || join(homedir(), '.task-manager');
8
+ }
9
+
10
+ function getTasksFile() {
11
+ return join(getDataDir(), 'tasks.json');
12
+ }
13
+
14
+ export function loadTasks() {
15
+ const file = getTasksFile();
16
+ if (!existsSync(file)) return [];
17
+ try {
18
+ return JSON.parse(readFileSync(file, 'utf-8'));
19
+ } catch {
20
+ return [];
21
+ }
22
+ }
23
+
24
+ export function saveTasks(tasks) {
25
+ const dir = getDataDir();
26
+ mkdirSync(dir, { recursive: true });
27
+ writeFileSync(getTasksFile(), JSON.stringify(tasks, null, 2), 'utf-8');
28
+ }
package/src/tasks.js ADDED
@@ -0,0 +1,99 @@
1
+ import { loadTasks, saveTasks } from './storage.js';
2
+ import { PRIORITY_ORDER } from './display.js';
3
+
4
+ export const VALID_PRIORITIES = ['high', 'medium', 'low'];
5
+
6
+ function nextId(tasks) {
7
+ return tasks.length === 0 ? 1 : Math.max(...tasks.map((t) => t.id)) + 1;
8
+ }
9
+
10
+ export function sortTasks(tasks) {
11
+ return [...tasks].sort((a, b) => {
12
+ if (a.completed !== b.completed) return a.completed ? 1 : -1;
13
+ const pd = PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
14
+ if (pd !== 0) return pd;
15
+ return a.id - b.id;
16
+ });
17
+ }
18
+
19
+ export function addTask(title, priority = 'medium') {
20
+ if (!VALID_PRIORITIES.includes(priority)) {
21
+ throw new Error(`Invalid priority "${priority}". Use: ${VALID_PRIORITIES.join(', ')}`);
22
+ }
23
+ const trimmed = title.trim();
24
+ if (!trimmed) throw new Error('Task title cannot be empty.');
25
+
26
+ const tasks = loadTasks();
27
+ const task = {
28
+ id: nextId(tasks),
29
+ title: trimmed,
30
+ priority,
31
+ completed: false,
32
+ createdAt: new Date().toISOString(),
33
+ completedAt: null,
34
+ };
35
+ tasks.push(task);
36
+ saveTasks(tasks);
37
+ return task;
38
+ }
39
+
40
+ export function listTasks({ filter, priority } = {}) {
41
+ let tasks = loadTasks();
42
+ if (filter === 'completed') tasks = tasks.filter((t) => t.completed);
43
+ else if (filter === 'pending') tasks = tasks.filter((t) => !t.completed);
44
+ if (priority) tasks = tasks.filter((t) => t.priority === priority);
45
+ return sortTasks(tasks);
46
+ }
47
+
48
+ export function completeTask(id) {
49
+ const tasks = loadTasks();
50
+ const task = tasks.find((t) => t.id === id);
51
+ if (!task) throw new Error(`Task #${id} not found.`);
52
+ if (task.completed) throw new Error(`Task #${id} is already completed.`);
53
+ task.completed = true;
54
+ task.completedAt = new Date().toISOString();
55
+ saveTasks(tasks);
56
+ return task;
57
+ }
58
+
59
+ export function deleteTask(id) {
60
+ const tasks = loadTasks();
61
+ const idx = tasks.findIndex((t) => t.id === id);
62
+ if (idx === -1) throw new Error(`Task #${id} not found.`);
63
+ const [task] = tasks.splice(idx, 1);
64
+ saveTasks(tasks);
65
+ return task;
66
+ }
67
+
68
+ export function editTask(id, { title, priority } = {}) {
69
+ if (!title && !priority) {
70
+ throw new Error('Provide at least --title or --priority to edit.');
71
+ }
72
+ if (priority && !VALID_PRIORITIES.includes(priority)) {
73
+ throw new Error(`Invalid priority "${priority}". Use: ${VALID_PRIORITIES.join(', ')}`);
74
+ }
75
+ const tasks = loadTasks();
76
+ const task = tasks.find((t) => t.id === id);
77
+ if (!task) throw new Error(`Task #${id} not found.`);
78
+ if (title) task.title = title.trim();
79
+ if (priority) task.priority = priority;
80
+ saveTasks(tasks);
81
+ return task;
82
+ }
83
+
84
+ export function searchTasks(keyword) {
85
+ const kw = keyword.toLowerCase();
86
+ return sortTasks(loadTasks().filter((t) => t.title.toLowerCase().includes(kw)));
87
+ }
88
+
89
+ export function clearCompleted() {
90
+ const tasks = loadTasks();
91
+ const remaining = tasks.filter((t) => !t.completed);
92
+ const count = tasks.length - remaining.length;
93
+ saveTasks(remaining);
94
+ return count;
95
+ }
96
+
97
+ export function getAllTasks() {
98
+ return loadTasks();
99
+ }