Skip to content

Commit

Permalink
Merge pull request #12 from Dejvino/dev
Browse files Browse the repository at this point in the history
version 0.2
  • Loading branch information
Dejvino authored Feb 6, 2022
2 parents 39c02ed + 170bbf4 commit c2cc55b
Show file tree
Hide file tree
Showing 10 changed files with 568 additions and 408 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Open [Screenshots Gallery](./screenshots/README.md) for more images.
- gradual volume increase of the alarm
- alarm test mode
- alarm accessible from a lockscreen (via MPRIS)
- landscape and portrait mode layouts

![Logo](com.github.dejvino.birdie.png)

Expand All @@ -42,7 +43,7 @@ sudo pip install mpris-server
sudo make install-deb
# on Arch:
sudo pacman -S gst-plugins-base gst-plugins-good
sudo pacman -S gst-plugins-base gst-plugins-good python-pip
pip3 install -r requirements.txt
sudo make install
Expand Down
834 changes: 453 additions & 381 deletions app.ui

Large diffs are not rendered by default.

132 changes: 107 additions & 25 deletions birdie
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

# GPL-2+ ("and any later version")
# Kai Lüke 2020
# Dejvino 2021
# Dejvino 2021-2022

import gi # Debian package: python3-gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gio, GLib # Debian package: gir1.2-glib-2.0, gir1.2-gtk-3.0
import os, sys, signal, psutil # Debian package: python3-psutil
import os, sys, signal
import psutil # Debian package: python3-psutil
from subprocess import Popen, PIPE
from pathlib import Path

from subprocess import check_output
Expand All @@ -17,33 +19,47 @@ user_folder = str(Path.home()) + "/.config/systemd/user"

APP_NAME = 'birdie'
APP_ID = 'com.github.dejvino.birdie'
VERSION = "0.1"
VERSION = "0.2"
LICENSE = Gtk.License.GPL_3_0

UI_FILE_LOCAL = "./app.ui"
UI_FILE_GLOBAL = f"/usr/share/{APP_NAME}/app.ui"
SET_USER_ALARM_COMMAND = f"/usr/lib/{APP_NAME}/libexec/set-user-alarm"
SET_USER_ALARM_COMMAND_LOCAL = "./set-user-alarm"
SET_USER_ALARM_COMMAND_GLOBAL = f"/usr/lib/{APP_NAME}/libexec/set-user-alarm"
INHIBIT_COMMAND_PREFIX = ["/usr/bin/gnome-session-inhibit", "--inhibit", "suspend,logout,idle"]
ALARM_SOUND_COMMAND = [f"/usr/lib/{APP_NAME}/libexec/play-alarm-sound"]
USE_INHIBIT = True # requires support in the session manager
ALARM_COMMAND = (INHIBIT_COMMAND_PREFIX if USE_INHIBIT else []) + ALARM_SOUND_COMMAND
USE_MPRIS = True # requires mpris_server
USE_MPRIS = True # requires mpris_server module, gets checked on module load
USE_INHIBIT = True # requires session manager support, gets checked on startup

dummy_time = "0000-01-01 00:00:00"
MSG_START = "Pick a time and day for waking up."
MSG_ALARM_SET = "Alarm is set!"
MSG_WAKE_UP = "<span color='orange'>Wake up!</span>"
MSG_SET_ALARM_ERROR = "<span color='red'>Error issuing alarm</span>"
MSG_SET_ALARM_ERROR = "<span color='red'>Error issuing alarm, check console.</span>"
MSG_PLAY_ALARM_ERROR = "<span color='red'>Error playing alarm, check console.</span>"
MSG_NO_TIME_LEFT = "No time left"
MSG_SCHEDULING_ALARM = "<span color='gray'>Scheduling alarm...</span>"

def showErrorDialog(text, title=None, parent=None):
dialog = Gtk.MessageDialog(
transient_for=parent,
flags=0,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.CLOSE,
text=text,
title=(APP_NAME + " :: " + (title if title is not None else "Error"))
)
print(f"Error dialog: {text}")
return dialog.run()

class AlarmApp(Gtk.Application):
alarmtime = None
alarm_set = False
alarm_pid = 0
alarm_proc = 0
alarm_testing = False
prev_alarm = None
event_handler = None
snooze_short = 3
snooze_short = 5
snooze_long = 10

def __init__(self, application_id):
Expand Down Expand Up @@ -79,11 +95,13 @@ class AlarmApp(Gtk.Application):
self.soundvolume = self.builder.get_object("soundvolume")
self.window = self.builder.get_object("appwindow")
self.window.set_application(self)
self.mainbox = self.builder.get_object("mainbox")
self.errorlabel = self.builder.get_object("error")
self.errorlabel.set_label(MSG_START)
self.setwakebutton = self.builder.get_object("setwake")
self.cancelbutton = self.builder.get_object("cancel")
self.snoozebutton = self.builder.get_object("snooze")
self.update_layout()
self.window.show_all()
try:
with open(user_folder + "/wake-up.timer", "r") as f:
Expand Down Expand Up @@ -136,27 +154,31 @@ class AlarmApp(Gtk.Application):
self.disable_elements(False)
self.errorlabel.set_label(MSG_START)
self.gen_user_timer(dummy_time)
print("Disabling alarm service")
check_output(["systemctl", "--user", "daemon-reload"]) # Debian package: systemd
check_output(["systemctl", "--user", "disable", "--now", "wake-up.timer"])
check_output(["set-user-alarm"])
self.run_set_user_alarm()
self.update()

def set_alarm(self, rewrite=True):
self.alarm_set = True
self.window.set_sensitive(False)
self.disable_elements(True)
self.errorlabel.set_markup(MSG_SCHEDULING_ALARM)
# TODO: spawn as async task
print("Enabling alarm service")
try:
if rewrite:
time_var = self.alarmtime.format("%Y-%m-%d %H:%M:%S")
check_output([SET_USER_ALARM_COMMAND, time_var])
self.run_set_user_alarm([time_var])
self.gen_user_service()
self.gen_user_timer(time_var)
self.load_user_timer()
self.errorlabel.set_label(MSG_ALARM_SET)
print(f"Alarm set for {self.alarmtime.format('%Y-%m-%d %H:%M:%S')}")
except Exception as e:
print(e)
self.errorlabel.set_markup("<span color='red'>Error setting wake time. Check console log.</span>")
self.errorlabel.set_markup(MSG_SET_ALARM_ERROR)
self.alarm_set = False
self.disable_elements(False)
self.window.set_sensitive(True)
Expand All @@ -168,6 +190,12 @@ class AlarmApp(Gtk.Application):
self.minute.set_value(self.alarmtime.get_minute())
#self.second.set_value(self.alarmtime.get_second())

def run_set_user_alarm(self, params=[]):
if os.path.isfile(SET_USER_ALARM_COMMAND_LOCAL):
check_output([SET_USER_ALARM_COMMAND_LOCAL] + params)
else:
check_output([SET_USER_ALARM_COMMAND_GLOBAL] + params)

def do_snooze(self, snooze_time):
self.set_alarm_time(GLib.DateTime.new_now_local().add_minutes(snooze_time))
self.end_alarm()
Expand All @@ -182,6 +210,13 @@ class AlarmApp(Gtk.Application):
def snooze_clicked(self, button):
self.do_snooze_short()

def time_changed_text(self, input):
# read uncommitted value?
pass

def time_changed(self, input):
self.update()

def all_days_toggle(self, checkbox):
state = self.dayall.get_active()
for day in range(0, 7):
Expand All @@ -198,8 +233,11 @@ class AlarmApp(Gtk.Application):
self.dayall.set_active(True)
else:
self.dayall.set_inconsistent(True)
self.update()

def test_alarm_clicked(self, button):
if self.alarm_testing:
return
self.alarm_testing = True
self.start_alarm()
self.snoozebutton.set_sensitive(False)
Expand All @@ -216,25 +254,28 @@ class AlarmApp(Gtk.Application):
check_output(["pactl", "set-sink-volume", first_sink, str(int(self.volumescale.get_value()))+ "%"])
except Exception as e:
print(e)
self.alarm_pid, _, _, _ = GLib.spawn_async(ALARM_COMMAND, standard_output=-1, standard_input=-1, standard_error=-1)
print("Spawning alarm sound app")
self.alarm_proc = Popen((INHIBIT_COMMAND_PREFIX if USE_INHIBIT else []) + ALARM_SOUND_COMMAND)
self.errorlabel.set_label(MSG_WAKE_UP)
self.lefttime.set_label(MSG_NO_TIME_LEFT)
if self.event_handler:
self.event_handler.event_start()
except Exception as e:
print(e)
self.errorlabel.set_label(MSG_SET_ALARM_ERROR)
self.errorlabel.set_label(MSG_PLAY_ALARM_ERROR)

def end_alarm(self):
if self.alarm_pid != 0:
if self.alarm_proc != 0:
try:
children = psutil.Process(pid=self.alarm_pid).children(recursive=True)
print("Terminating alarm sound app")
children = psutil.Process(pid=self.alarm_proc.pid).children(recursive=True)
for child in children:
os.kill(child.pid, signal.SIGTERM)
os.kill(self.alarm_pid, signal.SIGTERM)
self.alarm_proc.terminate()
self.errorlabel.set_label(MSG_START)
except Exception as e:
print(e)
self.alarm_pid = 0
self.alarm_proc = 0
self.alarm_testing = False
if self.event_handler:
self.event_handler.event_stop()
Expand Down Expand Up @@ -281,6 +322,9 @@ class AlarmApp(Gtk.Application):
self.end_alarm()
self.quit()

def on_resize(self, window):
self.update_layout()

def quit_cb(self, action, parameter):
self.on_window_destroy(self.window)

Expand Down Expand Up @@ -322,13 +366,19 @@ class AlarmApp(Gtk.Application):
min_left = ((int(total_sec_left/60) % 60))
hours_left = (int(total_sec_left/60/60))
label = f"{hours_left:02}:{min_left:02}:{sec_left:02} left"
if self.alarm_pid != 0:
if self.alarm_proc != 0:
self.lefttime.set_label(MSG_NO_TIME_LEFT)
elif self.alarm_set:
self.lefttime.set_label("<span color='orange'>" + label + "</span>")
else:
self.lefttime.set_label(label)

def update_layout(self):
wsize = self.window.get_size()
target_orientation = Gtk.Orientation.VERTICAL if wsize.width < wsize.height else Gtk.Orientation.HORIZONTAL
if (self.mainbox.get_orientation() != target_orientation):
self.mainbox.set_orientation(target_orientation)

def gen_user_service(self):
p = os.path.realpath(__file__)
content = f"""[Unit]
Expand Down Expand Up @@ -372,6 +422,11 @@ WantedBy=timers.target
app = AlarmApp(APP_ID)

if USE_MPRIS:
try:
import mpris_server
except ModuleNotFoundError:
showErrorDialog("mpris_server module is missing. Did you install dependencies?")
sys.exit(1)
from mpris_server.adapters import MprisAdapter
from mpris_server.events import EventAdapter
from mpris_server.server import Server
Expand Down Expand Up @@ -421,7 +476,7 @@ if USE_MPRIS:
app.do_stop()

def get_playstate(self) -> PlayState:
return PlayState.PLAYING if app.alarm_pid != 0 else PlayState.STOPPED
return PlayState.PLAYING if app.alarm_proc != 0 else PlayState.STOPPED

def is_repeating(self) -> bool:
return False
Expand Down Expand Up @@ -501,10 +556,37 @@ if USE_MPRIS:
event_handler = MyAppEventHandler(root=mpris.root, player=mpris.player)
app.register_event_handler(event_handler)

def runSanityCheck(command, message, details=None):
try:
check_output(command)
except Exception as e:
print("\nSanity check Failed!")
print("Command executed: ", command)
print("Error received: ", e)
print()
print(message)
if details != None:
print(details)
showErrorDialog(f"{message}\n\nCheck console output for details.", title="Sanity Check Failed")
sys.exit(1)

if __name__ == "__main__":
check_output(["mkdir", "-p", user_folder])
check_output(["gapplication"]) # Debian package: libglib2.0-bin
check_output(["pactl", "--version"]) # Debian package: pulseaudio-utils
check_output(["gnome-session-inhibit", "--version"]) # Debian package: gnome-session-bin
runSanityCheck(["mkdir", "-p", user_folder],
"Couldn't create configuration directory.")
runSanityCheck(["gapplication"],
"Install missing packages for Gtk.",
" Debian: libglib2.0-bin")
runSanityCheck(["pactl", "--version"],
"Install missing packages for PulseAudio.",
" Debian: pulseaudio-utils")
runSanityCheck(["gnome-session-inhibit", "--version"],
"Install missing packages for Gnome Session.",
" Debian: gnome-session-bin")
try:
check_output(["gnome-session-inhibit", "--inhibit", "suspend", "/bin/pwd"])
except Exception as e:
print("NOTICE: Gnome Session Inhibit is not supported by the environment.")
print("Alarm will not be able to keep the system from suspending!")
USE_INHIBIT = False
exit_status = app.run(sys.argv)
sys.exit(exit_status)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mpris_server==0.2.16
psutil==5.9.0
6 changes: 5 additions & 1 deletion screenshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Simple initial screen: set the time, enable, done!

![default](default.png)

Both portrait and landscape layouts are supported.

![landscape](landscape.png)

## Alarm Set and Waiting
Alarm is scheduled, settings are locked. Countdown to the alarm is shown.

Expand All @@ -23,7 +27,7 @@ Alarm can be stopped or snoozed directly from the lock screen, no need to log in
![lockscreen](lockscreen.png)

## Customization
Select different days, change the maximum alarm volume.
Select different days, change the maximum alarm volume, test how it sounds.

![setup](setup.png)

Binary file modified screenshots/alarm_ringing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/alarm_set.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/landscape.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/setup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit c2cc55b

Please sign in to comment.