text: import sublime
  import sublime_plugin
  import fnmatch
  import os
  import json
  import subprocess
  import shlex
  import threading
  
  
  PLUGIN_NAME = "Run Task"
  
  PROJECT_FILE_NAME_PATTERN = "*.sublime-project"
  OUTPUT_PANEL_NAME = "run_task"
  
  JSON_TASKS_KEY = PLUGIN_NAME + ".tasks"
  JSON_TASK_NAME_KEY = "name"
  JSON_TASK_TYPE_KEY = "type"
  JSON_TASK_COMMAND_KEY = "command"
  JSON_TASK_ARGS_KEY = "args"
  JSON_TASK_SHOW_OUTPUT_PANEL_KEY = "show_output_panel"
  JSON_TASK_WINDOWS_CONFIG_KEY = "windows"
  
  JSON_TASK_SHOW_OUTPUT_PANEL_DEFAULT_VALUE = True
  
  SUBLIME_TASK_TYPE = "sublime"
  SHELL_TASK_TYPE = "shell"
  
  VARIABLE_CWD = "${cwd}"
  
  
  class OSUtils():
  	@staticmethod
  	def find_directory(path, name):
  		directory = next((directory for directory in os.listdir(path) if os.path.isdir(os.path.join(path, directory)) and directory == name), None)
  		if directory:
  			return os.path.join(path, directory)
  		return None
  
  	@staticmethod
  	def find_file(path, name):
  		file = next((file for file in os.listdir(path) if os.path.isfile(os.path.join(path, file)) and file == name), None)
  		if file:
  			return os.path.join(path, file)
  		return None
  
  	@staticmethod
  	def find_file_with_pattern(path, pattern):
  		file = next((file for file in os.listdir(path) if os.path.isfile(os.path.join(path, file)) and fnmatch.fnmatch(file, pattern)), None)
  		if file:
  			return os.path.join(path, file)
  		return None
  
  	@staticmethod
  	def is_windows():
  		return os.name == 'nt'
  
  
  class ErrorMessage():
  
  	EXPECTED_BOOL_VALUE = 'Expected a boolean value.'
  	EXPECTED_JSON_OBJECT = 'Expected a JSON object.'
  	EXPECTED_STRING_OR_ARRAY = 'Expected a string or array value.'
  	EXPECTED_NON_EMPTY_STRING = 'Expected a non-empty string with at least one non-whitespace character.'
  	EXPECTED_TASK_TYPE = 'Expected "' + SHELL_TASK_TYPE + '" or "' + SUBLIME_TASK_TYPE + '".'
  
  	CHECK_CONFIGURATION_FILE = 'Please check the .sublime-project file.'
  
  	@staticmethod
  	def invalid_json():
  		return PLUGIN_NAME + ': Invalid JSON. ' + ErrorMessage.CHECK_CONFIGURATION_FILE
  
  	@staticmethod
  	def invalid_json_object(expected_type_name=None):
  		error_message = PLUGIN_NAME + ': Invalid JSON object. '
  		if expected_type_name is not None:
  			error_message += 'Expected ' + expected_type_name + '. '
  		error_message += ErrorMessage.CHECK_CONFIGURATION_FILE
  		return error_message
  
  	@staticmethod
  	def invalid_json_task_definition(task_index, task_error_message=None):
  		error_message = PLUGIN_NAME + ': Invalid JSON task definition at index ' + str(task_index) + '. '
  		error_message += ErrorMessage.CHECK_CONFIGURATION_FILE
  		if task_error_message is not None:
  			error_message += '\n\nError message: ' + task_error_message
  		return error_message
  
  	@staticmethod
  	def invalid_field_value(field_name, value_error_message=None):
  		error_message = 'Invalid value for field "' + field_name + '".'
  		if value_error_message is not None:
  			error_message += ' ' + value_error_message
  		return error_message
  
  	@staticmethod
  	def missing_required_field(field_name):
  		return 'Missing required field "' + field_name + '".'
  
  	@staticmethod
  	def task_execution_failed(task_name, execution_error_message=None):
  		error_message = PLUGIN_NAME + ': Execution failed for task "' + task_name + '".'
  		if execution_error_message is not None:
  			error_message += '\n\nError message: ' + execution_error_message
  		return error_message
  
  
  class OutputPanel():
  	def __init__(self, window):
  		self.window = window
  		self.panel_view = None
  
  	def show(self):
  		self.panel_view = self.__create_panel_view()
  		self.window.run_command("show_panel", {"panel": "output." + OUTPUT_PANEL_NAME})
  
  	def write(self, message):
  		if self.panel_view is not None:
  			self.panel_view.run_command("append", {"characters": message})
  
  	def __create_panel_view(self):
  		return self.window.create_output_panel(OUTPUT_PANEL_NAME)
  
  
  class ShellTaskThread(threading.Thread):
  	def __init__(self, task_name, window, args=[], cwd=None, show_output_panel=False):
  		super(ShellTaskThread, self).__init__(self)
  		self.task_name = task_name
  		self.window = window
  		self.args = args
  		self.cwd = cwd
  		self.show_output_panel = show_output_panel
  
  	def run(self):
  		if self.show_output_panel:
  			output_panel = OutputPanel(self.window)
  			output_panel.show()
  			try:
  				process = subprocess.Popen(self.args, bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.cwd, universal_newlines=True)
  			except Exception as ex:
  				sublime.error_message(ErrorMessage.task_execution_failed(self.task_name, str(ex)))
  				return
  			while True:
  				out_line = process.stdout.readline()
  				if out_line == '' and process.poll() is not None:
  					finish_msg = "| Task finished with return code " + str(process.poll()) + " |"
  					finish_msg = "\n" + ("-" * len(finish_msg)) + "\n" + finish_msg + "\n" + ("-" * len(finish_msg))
  					output_panel.write(finish_msg)
  					break
  				if out_line:
  					output_panel.write(out_line)
  		else:
  			try:
  				subprocess.Popen(self.args, cwd=self.cwd)
  			except Exception as ex:
  				sublime.error_message(ErrorMessage.task_execution_failed(self.task_name, str(ex)))
  
  
  class Task():
  	def __init__(self, name, task_type, command, args, show_output_panel):
  		self.name = name
  		self.task_type = task_type
  		self.command = command
  		self.args = args
  		self.show_output_panel = show_output_panel
  
  	def get_name(self):
  		return self._name
  
  	def set_name(self, value):
  		self._name = value
  
  	name = property(get_name, set_name)
  
  	def get_task_type(self):
  		return self._task_type
  
  	def set_task_type(self, value):
  		self._task_type = value
  
  	task_type = property(get_task_type, set_task_type)
  
  	def get_command(self):
  		return self._command
  
  	def set_command(self, value):
  		self._command = value
  
  	command = property(get_command, set_command)
  
  	def get_args(self):
  		return self._args
  
  	def set_args(self, value):
  		self._args = value
  
  	args = property(get_args, set_args)
  
  	def get_show_output_panel(self):
  		return self._show_output_panel
  
  	def set_show_output_panel(self, value):
  		self._show_output_panel = value
  
  	show_output_panel = property(get_show_output_panel, set_show_output_panel)
  
  	def execute(self, window, cwd):
  		if self.task_type == SUBLIME_TASK_TYPE:
  			window.run_command(self.command, self.args)
  		elif self.task_type == SHELL_TASK_TYPE:
  			args = [self.command]
  			if type(self.args) is list:
  				args.extend(self.args)
  			elif type(self.args) is str:
  				args.extend(shlex.split(self.args.strip()))
  			for idx, arg in enumerate(args):
  				args[idx] = arg.replace(VARIABLE_CWD, cwd)
  			ShellTaskThread(self.name, window, args, cwd, self.show_output_panel).start()
  
  
  class TaskParser():
  	def parse_tasks(self, tasks_json):
  		tasks = []
  		if type(tasks_json) is not dict:
  			return None, ErrorMessage.invalid_json_object()
  		if JSON_TASKS_KEY in tasks_json and type(tasks_json[JSON_TASKS_KEY]) is list:
  			parse_error_msg = None
  			for idx, task_json in enumerate(tasks_json[JSON_TASKS_KEY]):
  				task, parse_error_msg = self.parse_task(task_json)
  				if task is not None:
  					tasks.append(task)
  				elif parse_error_msg is not None:
  					return None, ErrorMessage.invalid_json_task_definition(idx, parse_error_msg)
  		else:
  			return None, ErrorMessage.invalid_json_object(expectedTypeName='"' + JSON_TASKS_KEY + '" array')
  		return tasks, None
  
  	def parse_task(self, task_json):
  		name, task_type, command, args, windows_config, show_output_panel = (None, None, None, None, None, None)
  		parse_error_msg = None
  		if type(task_json) is dict:
  			name, parse_error_msg = self.__parse_task_name(task_json)
  			if parse_error_msg is None:
  				task_type, parse_error_msg = self.__parse_task_type(task_json)
  			if parse_error_msg is None:
  				windows_config, parse_error_msg = self.__parse_task_windows_config(task_json)
  			if parse_error_msg is None:
  				if windows_config is not None:
  					command, parse_error_msg = self.__parse_task_command(windows_config)
  					if parse_error_msg is None:
  						args, parse_error_msg = self.__parse_task_args(task_type, windows_config)
  				else:
  					command, parse_error_msg = self.__parse_task_command(task_json)
  					if parse_error_msg is None:
  						args, parse_error_msg = self.__parse_task_args(task_type, task_json)
  			if parse_error_msg is None:
  				show_output_panel, parse_error_msg = self.__parse_task_show_output_panel(task_json)
  		if parse_error_msg is not None:
  			return None, parse_error_msg
  		return Task(name, task_type, command, args, show_output_panel), None
  
  	def __parse_task_name(self, task_json):
  		task_name = None
  		if JSON_TASK_NAME_KEY in task_json:
  			task_name = task_json[JSON_TASK_NAME_KEY]
  		else:
  			return None, ErrorMessage.missing_required_field(JSON_TASK_NAME_KEY)
  		if type(task_name) is not str or task_name.strip() == "":
  			return None, ErrorMessage.invalid_field_value(JSON_TASK_NAME_KEY, ErrorMessage.EXPECTED_NON_EMPTY_STRING)
  		return task_name.strip(), None
  
  	def __parse_task_type(self, task_json):
  		task_type = None
  		if JSON_TASK_TYPE_KEY in task_json:
  			task_type = task_json[JSON_TASK_TYPE_KEY]
  		else:
  			return None, ErrorMessage.missing_required_field(JSON_TASK_TYPE_KEY)
  		if type(task_type) is not str or (task_type != SUBLIME_TASK_TYPE and task_type != SHELL_TASK_TYPE):
  			return None, ErrorMessage.invalid_field_value(JSON_TASK_TYPE_KEY, ErrorMessage.EXPECTED_TASK_TYPE)
  		return task_type, None
  
  	def __parse_task_command(self, task_json):
  		task_command = None
  		if JSON_TASK_COMMAND_KEY in task_json:
  			task_command = task_json[JSON_TASK_COMMAND_KEY]
  		else:
  			return None, ErrorMessage.missing_required_field(JSON_TASK_COMMAND_KEY)
  		if type(task_command) is not str or task_command.strip() == "":
  			return None, ErrorMessage.invalid_field_value(JSON_TASK_COMMAND_KEY, ErrorMessage.EXPECTED_NON_EMPTY_STRING)
  		return task_command.strip(), None
  
  	def __parse_task_args(self, task_type, task_json):
  		task_args = None
  		if JSON_TASK_ARGS_KEY in task_json:
  			task_args = task_json[JSON_TASK_ARGS_KEY]
  		else:
  			return None, None
  		if task_type == SUBLIME_TASK_TYPE:
  			if type(task_args) is dict:
  				return task_args, None
  			else:
  				return None, ErrorMessage.invalid_field_value(JSON_TASK_ARGS_KEY, ErrorMessage.EXPECTED_JSON_OBJECT)
  		if task_type == SHELL_TASK_TYPE:
  			if (type(task_args) is str or type(task_args) is list):
  				return task_args, None
  			else:
  				return None, ErrorMessage.invalid_field_value(JSON_TASK_ARGS_KEY, ErrorMessage.EXPECTED_STRING_OR_ARRAY)
  		return None, ErrorMessage.invalid_field_value(JSON_TASK_ARGS_KEY)
  
  	def __parse_task_windows_config(self, task_json):
  		task_windows_config = None
  		if JSON_TASK_WINDOWS_CONFIG_KEY in task_json and OSUtils.is_windows():
  			task_windows_config = task_json[JSON_TASK_WINDOWS_CONFIG_KEY]
  		else:
  			return None, None
  		if type(task_windows_config) is not dict:
  			return None, ErrorMessage.invalid_field_value(JSON_TASK_WINDOWS_CONFIG_KEY, ErrorMessage.EXPECTED_JSON_OBJECT)
  		return task_windows_config, None
  
  	def __parse_task_show_output_panel(self, task_json):
  		task_show_output_panel = None
  		if JSON_TASK_SHOW_OUTPUT_PANEL_KEY in task_json:
  			task_show_output_panel = task_json[JSON_TASK_SHOW_OUTPUT_PANEL_KEY]
  		else:
  			return JSON_TASK_SHOW_OUTPUT_PANEL_DEFAULT_VALUE, None
  		if type(task_show_output_panel) is not bool:
  			return None, ErrorMessage.invalid_field_value(JSON_TASK_SHOW_OUTPUT_PANEL_KEY, ErrorMessage.EXPECTED_BOOL_VALUE)
  		return task_show_output_panel, None
  
  
  class RunTaskCommand(sublime_plugin.WindowCommand):
  	def run(self):
  		folders = self.window.folders()
  		if not folders or len(folders) == 0:
  			return
  
  		self.workspace = folders[0]
  		self.tasks = []
  
  		project_file = OSUtils.find_file_with_pattern(self.workspace, PROJECT_FILE_NAME_PATTERN)
  		if project_file:
  			with open(project_file, "r") as fp:
  				try:
  					tasks_json = json.load(fp)
  					self.tasks, parse_error_msg = TaskParser().parse_tasks(tasks_json)
  					if parse_error_msg is not None:
  						sublime.error_message(parse_error_msg)
  						return
  				except ValueError:
  					sublime.error_message(ErrorMessage.invalid_json())
  					return
  			if len(self.tasks) > 0:
  				labels = list(map(lambda task: task.name, self.tasks))
  				self.window.show_quick_panel(labels, self.on_done, sublime.MONOSPACE_FONT, 0, None)
  
  	def on_done(self, taskIndex):
  		if taskIndex < 0 or taskIndex >= len(self.tasks):
  			return
  		selectedTask = self.tasks[taskIndex]
  		print(PLUGIN_NAME + ': Running task "' + selectedTask.name + '"')
  		selectedTask.execute(window=self.window, cwd=self.workspace)
  