num_display.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. #!/usr/bin/env python3
  2. import os
  3. import sys
  4. import argparse
  5. import random
  6. import gi
  7. gi.require_version("Gtk", "3.0")
  8. gi.require_version("PangoCairo", "1.0")
  9. gi.require_version("Gdk", "3.0")
  10. gi.require_version("GLib", "2.0")
  11. from gi.repository import Gtk, Gio, cairo, Pango, Gdk, PangoCairo, GLib
  12. def get_parser():
  13. p = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
  14. p.add_argument(
  15. "--read-only",
  16. action="store_true",
  17. dest="readonly",
  18. help="Display but dont change the number",
  19. )
  20. p.add_argument(
  21. "--stdin",
  22. action="store_true",
  23. default=True,
  24. dest="stdin",
  25. help="stdin is read and shown",
  26. )
  27. p.add_argument("--fullscreen", action="store_true", default=True, dest="fullscreen")
  28. p.add_argument(
  29. "--no-fullscreen", action="store_false", default=True, dest="fullscreen"
  30. )
  31. p.add_argument("--background", default="#111")
  32. p.add_argument("--foreground", default="#eee")
  33. p.add_argument(
  34. "--invert-every",
  35. type=float,
  36. metavar="SECONDS",
  37. default=60,
  38. help=(
  39. "Swap foreground and background periodically; "
  40. "this is useful to avoid CRT-burning; use <= 0 to disable"
  41. ),
  42. )
  43. return p
  44. class App(Gtk.Application):
  45. def __init__(self, cli_args, *args, **kwargs):
  46. super().__init__(
  47. *args,
  48. application_id="org.hackmeeting.numeretti",
  49. flags=Gio.ApplicationFlags.FLAGS_NONE,
  50. **kwargs
  51. )
  52. self.window = None
  53. self.cli_args = cli_args
  54. def do_startup(self):
  55. Gtk.Application.do_startup(self)
  56. action = Gio.SimpleAction.new("quit", None)
  57. action.connect("activate", self.on_quit)
  58. self.add_action(action)
  59. # self.set_app_menu(…)
  60. def do_activate(self):
  61. # We only allow a single window and raise any existing ones
  62. if not self.window:
  63. # Windows are associated with the application
  64. # when the last one is closed the application shuts down
  65. self.window = AppWindow(application=self, title="Main Window")
  66. self.window.present()
  67. def on_quit(self, action, param):
  68. self.quit()
  69. def get_color(description: str) -> Gdk.RGBA:
  70. rgba = Gdk.RGBA()
  71. ret = rgba.parse(description)
  72. if ret is False:
  73. raise ValueError("Error parsing color! %s" % description)
  74. return rgba
  75. class AppWindow(Gtk.ApplicationWindow):
  76. def __init__(self, *args, **kwargs):
  77. self.app = kwargs["application"]
  78. super().__init__(*args, **kwargs)
  79. self.text = ""
  80. self.buffer = ""
  81. self.rotation = 0
  82. self.invert_colors = False
  83. self.drawing_area = Gtk.DrawingArea()
  84. self.drawing_area.set_can_focus(True)
  85. self.drawing_area.connect("draw", self.redraw)
  86. vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
  87. vbox.pack_start(self.drawing_area, True, True, 0)
  88. self.add(vbox)
  89. if self.app.cli_args.fullscreen:
  90. self.fullscreen()
  91. self.show_all()
  92. if self.app.cli_args.stdin:
  93. os.set_blocking(sys.stdin.fileno(), False)
  94. GLib.io_add_watch(sys.stdin.fileno(), GLib.IO_IN, self._on_stdin_data)
  95. if self.app.cli_args.invert_every > 0:
  96. GLib.timeout_add(self.app.cli_args.invert_every * 1000, self.swap_colors)
  97. def force_redraw(self):
  98. self.drawing_area.queue_draw()
  99. def _on_stdin_data(self, *args, **kwargs):
  100. max_line_length = 16
  101. incoming_data = sys.stdin.read(max_line_length)
  102. if not incoming_data:
  103. return True
  104. self.buffer += incoming_data
  105. if len(self.buffer) == max_line_length:
  106. self.buffer += '\n'
  107. if "\n" in self.buffer:
  108. parts = self.buffer.split("\n")
  109. self.buffer = parts[-1]
  110. self.change_text(parts[-2])
  111. return True
  112. def change_text(self, text: str):
  113. self.text = text
  114. self.force_redraw()
  115. def swap_colors(self):
  116. self.invert_colors = not self.invert_colors
  117. self.force_redraw()
  118. return True
  119. @property
  120. def font(self):
  121. font = Pango.FontDescription()
  122. font.set_family("sans-serif")
  123. font.set_size(200 * Pango.SCALE)
  124. @property
  125. def colors(self):
  126. fg = self.app.cli_args.foreground
  127. bg = self.app.cli_args.background
  128. if self.invert_colors:
  129. fg, bg = bg, fg
  130. return (fg, bg)
  131. @property
  132. def bg(self):
  133. return get_color(self.colors[1])
  134. @property
  135. def fg(self):
  136. return get_color(self.colors[0])
  137. def redraw(self, widget, cr):
  138. # clearly stolen from screen-message. thanks!
  139. draw = self.drawing_area
  140. Gdk.cairo_set_source_rgba(cr, self.bg)
  141. cr.paint()
  142. layout = draw.create_pango_layout(self.text)
  143. layout.set_font_description(self.font)
  144. layout.set_alignment(Pango.Alignment.CENTER)
  145. w1, h1 = layout.get_pixel_size()
  146. if w1 and h1:
  147. w2 = draw.get_allocated_width()
  148. h2 = draw.get_allocated_height()
  149. if self.rotation in [0, 2]:
  150. rw1 = w1
  151. rh1 = h1
  152. else:
  153. rw1 = h1
  154. rh1 = w1
  155. s = min(w2 / rw1, h2 / rh1)
  156. cr.translate(w2 // 2, h2 // 2)
  157. cr.rotate(0)
  158. cr.scale(s, s)
  159. cr.translate(-w1 / 2, -h1 / 2)
  160. Gdk.cairo_set_source_rgba(cr, self.fg)
  161. PangoCairo.show_layout(cr, layout)
  162. def main():
  163. args = get_parser().parse_args()
  164. app = App(args)
  165. app.run()
  166. if __name__ == "__main__":
  167. main()