num_display.py 6.8 KB

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