An (outdated) collection of plugins for Qtile.
git clone https://github.com/m-col/qtools
Files | Refs | Readme

-rw-r--r-- qtools/widget/habit_tracker.py


      1 # Copyright (c) 2020 Matt Colligan
      2 #
      3 # Permission is hereby granted, free of charge, to any person obtaining a copy
      4 # of this software and associated documentation files (the "Software"), to deal
      5 # in the Software without restriction, including without limitation the rights
      6 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      7 # copies of the Software, and to permit persons to whom the Software is
      8 # furnished to do so, subject to the following conditions:
      9 #
     10 # The above copyright notice and this permission notice shall be included in
     11 # all copies or substantial portions of the Software.
     12 #
     13 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     14 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     15 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     16 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     17 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     18 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     19 # SOFTWARE.
     20 
     21 
     22 import json
     23 import os
     24 from datetime import datetime, timedelta
     25 
     26 from libqtile import bar
     27 from libqtile.log_utils import logger
     28 from libqtile.utils import get_cache_dir
     29 from libqtile.widget import base
     30 
     31 
     32 _CACHE = os.path.join(get_cache_dir(), 'habit_tracker_count.json')
     33 
     34 
     35 class HabitTracker(base._Widget):
     36     """
     37     A don't-break-the-chain style habit tracker widget.
     38 
     39     The current chain lengths are stored in a JSON file containing a dictionary where
     40     each key is the name of a habit. This habit can be passed to the widget to identify
     41     a chain.
     42 
     43     The chain can be drawn in different styles:
     44 
     45         - "chain": A simple chain of connected squares that grows through the grid as it
     46           increases in length.
     47         - "base": A grid of squares where each column represents one digit in a counting
     48           scheme using the base (rows + 1). For example, HabitTracker(rows=1) would draw
     49           a single row of squares that are filled in to represent a binary count.
     50 
     51     """
     52     defaults = [
     53         ("colour", "1667EB", "Fill colour."),
     54         ("chain_file", _CACHE, "File that stores the chain lengths."),
     55         ("habit", "anon", "Habit name. Used for identifying the chain in the cache file."),
     56         ("margin_x", 4, "X margin."),
     57         ("margin_y", 4, "Y margin."),
     58         ("style", "chain", "Counter style, one of: chain, base"),
     59         ("rows", 2, "Number of rows."),
     60         ("columns", 4, "Number of columns."),
     61         ("blank_colour", None, "Colour for placeholder blocks."),
     62     ]
     63 
     64     def __init__(self, **config):
     65         base._Widget.__init__(self, bar.CALCULATED, **config)
     66         self.add_defaults(HabitTracker.defaults)
     67         self._chain = None
     68         self._block_size = 0
     69 
     70         self._load_chain()
     71 
     72         if not hasattr(self, "draw_{0}".format(self.style)):
     73             logger.warning("HabitTracker style '{0}' invalid.".format(self.style))
     74             self.style = "chain"
     75 
     76         if 'Button1' not in self.mouse_callbacks:
     77             self.mouse_callbacks.update({'Button1': self.cmd_increment})
     78         if 'Button2' not in self.mouse_callbacks:
     79             self.mouse_callbacks.update({'Button2': self.cmd_reset})
     80         if 'Button3' not in self.mouse_callbacks:
     81             self.mouse_callbacks.update({'Button3': self.cmd_decrement})
     82 
     83     def _load_chain(self):
     84         if os.path.isfile(self.chain_file):
     85             with open(self.chain_file, 'r') as fd:
     86                 cache = json.load(fd)
     87             if self.habit in cache.keys():
     88                 start_date = datetime.strptime(cache.get(self.habit), "%Y-%m-%d")
     89                 self._chain = (datetime.now() - start_date).days
     90                 return
     91         self._chain = 0
     92         self._save_chain()
     93 
     94     def _save_chain(self):
     95         cache = {}
     96         if os.path.isfile(self.chain_file):
     97             with open(self.chain_file, 'r') as fd:
     98                 cache.update(json.load(fd))
     99         start_date = datetime.now() - timedelta(days=self._chain)
    100         cache.update({self.habit: start_date.strftime("%Y-%m-%d")})
    101         with open(self.chain_file, 'w') as fd:
    102             json.dump(cache, fd)
    103 
    104     def cmd_increment(self, qtile=None):
    105         self._chain += 1
    106         self._save_chain()
    107         self.draw()
    108 
    109     def cmd_decrement(self, qtile=None):
    110         if self._chain > 0:
    111             self._chain -= 1
    112             self._save_chain()
    113             self.draw()
    114 
    115     def cmd_reset(self, qtile=None):
    116         self._chain = 0
    117         self._save_chain()
    118         self.draw()
    119 
    120     def calculate_length(self):
    121         space = self.bar.height - self.margin_y * 2
    122         self._block_size = space // (2 * self.rows - 1)
    123         length = self._block_size * (2 * self.columns - 1)
    124         return length + self.margin_x * 2
    125 
    126     def draw(self):
    127         self.drawer.clear(self.background or self.bar.background)
    128         getattr(self, "draw_{0}".format(self.style))()
    129 
    130     def draw_chain(self):
    131         block_size = self._block_size
    132         start_y = self.bar.height - self.margin_y - block_size
    133 
    134         if self.blank_colour:
    135             self.drawer.set_source_rgb(self.blank_colour)
    136             for col in range(self.columns):
    137                 x_pos = self.margin_x + col * 2 * block_size
    138                 for row in range(self.rows):
    139                     y_pos = start_y - row * 2 * block_size
    140                     self.drawer.ctx.rectangle(x_pos, y_pos, block_size, block_size)
    141             self.drawer.ctx.fill()
    142 
    143         self.drawer.set_source_rgb(self.colour)
    144         chain = self._chain
    145         for col in range(chain // self.rows + 1):
    146             x_pos = self.margin_x + col * 2 * block_size
    147             this_col = min(chain - col * self.rows, self.rows)
    148             rows = range(this_col)
    149             if col % 2:
    150                 rows = [self.rows - 1 - i for i in rows]
    151             for row in rows:
    152                 chain % ((col + 1) * self.rows)
    153                 y_pos = start_y - row * 2 * block_size
    154                 self.drawer.ctx.rectangle(x_pos, y_pos, block_size, block_size)
    155 
    156         self.drawer.ctx.fill()
    157         self.drawer.draw(offsetx=self.offset, width=self.length)
    158 
    159     def draw_base(self):
    160         block_size = self._block_size
    161         start_y = self.bar.height - self.margin_y - block_size
    162 
    163         if self.blank_colour:
    164             self.drawer.set_source_rgb(self.blank_colour)
    165             for col in range(self.columns):
    166                 x_pos = self.margin_x + col * 2 * block_size
    167                 for row in range(self.rows):
    168                     y_pos = start_y - row * 2 * block_size
    169                     self.drawer.ctx.rectangle(x_pos, y_pos, block_size, block_size)
    170             self.drawer.ctx.fill()
    171 
    172         self.drawer.set_source_rgb(self.colour)
    173         chain = self._chain
    174         for col in reversed(range(self.columns)):
    175             units, chain = divmod(chain, (self.rows + 1) ** col)
    176             if units:
    177                 x_pos = self.margin_x + col * 2 * block_size
    178                 for row in range(units):
    179                     y_pos = start_y - row * 2 * block_size
    180                     self.drawer.ctx.rectangle(x_pos, y_pos, block_size, block_size)
    181 
    182         self.drawer.ctx.fill()
    183         self.drawer.draw(offsetx=self.offset, width=self.length)
    184