from sys import path import os from time import strftime from functools import partial import re fl0w_path = os.path.dirname(os.path.realpath(__file__)) shared_path = os.path.dirname(os.path.realpath(__file__)) + "/Shared/" if fl0w_path not in path: path.append(fl0w_path) if shared_path not in path: path.append(shared_path) import sublime import sublime_plugin from Highway import Client, Route, Pipe, DummyPipe from Utils import get_hostname from SublimeMenu import * import Logging import webbrowser from time import sleep import os CHANNEL = 1 FL0W_STATUS = "fl0w" def plugin_unloaded(): for window in windows: if hasattr(window, "fl0w") and window.fl0w.connected: window.fl0w.invoke_disconnect() for sensor_type in ("analog", "digital"): window.active_view().erase_phantoms(sensor_type) PARENTHESES_REGEX = re.compile("\((.*?)\)") STYLE_OPEN = "" STYLE_CLOSE = "" ERROR_OPEN = "" ERROR_CLOSE = "" windows = [] sensor_phantoms = [] def set_status(status, window): window.active_view().set_status(FL0W_STATUS, "fl0w: %s" % status) class Fl0wClient(Client): def setup(self, routes, fl0w, debug=False): super().setup(routes, debug=debug) self.fl0w = fl0w def ready(self): self.fl0w.connected = True if self.fl0w.debug: Logging.info("Connection ready!") # Enlist on editor channel self.send({"channel" : 1, "name" : get_hostname()}, "subscribe") # Subscribe to controller channel self.send({"subscribe" : [2]}, "peers") def closed(self, code, reason): self.fl0w.connected = False if self.fl0w.debug: Logging.info("Connection closed: %s (%s)" % (reason, code)) def peer_unavaliable(self, peer): sublime.error_message("The specifed controller is not connected anymore.") class Info(Route): def run(self, data, handler): info = "" for key in data: info += "%s: %s\n" % (key.capitalize(), ", ".join(data[key])) sublime.message_dialog(info) handler.fl0w.meta.invoke(handler.fl0w.window, back=handler.fl0w.main_menu) class Sensor(Pipe): def run(self, data, peer, handler): for sensor_phantom in handler.fl0w.subscriptions: sensor_phantom.update_sensor_values(data) class Peers(Route): def start(self, handler): self.selected_action_menu = None def run(self, data, handler): handler.fl0w.controller_menu.clear() for id_ in data: action_menu = Menu() action_menu.id_ = id_ action_menu += Entry("Set Target", "Set controller as target for program execution and sensor readouts.", action=partial(lambda handler, id_: self.set_target(handler, id_), handler, id_)) action_menu += Entry("Programs", "Lists all executable programs on the controller.", action=partial(lambda handler, id_: handler.pipe(None, "list_programs", id_), handler, id_)) action_menu += Entry("Set Name", "Sets the hostname of the selected controller", action=partial(lambda handler, id_: Input("New Hostname:", initial_text=data[id_]["name"], on_done=lambda hostname: handler.pipe( {"set" : hostname}, "hostname", id_)).invoke(handler.fl0w.window), handler, id_)) action_menu += Entry("Processes", "Lists processes currently running on controller.", action=partial(lambda handler, id_: handler.pipe(None, "processes", id_), handler, id_)) action_menu += Entry("Identify", "Plays an identification sound on the controller.", action=partial(lambda handler, id_: handler.pipe(None, "identify", id_), handler, id_)) action_menu.back = handler.fl0w.controller_menu handler.fl0w.controller_menu += Entry(data[id_]["name"], id_, sub_menu=action_menu, action=self.set_selected_action_menu, kwargs={"selected_action_menu" : action_menu}) def set_target(self, handler, peer): handler.fl0w.target = peer if handler.fl0w.debug: set_status("Target: %s" % peer, handler.fl0w.window) def set_selected_action_menu(self, selected_action_menu): self.selected_action_menu = selected_action_menu class Processes(Pipe): def run(self, data, peer, handler): view = handler.fl0w.window.new_file() view.set_name("Processes") view.settings().set("draw_indent_guides", False) for line in data: view.run_command("append", {"characters": line + "\n"}) view.set_read_only(True) class ListPrograms(Pipe): def run(self, data, peer, handler): program_menu = Menu(subtitles=False) for program in data: program_menu += Entry(program, action=lambda handler: handler.pipe(program, "run_program", handler.routes["peers"].selected_action_menu.id_), kwargs={"handler" : handler}) program_menu.invoke(handler.fl0w.window, back=handler.routes["peers"].selected_action_menu) class StdStream(Pipe): def run(data, peer, handler): pass class Fl0w: def __init__(self, window, debug=False): self.window = window self.folder = window.folders()[0] if self.folder != "/": self.folder = self.folder + "/" self.connected = False self.subscriptions = {} self._combined_subscriptions = {"analog" : [], "digital" : []} self._target = None self._debug = debug self.start_menu = Menu() self.start_menu += Entry("Connect", "Connect to a fl0w server", action=self.invoke_connect) self.start_menu += Entry("About", "Information about fl0w", action=self.invoke_about) self.debug_menu = Menu(subtitles=False) self.debug_menu += Entry("On", action=lambda: self.set_debug(True)) self.debug_menu += Entry("Off", action=lambda: self.set_debug(False)) self.settings = Menu() self.settings += Entry("Debug", "Toggle debug mode", sub_menu=self.debug_menu) self.meta = Menu() self.meta += Entry("Info", "Server info", action=lambda: self.ws.send(None, "info")) self.meta_entry = Entry("Meta", "Debug information about fl0w", sub_menu=self.meta) if self.debug: self.main_menu += self.meta_entry self.main_menu = Menu() self.controller_menu = Menu() self.main_menu += Entry("Controllers", "All connected controllers", sub_menu=self.controller_menu) self.main_menu += Entry("Settings", "General purpose settings", sub_menu=self.settings) self.main_menu += Entry("Disconnect", "Disconnect from server", action=self.invoke_disconnect) # Patch all sensor phantom that have been created before a fl0w instance # was attached to the window for sensor_phantom in sensor_phantoms: if sensor_phantom.window.id() == self.window.id(): sensor_phantom.fl0w = self if self.debug: Logging.info("Patched sensor phantom '%s'" % str(sensor_phatom)) @property def target(self): return self._target @target.setter def target(self, target): if self.target != None: self.ws.pipe("unsubscribe", "sensor", self.target) self._target = target set_status("Set target: %s" % target, self.window) if self.combined_subscriptions != {"analog" : [], "digital" : []}: self.ws.pipe({"subscribe" : self.combined_subscriptions_}, "sensor", target) @property def debug(self): return self._debug def set_debug(self, debug): self.debug = debug @debug.setter def debug(self, debug): if debug: self._debug = True if not self.meta_entry in self.main_menu.entries.values(): self.main_menu += self.meta_entry else: self._debug = False self.main_menu -= self.meta_entry set_status("Debug set to %s" % self._debug, self.window) @property def combined_subscriptions(self): return self._combined_subscriptions @combined_subscriptions.setter def combined_subscriptions(self, combined_subscriptions_): if self.combined_subscriptions != combined_subscriptions_: self._combined_subscriptions = combined_subscriptions_ if self.connected and self.target != None: self.ws.pipe("unsubscribe", "sensor", self.target) self.ws.pipe({"subscribe" : combined_subscriptions_}, "sensor", self.target) def subscribe(self, sensor_phatom, subscriptions): self.subscriptions[sensor_phatom] = subscriptions self.make_subscriptions() def unsubscribe(self, sensor_phantom): if sensor_phantom in self.subscriptions: del self.subscriptions[sensor_phantom] self.make_subscriptions() def make_subscriptions(self): combined_subscriptions = {"analog" : [], "digital" : []} for sensor_phantom in self.subscriptions: for sensor_type in ("analog", "digital"): combined_subscriptions[sensor_type] = list( set(combined_subscriptions[sensor_type]) | set(self.subscriptions[sensor_phantom][sensor_type]) ) self.combined_subscriptions = combined_subscriptions def run_program(self, path): if self.connected and self.target != None: relpath = os.path.relpath(path, self.folder) if os.path.isfile(self.folder + relpath): if self.debug: Logging.info("Running program '%s'" % relpath) self.ws.pipe(relpath, "run_program", self.target) def invoke_start_menu(self): self.start_menu.invoke(self.window) def invoke_main_menu(self): self.main_menu.invoke(self.window) def invoke_about(self): if sublime.ok_cancel_dialog("fl0w by @robot0nfire", "robot0nfire.com"): webbrowser.open("http://robot0nfire.com") def connect(self, connect_details): try: self.ws = Fl0wClient('ws://%s' % connect_details) self.ws.setup({"info" : Fl0wClient.Info(), "peers" : Fl0wClient.Peers(), "processes" : Fl0wClient.Processes(), "list_programs" : Fl0wClient.ListPrograms(), "sensor" : Fl0wClient.Sensor()}, self, debug=True) self.ws.connect() sublime.set_timeout_async(self.ws.run_forever, 0) set_status("Connection opened '%s'" % self.folder, self.window) self.connected = True except OSError as e: sublime.error_message("Error during connection creation:\n %s" % str(e)) def invoke_connect(self): # Will be removed once autoconnect works self.connect("127.0.0.1:3077") def invoke_disconnect(self): if self.connected: for sensor_phantom in sensor_phantoms: if sensor_phantom.window.id() == self.window.id(): sensor_phantom.enabled = False self.ws.close() set_status("Connection closed '%s'" % self.folder, self.window) self.connected = False class Fl0wCommand(sublime_plugin.WindowCommand): def run(self): valid_window_setup = True folder_count = len(self.window.folders()) if folder_count > 1: sublime.error_message("Only one open folder per window is allowed.") valid_window_setup = False elif folder_count == 0: sublime.error_message("No folder open in window.") valid_window_setup = False if valid_window_setup: if not hasattr(self.window, "fl0w"): folder = self.window.folders()[0] files = os.listdir(folder) if not ".no-fl0w" in files: if not ".fl0w" in files: open(folder + "/.fl0w", 'a').close() self.window.fl0w = Fl0w(self.window) windows.append(self.window) self.window.fl0w.start_menu.invoke(self.window) else: self.window.fl0w = Fl0w(self.window) windows.append(self.window) self.window.fl0w.start_menu.invoke(self.window) else: sublime.error_message("fl0w can't be opened in your current directory (.no-fl0w file exists)") else: if not self.window.fl0w.connected: self.window.fl0w.invoke_start_menu() else: self.window.fl0w.invoke_main_menu() else: if hasattr(self.window, "fl0w"): sublime.error_message("Window setup was invalidated (Don't close or open any additional folders in a fl0w window)") self.window.fl0w.invoke_disconnect() class RunCommand(sublime_plugin.WindowCommand): def run(self): if hasattr(self.window, "fl0w"): if self.window.fl0w.connected: if self.window.fl0w.target == None: sublime.error_message("A target controller has to be set to " "run programs.") else: file_name = self.window.active_view().file_name() if file_name != None and file_name.endswith(".c"): self.window.fl0w.run_program(file_name) else: sublime.error_message("fl0w is not connected.") else: sublime.error_message("fl0w is not running in your current window.") class SensorCommand(sublime_plugin.WindowCommand): def __init__(self, window): super().__init__(window) self.enabled = False def run(self): if not self.enabled: if hasattr(self.window, "fl0w"): if self.window.fl0w.connected: if self.window.fl0w.target == None: sublime.error_message("A target controller has to be set to " "enable inline sensor readouts.") else: self.enabled = not self.enabled for sensor_phantom in sensor_phantoms: if sensor_phantom.window.id() == self.window.id(): sensor_phantom.enabled = self.enabled set_status("Enabled sensor phantoms.", self.window) else: sublime.error_message("fl0w is not connected.") else: sublime.error_message("fl0w is not running in your current window.") else: self.enabled = not self.enabled for sensor_phantom in sensor_phantoms: if sensor_phantom.window.id() == self.window.id(): sensor_phantom.enabled = self.enabled set_status("Disabled sensor phantoms.", self.window) class SensorPhantom(sublime_plugin.ViewEventListener): def __init__(self, view): self.view = view self.window = view.window() # Is patched by the fl0w instance that is in control of the same window self.fl0w = None self._enabled = False self._matches = {"analog" : [], "digital" : []} self.timeout_scheduled = False self.needs_update = False for window in windows: if hasattr(window, "fl0w"): self.fl0w = window.fl0w if not self in sensor_phantoms: sensor_phantoms.append(self) @property def enabled(self): return self._enabled @enabled.setter def enabled(self, enabled_): self._enabled = enabled_ if enabled_: if self.fl0w != None: self.find_matches() self.fl0w.subscribe(self, self.subscriptions) else: if self.fl0w != None: self.fl0w.unsubscribe(self) for sensor_type in ("analog", "digital"): self.window.active_view().erase_phantoms(sensor_type) @property def matches(self): return self._matches @matches.setter def matches(self, matches_): if not matches_ == self.matches: self._matches = matches_ self.fl0w.subscribe(self, self.subscriptions) @property def subscriptions(self): subscriptions_ = {"analog" : [], "digital" : []} for sensor_type in ("analog", "digital"): subscriptions_[sensor_type] = [sensor[0] for sensor in self.matches[sensor_type]] return subscriptions_ def find_matches(self): matches = {"analog" : [], "digital" : []} # Don't do any calculations on 1MB or larger files if self.view.size() < 2**20: for method_name in ("analog", "digital"): candidates = self.view.find_all("%s\(\d*\)" % method_name) for candidate in candidates: line = self.view.substr(candidate) port_candidates = re.findall(PARENTHESES_REGEX, line) if len(port_candidates) == 1: if port_candidates[0].isnumeric(): matches[method_name].append( ( int(port_candidates[0]), sublime.Region(self.view.line(candidate.a).b) )) self.matches = matches # Called by fl0w instance def update_sensor_values(self, readouts): for sensor_type in ("analog", "digital"): self.view.erase_phantoms(sensor_type) for match in self.matches[sensor_type]: try: self.view.add_phantom(sensor_type, match[1], STYLE_OPEN + str(readouts[sensor_type][str(match[0])]) + STYLE_CLOSE, sublime.LAYOUT_INLINE) except KeyError: self.view.add_phantom(sensor_type, match[1], ERROR_OPEN + "!" + ERROR_CLOSE, sublime.LAYOUT_INLINE) def handle_timeout(self): self.timeout_scheduled = False if self.needs_update: self.needs_update = False self.find_matches() def on_modified(self): if self.enabled: if self.timeout_scheduled: self.needs_update = True else: sublime.set_timeout(lambda: self.handle_timeout(), 500) self.find_matches() def __del__(self): self.enabled = False if self in sensor_phantoms: del sensor_phantoms[sensor_phantoms.index(self)]