main.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import os
  2. import json
  3. import logging
  4. import distutils.spawn
  5. from ulauncher.api.client.Extension import Extension
  6. from ulauncher.api.client.EventListener import EventListener
  7. from ulauncher.api.shared.event import KeywordQueryEvent, ItemEnterEvent
  8. from ulauncher.api.shared.item.ExtensionResultItem import ExtensionResultItem
  9. from ulauncher.api.shared.item.SmallResultItem import SmallResultItem
  10. from ulauncher.api.shared.action.RenderResultListAction import RenderResultListAction
  11. from ulauncher.api.shared.action.RunScriptAction import RunScriptAction
  12. from ulauncher.api.shared.action.ExtensionCustomAction import ExtensionCustomAction
  13. logging.basicConfig()
  14. logger = logging.getLogger(__name__)
  15. global usage_cache
  16. usage_cache = {}
  17. # Usage tracking
  18. script_directory = os.path.dirname(os.path.realpath(__file__))
  19. usage_db = os.path.join(script_directory, "usage.json")
  20. if os.path.exists(usage_db):
  21. with open(usage_db, 'r') as db:
  22. # Read JSON string
  23. raw = db.read()
  24. # JSON to dict
  25. usage_cache = json.loads(raw)
  26. # Initialize items cache and Remmina profiles path
  27. remmina_bin = ""
  28. # Locate Remmina profiles and binary
  29. default_paths = ["{}/.local/share/remmina".format(os.environ.get('HOME')),
  30. "{}/.remmina".format(os.environ.get('HOME'))]
  31. # remmina_profiles_path = "{}/.local/share/remmina".format(os.environ.get('HOME'))
  32. # remmina_profiles_path_alt = "{}/.remmina".format(os.environ.get('HOME'))
  33. remmina_bin = distutils.spawn.find_executable('remmina')
  34. # This extension is useless without remmina
  35. if remmina_bin is None or remmina_bin == "":
  36. logger.error("Remmina executable path could not be determined")
  37. exit()
  38. # Check if Remmina profiles directory exists
  39. remmina_profiles_path = None
  40. # Check default paths first
  41. for p in default_paths:
  42. if os.path.isdir(p):
  43. remmina_profiles_path = p
  44. class RemminaExtension(Extension):
  45. def __init__(self):
  46. super(RemminaExtension, self).__init__()
  47. self.subscribe(KeywordQueryEvent, KeywordQueryEventListener())
  48. self.subscribe(ItemEnterEvent, ItemEnterEventListener())
  49. def list_profiles(self, query):
  50. profiles = []
  51. items_cache = []
  52. try:
  53. # Get list of profile files from Remmina directory
  54. for profile in os.listdir(remmina_profiles_path):
  55. if profile.endswith(".remmina"):
  56. profiles.append(os.path.join(remmina_profiles_path, profile))
  57. # Get sorted list of profiles
  58. temp = profiles
  59. profiles = sorted(temp)
  60. except Exception as e:
  61. logger.error("Failed getting Remmina profile files")
  62. for p in profiles:
  63. base = os.path.basename(p)
  64. title, desc, proto = profile_details(p)
  65. # Search for query inside filename and profile description
  66. # Multiple strings can be used to search in description
  67. # all() is used to achieve a AND search (include all keywords)
  68. keywords = query.split(" ")
  69. # if (query in base.lower()) or (query in desc.lower()):
  70. if (query.lower() in base.lower()) or \
  71. (query.lower() in title.lower()) or \
  72. all(x.lower() in desc.lower() for x in keywords):
  73. items_cache.append(create_item(title, proto, p, desc, p))
  74. items_cache = sorted(items_cache, key=sort_by_usage, reverse=True)
  75. return items_cache
  76. class KeywordQueryEventListener(EventListener):
  77. def on_event(self, event, extension):
  78. global remmina_profiles_path
  79. if extension.preferences["profiles"] is not "" \
  80. or not remmina_profiles_path:
  81. # Tilde (~) won't work alone, need expanduser()
  82. remmina_profiles_path = os.path.expanduser(extension.preferences["profiles"])
  83. # pref_profiles_path = extension.preferences['profiles']
  84. logger.debug("Remmina profiles path: {}".format(remmina_profiles_path))
  85. # Get query
  86. term = (event.get_argument() or "").lower()
  87. # Display all items when query empty
  88. profiles_list = extension.list_profiles(term)
  89. return RenderResultListAction(profiles_list[:8])
  90. class ItemEnterEventListener(EventListener):
  91. def on_event(self, event, extension):
  92. global usage_cache
  93. # Get query
  94. data = event.get_data()
  95. on_enter = data["id"]
  96. # The profilefile name is the ID
  97. base = os.path.basename(on_enter)
  98. b = os.path.splitext(base)[0]
  99. # Check usage and increment
  100. if b in usage_cache:
  101. usage_cache[b] = usage_cache[b]+1
  102. else:
  103. usage_cache[b] = 1
  104. # Update usage JSON
  105. with open(usage_db, 'w') as db:
  106. db.write(json.dumps(usage_cache, indent=2))
  107. return RunScriptAction('#!/usr/bin/env bash\n{} -c {}\n'.format(remmina_bin, on_enter), None).run()
  108. def create_item(name, icon, keyword, description, on_enter):
  109. return ExtensionResultItem(
  110. name=name,
  111. description=description,
  112. icon="images/{}.svg".format(icon),
  113. on_enter=ExtensionCustomAction(
  114. {"id": on_enter})
  115. )
  116. def sort_by_usage(i):
  117. global usage_cache
  118. # Convert item name to ID format
  119. j = i._name.lower()
  120. # Return score according to usage
  121. if j in usage_cache:
  122. return usage_cache[j]
  123. # Default is 0 (no usage rank / unused)
  124. return 0
  125. def profile_details(profile_path):
  126. if os.path.isfile(profile_path):
  127. with open(profile_path, "r") as f:
  128. # Read profile file lines
  129. lines = f.read().split("\n")
  130. # Initialize strings
  131. desc = name = username = group = proto = ""
  132. # Parse lines for relevant details
  133. for line in lines:
  134. # Profile name
  135. if line.startswith("name="):
  136. elem = line.split("name=")
  137. if len(elem[1]) > 0:
  138. name = elem[1]
  139. # Profile username (optional)
  140. if "username=" in line:
  141. elem = line.split("username=")
  142. # if len(elem) > 1:
  143. if len(elem[0]) == 0 and len(elem[1]) > 0:
  144. username = elem[1]
  145. elif len(elem[0]) > 0 and len(elem[1]) > 0:
  146. username = elem[1]
  147. # Profile server and port
  148. if line.startswith("server="):
  149. elem = line.split("server=")
  150. if len(elem[1]) > 0:
  151. server = elem[1]
  152. # Profile group name
  153. if line.startswith("group="):
  154. elem = line.split("group=")
  155. if len(elem[1]) > 0:
  156. group = elem[1]
  157. # Profile protocol (for different icons)
  158. if line.startswith("protocol="):
  159. elem = line.split("protocol=")
  160. if len(elem[1]) > 0:
  161. proto = elem[1].strip().lower()
  162. else:
  163. pass
  164. if len(username) > 0:
  165. server = "{username}@{server}".format(username=username,
  166. server=server)
  167. if len(proto) > 0:
  168. server = "{proto}://{server}".format(proto=proto,
  169. server=server)
  170. if len(group) > 0:
  171. group = " | {group}".format(group=group)
  172. # Final description string
  173. desc = "{server} {group}".format(server=server,
  174. group=group)
  175. return name, desc, proto
  176. else:
  177. # Default values
  178. return "", "", "rdp"
  179. if __name__ == "__main__":
  180. RemminaExtension().run()