Skip to content

Commit

Permalink
Add initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
llucax committed Oct 6, 2020
1 parent 2a8598a commit f710593
Show file tree
Hide file tree
Showing 8 changed files with 590 additions and 2 deletions.
61 changes: 61 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/bin/bash
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".

if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=$(git hash-object -t tree /dev/null)
fi

# If you want to allow non-ASCII filenames set this variable to true.
allownonascii=$(git config --type=bool hooks.allownonascii)

# Redirect output to stderr.
exec 1>&2

# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
# Note that the use of brackets around a tr range is ok here, (it's
# even required, for portability to Solaris 10's /usr/bin/tr), since
# the square bracket bytes happen to fall in the designated range.
test $(git diff --cached --name-only --diff-filter=A -z $against |
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
cat <<\EOF
Error: Attempt to add a non-ASCII file name.
This can cause problems if you want to work with people on other platforms.
To be portable it is advisable to rename the file.
If you know what you are doing you can disable this check using:
git config hooks.allownonascii true
EOF
exit 1
fi

# Run flutter format, analyze and test
(
git stash --include-untracked --keep-index &&
trap 'r=$?; git stash pop; exit $r' EXIT
git status
dart format --summary none --set-exit-if-changed -o none lib test || exit $?
dart analyze || exit $?
dart test || exit $?
) || exit $?

# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --

# vim: set et sw=2 sts=2 :
62 changes: 62 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/bin/sh

# An example hook script to verify what is about to be pushed. Called by "git
# push" after it has checked the remote status, but before anything has been
# pushed. If this script exits with a non-zero status nothing will be pushed.
#
# This hook is called with the following parameters:
#
# $1 -- Name of the remote to which the push is being done
# $2 -- URL to which the push is being done
#
# If pushing without using a named remote those arguments will be equal.
#
# Information about the commits which are being pushed is supplied as lines to
# the standard input in the form:
#
# <local ref> <local sha1> <remote ref> <remote sha1>
#
# This sample shows how to prevent push of commits where the log message starts
# with "WIP" (work in progress).

remote="$1"
url="$2"

z40=0000000000000000000000000000000000000000

while read local_ref local_sha remote_ref remote_sha
do
if echo "$local_ref" | grep -q '/local/'
then
echo "Found local ref name '$local_ref' has '/local/' in it." >&2
echo "Not pushing refs with containing that as they are " >&2
echo "supposed to be local only." >&2
exit 1
fi
if [ "$local_sha" = $z40 ]
then
# Handle delete
:
else
if [ "$remote_sha" = $z40 ]
then
# New branch, examine all commits
range="$local_sha"
else
# Update to existing branch, examine new commits
range="$remote_sha..$local_sha"
fi

# Check for WIP commit
commit=`git rev-list -n 1 --grep '^\(WIP\|fixup!\|squash!\)' "$range"`
if [ -n "$commit" ]
then
echo "Found WIP commit in $local_ref, not pushing" >&2
exit 1
fi
fi
done

exit 0

# vim: set et sw=2 sts=2 :
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 0.1.0

- Initial version.
71 changes: 69 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,69 @@
# pausable_timer
A [Dart](https://dart.dev/) [timer](https://api.dart.dev/stable/dart-async/Timer/Timer.html) that can be paused, resumed and reset.
# pausable\_timer

A [Dart](https://dart.dev/)
[timer](https://api.dart.dev/stable/dart-async/Timer/Timer.html) that can be
paused, resumed and reset.

## Example

```dart
import 'package:pausable_timer/pausable_timer.dart';
void main() async {
final timer = PausableTimer(Duration(seconds: 1), () => print('yes!'));
// PausableTimer starts paused, so we have to start it manually.
timer.start();
Future<void>.delayed(timer.duration ~/ 2);
print('Not yet fired, still 1/2 second to go!');
timer.pause();
// When paused, time can pass but the timer won't be fired
Future<void>.delayed(timer.duration);
// Now we can resume the timer
timer.start();
Future<void>.delayed(timer.duration ~/ 2);
// Now it should have fired and "yes!" should have been printed, but we can
// re-arm the timer via reset() and use it again.
timer.reset();
timer.start();
Future<void>.delayed(timer.duration);
// And it should fire again.
print('We should have 2 ticks now: ${timer.ticks}');
// And we can arm it again
timer.reset();
timer.start();
// And we can cancel it, but once the timer is cancelled, it can't be armed
// again, but it can still be queried for information.
timer.cancel();
print('${timer.duration} ${timer.elapsed} ${timer.ticks} ${timer.isPaused}');
}
```

## Development

### Git Hooks

This repository provides some useful Git hooks to make sure new commits have
some basic health.

The hooks are provided in the `.githooks/` directory and can be easily used by
configuring git to use this directory for hooks instead of the default
`.git/hooks/`:

```sh
git config core.hooksPath .githooks
```

So far there is a hook to prevent commits with the `WIP` word in the message to
be pushed, and one hook to run `flutter analyze` and `flutter test` before
a new commit is created. The later can take some time, but it can be easily
disabled temporarily by using `git commit --no-verify` if you are, for example,
just changing the README file or amending a commit message.
6 changes: 6 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
include: package:pedantic/analysis_options.1.9.0.yaml

analyzer:
strong-mode:
implicit-casts: false
implicit-dynamic: false
167 changes: 167 additions & 0 deletions lib/pausable_timer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import 'dart:async' show Timer, Zone;

import 'package:clock/clock.dart' show clock;

/// A [Timer] that can be paused, resumed and reset.
///
/// Based on:
/// https://github.com/dart-lang/sdk/issues/43329#issuecomment-687024252
class PausableTimer implements Timer {
/// The [Zone] where the [_callback] will be run.
///
/// Dart generally calls asynchronous callbacks in the zone where they were
/// originally "created".
///
/// Such callbacks are first registered, because that makes it possible for
/// special zones to record more information about the point where the
/// callback is created (say, remember the stack trace, which is what the
/// stack_trace package does).
///
/// That is, we call [Zone.registeCallback] to enable zones to know about the
/// callback creation as well as the later [Zone.run] for running it.
///
/// If you just store the callback, but don't register it at the time it's
/// stored, then we can still run it in the correct zone when necessary, but
/// such special zones would stop working for that callback.
///
/// This explanation comes from:
/// https://github.com/dart-lang/sdk/issues/43329#issuecomment-687720625
final Zone _zone;

/// The [Stopwatch] used to keep track of the elapsed time.
///
/// This allows us to pause the timer and resume from where it left of.
///
/// When the timer expires, this stopwatch is set to null.
Stopwatch _stopwatch = clock.stopwatch();

/// The currently active [Timer].
///
/// This is null whenever this timer is not currently active.
Timer _timer;

/// The callback to call when this timer expires.
///
/// If this timer was [cancel]ed, then this callback is null.
void Function() _callback;

/// The number of times this timer has expired.
int _tick = 0;

/// Starts the [_timer] to run [_callback] in [_zone] and increment [_tick].
///
/// It also starts the [_stopwatch] and clears [_timer] and [_stopwatch] when
/// the [_timer] expires.
void _startTimer() {
_timer = _zone.createTimer(_originalDuration - _stopwatch.elapsed, () {
_tick++;
_timer = null;
_stopwatch = null;
_zone.run(_callback);
});
_stopwatch.start();
}

/// Creates a new timer.
///
/// The [callback] is invoked after the given [duration], but can be [pause]d
/// in between or [reset]. The [elapsed] time is only accounted for while the
/// timer [isActive].
///
/// The timer [isPaused] when created, and must be [start]ed manually.
///
/// The [duration] must be non-null and equals or bigger than [Duration.zero].
/// If it is [Duration.zero], the [callback] will still not be called until
/// the timer is [start]ed.
///
/// [callback] must be non-null.
PausableTimer(Duration duration, void Function() callback)
: assert(duration != null),
assert(duration >= Duration.zero),
assert(callback != null),
_originalDuration = duration,
_zone = Zone.current {
_callback = _zone.bindCallback(callback);
}

/// The original duration this [Timer] was created with.
Duration get duration => _originalDuration;
final Duration _originalDuration;

/// The time this [Timer] have been active.
///
/// If the timer is paused, the elapsed time is also not computed anymore, so
/// [elapsed] is always less than or equals to the [duration].
Duration get elapsed => _stopwatch?.elapsed ?? _originalDuration;

/// True if this [Timer] is armed but not currently active.
///
/// If this timer [isExpired] or [isCancelled], it is not considered to be
/// paused.
bool get isPaused => _timer == null && !isExpired && !isCancelled;

/// True if this [Timer] has expired.
bool get isExpired => _stopwatch == null;

/// True if this [Timer] was cancelled.
bool get isCancelled => _callback == null;

/// True if this [Timer] is armed and counting.
@override
bool get isActive => _timer != null;

@override
int get tick => _tick;

/// Cancels the timer.
///
/// Once a [Timer] has been canceled, the callback function will not be called
/// by the timer and the timer can't be activated again. Calling [start],
/// [pause] or [reset] will have no effect. Calling [cancel] more than once on
/// a [Timer] is also allowed, and will have no further effect.
@override
void cancel() {
_stopwatch?.stop();
_timer?.cancel();
_timer = null;
_callback = null;
}

/// Starts (or resumes) the timer.
///
/// Starts counting for the original duration or from where it was left of if
/// [pause]ed.
///
/// It does nothing if the timer [isActive], [isExpired] or [isCancelled].
void start() {
if (isActive || isExpired || isCancelled) return;
_startTimer();
}

/// Pauses an active timer.
///
/// The [elapsed] time is not accounted anymore and the timer will not be
/// fired until it is [start]ed again.
///
/// Nothing happens if the timer [isPaused], [isExpired] or [isCancelled].
void pause() {
_stopwatch?.stop();
_timer?.cancel();
_timer = null;
}

/// Resets the timer.
///
/// Sets the timer to its original [duration] and rearms it if it was already
/// expired (so it can be started again).
///
/// Does not change whether the timer [isActive] or [isPaused].
void reset() {
if (isCancelled) return;
_stopwatch = clock.stopwatch();
if (isActive) {
_timer.cancel();
_startTimer();
}
}
}
11 changes: 11 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: pausable_timer
description: A timer that can be paused, resumed and reset.
version: 0.1.0

dependencies:
clock: '^1.0.1'

dev_dependencies:
test: '^1.15.2'
pedantic: '^1.9.0'
fake_async: '^1.0.1'
Loading

0 comments on commit f710593

Please sign in to comment.