Commit 4bba4988 authored by Dylan McCall's avatar Dylan McCall

Initial commit.

Basic skeleton application with timers to count down to (but not execute) two types of breaks according to session idle time.
parents
Dylan McCall <dylanmccall@ubuntu.com>
import yaml
import re
WAF_TOOLS = {'cc': 'compiler_cc',
'c++': 'compiler_cxx',
'vala': 'compiler_cc vala'}
# (Tool,Type) -> Waf features map
FEATURES_MAP = {('cc', 'program'): 'c cprogram',
('cc', 'sharedlib'): 'c cshlib',
('cc', 'staticlib'): 'c cstlib',
('c++', 'program'): 'cxx cprogram',
('c++', 'sharedlib'): 'cxxshlib',
('c++', 'staticlib'): 'cxxstlib',
('vala', 'program'): 'c cprogram',
('vala', 'sharedlib'): 'c cshlib',
('vala', 'staticlib'): 'c cstlib'}
CC_TOOLCHAIN = {'ADDR2LINE': 'addr2line',
'AS': 'as', 'CC': 'gcc', 'CPP': 'cpp',
'CPPFILT': 'c++filt', 'CXX': 'g++',
'DLLTOOL': 'dlltool', 'DLLWRAP': 'dllwrap',
'GCOV': 'gcov', 'LD': 'ld', 'NM': 'nm',
'OBJCOPY': 'objcopy', 'OBJDUMP': 'objdump',
'READELF': 'readelf', 'SIZE': 'size',
'STRINGS': 'strings', 'WINDRES': 'windres',
'AR': 'ar', 'RANLIB': 'ranlib', 'STRIP': 'strip'}
DEFAULT_BUILDJ_FILE="project.yaml"
def normalize_package_name (name):
name = name.upper ()
nonalpha = re.compile (r'\W')
return nonalpha.sub ('_', name)
class ProjectFile:
def __init__ (self, project=DEFAULT_BUILDJ_FILE):
prj = open(project)
data = prj.read ()
self._project = yaml.load (data)
prj.close ()
#TODO: try to raise some meaningful (and consistent) error
self._project_name = self._project['project']['name']
self._project_version = self._project['project']['version']
self._targets = []
for target_name, target_data in self._project['targets'].iteritems():
self._targets.append(ProjectTarget(target_name, target_data))
for subdir in self._project.get ('subdirs', []):
prj = open ('%s/%s' % (subdir, project))
data = prj.read ()
subproject = yaml.load (data)
for target_name, target_data in subproject['targets'].iteritems():
assert target_name not in self._project['targets']
if 'path' in target_data:
path = '%s/%s' % (subdir, target_data['path'])
else:
path = subdir
target_data['path'] = path
self._project['targets'][target_name] = target_data
self._targets.append(ProjectTarget(target_name, target_data))
def __repr__ (self):
enc = json.encoder.JSONEncoder ()
return enc.encode (self._project)
def get_project_version (self):
return self._project_version
def get_project_name (self):
return self._project_name
def get_options (self):
project = self._project
if not "options" in project:
return []
option_list = []
for option_name in project["options"]:
option_list.append (ProjectOption (str(option_name),
project["options"][option_name]))
return option_list
def get_targets (self):
names = dict([(tgt.get_name(), tgt) for tgt in self._targets])
deps = dict([(tgt.get_name(), tgt.get_uses()) for tgt in self._targets])
S = [tgt for tgt in deps if not deps[tgt]]
targets = []
while S:
n = S.pop(0)
targets.append(names[n])
for m in deps:
if n in deps[m]:
deps[m].remove(n)
if not deps[m]:
S.insert(0,m)
return targets
def get_tools (self):
tools = []
for target in self._targets:
tool = target.get_tool ()
if tool and tool != "data":
tools.append (tool)
return tools
def get_requires (self):
project = self._project
if not "requires" in project:
return
return [ProjectRequirement(require, project["requires"][require])
for require in project["requires"]]
def get_packages_required (self):
"List of pkg-config packages required"
requires = self.get_requires ()
return [require for require in requires if require.get_type () == "package"]
def replace_options (self, *args):
pass
class ProjectTarget(object):
def __new__(cls, name, target):
if not isinstance (target, dict):
raise ValueError, "Target %s: the target argument must be a dictionary" % name
if 'tool' in target:
cls = TOOL_CLASS_MAP[target['tool']]
else:
sources = target['input']
tools = set ()
for src in sources:
for tool, exts in EXT_TOOL_MAP.iteritems ():
if any([src.endswith (ext) for ext in exts]):
tools.add (tool)
tools = tuple(sorted(tools))
if len(tools) == 1:
tool = tools[0]
elif tools in MULTI_TOOL_MAP:
tool = MULTI_TOOL_MAP[tools]
else:
raise NotImplementedError, "Target %s: you need to specify a tool"
target['tool'] = tool
cls = TOOL_CLASS_MAP[tool]
return object.__new__(cls)
def __init__(self, name, target):
self._name = name
self._target = target
def get_name (self):
return str(self._name)
def get_output (self):
if "output" in self._target:
return str(self._target["output"])
return self.get_name()
def get_tool (self):
if "tool" not in self._target:
return None
return str(self._target["tool"])
def get_type (self):
if "type" not in self._target:
return
return str(self._target["type"])
def get_path (self):
return str(self._target.get ("path", ""))
def get_features (self):
tool = self.get_tool ()
output_type = self.get_type ()
if not tool or not output_type:
#TODO: Report tool and target type needed
return
if (tool, output_type) in FEATURES_MAP:
return FEATURES_MAP[(tool, output_type)]
else:
#TODO: Report lack of support for this combination
return
def _get_string_list (self, key):
if key not in self._target:
return []
target_input = self._target[key]
if isinstance (target_input, unicode):
return [str(target_input),]
elif isinstance (target_input, list):
#TODO: Check if everything is str
return [str(t) for t in target_input]
#TODO: Report warning, empty input
return []
def get_input (self):
return self._get_string_list ("input")
def get_uses (self):
return self._get_string_list ("uses")
def get_version (self):
if "version" not in self._target:
return None
return str(self._target["version"])
def get_packages (self):
return self._get_string_list ("packages")
def get_defines (self):
return self._get_string_list ("defines")
def get_build_arguments (self):
"WAF bld arguments dictionary"
args = {"features": self.get_features (),
"source": self.get_input (),
"name": self.get_name (),
"target": self.get_output ()}
return args
def get_install_files (self):
return
def get_install_path (self):
return
class CcTarget (ProjectTarget):
def get_build_arguments (self):
args = ProjectTarget.get_build_arguments (self)
uses = self.get_uses ()
if uses:
# waf vala support will modify the list if we pass one
args["uselib_local"] = " ".join (uses)
if self.get_type () == "sharedlib" and self.get_version ():
args["vnum"] = self.get_version ()
args["uselib"] = []
for pkg in self.get_packages ():
args["uselib"].append (normalize_package_name(pkg))
defines = self.get_defines ()
if defines:
args["defines"] = defines
if self.get_type () in ("sharedlib", "staticlib"):
args["export_incdirs"] = '.'
return args
class ValaTarget (CcTarget):
def get_vapi (self):
if "vapi" in self._target:
return str (self._target["vapi"])
def get_gir (self):
if "gir" in self._target:
gir = str(self._target["gir"])
match = re.match (".*-.*", gir)
if match:
return gir
return None
def get_build_arguments (self):
"WAF bld arguments dictionary"
args = CcTarget.get_build_arguments (self)
packages = self.get_packages ()
if "glib-2.0" not in packages:
packages.append ("glib-2.0")
if "uselib" in args:
args["uselib"].append (normalize_package_name("glib-2.0"))
else:
args["uselib"] = [normalize_package_name("glib-2.0")]
args["packages"] = packages
gir = self.get_gir ()
if gir:
args["gir"] = gir
return args
class DataTarget (ProjectTarget):
def get_build_arguments (self):
return {}
def get_install_files (self):
if "input" not in self._target:
return []
return self.get_input ()
def get_install_path (self):
return "${PREFIX}/share/" + self.get_output ()
class ProjectRequirement:
def __init__ (self, name, requirement):
self._name = name
self._requirement = requirement
def get_name (self):
return str(self._name)
def get_type (self):
if "type" not in self._requirement:
#TODO: Type is required
return
return str(self._requirement["type"])
def get_version (self):
if "version" not in self._requirement:
return
return str(self._requirement["version"])
def is_mandatory (self):
if "mandatory" not in self._requirement:
return False
mandatory = self._requirement["mandatory"]
if "True" == mandatory:
return True
elif "False" == mandatory:
return False
else:
#TODO: Warn about wrong mandatory
pass
def get_check_pkg_args (self):
"WAF check_pkg arguments dictionary"
args = {"package": self.get_name ()}
#Correctly sets the version
if self.get_version():
version = self.get_version()
if version.startswith ("= "):
args["exact_version"] = str(version[2:])
if version.startswith ("== "):
args["exact_version"] = str(version[3:])
elif version.startswith (">= "):
args["atleast_version"] = str(version[3:])
elif version.startswith ("<= "):
args["max_version"] = str(version[3:])
else:
#FIXME: < and > are supported as an argument but not by waf
#TODO: Warn that >= is recommended
args["atleast_version"] = str(version)
pass
if self.get_type () == "package":
args["mandatory"] = self.is_mandatory ()
args["args"] = "--cflags --libs"
args["uselib_store"] = normalize_package_name (self.get_name ())
return args
class ProjectOption:
def __init__ (self, name, option):
self._name = str(name)
self._option = option
if not "default" in option:
#TODO: Report lack of default value, default is mandatory
return
if "description" not in option:
#TODO: Report lack of default description as a warning
pass
self._description = str(option["description"])
self._default = str(option["default"])
self._value = self._default
def get_name (self):
return self._name
def get_description (self):
return self._description
def get_default (self):
return self._default
def get_value (self):
return self._value
def set_value (self, value):
self._value = value
def get_option_arguments (self):
"WAF option arguments dictionary"
return {"default": self.get_default (),
"action": "store",
"help": self.get_description ()}
#Mapping between tools and target classes
TOOL_CLASS_MAP = {'cc': CcTarget,
'c++': CcTarget,
'vala': ValaTarget,
'data': DataTarget}
# Mapping between file extensions and tools
EXT_TOOL_MAP = {'cc': ('.c', '.h'),
'c++': ('.cpp', '.cxx'),
'vala': ('.vala', '.gs')}
# Mapping used when multiple tools are fond (using file extensions)
# Keys must be sorted tuples
MULTI_TOOL_MAP = {('c++', 'cc'): 'c++',
('cc', 'vala'): 'vala'}
#!/bin/sh
env CFLAGS="--debug" ./waf configure
project:
name: Brain Break
version: 1
url: http://www.dylanmccall.com
requires:
glib-2.0:
type: package
mandatory: True
xcb:
type: package
version: 1.7
mandatory: True
xcb-screensaver:
type: package
version: 1.7
mandatory: True
gtk+-3.0:
type: package
version: 3.0.8
mandatory: True
libnotify:
type: package
version: 0.4.5
mandatory: True
targets:
brainbreak:
tool: vala
type: program
path: src
input:
- main.vala
- Magic.vala
- Scheduler.vala
- RestScheduler.vala
- PauseScheduler.vala
uses:
- magic_core
packages:
- glib-2.0
- gtk+-3.0
- libnotify
magic_core:
tool: cc
type: staticlib
path: src
input:
- magic_core.c
packages:
- xcb
- xcb-screensaver
namespace Magic {
[Import] public static extern void begin ();
[Import] public static extern uint32 get_idle_time ();
}
using Notify;
public class PauseScheduler : Scheduler {
/* TODO: test if we should manually add idle time every second,
* (which implicitly pauses when computer is in use),
* or use a real Timer
*/
private Timer time_idle;
public PauseScheduler () {
/* 480s = 8 minutes */
/* 20s */
base(480, 20);
time_idle = new Timer();
}
/**
* Per-second timeout during pause break.
*/
private bool active_timeout () {
/* TODO: Delay during active computer use */
/* Update user interface (count every minute) */
stdout.printf("%f spent idle", time_idle.elapsed());
/* End break */
if (time_idle.elapsed() >= duration) {
end(false);
return false;
} else {
return true;
}
}
public override void activate () {
base.activate ();
/* TODO: Start with a notification, then transition to a more visible interface after 30s */
time_idle.start();
Timeout.add_seconds (1, active_timeout);
Notify.Notification notification = new Notification ("Micro break", "It's time for a short break.", null);
notification.show();
}
public override void end (bool quiet = false) {
base.end(quiet);
/* display a happy notification if quiet == false */
}
}
public class RestScheduler : Scheduler {
/* TODO: test if we should manually add idle time every second,
* (which implicitly pauses when computer is in use),
* or use a real Timer
*/
private Timer time_idle;
public RestScheduler () {
/* 2400s = 40 minutes */
/* 360s = 6 minutes */
base(2400, 360);
time_idle = new Timer();
}
/**
* Per-second timeout during rest break.
*/
private bool active_timeout () {
/* TODO: Delay during active computer use */
/* Update user interface (count every minute) */
stdout.printf("%f spent idle", time_idle.elapsed());
/* End break */
if (time_idle.elapsed() >= duration) {
end(false);
return false;
} else {
return true;
}
}
public override void activate () {
base.activate();
/* TODO: Start with a notification, then transition to a more visible interface after 60s */
time_idle.start();
Timeout.add_seconds (1, active_timeout);
}
public override void end (bool quiet = false) {
base.end(quiet);
/* display a happy notification if quiet == false */
}
}
/**
* Interface for a type of break. Each break type has a unique feedback
* mechanism triggered by calling the begin method.
*/
public abstract class Scheduler {
public uint interval {get; set;}
/* TODO: duration should be private to child class */
public uint duration {get; set;}
/** Called when a break starts to run */
public signal void started ();
/** Called when a break is finished running */
public signal void finished ();
protected Timer start_timer;
public Scheduler (uint interval, uint duration) {
this.interval = interval;
this.duration = duration;
start_timer = new Timer();
/* FIXME: We need LCD of duration and interval so we catch idle>duration as well as start the rest on time */
Timeout.add_seconds (duration, idle_timeout);
}
/**
* Periodically tests if it is time for a break
*/
protected bool idle_timeout () {
uint idle_time = (uint)Magic.get_idle_time () / 1000;
/* Reset timer if the user takes a sufficiently long break */
if ((idle_time) > duration) {
stdout.printf("Resetting break timer!\n");
start_timer.start();
}
/* Start break if the user has been active for interval */
if (start_timer.elapsed() >= interval) {
stdout.printf("Activating break!\n");
activate();
}
return true;
}
/**
* It is time for a break!
*/
public virtual void activate () {
started();
}
public virtual void end (bool quiet = false) {
finished();
}
}
#include <stdlib.h>
#include <xcb/xcb.h>
#include <xcb/screensaver.h>
static xcb_connection_t * connection;
static xcb_screen_t * screen;
/**
* Connects to the X server (via xcb) and gets the screen
*/
void magic_begin () {
connection = xcb_connect (NULL, NULL);
screen = xcb_setup_roots_iterator (xcb_get_setup (connection)).data;
}
/**
* Asks X for the time the user has been idle
* @returns idle time in milliseconds
*/
unsigned long magic_get_idle_time () {
xcb_screensaver_query_info_cookie_t cookie;
xcb_screensaver_query_info_reply_t *info;
cookie = xcb_screensaver_query_info (connection,screen->root);
info = xcb_screensaver_query_info_reply (connection, cookie, NULL);
uint32_t idle = info->ms_since_user_input; // get idle time
free (info);
return idle;
}
/*
Brain Break
...Or Yet Another RSI Prevention Tool. This time prettier and happier.
Copyright (c) 2011, Dylan McCall and Brain Break contributors.
<dylanmccall@gmail.com>
-----
This tool is designed to satisfy those who are not suffering from RSI,
but are concerned about the mental and physical issues associated with
heavy computer use. As a result, it is not particular about whether the
user is using a keyboard or a mouse or simply looking at the screen; the
goal here is to encourage people to space computer use around other
things.
Brain Break should stay out of the way and be as non-destructive as
possible, accommodating users regardless of their current tasks without
requiring that they change its aggressiveness themselves, (for example
between Quiet, Postponed and Active mode in Workrave). It should avoid
threatening users with things such as permanent statistics, but provide
them with incentives to be healthy that fit into the moment at hand.
The expected non-destructiveness is easily attainable with libnotify,
especially combined with the excellently transient notifications in
Ubuntu 9.04 and above.
*/
/* nice resource... <http://www.rsiguard.com/> */
using GLib;
using Gdk;
using Notify;
public class BrainBreak : Gtk.Application {
const string app_id = "com.dylanmccall.BrainBreak";
const string app_name = "Brain Break"; /* TODO: translate */
private enum UserMode {
ACTIVE,
PAUSE,
REST
}
private static UserMode user_mode;
private static RestScheduler rest;
private static PauseScheduler pause;
public BrainBreak () {
Object (application_id: app_id, flags: ApplicationFlags.FLAGS_NONE);
GLib.Environment.set_application_name (app_name);
}
public override void activate () {
}
public override void startup () {
this.hold(); /* we're doing stuff, even if no windows are open */
Notify.init (app_id);
Magic.begin();
rest = new RestScheduler();