#!/usr/bin/env python3 import os import sys import argparse import random from subprocess import Popen import gi gi.require_version("Gtk", "3.0") gi.require_version("PangoCairo", "1.0") gi.require_version("Gdk", "3.0") gi.require_version("GLib", "2.0") from gi.repository import Gtk, Gio, cairo, Pango, Gdk, PangoCairo, GLib, GObject def get_parser(): p = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) p.add_argument( "--read-only", action="store_true", dest="readonly", help="Display but dont change the number", ) p.add_argument( "--stdin", action="store_true", default=True, dest="stdin", help="stdin is read and shown", ) p.add_argument("--fullscreen", action="store_true", default=True, dest="fullscreen") p.add_argument( "--no-fullscreen", action="store_false", default=True, dest="fullscreen" ) p.add_argument("--background", default="#111") p.add_argument("--foreground", default="#eee") p.add_argument( "--invert-every", type=float, metavar="SECONDS", default=60, help=( "Swap foreground and background periodically; " "this is useful to avoid CRT-burning; use <= 0 to disable" ), ) p.add_argument("--on-key-press", type=str, default=None, help="A program to call on up/down keypress. " "It will be called with appropriate arguments " "representing the action to take.") return p class App(Gtk.Application): def __init__(self, cli_args, *args, **kwargs): super().__init__( *args, application_id="org.hackmeeting.numeretti", flags=Gio.ApplicationFlags.FLAGS_NONE, **kwargs ) self.window = None self.cli_args = cli_args self._text = "" self._stdin_buffer = "" if self.cli_args.stdin: os.set_blocking(sys.stdin.fileno(), False) GLib.io_add_watch(sys.stdin.fileno(), GLib.IO_IN, self._on_stdin_data) def _on_stdin_data(self, *args, **kwargs): max_line_length = 16 incoming_data = sys.stdin.read(max_line_length) if not incoming_data: return True self._stdin_buffer += incoming_data if len(self._stdin_buffer) == max_line_length: self._stdin_buffer += '\n' if "\n" in self._stdin_buffer: parts = self._stdin_buffer.split("\n") self._stdin_buffer = parts[-1] self.text = parts[-2] return True def do_startup(self): Gtk.Application.do_startup(self) action = Gio.SimpleAction.new("quit", None) action.connect("activate", self.on_quit) self.add_action(action) # self.set_app_menu(…) def do_activate(self): # We only allow a single window and raise any existing ones if not self.window: # Windows are associated with the application # when the last one is closed the application shuts down self.window = AppWindow(application=self, title="Main Window") self.window.present() def on_quit(self, action, param): sys.stdin.close() self.quit() @GObject.Property(type=str, default="", flags=GObject.ParamFlags.READWRITE) def text(self) -> str: return self._text @text.setter def text(self, new_value: str): self._text = new_value def on_binding_pressed(self, action): print('calling:', action) Popen(self.cli_args.on_key_press.split() + [action]) def get_color(description: str) -> Gdk.RGBA: rgba = Gdk.RGBA() ret = rgba.parse(description) if ret is False: raise ValueError("Error parsing color! %s" % description) return rgba class AppWindow(Gtk.ApplicationWindow): def __init__(self, *args, **kwargs): self.app = kwargs["application"] super().__init__(*args, **kwargs) self.rotation = 0 self.invert_colors = False self.drawing_area = Gtk.DrawingArea() self.drawing_area.set_can_focus(True) self.drawing_area.connect("draw", self.redraw) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) vbox.pack_start(self.drawing_area, True, True, 0) self.add(vbox) if self.app.cli_args.fullscreen: self.fullscreen() self.show_all() if self.app.cli_args.invert_every > 0: GLib.timeout_add(self.app.cli_args.invert_every * 1000, self.swap_colors) self.app.connect("notify::text", self.on_text_changed) if self.app.cli_args.on_key_press: self.connect("key-press-event", self.on_key_press) def on_key_press(self, widget, key: Gdk.EventKey): if key.state != 0 or key.type != Gdk.EventType.KEY_PRESS: return if key.keyval == Gdk.KEY_Up: self.app.on_binding_pressed('+1') elif key.keyval == Gdk.KEY_Down: self.app.on_binding_pressed('-1') def force_redraw(self): self.drawing_area.queue_draw() def on_text_changed(self, app, text: str): self.force_redraw() def swap_colors(self): self.invert_colors = not self.invert_colors self.force_redraw() return True @property def font(self): font = Pango.FontDescription() font.set_family("sans-serif") font.set_size(200 * Pango.SCALE) @property def colors(self): fg = self.app.cli_args.foreground bg = self.app.cli_args.background if self.invert_colors: fg, bg = bg, fg return (fg, bg) @property def bg(self): return get_color(self.colors[1]) @property def fg(self): return get_color(self.colors[0]) def redraw(self, widget, cr): # clearly stolen from screen-message. thanks! draw = self.drawing_area Gdk.cairo_set_source_rgba(cr, self.bg) cr.paint() layout = draw.create_pango_layout(self.app.text) layout.set_font_description(self.font) layout.set_alignment(Pango.Alignment.CENTER) w1, h1 = layout.get_pixel_size() if w1 and h1: w2 = draw.get_allocated_width() h2 = draw.get_allocated_height() if self.rotation in [0, 2]: rw1 = w1 rh1 = h1 else: rw1 = h1 rh1 = w1 s = min(w2 / rw1, h2 / rh1) cr.translate(w2 // 2, h2 // 2) cr.rotate(0) cr.scale(s, s) cr.translate(-w1 / 2, -h1 / 2) Gdk.cairo_set_source_rgba(cr, self.fg) PangoCairo.show_layout(cr, layout) def main(): args = get_parser().parse_args() app = App(args) app.run() if __name__ == "__main__": main()