diff --git a/configuration.nix b/configuration.nix index ba41e3e..71bf094 100644 --- a/configuration.nix +++ b/configuration.nix @@ -2,7 +2,7 @@ # your system. Help is available in the configuration.nix(5) man page # and in the NixOS manual (accessible by running ‘nixos-help’). -{ pkgs, name, flakes, ... }: +{ pkgs, name, flakes, flakeOutputs, ... }: { config, pkgs, ...}: { imports = @@ -54,7 +54,7 @@ experimental-features = nix-command flakes ''; - nixpkgs.overlays = [ flakes.emacs-overlay.overlay ]; + nixpkgs.overlays = [ flakes.emacs-overlay.overlay flakeOutputs.overlay ]; # Enable the X11 windowing system. services.xserver.enable = true; diff --git a/default.nix b/default.nix index db298aa..2d05311 100644 --- a/default.nix +++ b/default.nix @@ -7,7 +7,7 @@ extraModules = if args ? extraModules then args.extraModules else [ ]; extraOverlays = if args ? extraOverlays then args.extraOverlays else [ ]; pkgs = flakes.nixpkgs; - configuration = import ./configuration.nix {inherit extraOverlays system pkgs name flakes;} ; + configuration = import ./configuration.nix {inherit extraOverlays system pkgs name flakes flakeOutputs;} ; in { inherit name; diff --git a/flake.nix b/flake.nix index c319f99..06d36eb 100644 --- a/flake.nix +++ b/flake.nix @@ -23,6 +23,7 @@ outputs = {self, ...}@inputs: let outputs = rec { + overlay = import ./local-overlay; nixosConfigurations = import self { flakes = inputs; flakeOutputs = outputs; diff --git a/local-overlay/default.nix b/local-overlay/default.nix new file mode 100644 index 0000000..53c4825 --- /dev/null +++ b/local-overlay/default.nix @@ -0,0 +1,4 @@ +final: prev: +{ + tray-calendar = final.callPackage ./pkgs/tray-calendar {}; +} diff --git a/local-overlay/pkgs/tray-calendar/default.nix b/local-overlay/pkgs/tray-calendar/default.nix new file mode 100644 index 0000000..ad6df00 --- /dev/null +++ b/local-overlay/pkgs/tray-calendar/default.nix @@ -0,0 +1,29 @@ +{ stdenv +, python3 +, gtk3 +, gobject-introspection +, wrapGAppsHook +, lib +}: + +stdenv.mkDerivation rec { + pname = "tray-calendar"; + version = "0.9"; + src = ./traycalendar.py; + + buildInputs = [ + (python3.withPackages (pyPkgs: with pyPkgs; [ + pygobject3 + ])) + gtk3 + gobject-introspection + ]; + nativeBuildInputs = [ wrapGAppsHook ]; + + dontUnpack = true; + installPhase = "install -m755 -D $src $out/bin/traycalendar"; + meta = { + license = lib.licenses.gpl3Only; + homepage = "https://github.com/vifon/TrayCalendar"; + }; +} diff --git a/local-overlay/pkgs/tray-calendar/traycalendar.py b/local-overlay/pkgs/tray-calendar/traycalendar.py new file mode 100755 index 0000000..92859b9 --- /dev/null +++ b/local-overlay/pkgs/tray-calendar/traycalendar.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +######################################################################## +# Copyright (C) 2015-2018 Wojciech Siewierski # +# # +# This program is free software; you can redistribute it and/or # +# modify it under the terms of the GNU General Public License # +# as published by the Free Software Foundation; either version 3 # +# of the License, or (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +######################################################################## + + +import functools +import glob +import os.path +import re +from collections import defaultdict +from os import getenv + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gdk + + +DEFAULT_ORG_DIRECTORY = os.path.join(getenv('HOME'), "org") +ORG_GLOB = '*.org' +ORG_ARCHIVE_SUFFIX = '_archive.org' + + +def org_agenda_files(directory): + org_abs = functools.partial(os.path.join, directory) + agenda_files_path = org_abs('.agenda-files') + try: + with open(agenda_files_path) as agenda_files: + yield from (org_abs(f.rstrip('\n')) for f in agenda_files) + except FileNotFoundError: + for filename in glob.iglob(os.path.join(directory, ORG_GLOB)): + if not filename.endswith(ORG_ARCHIVE_SUFFIX): + yield filename + + +def scan_org_for_events(org_directories): + """Search the org files for the calendar events. + + Scans the passed directories for the .org files and saves the events + found there into a multilevel dict of lists: events[year][month][day] + + The returned dict uses defaultdict so *do not* rely on the + KeyError exception etc.! Check if the element exists with + .get(key) before accessing it! + + """ + + def year_dict(): + return defaultdict(month_dict) + def month_dict(): + return defaultdict(day_dict) + def day_dict(): + return defaultdict(event_list) + def event_list(): + return list() + + events = year_dict() + for org_directory in org_directories: + for filename in org_agenda_files(org_directory): + with open(filename, "r") as filehandle: + last_heading = None + for line in filehandle: + heading_match = re.search(r'^\*+\s+(.*)', line) + if heading_match: + last_heading = heading_match.group(1) + # strip the tags + last_heading = re.sub(r'\s*\S*$', last_heading, '') + match = re.search(r'<(\d{4})-(\d{2})-(\d{2}).*?>', line) + if match: + year, month, day = [ int(field) for field in match.group(1,2,3) ] + month -= 1 # months are indexed from 0 in Gtk.Calendar + events[year][month][day].append(last_heading) + return events + +class CalendarWindow(object): + + def __init__(self, org_directories): + self.window = Gtk.Window() + self.window.set_wmclass("traycalendar", "TrayCalendar") + + self.window.set_resizable(False) + self.window.set_decorated(False) + self.window.set_gravity(Gdk.Gravity.STATIC) + + window_width = 300 + + # Set the window geometry. + geometry = Gdk.Geometry() + geometry.min_width = window_width + geometry.max_width = window_width + geometry.base_width = window_width + self.window.set_geometry_hints( + None, geometry, + Gdk.WindowHints.MIN_SIZE | + Gdk.WindowHints.MAX_SIZE | + Gdk.WindowHints.BASE_SIZE) + + # Create the listview for the calendar events. + list_model = Gtk.ListStore(str) + list_view = Gtk.TreeView(list_model) + list_column = Gtk.TreeViewColumn("Events", Gtk.CellRendererText(), text=0) + list_column.set_fixed_width(window_width) + list_view.append_column(list_column) + + # Create the calendar widget. + calendar = Gtk.Calendar() + self.calendar_events = scan_org_for_events(org_directories) + calendar.connect('month-changed', self.mark_calendar_events) + calendar.connect('day-selected', self.display_event_list, list_model) + self.mark_calendar_events(calendar) + self.display_event_list(calendar, list_model) + + close_button = Gtk.Button("Close") + close_button.connect('clicked', lambda event: self.window.destroy()) + + vbox = Gtk.VBox() + vbox.add(close_button) + vbox.add(calendar) + vbox.add(list_view) + + self.window.add(vbox) + + rootwin = self.window.get_screen().get_root_window() + # get_pointer is deprecated but using Gdk.Device.get_position + # is not viable here: we have no access to the pointing device. + screen, x, y, mask = rootwin.get_pointer() + x -= window_width + # Show the window right beside the cursor. + self.window.move(x,y) + + self.window.show_all() + + def mark_calendar_events(self, calendar): + """Update the days with calendar events list for the selected month.""" + year, month, day = calendar.get_date() + calendar.freeze_notify() + calendar.clear_marks() + for day in self.calendar_events[year][month]: + calendar.mark_day(day) + calendar.thaw_notify() + + def display_event_list(self, calendar, event_list): + """Update the calendar event list for the selected day.""" + year, month, day = calendar.get_date() + event_list.clear() + + # get(day) used instead of [day] because we use defaultdict + # and it would create a new element. + events = self.calendar_events[year][month].get(day) + if events: + for event in events: + event_list.append([event]) + + +def tray_mode(org_directories): + def on_left_click(event): + window = CalendarWindow(org_directories) + def on_right_click(button, time, data): + Gtk.main_quit() + statusicon = Gtk.StatusIcon() + statusicon.set_from_icon_name('x-office-calendar') + statusicon.connect('activate', on_left_click) + statusicon.connect('popup-menu', on_right_click) + Gtk.main() + +def window_mode(org_directories): + window = CalendarWindow(org_directories) + window.window.connect('destroy', Gtk.main_quit) + Gtk.main() + +def main(argv=None): + import argparse + parser = argparse.ArgumentParser() + parser.add_argument( + "--no-tray", + help="Show the calendar windows immediately and quit after it's closed.", + action='store_true', + ) + parser.add_argument( + "--org-directory", "-d", + help="Directories to search for *.org; default: ~/org/.", + action='append', + dest='org_directories', + ) + args = parser.parse_args() + + if not args.org_directories: + args.org_directories = [DEFAULT_ORG_DIRECTORY] + + if args.no_tray: + window_mode(args.org_directories) + else: + tray_mode(args.org_directories) + +if __name__ == "__main__": + from sys import argv + + # workaround for a pygobject bug + import signal + signal.signal(signal.SIGINT, signal.SIG_DFL) + + main(argv) diff --git a/users/ellmau/polybar.nix b/users/ellmau/polybar.nix index 9033866..a18baf7 100644 --- a/users/ellmau/polybar.nix +++ b/users/ellmau/polybar.nix @@ -233,7 +233,7 @@ #format-prefix-foreground = foreground_altcol; format-underline = "#0a6cf5"; - label = "%date% %time%"; + label = "%{A1:${pkgs.tray-calendar}/bin/traycalendar --no-tray:}%{A} %date% %time%"; }; "module/battery" = { type = "internal/battery";