psw 0.1.4__py3-none-any.whl
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/__init__.py +10 -0
- psw/__main__.py +7 -0
- psw/cli.py +96 -0
- psw/stopwatch.py +91 -0
- psw/timer.py +197 -0
- psw/utils.py +50 -0
- psw-0.1.4.dist-info/METADATA +96 -0
- psw-0.1.4.dist-info/RECORD +11 -0
- psw-0.1.4.dist-info/WHEEL +4 -0
- psw-0.1.4.dist-info/entry_points.txt +2 -0
- psw-0.1.4.dist-info/licenses/LICENSE +21 -0
psw/__init__.py
ADDED
|
@@ -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/__main__.py
ADDED
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()
|
psw/stopwatch.py
ADDED
|
@@ -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)
|
psw/timer.py
ADDED
|
@@ -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()
|
psw/utils.py
ADDED
|
@@ -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)
|
|
@@ -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.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
psw/__init__.py,sha256=krDS7UcIYTts333VG3dvhp0E4tmcTxEIOM16rzVua-c,453
|
|
2
|
+
psw/__main__.py,sha256=mA4c_Qd7Nt6JVSqlGkzqa3LAkR8tlMXLgmGfsnOvm_g,210
|
|
3
|
+
psw/cli.py,sha256=24IlFzII7lNEtQYVgHCsmVamgYGjpvKgtqA8_g3qRkc,3118
|
|
4
|
+
psw/stopwatch.py,sha256=Yf7cCiCurr4EeTiSlk7XCN-pdCrMKsPOxm4wJ01mIaM,3539
|
|
5
|
+
psw/timer.py,sha256=GgbrwAr4lq_r6JaTz1MaN4TlK4JS1F_qdN80wYTUo-Y,7017
|
|
6
|
+
psw/utils.py,sha256=cNv_1FJetMc7OalW8Y82G8_iXh6lLitVLU-2Tkp2Ekk,1727
|
|
7
|
+
psw-0.1.4.dist-info/METADATA,sha256=yJFxxumpdmnvAEz0s1Xa7Blc0-3_5oSiFCCbOTYop3M,2942
|
|
8
|
+
psw-0.1.4.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
9
|
+
psw-0.1.4.dist-info/entry_points.txt,sha256=L5f-rin-ZZigbuu7NBaFiX8-8L0HA9VVdgasWNQgT6w,37
|
|
10
|
+
psw-0.1.4.dist-info/licenses/LICENSE,sha256=8rbF82lNR6xmU0nap8dg72OWYgQQ3qoewPKhpNRRamo,1069
|
|
11
|
+
psw-0.1.4.dist-info/RECORD,,
|
|
@@ -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.
|