├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── ding ├── __init__.py └── ding.py ├── setup.py ├── tests ├── test_arguments.py ├── test_functions.py └── test_integration.py ├── tox.ini └── usage.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.2" 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | #- "nightly" # currently points to 3.6-dev 9 | 10 | install: pip install . 11 | script: py.test 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project pretends to adhere to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [2.1.0] 8 | 9 | ### Added 10 | 11 | - Added `every` mode to enable recurring beep. ([anemones](https://github.com/anemones) -- [#11](https://github.com/liviu-/ding/pull/11)) 12 | 13 | ## [2.0.0] 14 | 15 | ### Added 16 | - Added a `--no-timer` option to silence the timer ([mikaylathompson](https://github.com/mikaylathompson) -- [#9](https://github.com/liviu-/ding/pull/9)) 17 | - Added a `--command` option to optionally replace the `sys.stdout.write('\a')`. This way, one may use a custom command to be called when the timer finishes ([#8](https://github.com/liviu-/ding/issues/8), [#6](https://github.com/liviu-/ding/pull/6), [#5](https://github.com/liviu-/ding/issues/5)) 18 | 19 | ## Changed 20 | - Updated README.md documentation. 21 | 22 | ## Removed 23 | - Removed Python2.6 support because very few people still use it, and it requires more boilercode code to keep it compatible. 24 | - Temporarily remove support for the development Python version cause Travis [fails with seg fault](https://travis-ci.org/liviu-/ding/jobs/166043298) although it works locally. I'll add it back in the future versions. 25 | 26 | ## [1.3.0] - 2016-10-04 27 | 28 | ### Changed 29 | 30 | - Eliminated effect on scrollback buffer: all the printed countdown steps were preserved in the terminal creating a long scrollback especially for big waiting times ([andars](https://github.com/andars) -- [#4](https://github.com/liviu-/ding/pull/4)) 31 | - Changed the regex in the help message for the 3rd time. The previous regex was wrong as it was indicating that 1h3m is valid input while the tool expects them to be separated by space. 32 | 33 | ## [1.2.0] - 2016-10-03 34 | 35 | ### Added 36 | - Added Windows support 37 | 38 | ### Changed 39 | - Updated regex from the help section to make it smaller and easier to understand hopefully. 40 | 41 | ## [1.1.0] - 2016-10-02 42 | 43 | ### Added 44 | - Added a countdown for the script 45 | 46 | ### Changed 47 | - Updated regex from the help section. 48 | 49 | ## [1.0.0] - 2016-10-02 50 | 51 | ### Added 52 | - Added more tests including integration tests, tox, travis for py26, py27, py32, py33, py34, py35, and python nightly 53 | - Added usage gif to the README.md file 54 | - Added this change log 55 | 56 | ## [0.0.1] - 2016-10-02 57 | 58 | Initial stable release 59 | 60 | [2.1.0]: https://github.com/liviu-/ding/compare/v2.0.0..2.1.0 61 | [2.0.0]: https://github.com/liviu-/ding/compare/v1.3.0..v2.0.0 62 | [1.3.0]: https://github.com/liviu-/ding/compare/v1.2.0..v1.3.0 63 | [1.2.0]: https://github.com/liviu-/ding/compare/v1.1.0..v1.2.0 64 | [1.1.0]: https://github.com/liviu-/ding/compare/v1.0.0...v1.1.0 65 | [1.0.0]: https://github.com/liviu-/ding/compare/v0.0.1...v1.0.0 66 | [0.0.1]: https://github.com/liviu-/ding/tree/v0.0.1 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ding [![Build Status](https://travis-ci.org/liviu-/ding.svg?branch=develop)](https://travis-ci.org/liviu-/ding) 2 | 3 | ![usage_gif](usage.gif) 4 | 5 | Tired of `$ sleep 4231; beep`? This is a very simple solution to help with short-term time management. The beep sound uses the motherboard audio, so it works even if your speakers are muted, but not if you muted the PC speakers :stuck_out_tongue: . Furthermore, it works wherever there's a Linux terminal, and that includes ssh sessions. 6 | 7 | - No dependencies other than Python itself :dizzy: 8 | - Install with `pip` or just copy the binary somewhere in `$PATH` :sparkles: 9 | - Python2 and Python3 compatible :star2: 10 | - Around 100 LOC :boom: 11 | - Runs on Linux, OS X, Windows, and maybe more :tada: 12 | 13 | ## Installation 14 | 15 | ``` 16 | $ pip install ding-ding 17 | ``` 18 | 19 | (`ding` was taken) 20 | 21 | Alternatively, download the [ding.py](https://github.com/liviu-/ding/blob/develop/ding/ding.py) file and run it however you please. 22 | 23 | ``` 24 | $ ./ding.py in 1s 25 | ``` 26 | 27 | ## Scenarios 28 | 29 | - You want to start work after browsing the news for a bit, but you don't want to get carried away and do no work. Set a timer for 15 minutes: 30 | ``` 31 | $ ding in 15m 32 | ``` 33 | - You need to leave at 17:00 and you want to have time to get ready: 34 | ``` 35 | $ ding at 16:45 36 | ``` 37 | 38 | - [Pomodoro technique](https://en.wikipedia.org/wiki/Pomodoro_Technique) 39 | ``` 40 | $ alias pomo="ding in 25m" 41 | $ pomo 42 | ``` 43 | 44 | 45 | ## Example usage: 46 | 47 | ``` 48 | # Relative time 49 | $ ding in 2m 50 | $ ding in 2h 15m 51 | $ ding in 2m 15s 52 | 53 | # Absolute time 54 | $ ding at 12 55 | $ ding at 17:30 56 | $ ding at 17:30:21 57 | 58 | # Recurrent notification 59 | $ ding every 15m 60 | 61 | # Custom command for beeping 62 | $ ding in 1s --command "paplay --volume 15000 beep.wav" 63 | 64 | # Hide timer 65 | $ ding in 1s --no-timer 66 | ``` 67 | For more help, try: 68 | 69 | ``` 70 | $ ding in --help 71 | $ ding at --help 72 | $ ding every --help 73 | ``` 74 | 75 | ## FAQ 76 | 77 | ### How come I don't hear anything? 78 | 79 | This happens when the audible bell was muted for your terminal or for your system. Enabling it differs between environments, so I would suggest trying out some Google searches on how to enable it. For a discussion on this, check this [issue](https://github.com/liviu-/ding/issues/5). You can also use your own custom command that actually beeps for you using `$ ding in 1s -c your-command` 80 | 81 | ### How can I use a custom command all the time? 82 | 83 | Try adding to your start-up script: 84 | ```bash 85 | function ding() { ding $@ -c custom-command} 86 | ``` 87 | (inspired from [mikaylathompson](https://github.com/mikaylathompson)'s comment [here](https://github.com/liviu-/ding/pull/9)) 88 | 89 | ### How can I to run it in the background? 90 | 91 | ``` 92 | $ ding in 1s --no-timer& 93 | ``` 94 | 95 | ### Can I use desktop notifications? 96 | 97 | Unfortunately, desktop notifications typically come with big GUI dependencies and tend to be less portable. However, you can integrate notifications using a custom command like this `$ ding in 1s --command "notify-send 'Sup'"`. This will display the notification 4 times by default, but if you think it would be useful to have an option to specify the number of calls, let me know by opening an [issue](https://github.com/liviu-/ding/issues) or a [PR](https://github.com/liviu-/ding/pulls). 98 | -------------------------------------------------------------------------------- /ding/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liviu-/ding/14e49439b3898ba364921fe8519b3befd2f2ef01/ding/__init__.py -------------------------------------------------------------------------------- /ding/ding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Simple CLI beep tool""" 3 | 4 | from __future__ import unicode_literals 5 | from __future__ import print_function 6 | 7 | import re 8 | import os 9 | import sys 10 | import time 11 | import datetime 12 | import argparse 13 | 14 | 15 | VERSION = '2.1.0' 16 | N_BEEPS = 4 17 | WAIT_BEEPS = 0.15 18 | 19 | 20 | def relative_time(arg): 21 | """Validate user provided relative time""" 22 | if not re.match('\d+[smh]( +\d+[smh])*', arg): 23 | raise argparse.ArgumentTypeError("Invalid time format: {}".format(arg)) 24 | return arg 25 | 26 | 27 | def absolute_time(arg): 28 | """Validate user provided absolute time""" 29 | if not all([t.isdigit() for t in arg.split(':')]): 30 | raise argparse.ArgumentTypeError("Invalid time format: {}".format(arg)) 31 | # Valid time (e.g. hour must be between 0..23) 32 | try: 33 | datetime.time(*map(int, arg.split(':'))) 34 | except ValueError as e: 35 | raise argparse.ArgumentTypeError("Invalid time format: {}".format(e)) 36 | return arg 37 | 38 | 39 | def get_args(args): 40 | """Parse commandline arguments""" 41 | parent_parser = argparse.ArgumentParser( 42 | add_help=False, description='Lightweight time management CLI tool') 43 | parent_parser.add_argument( 44 | '-n', '--no-timer', action='store_true', help='Hide the countdown timer') 45 | parent_parser.add_argument( 46 | '-c', '--command', type=str, help='Use a custom command instead of the default beep') 47 | 48 | parser = argparse.ArgumentParser() 49 | parser.add_argument('-v', '--version', action='version', version=VERSION) 50 | subparsers = parser.add_subparsers(dest='mode') 51 | subparsers.required = True 52 | 53 | parser_in = subparsers.add_parser('in', parents=[parent_parser]) 54 | parser_in.add_argument('time', nargs='+', type=relative_time, 55 | help='relative time \d+[smh]( +\d+[smh])* (e.g. 1h 30m)') 56 | 57 | parser_every = subparsers.add_parser('every', parents=[parent_parser]) 58 | parser_every.add_argument('time', nargs='+', type=relative_time, 59 | help='relative time \d+[smh]( +\d+[smh])* (e.g. 2m 15s)') 60 | 61 | parser_at = subparsers.add_parser('at', parents=[parent_parser]) 62 | parser_at.add_argument('time', type=absolute_time, help='absolute time [hh:[mm[:ss]]]') 63 | 64 | return parser.parse_args(args) 65 | 66 | class TimeParser(): 67 | """Class helping with parsing user provided time into seconds""" 68 | time_map = { 69 | 's': 1, 70 | 'm': 60, 71 | 'h': 60 * 60, 72 | } 73 | 74 | def __init__(self, time, relative): 75 | self.time = time 76 | self.relative = relative 77 | 78 | def get_seconds(self): 79 | return self._get_seconds_relative() if self.relative else self._get_seconds_absolute() 80 | 81 | def _get_seconds_relative(self): 82 | return sum([self.time_map[t[-1]] * int(t[:-1]) for t in self.time]) 83 | 84 | def _get_seconds_absolute(self): 85 | now = datetime.datetime.now() 86 | user_time = (datetime.datetime.combine(datetime.date.today(), 87 | datetime.time(*map(int, self.time.split(':'))))) 88 | return ((user_time - now).seconds if user_time > now 89 | else (user_time + datetime.timedelta(days=1) - now).seconds) 90 | 91 | 92 | def countdown(seconds, notimer=False): 93 | """Countdown for `seconds`, printing values unless `notimer`""" 94 | if not notimer: 95 | os.system('cls' if os.name == 'nt' else 'clear') # initial clear 96 | while seconds > 0: 97 | start = time.time() 98 | 99 | # print the time without a newline or carriage return 100 | # this leaves the cursor at the end of the time while visible 101 | if not notimer: 102 | print(datetime.timedelta(seconds=seconds), end='') 103 | sys.stdout.flush() 104 | seconds -= 1 105 | time.sleep(1 - time.time() + start) 106 | 107 | # emit a carriage return 108 | # this moves the cursor back to the beginning of the line 109 | # so the next time overwrites the current time 110 | if not notimer: 111 | print(end='\r') 112 | 113 | 114 | def beep(seconds, command): 115 | """Make the beep noise""" 116 | for _ in range(N_BEEPS): 117 | if command: 118 | os.system(command) 119 | else: 120 | sys.stdout.write('\a') 121 | sys.stdout.flush() 122 | time.sleep(WAIT_BEEPS) 123 | 124 | 125 | def parse_time(args): 126 | """Figure out the number of seconds to wait""" 127 | relative = args.mode == 'in' or args.mode == "every" 128 | parser = TimeParser(args.time, relative) 129 | return parser.get_seconds() 130 | 131 | 132 | def main(args=sys.argv[1:]): 133 | args = get_args(args) 134 | while True: 135 | try: 136 | seconds = parse_time(args) 137 | countdown(seconds, args.no_timer) 138 | beep(seconds, args.command) 139 | # doing `if` here so there just can't be any stack printed for an interrupt 140 | if args.mode != "every": 141 | break 142 | except KeyboardInterrupt: 143 | print() # ending current line 144 | break # without printing useless stack... 145 | 146 | 147 | if __name__ == '__main__': 148 | main() 149 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | def get_version(): 4 | with open('ding/ding.py') as f: 5 | version = next(line for line in f.read().splitlines() if 'VERSION' in line) 6 | version = version.split(' = ')[-1].strip("'") 7 | return version 8 | 9 | setup( 10 | name='ding-ding', 11 | description='Quick organisation via the commandline', 12 | url='https://github.com/liviu-/ding', 13 | version=get_version(), 14 | license='MIT', 15 | packages=find_packages(), 16 | entry_points = { 17 | "console_scripts": ['ding = ding.ding:main'] 18 | }, 19 | ) 20 | -------------------------------------------------------------------------------- /tests/test_arguments.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import datetime 3 | import argparse 4 | 5 | from ding import ding 6 | 7 | # Valid arguments 8 | 9 | def test_time_a_second_in_the_past(): 10 | a_second_ago = datetime.datetime.now() - datetime.timedelta(seconds=1) 11 | time_str = str(a_second_ago.time()).split('.')[0] 12 | assert ding.get_args(['at', time_str]) 13 | 14 | def test_time_a_minute_in_the_future(): 15 | a_second_ago = datetime.datetime.now() + datetime.timedelta(minutes=1) 16 | time_str = str(a_second_ago.time()).split('.')[0] 17 | assert ding.get_args(['at', time_str]) 18 | 19 | def test_time_in_1s(): 20 | assert ding.get_args(['in', '1s']) 21 | 22 | def test_time_in_1m(): 23 | assert ding.get_args(['in', '1m']) 24 | 25 | def test_time_in_1m(): 26 | assert ding.get_args(['in', '1h']) 27 | 28 | def test_time_in_1h_1m_1s(): 29 | assert ding.get_args(['in', '1h', '1m', '1s']) 30 | 31 | # Invalid arguments 32 | def test_no_arguments(): 33 | with pytest.raises(SystemExit) as excinfo: 34 | assert ding.get_args([]) 35 | 36 | def test_insufficient_arguments_in(): 37 | with pytest.raises(SystemExit) as excinfo: 38 | assert ding.get_args(['in']) 39 | 40 | def test_insufficient_arguments_at(): 41 | with pytest.raises(SystemExit) as excinfo: 42 | assert ding.get_args(['at']) 43 | 44 | def test_insufficient_arguments_at(): 45 | with pytest.raises(SystemExit) as excinfo: 46 | assert ding.get_args(['every']) 47 | 48 | def test_in_wrong_suffix(): 49 | with pytest.raises(SystemExit) as excinfo: 50 | assert ding.get_args(['in', '1x']) 51 | 52 | def test_in_partly_wrong_suffix(): 53 | with pytest.raises(SystemExit) as excinfo: 54 | assert ding.get_args(['in', '1s', '1x']) 55 | 56 | def test_at_invalid_separator(): 57 | with pytest.raises(SystemExit) as excinfo: 58 | assert ding.get_args(['in', '15', '30']) 59 | 60 | def test_at_invalid_hour(): 61 | with pytest.raises(SystemExit) as excinfo: 62 | assert ding.get_args(['at', '25']) 63 | 64 | def test_at_invalid_minute(): 65 | with pytest.raises(SystemExit) as excinfo: 66 | assert ding.get_args(['at', '22:71']) 67 | 68 | def test_at_characters_in_string(): 69 | with pytest.raises(SystemExit) as excinfo: 70 | assert ding.get_args(['at', '22a:71']) 71 | 72 | def test_notimer_not_at_end(): 73 | with pytest.raises(SystemExit) as excinfo: 74 | assert ding.get_args(['--no-timer', 'in', '1s']) 75 | 76 | # Test optional args 77 | def test_argument_no_timer(): 78 | assert ding.get_args(['in', '1s', '--no-timer']) 79 | assert ding.get_args(['in', '1s', '-n']) 80 | 81 | def test_argument_alternative_command(): 82 | assert ding.get_args(['in', '1s', '--command', 'beep']) 83 | assert ding.get_args(['in', '1s', '-c', 'beep']) 84 | 85 | def test_argument_inexistent(): 86 | with pytest.raises(SystemExit) as excinfo: 87 | assert ding.get_args(['in', '1s', '--inexistent-argument']) 88 | -------------------------------------------------------------------------------- /tests/test_functions.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import argparse 3 | 4 | import pytest 5 | from ding import ding 6 | 7 | def test_time_parser_relative_1s(): 8 | parser = ding.TimeParser(['1s'], relative=True) 9 | assert parser.get_seconds() == 1 10 | 11 | def test_time_parser_relative_1m(): 12 | parser = ding.TimeParser(['1m'], relative=True) 13 | assert parser.get_seconds() == 60 14 | 15 | def test_time_parser_relative_1h(): 16 | parser = ding.TimeParser(['1h'], relative=True) 17 | assert parser.get_seconds() == 60 * 60 18 | 19 | def test_time_parser_relative_1h_30m(): 20 | parser = ding.TimeParser(['1h', '30m'], relative=True) 21 | assert parser.get_seconds() == 60 * 60 + 30 * 60 22 | 23 | def test_time_parser_relative_1h_30m(): 24 | parser = ding.TimeParser(['1h', '30m'], relative=True) 25 | assert parser.get_seconds() == 60 * 60 + 30 * 60 26 | 27 | def test_time_parser_relative_1h_30m_10s(): 28 | parser = ding.TimeParser(['1h', '30m', '10s'], relative=True) 29 | assert parser.get_seconds() == 60 * 60 + 30 * 60 + 10 30 | 31 | def test_time_parser_absolute_10s(): 32 | new_time = str((datetime.now() + timedelta(seconds=10)).time()).split('.')[0] 33 | parser = ding.TimeParser(new_time, relative=False) 34 | assert abs(parser.get_seconds() - 10) < 2 35 | 36 | def test_time_parser_absolute_1h(): 37 | new_hour = str((datetime.now() + timedelta(hours=1)).hour) 38 | parser = ding.TimeParser(new_hour, relative=False) 39 | assert 0 < parser.get_seconds() < 60 * 60 40 | 41 | def test_time_parser_absolute_5m(): 42 | new_time = ':'.join(str((datetime.now() + timedelta(minutes=5)).time()).split(':')[:2]) 43 | parser = ding.TimeParser(new_time, relative=False) 44 | assert 60 * 4 <= parser.get_seconds() < 60 * 6 45 | 46 | def test_relative_time_regex_very_wrong_regex(): 47 | with pytest.raises(argparse.ArgumentTypeError)as excinfo: 48 | ding.relative_time('this is very wrong') 49 | 50 | def test_relative_time_regex_1s(): 51 | assert ding.relative_time('1s') 52 | 53 | def test_relative_time_regex_h_m_s(): 54 | assert ding.relative_time('12h 12m 34s') 55 | 56 | def test_relative_time_regex_extra_space(): 57 | assert ding.relative_time('12h 12m') 58 | 59 | def test_absolute_time_hh_mm_ss(): 60 | assert ding.absolute_time('12:12:12') 61 | 62 | def test_absolute_time_hh(): 63 | assert ding.absolute_time('12') 64 | 65 | def test_absolute_time_hh_mm_ss_invalid_hour(): 66 | with pytest.raises(argparse.ArgumentTypeError)as excinfo: 67 | assert ding.absolute_time('32:12:12') 68 | 69 | def test_beep_with_custom_command(capfd): 70 | ding.beep(1, 'echo "test"') 71 | out, err = capfd.readouterr() 72 | assert out == 'test\n' * ding.N_BEEPS 73 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | from subprocess import Popen, PIPE 2 | 3 | def run_tool(args): 4 | p = Popen(['ding'] + args, stdout=PIPE, stderr=PIPE) 5 | stdout, stderr = (out.decode('utf-8') for out in p.communicate()) 6 | return p, stdout, stderr 7 | 8 | def test_integration_in_1_second(): 9 | run_tool(['in', '1s']) 10 | 11 | def test_integration_with_notimer(): 12 | p, stdout, stderr = run_tool(['in', '1s', '--no-timer']) 13 | assert(stderr == '') 14 | assert(stdout == "\07" * 4) # these are the beeps 15 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py35 3 | [testenv] 4 | deps=pytest 5 | commands=py.test 6 | -------------------------------------------------------------------------------- /usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liviu-/ding/14e49439b3898ba364921fe8519b3befd2f2ef01/usage.gif --------------------------------------------------------------------------------