Add Python version and remove JavaScript version
authorAdrian Iain Lam <adrianiainlam@users.noreply.github.com>
Sun, 2 Jun 2019 15:50:35 +0000 (16:50 +0100)
committerAdrian Iain Lam <adrianiainlam@users.noreply.github.com>
Sun, 2 Jun 2019 15:50:35 +0000 (16:50 +0100)
The node.js version has many dependencies which require custom
fixes, and depends on abandoned libraries. In addition, node does
not come by default on Ubuntu. After translating parts of the
lunar-calendar library to Python, this indicator can now be
implemented in Python instead, making installation easier for
most Ubuntus and requires less RAM and storage, and uses the more
stable Python GTK bindings.

.gitmodules [new file with mode: 0644]
LunarCalendarPy [new submodule]
README.md
indicator-lunar-calendar.js [deleted file]
indicator-lunar-calendar.py [new file with mode: 0755]

diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..74338fa
--- /dev/null
@@ -0,0 +1,3 @@
+[submodule "LunarCalendarPy"]
+       path = LunarCalendarPy
+       url = git://adrianiainlam.tk/LunarCalendarPy.git
diff --git a/LunarCalendarPy b/LunarCalendarPy
new file mode 160000 (submodule)
index 0000000..e0750e8
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit e0750e842201b71c37fdb55a6d6f84bfb39d2c5f
index 962a09b..c1462ca 100644 (file)
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
 An application indicator for Unity that displays the current date and time
 in lunar calendar.
 
-Copyright (c) 2016 Adrian I Lam <adrianiainlam@gmail.com>
+Copyright (c) 2016-2019 Adrian I Lam <spam@adrianiainlam.tk> s/spam/me/
 
 *Not to be confused with [indicator-lunar][1], which shows attributes and
 ephemerides for astronomical objects.*
@@ -30,51 +30,41 @@ shown above is using timezone UTC+1.
 
 ## Dependencies
 
- - [Node.js][2]
-   
-   Note: node-gtk, one of this program's dependencies, requires nodejs version
-   5 or above.
-   
-   [2]: https://nodejs.org/en/
-
- - [node-gtk][3] (by @WebReflection)
-   npm package: https://www.npmjs.com/package/node-gtk
-   
-   Dependencies: build-essential, git, nodejs (>= 5), gobject-introspection,
-   libgirepository1.0-dev
-   
-   Note: This package failed to build for me. I had to remove `-Werror` from
-   `cflags` in file "bindings.gyp" to get it to build.
-   
-   [3]: https://github.com/WebReflection/node-gtk
-   
- - [lunar-calendar-zh][4] (by @roadmanfong)
-
-   npm package: https://www.npmjs.com/package/lunar-calendar-zh
-   
-   Note: This package contains a bug which renders it useless if your computer
-   is set to a time zone which observes Daylight Saving. I have forked it and
-   fixed it in <https://github.com/adrianiainlam/LunarCalendar>.
-   
-   [4]: https://github.com/roadmanfong/LunarCalendar
-   
- - [node-cron (cron)][5] (by @ncb000gt)
-   npm package: https://www.npmjs.com/package/cron
-   
-   [5]: https://github.com/ncb000gt/node-cron
-   
- - [node-dbus (dbus-native)][6] (by @sidorares)
-   npm package: https://www.npmjs.com/package/dbus-native
-   
-   [6]: https://github.com/sidorares/node-dbus
+ - Python 3
+
+ - [LunarCalendarPy][lcp] (included as submodule here)
+
+   Translated from the JavaScript [LunarCalendar][lc] library by GitHub user
+   @zzyss86.
+
+   [lc]: https://github.com/zzyss86/LunarCalendar
+   [lcp]: https://adrianiainlam.tk/git/?p=LunarCalendarPy.git;a=summary
+
+ - [schedule][schedule]
+
+   Used for periodic update of the indicator.
+
+   [schedule]: https://pypi.org/project/schedule/
+
+ - [dbus-python][dbus]
+
+   Detects suspends/hibernates which would cause incorrect timings
+   used by schedule.
+
+   [dbus]: https://pypi.org/project/dbus-python/
+
+This indicator used to be written in JavaScript (node.js) using the
+node-gtk package, but it was eventually abandoned, got replaced,
+the replacement was abandoned, etc. The situation was a bit too messy
+for me so I decided to just rewrite the whole thing in Python, which
+would also make installation easier for most standard Ubuntus, and
+would use less RAM.
+
 
 ## Usage
 
- 1. Install the dependencies listed above.
- 2. Clone this repository.
+ 1. Install schedule and dbus-python (`pip install schedule dbus-python`).
+ 2. Clone this repository (`git clone --recurse-submodules git://adrianiainlam.tk/indicator-lunar-calendar.git`).
  3. Add the script as a startup application.
  4. Run the script manually for the first time. (Alternatively, log out
     and log in again.)
@@ -86,4 +76,4 @@ shown above is using timezone UTC+1.
 ## License
 
 This program is released under the MIT License. For the full text of this
-license, please refer to the file "indicator-lunar-calendar.js".
+license, please refer to the file "indicator-lunar-calendar.py".
diff --git a/indicator-lunar-calendar.js b/indicator-lunar-calendar.js
deleted file mode 100755 (executable)
index 16af28c..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-#!/usr/bin/env node
-/*
- * indicator-lunar-calendar - shows lunar calendar information
- * Copyright (c) 2016 Adrian I Lam <adrianiainlam@gmail.com>
- * 
- * Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- * 
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- * 
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
- * THE AUTHOR OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
- * IN THE SOFTWARE.
- */
-
-/* import dependencies */
-const gi = require('node-gtk');
-const Gtk = gi.require('Gtk', '3.0');
-const AppIndicator3 = gi.require('AppIndicator3');
-var CronJob = require('cron').CronJob;
-var LunarCalendar = require('lunar-calendar-zh');
-var DBus = require('dbus-native');
-
-/* setup indicator object */
-gi.startLoop();
-Gtk.init();
-var indicator = AppIndicator3.Indicator.new(
-    "lunar-indicator",
-    __dirname + '/icons/鼠.svg',
-    AppIndicator3.IndicatorCategory.APPLICATION_STATUS
-);
-indicator.setStatus(AppIndicator3.IndicatorStatus.ACTIVE);
-var menu = new Gtk.Menu();
-var item = new Gtk.MenuItem();
-menu.append(item);
-indicator.setMenu(menu);
-menu.showAll();
-
-function update_indicator() {
-    /* get current time at UTC+8, add 1 to date if after 23:00 (子時) */
-    var now = new Date(new Date().getTime() + 8 * 3600 * 1000);
-    var hour = now.getUTCHours();
-    if(hour >= 23) { // 子時 of the next day
-        now = new Date(now.getTime() + 24 * 3600 * 1000);
-    }
-    var year  = now.getUTCFullYear();
-    var month = now.getUTCMonth() + 1;
-    var day   = now.getUTCDate();
-    
-    /* obtain date/time in lunar calendar */
-    var lunar = LunarCalendar.solarToLunar(year, month, day);
-    lunar.hour = '子丑寅卯辰巳午未申酉戌亥'[Math.floor((hour + 1) % 24 / 2)];
-    
-    /* output formatting */
-    var compact_date = lunar.lunarMonthName + lunar.lunarDayName;
-    var long_date = lunar.GanZhiYear + '年(' + lunar.zodiac + '年)\n' +
-                    lunar.lunarMonthName + lunar.lunarDayName;
-    if(lunar.term) { // add solar term (節氣) to output if at solar term
-        compact_date += ' ' + lunar.term;
-        long_date += ' ' + lunar.term;
-    }
-    long_date += '\n' + lunar.hour + '時';
-    
-    /* output to indicator */
-    indicator.setIcon(__dirname + '/icons/' + lunar.zodiac + '.svg');
-    indicator.setLabel(compact_date, '');
-    item.setLabel(long_date);
-    
-    console.log('Indicator updated. ' + lunar.hour + ' Time: ' + new Date());
-    /* DO NOT REMOVE THE ABOVE LINE.
-     * I have no absolutely no idea why but the indicator doesn't get
-     * updated if this line is removed.
-     * It wasn't needed though before implementing the DBus section below.
-     * So perhaps some kind of conflict between the two?
-     */
-}
-
-var job = new CronJob({
-    cronTime: '0 * * * *', // every hour
-    onTick: update_indicator,
-    start: true
-});
-
-update_indicator();
-
-/* Detect resume from suspend and update date/time */
-var bus = DBus.systemBus();
-var service = bus.getService('org.freedesktop.login1');
-service.getInterface(
-    '/org/freedesktop/login1',
-    'org.freedesktop.login1.Manager',
-    function(err, nm) {
-        nm.addListener('PrepareForSleep', function(arg) {
-            // PrepareForSleep returns false when resuming from suspend
-            if(!arg) {
-                job.stop(); // force cronjob to recalculate time
-                job.start();
-                update_indicator();
-            }
-        });
-    }
-);
-
-Gtk.main();
diff --git a/indicator-lunar-calendar.py b/indicator-lunar-calendar.py
new file mode 100755 (executable)
index 0000000..9f3ef01
--- /dev/null
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# indicator-lunar-calendar - shows lunar calendar information
+# Copyright (c) 2019 Adrian I Lam <spam@adrianiainlam.tk> s/spam/me/
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHOR OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+# I would like to thank Tobias Schlitt <toby@php.net>, who wrote
+# indicator-chars <https://github.com/tobyS/indicator-chars> which I used
+# as a reference when writing this software.
+
+import signal
+import datetime
+import os
+import gi
+gi.require_version('Gdk', '3.0')
+gi.require_version('Gtk', '3.0')
+gi.require_version('AppIndicator3', '0.1')
+from gi.repository import Gdk, Gtk, AppIndicator3
+from LunarCalendarPy.LunarCalendar import LunarCalendar
+import schedule
+import threading
+import time
+import dbus
+from dbus.mainloop.glib import DBusGMainLoop
+
+APP_NAME = 'indicator-lunar-calendar-py'
+APP_VERSION = '1.2+py'
+
+SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
+
+
+class IndicatorLunarCalendar:
+    def __init__(self):
+        self.indicator = AppIndicator3.Indicator.new(
+            'lunar-indicator',
+            os.path.join(SCRIPT_DIR, 'icons', '鼠.svg'),
+            AppIndicator3.IndicatorCategory.APPLICATION_STATUS)
+        self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
+
+        self.menu = Gtk.Menu()
+        self.item = Gtk.MenuItem()
+        self.item.connect("activate", self.do_nothing)
+        self.menu.append(self.item)
+        self.indicator.set_menu(self.menu)
+        self.menu.show_all()
+
+        self.lc = LunarCalendar()
+        self.update_indicator()
+
+    def update_indicator(self):
+        # get current time at UTC+8, add 1 to date if after 23:00 (子時)
+        now = datetime.datetime.utcnow() + datetime.timedelta(hours=8)
+        hour = now.hour
+        if hour >= 23:
+            now = now.date() + datetime.timedelta(days=1)
+
+        lunar = self.lc.solarToLunar(now.year, now.month, now.day)
+        lunar['hour'] = '子丑寅卯辰巳午未申酉戌亥'[(hour + 1) % 24 // 2]
+
+        compact_date = lunar['lunarMonthName'] + lunar['lunarDayName']
+        long_date = (
+            lunar['GanZhiYear'] + '年(' + lunar['zodiac'] + '年)\n' +
+            lunar['lunarMonthName'] + lunar['lunarDayName']
+        )
+        if lunar['term']:
+            compact_date += ' ' + lunar['term']
+            long_date += ' ' + lunar['term']
+        long_date += '\n' + lunar['hour'] + '時'
+
+        self.indicator.set_icon(
+            os.path.join(SCRIPT_DIR, 'icons', lunar['zodiac'] + '.svg')
+        )
+        self.indicator.set_label(compact_date, '')
+        self.item.set_label(long_date)
+
+    def do_nothing(self, arg):
+        pass
+
+
+def run_schedule_one_iteration():
+    prev_idle_sec = 60 * 60
+    idle_sec = schedule.idle_seconds()
+    while idle_sec < prev_idle_sec:
+        prev_idle_sec = idle_sec
+
+        if idle_sec > 2:
+            time.sleep(idle_sec - 2)
+        elif idle_sec > 0.199:
+            time.sleep(idle_sec - 0.199)
+        else:
+            schedule.run_pending()
+
+        idle_sec = schedule.idle_seconds()
+
+
+def cronThreadBody():
+    while True:
+        run_schedule_one_iteration()
+
+
+def updateJob(i):
+    i.update_indicator()
+
+
+if __name__ == '__main__':
+    signal.signal(signal.SIGINT, lambda signum, frame: Gtk.main_quit())
+
+    i = IndicatorLunarCalendar()
+
+    schedule.every().hour.at(":00").do(updateJob, i=i)
+    cronThread = threading.Thread(target=cronThreadBody)
+    cronThread.start()
+
+    dbusloop = DBusGMainLoop()
+    bus = dbus.SystemBus(mainloop=dbusloop)
+    obj = bus.get_object('org.freedesktop.login1', '/org/freedesktop/login1')
+    iface = dbus.Interface(obj,
+                           dbus_interface='org.freedesktop.login1.Manager')
+
+    def sleepHandler(arg):
+        if arg == 0:
+            i.update_indicator()
+            schedule.clear()
+            schedule.every().minute.at(":00").do(updateJob, i=i)
+            newThread = threading.Thread(target=run_schedule_one_iteration)
+            newThread.start()
+
+    iface.connect_to_signal('PrepareForSleep', sleepHandler)
+
+    Gtk.main()