psw 0.1.4__tar.gz
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.
- psw-0.1.4/.github/workflows/workflow.yml +35 -0
- psw-0.1.4/CONTRIBUTING.md +96 -0
- psw-0.1.4/LICENSE +21 -0
- psw-0.1.4/PKG-INFO +96 -0
- psw-0.1.4/README.md +86 -0
- psw-0.1.4/pyproject.toml +24 -0
- psw-0.1.4/src/psw/__init__.py +10 -0
- psw-0.1.4/src/psw/__main__.py +7 -0
- psw-0.1.4/src/psw/cli.py +96 -0
- psw-0.1.4/src/psw/stopwatch.py +91 -0
- psw-0.1.4/src/psw/timer.py +197 -0
- psw-0.1.4/src/psw/utils.py +50 -0
- psw-0.1.4/tests/__init__.py +0 -0
- psw-0.1.4/tests/test_cli.py +58 -0
- psw-0.1.4/tests/test_timer.py +32 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published] # GitHubで「Release」を作成・公開したときに自動起動
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build-n-publish:
|
|
9
|
+
name: Build and publish Python distro to PyPI
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
# ⚠️ Trusted Publisher を使うために必須の権限設定
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write # PyPIとのOIDC認証に必要
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout code
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: '3.12'
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies (Hatch)
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install --upgrade pip
|
|
28
|
+
pip install hatch
|
|
29
|
+
|
|
30
|
+
- name: Build package
|
|
31
|
+
run: |
|
|
32
|
+
hatch build
|
|
33
|
+
|
|
34
|
+
- name: Publish package distribution to PyPI
|
|
35
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Contributing to PSW
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing to PSW! This document provides guidelines for setting up your development environment, running tests, and submitting your contributions.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🛠️ Development Environment Setup
|
|
8
|
+
|
|
9
|
+
To run and test the program during development without installing it globally, you need to add the `src` directory to your `PYTHONPATH` environment variable.
|
|
10
|
+
|
|
11
|
+
Choose the command corresponding to your operating system and terminal:
|
|
12
|
+
|
|
13
|
+
### macOS / Linux (Bash, Zsh, etc.)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export PYTHONPATH=src
|
|
17
|
+
python -m psw timer 5s
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Windows (PowerShell)
|
|
21
|
+
|
|
22
|
+
```powershell
|
|
23
|
+
$env:PYTHONPATH="src"
|
|
24
|
+
python -m psw timer 5s
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Windows (Command Prompt - `cmd.exe`)
|
|
28
|
+
|
|
29
|
+
```cmd
|
|
30
|
+
set PYTHONPATH=src
|
|
31
|
+
python -m psw timer 5s
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 🧪 Testing
|
|
35
|
+
|
|
36
|
+
We use `pytest` for running our automated test suite. Before running tests, ensure you have the development dependencies installed.
|
|
37
|
+
|
|
38
|
+
### 1. Install Development Dependencies
|
|
39
|
+
|
|
40
|
+
Navigate to the root directory of the project and install the package in editable mode with test dependencies:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install -e ".[test]"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2. Run Tests
|
|
47
|
+
|
|
48
|
+
You can run the tests using either of the following commands:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Recommended
|
|
52
|
+
python -m pytest tests
|
|
53
|
+
|
|
54
|
+
# Alternative
|
|
55
|
+
pytest
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 🤝 How to Contribute
|
|
59
|
+
|
|
60
|
+
### 1. Reporting Bugs & Suggesting Features
|
|
61
|
+
|
|
62
|
+
* Check the [Issues] tab to see if your bug or feature request has already been reported.
|
|
63
|
+
* If not, open a new issue. Please provide a clear description, steps to reproduce (for bugs), and your OS/Python version.
|
|
64
|
+
|
|
65
|
+
### 2. Submitting Pull Requests
|
|
66
|
+
|
|
67
|
+
1. **Fork** the repository and create your branch from `main`.
|
|
68
|
+
```bash
|
|
69
|
+
git checkout -b feature/your-feature-name
|
|
70
|
+
```
|
|
71
|
+
2. **Implement your changes** and make sure you adhere to the project's coding style.
|
|
72
|
+
3. **Write tests** for any new functionality or bug fixes.
|
|
73
|
+
4. **Run the test suite** to ensure everything passes:
|
|
74
|
+
```bash
|
|
75
|
+
python -m pytest tests
|
|
76
|
+
```
|
|
77
|
+
5. **Commit your changes** with a clear and descriptive commit message.
|
|
78
|
+
6. **Push** to your fork and **submit a Pull Request (PR)** to the `main` branch of this repository.
|
|
79
|
+
|
|
80
|
+
## 🎨 Coding Guidelines
|
|
81
|
+
|
|
82
|
+
To keep the codebase clean and consistent, please follow these guidelines:
|
|
83
|
+
|
|
84
|
+
* **PEP 8**: Follow standard Python coding style guidelines.
|
|
85
|
+
* **Type Hints**: Use type hinting for function arguments and return values where appropriate.
|
|
86
|
+
* **Docstrings**: Add docstrings to new classes and functions to explain their purpose and behavior.
|
|
87
|
+
|
|
88
|
+
## To Build Package
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
# 1. Install packaging tool
|
|
92
|
+
pip install pyinstaller
|
|
93
|
+
|
|
94
|
+
# 2. Build
|
|
95
|
+
python -m PyInstaller --onefile --paths src --name psw src/psw/__main__.py
|
|
96
|
+
```
|
psw-0.1.4/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kenji Otsuka
|
|
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.
|
psw-0.1.4/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: psw
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: A lightweight CLI stopwatch and timer in Python
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Provides-Extra: test
|
|
8
|
+
Requires-Dist: pytest>=7.0.0; extra == 'test'
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# PSW - Python Stop Watch
|
|
12
|
+
|
|
13
|
+
`psw` is a lightweight, intuitive Command Line Interface (CLI) stopwatch and timer utility written in Python. It features live console rendering, interactive real-time controls, and built-in repeat functionalities without requiring any complex external audio dependencies.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
You can install `psw` via pip:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install psw
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### 1. Stopwatch Mode
|
|
26
|
+
|
|
27
|
+
Measure elapsed time directly in your terminal.
|
|
28
|
+
|
|
29
|
+
#### Start the Stopwatch
|
|
30
|
+
|
|
31
|
+
To start the stopwatch, run:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
psw start
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The live clock will be displayed in the console:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
00 h 00 m 00.000 s
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### Options
|
|
44
|
+
|
|
45
|
+
* **`-p, --precision`**: Specifies the number of decimal places for seconds.
|
|
46
|
+
* **Default**: `3` (millisecond precision)
|
|
47
|
+
* **Example**: `psw start -p 2` will display `00 h 00 m 00.00 s`
|
|
48
|
+
|
|
49
|
+
#### Interactive Controls (While Running)
|
|
50
|
+
|
|
51
|
+
While the stopwatch is active, you can control it in real-time using the following keys:
|
|
52
|
+
|
|
53
|
+
* **`s`**: Pause / Resume the stopwatch.
|
|
54
|
+
* **`l`**: Record a lap time. The current lap time will be printed below the running clock without stopping the main timer.
|
|
55
|
+
* **`q` (or `Ctrl+C`)**: Quit the stopwatch and display the final summary.
|
|
56
|
+
|
|
57
|
+
### 2. Timer Mode
|
|
58
|
+
|
|
59
|
+
Count down from a specified duration with advanced repeat and notification options.
|
|
60
|
+
|
|
61
|
+
#### Start the Timer
|
|
62
|
+
|
|
63
|
+
To start a countdown, use the `timer` command followed by the duration. You can specify hours (`h`), minutes (`m`), and seconds (`s`).
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Set a timer for 5 minutes and 30 seconds
|
|
67
|
+
psw timer 5m 30s
|
|
68
|
+
|
|
69
|
+
# Set a timer for 1 hour
|
|
70
|
+
psw timer 1h
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The countdown will be displayed in the console:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
00 h 05 m 30.000 s
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### Options
|
|
81
|
+
|
|
82
|
+
* **`-p, --precision`**: Same as the stopwatch mode, specifies the decimal places for seconds (Default: `3`).
|
|
83
|
+
* **`-r, --repeat [<integer>]`**: Restarts the timer when it reaches zero.
|
|
84
|
+
* Use without arguments for an **infinite loop**.
|
|
85
|
+
* *Example*: `psw timer 1m -r`
|
|
86
|
+
* Specify an integer to repeat a **specific number of times**.
|
|
87
|
+
* *Example*: `psw timer 1m -r 3` (Runs a 1-minute timer 3 times)
|
|
88
|
+
* **`-m, --mute`**: Disables the system alert sound (terminal bell) when the timer ends.
|
|
89
|
+
|
|
90
|
+
#### Interactive Controls (While Running)
|
|
91
|
+
|
|
92
|
+
* **`s`**: Pause / Resume the countdown.
|
|
93
|
+
* **`q` (or `Ctrl+C`)**: Cancel and exit the timer.
|
|
94
|
+
|
|
95
|
+
> [!NOTE]
|
|
96
|
+
> When the timer reaches `00 h 00 m 00.000 s`, the application plays a platform-appropriate alert notification (using `winsound.MessageBeep()` on Windows, and emitting a standard terminal bell sound `\a` on other platforms or as a fallback). No special OS-level audio permissions or external audio library imports are required, making it lightweight and cross-platform compatible.
|
psw-0.1.4/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# PSW - Python Stop Watch
|
|
2
|
+
|
|
3
|
+
`psw` is a lightweight, intuitive Command Line Interface (CLI) stopwatch and timer utility written in Python. It features live console rendering, interactive real-time controls, and built-in repeat functionalities without requiring any complex external audio dependencies.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
You can install `psw` via pip:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install psw
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### 1. Stopwatch Mode
|
|
16
|
+
|
|
17
|
+
Measure elapsed time directly in your terminal.
|
|
18
|
+
|
|
19
|
+
#### Start the Stopwatch
|
|
20
|
+
|
|
21
|
+
To start the stopwatch, run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
psw start
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The live clock will be displayed in the console:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
00 h 00 m 00.000 s
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
#### Options
|
|
34
|
+
|
|
35
|
+
* **`-p, --precision`**: Specifies the number of decimal places for seconds.
|
|
36
|
+
* **Default**: `3` (millisecond precision)
|
|
37
|
+
* **Example**: `psw start -p 2` will display `00 h 00 m 00.00 s`
|
|
38
|
+
|
|
39
|
+
#### Interactive Controls (While Running)
|
|
40
|
+
|
|
41
|
+
While the stopwatch is active, you can control it in real-time using the following keys:
|
|
42
|
+
|
|
43
|
+
* **`s`**: Pause / Resume the stopwatch.
|
|
44
|
+
* **`l`**: Record a lap time. The current lap time will be printed below the running clock without stopping the main timer.
|
|
45
|
+
* **`q` (or `Ctrl+C`)**: Quit the stopwatch and display the final summary.
|
|
46
|
+
|
|
47
|
+
### 2. Timer Mode
|
|
48
|
+
|
|
49
|
+
Count down from a specified duration with advanced repeat and notification options.
|
|
50
|
+
|
|
51
|
+
#### Start the Timer
|
|
52
|
+
|
|
53
|
+
To start a countdown, use the `timer` command followed by the duration. You can specify hours (`h`), minutes (`m`), and seconds (`s`).
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Set a timer for 5 minutes and 30 seconds
|
|
57
|
+
psw timer 5m 30s
|
|
58
|
+
|
|
59
|
+
# Set a timer for 1 hour
|
|
60
|
+
psw timer 1h
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The countdown will be displayed in the console:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
00 h 05 m 30.000 s
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Options
|
|
71
|
+
|
|
72
|
+
* **`-p, --precision`**: Same as the stopwatch mode, specifies the decimal places for seconds (Default: `3`).
|
|
73
|
+
* **`-r, --repeat [<integer>]`**: Restarts the timer when it reaches zero.
|
|
74
|
+
* Use without arguments for an **infinite loop**.
|
|
75
|
+
* *Example*: `psw timer 1m -r`
|
|
76
|
+
* Specify an integer to repeat a **specific number of times**.
|
|
77
|
+
* *Example*: `psw timer 1m -r 3` (Runs a 1-minute timer 3 times)
|
|
78
|
+
* **`-m, --mute`**: Disables the system alert sound (terminal bell) when the timer ends.
|
|
79
|
+
|
|
80
|
+
#### Interactive Controls (While Running)
|
|
81
|
+
|
|
82
|
+
* **`s`**: Pause / Resume the countdown.
|
|
83
|
+
* **`q` (or `Ctrl+C`)**: Cancel and exit the timer.
|
|
84
|
+
|
|
85
|
+
> [!NOTE]
|
|
86
|
+
> When the timer reaches `00 h 00 m 00.000 s`, the application plays a platform-appropriate alert notification (using `winsound.MessageBeep()` on Windows, and emitting a standard terminal bell sound `\a` on other platforms or as a fallback). No special OS-level audio permissions or external audio library imports are required, making it lightweight and cross-platform compatible.
|
psw-0.1.4/pyproject.toml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "psw"
|
|
7
|
+
version = "0.1.4"
|
|
8
|
+
description = "A lightweight CLI stopwatch and timer in Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
# external libraries
|
|
12
|
+
dependencies = []
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
# psw in command calls main function in cli.py
|
|
16
|
+
psw = "psw.cli:main"
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
test = [
|
|
20
|
+
"pytest>=7.0.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["src/psw"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# パッケージ外から直接 Stopwatch や Timer をインポートできるように公開します。
|
|
2
|
+
# 例: from psw import Stopwatch, Timer
|
|
3
|
+
from psw.stopwatch import Stopwatch
|
|
4
|
+
from psw.timer import Timer
|
|
5
|
+
|
|
6
|
+
# パッケージのバージョン(pyproject.toml と合わせておくと管理しやすいです)
|
|
7
|
+
__version__ = "0.1.4"
|
|
8
|
+
|
|
9
|
+
# `from psw import *` をした際にインポートされるクラスを指定
|
|
10
|
+
__all__ = ["Stopwatch", "Timer"]
|
psw-0.1.4/src/psw/cli.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
# 親パーサーの設定(プログラム全体のヘルプや基本情報を定義)
|
|
7
|
+
parser = argparse.ArgumentParser(
|
|
8
|
+
description="PSW - Python Stop Watch & Timer CLI tool",
|
|
9
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# サブコマンド(start / timer)を管理するパーサーを追加
|
|
13
|
+
subparsers = parser.add_subparsers(
|
|
14
|
+
dest="command",
|
|
15
|
+
required=True,
|
|
16
|
+
help="Subcommands"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------
|
|
20
|
+
# 1. 'start' (Stopwatch) サブコマンドの設定
|
|
21
|
+
# ---------------------------------------------------------
|
|
22
|
+
parser_start = subparsers.add_parser(
|
|
23
|
+
"start",
|
|
24
|
+
help="Start the stopwatch"
|
|
25
|
+
)
|
|
26
|
+
parser_start.add_argument(
|
|
27
|
+
"-p", "--precision",
|
|
28
|
+
type=int,
|
|
29
|
+
default=3,
|
|
30
|
+
help="Decimal places for seconds (default: 3)"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------
|
|
34
|
+
# 2. 'timer' サブコマンドの設定
|
|
35
|
+
# ---------------------------------------------------------
|
|
36
|
+
parser_timer = subparsers.add_parser(
|
|
37
|
+
"timer",
|
|
38
|
+
help="Start the countdown timer"
|
|
39
|
+
)
|
|
40
|
+
# 時間指定(例: 5m, 30s, 1h 30m など可変長の引数を受け取る)
|
|
41
|
+
parser_timer.add_argument(
|
|
42
|
+
"duration",
|
|
43
|
+
nargs="+",
|
|
44
|
+
help="Duration for the timer (e.g., '5m 30s', '1h', '45s')"
|
|
45
|
+
)
|
|
46
|
+
parser_timer.add_argument(
|
|
47
|
+
"-p", "--precision",
|
|
48
|
+
type=int,
|
|
49
|
+
default=3,
|
|
50
|
+
help="Decimal places for seconds (default: 3)"
|
|
51
|
+
)
|
|
52
|
+
parser_timer.add_argument(
|
|
53
|
+
"-r", "--repeat",
|
|
54
|
+
nargs="?", # optional
|
|
55
|
+
const=-1, # -1 when only `-r` is used without scceeding number
|
|
56
|
+
default=1,
|
|
57
|
+
type=int, # Recognize value as Integer when specified
|
|
58
|
+
help="Repeat the timer. Use '-r' for infinite loop, or '-r N' to repeat N times."
|
|
59
|
+
)
|
|
60
|
+
parser_timer.add_argument(
|
|
61
|
+
"-m", "--mute",
|
|
62
|
+
action="store_true",
|
|
63
|
+
help="Mute the terminal bell (\a) alert sound when the timer ends"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
args = parser.parse_args()
|
|
67
|
+
|
|
68
|
+
# 各コマンドに応じた処理の分岐
|
|
69
|
+
if args.command == "start":
|
|
70
|
+
try:
|
|
71
|
+
from psw.stopwatch import Stopwatch
|
|
72
|
+
# ストップウォッチの初期化と実行
|
|
73
|
+
sw = Stopwatch(precision=args.precision)
|
|
74
|
+
sw.run()
|
|
75
|
+
except ImportError:
|
|
76
|
+
print("[Error] 'stopwatch.py' is not implemented yet.", file=sys.stderr)
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
|
|
79
|
+
elif args.command == "timer":
|
|
80
|
+
try:
|
|
81
|
+
from psw.timer import Timer
|
|
82
|
+
# タイマーの初期化と実行
|
|
83
|
+
timer = Timer(
|
|
84
|
+
duration_strings=args.duration,
|
|
85
|
+
precision=args.precision,
|
|
86
|
+
repeat=args.repeat,
|
|
87
|
+
mute=args.mute
|
|
88
|
+
)
|
|
89
|
+
timer.run()
|
|
90
|
+
except ImportError:
|
|
91
|
+
print("[Error] 'timer.py' is not implemented yet.", file=sys.stderr)
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
main()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import sys
|
|
3
|
+
from psw.utils import KeyListener
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Stopwatch:
|
|
7
|
+
def __init__(self, precision: int = 3):
|
|
8
|
+
if precision < 0:
|
|
9
|
+
raise ValueError("precision must be >= 0")
|
|
10
|
+
self.precision = precision
|
|
11
|
+
self.listener = KeyListener()
|
|
12
|
+
|
|
13
|
+
self.start_time = 0.0
|
|
14
|
+
self.elapsed_time = 0.0
|
|
15
|
+
self.running = False
|
|
16
|
+
self.laps = []
|
|
17
|
+
|
|
18
|
+
def format_time(self, seconds: float) -> str:
|
|
19
|
+
"""秒数を '00 h 00 m 00.000 s' のフォーマットに整形する"""
|
|
20
|
+
h = int(seconds // 3600)
|
|
21
|
+
m = int((seconds % 3600) // 60)
|
|
22
|
+
s = seconds % 60
|
|
23
|
+
|
|
24
|
+
# 指定された精度(小数点以下)で秒数をフォーマット
|
|
25
|
+
seconds_str = f"{s:02.{self.precision}f}"
|
|
26
|
+
# 万が一秒が繰り上がって60.00...になった場合の簡易補正
|
|
27
|
+
if float(seconds_str) >= 60.0:
|
|
28
|
+
seconds_str = f"{0.0:02.{self.precision}f}"
|
|
29
|
+
m += 1
|
|
30
|
+
if m >= 60:
|
|
31
|
+
m = 0
|
|
32
|
+
h += 1
|
|
33
|
+
|
|
34
|
+
return f"{h:02d} h {m:02d} m {seconds_str} s"
|
|
35
|
+
|
|
36
|
+
def run(self):
|
|
37
|
+
self.start_time = time.monotonic()
|
|
38
|
+
self.running = True
|
|
39
|
+
|
|
40
|
+
print("Stopwatch started. Press 's' to pause/resume, 'l' to record lap, 'q' to quit.\n")
|
|
41
|
+
# 1行分のスペースをあける(リアルタイム描画用)
|
|
42
|
+
print()
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
while True:
|
|
46
|
+
if self.running:
|
|
47
|
+
self.elapsed_time = time.monotonic() - self.start_time
|
|
48
|
+
|
|
49
|
+
# カーソルを上に戻して、現在の時間を描画
|
|
50
|
+
# \033[F moves cursor to the previous line,
|
|
51
|
+
# \033[K clear the text to the end of the line.
|
|
52
|
+
sys.stdout.write(f"\033[F\033[K{self.format_time(self.elapsed_time)}\n")
|
|
53
|
+
sys.stdout.flush()
|
|
54
|
+
|
|
55
|
+
# check key input
|
|
56
|
+
char = self.listener.get_char()
|
|
57
|
+
if char == 'q':
|
|
58
|
+
break
|
|
59
|
+
elif char == 's':
|
|
60
|
+
if self.running:
|
|
61
|
+
# stop
|
|
62
|
+
self.elapsed_time = time.monotonic() - self.start_time
|
|
63
|
+
self.running = False
|
|
64
|
+
else:
|
|
65
|
+
# resume
|
|
66
|
+
self.start_time = time.monotonic() - self.elapsed_time
|
|
67
|
+
self.running = True
|
|
68
|
+
elif char == 'l':
|
|
69
|
+
if self.running:
|
|
70
|
+
lap_num = len(self.laps) + 1
|
|
71
|
+
lap_str = f" Lap #{lap_num:02d}: {self.format_time(self.elapsed_time)}"
|
|
72
|
+
self.laps.append(lap_str)
|
|
73
|
+
# 一旦現在の時間表示の下にラップを挿入するために、描画位置を調整
|
|
74
|
+
print(lap_str)
|
|
75
|
+
print() # 次の時間描画のための空行
|
|
76
|
+
|
|
77
|
+
# CPU負荷を下げるためのウェイト(ミリ秒精度に合わせて調整)
|
|
78
|
+
# 精度が高い場合は更新頻度を上げ、低い場合は下げる
|
|
79
|
+
sleep_time = 0.01 if self.precision > 2 else 0.05
|
|
80
|
+
time.sleep(sleep_time)
|
|
81
|
+
|
|
82
|
+
finally:
|
|
83
|
+
self.listener.close()
|
|
84
|
+
|
|
85
|
+
# 終了時のまとめ表示
|
|
86
|
+
print(f"\n--- Finished ---")
|
|
87
|
+
print(f"Total Time: {self.format_time(self.elapsed_time)}")
|
|
88
|
+
if self.laps:
|
|
89
|
+
print("Laps:")
|
|
90
|
+
for lap in self.laps:
|
|
91
|
+
print(lap)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import sys
|
|
3
|
+
import re
|
|
4
|
+
import os
|
|
5
|
+
from psw.utils import KeyListener
|
|
6
|
+
|
|
7
|
+
is_windows = os.name == "nt" or sys.platform.startswith("win")
|
|
8
|
+
if is_windows:
|
|
9
|
+
import winsound
|
|
10
|
+
|
|
11
|
+
class Timer:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
duration_strings: list,
|
|
15
|
+
precision: int = 3,
|
|
16
|
+
repeat: int = 1,
|
|
17
|
+
mute: bool = False,
|
|
18
|
+
):
|
|
19
|
+
if precision < 0:
|
|
20
|
+
raise ValueError("precision must be >= 0")
|
|
21
|
+
self.duration_strings = duration_strings
|
|
22
|
+
self.precision = precision
|
|
23
|
+
self.repeat = repeat
|
|
24
|
+
self.mute = mute
|
|
25
|
+
|
|
26
|
+
self.listener = KeyListener()
|
|
27
|
+
|
|
28
|
+
# 入力された文字列(['5m', '30s'] など)から合計秒数を算出
|
|
29
|
+
self.total_seconds = self.parse_duration(duration_strings)
|
|
30
|
+
self.remaining_time = self.total_seconds
|
|
31
|
+
|
|
32
|
+
self.running = False
|
|
33
|
+
self.start_time = 0.0
|
|
34
|
+
|
|
35
|
+
def parse_duration(self, duration_strings: list) -> float:
|
|
36
|
+
"""
|
|
37
|
+
['1h', '30m', '45s'] のような入力リストを解析し、合計秒数を返す。
|
|
38
|
+
単位が指定されていない単なる数値は「秒」として扱う。
|
|
39
|
+
"""
|
|
40
|
+
total = 0.0
|
|
41
|
+
# 時間パース用の正規表現 (例: "5.5m", "10s", "1h")
|
|
42
|
+
pattern = re.compile(r"^([\d.]+)([hms]?)$")
|
|
43
|
+
|
|
44
|
+
for s in duration_strings:
|
|
45
|
+
s = s.strip().lower()
|
|
46
|
+
match = pattern.match(s)
|
|
47
|
+
if not match:
|
|
48
|
+
print(f"[Warning] Could not parse duration part: '{s}'. Ignored.", file=sys.stderr)
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
value_str, unit = match.groups()
|
|
52
|
+
try:
|
|
53
|
+
value = float(value_str)
|
|
54
|
+
except ValueError:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
if unit == "h":
|
|
58
|
+
total += value * 3600
|
|
59
|
+
elif unit == "m":
|
|
60
|
+
total += value * 60
|
|
61
|
+
elif unit == "s" or unit == "":
|
|
62
|
+
total += value
|
|
63
|
+
|
|
64
|
+
if total <= 0:
|
|
65
|
+
print("[Error] Total duration must be greater than 0.", file=sys.stderr)
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
return total
|
|
69
|
+
|
|
70
|
+
def format_time(self, seconds: float) -> str:
|
|
71
|
+
"""秒数を '00 h 00 m 00.000 s' のフォーマットに整形する"""
|
|
72
|
+
if seconds < 0:
|
|
73
|
+
seconds = 0.0
|
|
74
|
+
|
|
75
|
+
h = int(seconds // 3600)
|
|
76
|
+
m = int((seconds % 3600) // 60)
|
|
77
|
+
s = seconds % 60
|
|
78
|
+
|
|
79
|
+
seconds_str = f"{s:02.{self.precision}f}"
|
|
80
|
+
if float(seconds_str) >= 60.0:
|
|
81
|
+
seconds_str = f"{0.0:02.{self.precision}f}"
|
|
82
|
+
m += 1
|
|
83
|
+
if m >= 60:
|
|
84
|
+
m = 0
|
|
85
|
+
h += 1
|
|
86
|
+
|
|
87
|
+
return f"{h:02d} h {m:02d} m {seconds_str} s"
|
|
88
|
+
|
|
89
|
+
def run_single_timer(self, cycle_num: int = None) -> bool:
|
|
90
|
+
"""
|
|
91
|
+
1回分のタイマーカウントダウンを実行する。
|
|
92
|
+
戻り値: True (完走した), False (ユーザーが 'q' で中断した)
|
|
93
|
+
"""
|
|
94
|
+
self.remaining_time = self.total_seconds
|
|
95
|
+
self.start_time = time.monotonic()
|
|
96
|
+
self.running = True
|
|
97
|
+
|
|
98
|
+
# 進捗タイトルの表示
|
|
99
|
+
if cycle_num is not None:
|
|
100
|
+
print(f"--- Timer Cycle #{cycle_num} ---")
|
|
101
|
+
else:
|
|
102
|
+
print("--- Timer Started ---")
|
|
103
|
+
print("Press 's' to pause/resume, 'q' to quit.\n\n")
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
while self.remaining_time > 0:
|
|
107
|
+
if self.running:
|
|
108
|
+
# 経過時間を引き、残りの時間を計算
|
|
109
|
+
elapsed = time.monotonic() - self.start_time
|
|
110
|
+
self.remaining_time = self.total_seconds - elapsed
|
|
111
|
+
if self.remaining_time < 0:
|
|
112
|
+
self.remaining_time = 0.0
|
|
113
|
+
|
|
114
|
+
# カーソルを上に戻して、現在の時間を描画
|
|
115
|
+
sys.stdout.write(f"\033[F\033[K{self.format_time(self.remaining_time)}\n")
|
|
116
|
+
sys.stdout.flush()
|
|
117
|
+
|
|
118
|
+
if self.remaining_time <= 0:
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
# キー入力チェック
|
|
122
|
+
char = self.listener.get_char()
|
|
123
|
+
if char == 'q':
|
|
124
|
+
return False
|
|
125
|
+
elif char == 's':
|
|
126
|
+
if self.running:
|
|
127
|
+
# 一時停止
|
|
128
|
+
self.running = False
|
|
129
|
+
else:
|
|
130
|
+
# 再開
|
|
131
|
+
elapsed = self.total_seconds - self.remaining_time
|
|
132
|
+
self.start_time = time.monotonic() - elapsed
|
|
133
|
+
self.running = True
|
|
134
|
+
|
|
135
|
+
sleep_time = 0.01 if self.precision > 2 else 0.05
|
|
136
|
+
time.sleep(sleep_time)
|
|
137
|
+
|
|
138
|
+
# タイムアップ時、00:00:00.000を表示させる
|
|
139
|
+
sys.stdout.write(f"\033[F\033[K{self.format_time(0.0)}\n")
|
|
140
|
+
sys.stdout.flush()
|
|
141
|
+
|
|
142
|
+
# ビープ音の鳴動
|
|
143
|
+
if not self.mute:
|
|
144
|
+
if is_windows:
|
|
145
|
+
try:
|
|
146
|
+
# winsound.MessageBeep() は Windows標準のアラート音を鳴らします
|
|
147
|
+
# (winsound.Beep(1000, 500) で特定の周波数を鳴らすことも可能です)
|
|
148
|
+
winsound.MessageBeep()
|
|
149
|
+
except Exception:
|
|
150
|
+
sys.stdout.write("\a")
|
|
151
|
+
sys.stdout.flush()
|
|
152
|
+
else:
|
|
153
|
+
# mac or linux
|
|
154
|
+
sys.stdout.write("\a")
|
|
155
|
+
sys.stdout.flush()
|
|
156
|
+
|
|
157
|
+
print("\nTime's up!")
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
finally:
|
|
161
|
+
# 1回分の表示領域をクリアして改行を整える
|
|
162
|
+
print()
|
|
163
|
+
|
|
164
|
+
def run(self):
|
|
165
|
+
print("run")
|
|
166
|
+
try:
|
|
167
|
+
# 1. 回数指定リピートがある場合
|
|
168
|
+
if self.repeat > 0:
|
|
169
|
+
for i in range(1, self.repeat + 1):
|
|
170
|
+
completed = self.run_single_timer(cycle_num=i)
|
|
171
|
+
if not completed:
|
|
172
|
+
print("Timer canceled by user.")
|
|
173
|
+
break
|
|
174
|
+
# ループ間に少しウェイトを挟む(即座に次が始まって画面が崩れるのを防ぐ)
|
|
175
|
+
if i < self.repeat:
|
|
176
|
+
time.sleep(1)
|
|
177
|
+
|
|
178
|
+
# 2. 無限リピート(-r)の場合
|
|
179
|
+
elif self.repeat == -1:
|
|
180
|
+
cycle = 1
|
|
181
|
+
while True:
|
|
182
|
+
completed = self.run_single_timer(cycle_num=cycle)
|
|
183
|
+
if not completed:
|
|
184
|
+
print("Timer canceled by user.")
|
|
185
|
+
break
|
|
186
|
+
cycle += 1
|
|
187
|
+
time.sleep(1)
|
|
188
|
+
|
|
189
|
+
# 3. 通常の1回切りタイマーの場合
|
|
190
|
+
else:
|
|
191
|
+
print("Invalid repeat count. It must be greater than 0, or -1 for infinite.")
|
|
192
|
+
# completed = self.run_single_timer()
|
|
193
|
+
# if not completed:
|
|
194
|
+
# print("Timer canceled by user.")
|
|
195
|
+
|
|
196
|
+
finally:
|
|
197
|
+
self.listener.close()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
# OS判定
|
|
4
|
+
is_windows = sys.platform.startswith("win")
|
|
5
|
+
|
|
6
|
+
if is_windows:
|
|
7
|
+
import msvcrt
|
|
8
|
+
else:
|
|
9
|
+
import select
|
|
10
|
+
import tty
|
|
11
|
+
import termios
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class KeyListener:
|
|
15
|
+
"""ノンブロッキングでキー入力を監視するクラス"""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.old_settings = None
|
|
19
|
+
if not is_windows and sys.stdin.isatty():
|
|
20
|
+
# ターミナルの標準入力設定を保存
|
|
21
|
+
self.old_settings = termios.tcgetattr(sys.stdin)
|
|
22
|
+
|
|
23
|
+
def get_char(self) -> str:
|
|
24
|
+
"""押されたキーを1文字取得する(押されていなければ空文字を返す)"""
|
|
25
|
+
if is_windows:
|
|
26
|
+
if msvcrt.kbhit():
|
|
27
|
+
# Windowsのキー取得
|
|
28
|
+
try:
|
|
29
|
+
char = msvcrt.getch().decode("utf-8").lower()
|
|
30
|
+
return char
|
|
31
|
+
except UnicodeDecodeError:
|
|
32
|
+
return ""
|
|
33
|
+
return ""
|
|
34
|
+
else:
|
|
35
|
+
# macOS/Linuxのキー取得(cbreakモードに一時的に切り替える)
|
|
36
|
+
try:
|
|
37
|
+
tty.setcbreak(sys.stdin.fileno())
|
|
38
|
+
# 読み込み可能か確認(タイムアウト0秒)
|
|
39
|
+
dr, _, _ = select.select([sys.stdin], [], [], 0)
|
|
40
|
+
if dr:
|
|
41
|
+
return sys.stdin.read(1).lower()
|
|
42
|
+
return ""
|
|
43
|
+
finally:
|
|
44
|
+
# ターミナル設定を元に戻す
|
|
45
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)
|
|
46
|
+
|
|
47
|
+
def close(self):
|
|
48
|
+
"""終了処理(ターミナルの設定を完全に復元)"""
|
|
49
|
+
if not is_windows and self.old_settings:
|
|
50
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
import pytest
|
|
4
|
+
from psw.cli import main
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# 1. 存在しないサブコマンドや不正なオプションが指定された場合のテスト
|
|
8
|
+
def test_cli_invalid_command():
|
|
9
|
+
# 引数に存在しないコマンド 'invalid_cmd' を渡す模擬設定
|
|
10
|
+
with patch.object(sys, "argv", ["psw", "invalid_cmd"]):
|
|
11
|
+
# argparseはパースに失敗すると SystemExit(code=2) を発生させます
|
|
12
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
13
|
+
main()
|
|
14
|
+
assert excinfo.value.code == 2
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# 2. 'start' (Stopwatch) コマンドのパーステスト
|
|
18
|
+
@patch("psw.stopwatch.Stopwatch")
|
|
19
|
+
def test_cli_start_command(mock_stopwatch):
|
|
20
|
+
# 'psw start -p 2' が入力されたと仮定
|
|
21
|
+
with patch.object(sys, "argv", ["psw", "start", "-p", "2"]):
|
|
22
|
+
main()
|
|
23
|
+
|
|
24
|
+
# Stopwatchクラスが正しい引数でインスタンス化され、run()が実行されたか検証
|
|
25
|
+
mock_stopwatch.assert_called_once_with(precision=2)
|
|
26
|
+
mock_stopwatch.return_value.run.assert_called_once()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# 3. 'timer' コマンドのパーステスト
|
|
30
|
+
@patch("psw.timer.Timer")
|
|
31
|
+
def test_cli_timer_command(mock_timer):
|
|
32
|
+
# 'psw timer 1m 30s -p 1 -r -m' が入力されたと仮定
|
|
33
|
+
with patch.object(sys, "argv", ["psw", "timer", "1m", "30s", "-p", "1", "-r", "-m"]):
|
|
34
|
+
main()
|
|
35
|
+
|
|
36
|
+
# Timerクラスが各オプションを正しく受け取ってインスタンス化されたか検証
|
|
37
|
+
mock_timer.assert_called_once_with(
|
|
38
|
+
duration_strings=["1m", "30s"],
|
|
39
|
+
precision=1,
|
|
40
|
+
repeat=-1,
|
|
41
|
+
mute=True
|
|
42
|
+
)
|
|
43
|
+
mock_timer.return_value.run.assert_called_once()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# 4. 'timer' コマンドで count オプションが指定された場合のパーステスト
|
|
47
|
+
@patch("psw.timer.Timer")
|
|
48
|
+
def test_cli_timer_count_option(mock_timer):
|
|
49
|
+
# 'psw timer 10s -r 3' が入力されたと仮定
|
|
50
|
+
with patch.object(sys, "argv", ["psw", "timer", "10s", "-r", "3"]):
|
|
51
|
+
main()
|
|
52
|
+
|
|
53
|
+
mock_timer.assert_called_once_with(
|
|
54
|
+
duration_strings=["10s"],
|
|
55
|
+
precision=3, # デフォルト値
|
|
56
|
+
repeat=3, # 3 times repeat
|
|
57
|
+
mute=False # デフォルト値
|
|
58
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from psw.timer import Timer
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# 1. 正常な時間パースのテスト
|
|
6
|
+
@pytest.mark.parametrize(
|
|
7
|
+
"duration_list, expected_seconds",
|
|
8
|
+
[
|
|
9
|
+
(["10s"], 10.0),
|
|
10
|
+
(["5m"], 300.0),
|
|
11
|
+
(["1h"], 3600.0),
|
|
12
|
+
(["1h", "30m", "15s"], 5415.0), # 複数引数の組み合わせ
|
|
13
|
+
(["0.5m"], 30.0), # 小数点
|
|
14
|
+
(["10"], 10.0), # 単位なし(デフォルト秒)
|
|
15
|
+
],
|
|
16
|
+
)
|
|
17
|
+
def test_parse_duration_success(duration_list, expected_seconds):
|
|
18
|
+
# Timerインスタンスを作成してパース結果を検証
|
|
19
|
+
# (テスト実行時にタイマー自体は動かさないため、ダミーの値を渡します)
|
|
20
|
+
timer = Timer(duration_strings=duration_list, mute=True)
|
|
21
|
+
assert timer.total_seconds == expected_seconds
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# 2. 異常な入力に対するテスト
|
|
25
|
+
def test_parse_duration_invalid_and_exit():
|
|
26
|
+
# 0秒以下や、解釈不能な文字列だけで実行された場合、
|
|
27
|
+
# プログラムが SystemExit (sys.exit(1)) で終了することを確認します。
|
|
28
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
29
|
+
Timer(duration_strings=["invalid_string"], mute=True)
|
|
30
|
+
|
|
31
|
+
# 終了コードが 1 であることを検証
|
|
32
|
+
assert excinfo.value.code == 1
|