Coverage for flogin/__main__.py: 0%

115 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-03 22:51 +0000

1import argparse 

2import importlib 

3import importlib.metadata 

4import json 

5import platform 

6import sys 

7import uuid 

8from pathlib import Path 

9 

10from . import version_info 

11 

12 

13def show_version() -> None: 

14 entries: list[str] = [] 

15 

16 entries.append( 

17 f"- Python v{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}-{sys.version_info.releaselevel}" 

18 ) 

19 

20 entries.append( 

21 f"- flogin v{version_info.major}.{version_info.minor}.{version_info.micro}-{version_info.releaselevel}" 

22 ) 

23 

24 try: 

25 version = importlib.metadata.version("flogin") 

26 if version: 

27 entries.append(f" - flogin metadata: v{version}") 

28 except importlib.metadata.PackageNotFoundError: 

29 pass 

30 

31 uname = platform.uname() 

32 entries.append(f"- system info: {uname.system} {uname.release} {uname.version}") 

33 print("\n".join(entries)) 

34 

35 

36def core(parser: argparse.ArgumentParser, args: argparse.Namespace) -> None: 

37 if args.version: 

38 show_version() 

39 else: 

40 parser.print_help() 

41 

42 

43_settings_template = """ 

44body: 

45 - type: textBlock 

46 attributes: 

47 description: Welcome to the settings page for my plugin. Here you can configure the plugin to your liking. 

48 - type: input 

49 attributes: 

50 name: user_name 

51 label: How should I call you? 

52 defaultValue: the user 

53 - type: textarea 

54 attributes: 

55 name: prepend_result 

56 label: Text to prepend to result output 

57 description: > 

58 This text will be added to the beginning of the result output. For example, if you set this to 

59 "The result is: ", and the result is "42", the output will be "The result is: 42". 

60 - type: dropdown 

61 attributes: 

62 name: programming_language 

63 label: Programming language to prefer for answers 

64 defaultValue: TypeScript 

65 options: 

66 - JavaScript 

67 - TypeScript 

68 - Python 

69 - "C#" 

70 - type: checkbox 

71 attributes: 

72 name: prefer_shorter_aswers 

73 label: Prefer shorter answers 

74 description: If checked, the plugin will try to give answer much shorter than the usual ones. 

75 defaultValue: false 

76""" 

77_gh_bug_report_issue_template = """ 

78name: Bug Report 

79description: Report broken or incorrect behaviour 

80labels: unconfirmed bug 

81body: 

82 - type: markdown 

83 attributes: 

84 value: > 

85 Thanks for taking the time to fill out a bug. 

86 

87 Please note that this form is for bugs only! 

88 - type: input 

89 attributes: 

90 label: Summary 

91 description: A simple summary of your bug report 

92 validations: 

93 required: true 

94 - type: textarea 

95 attributes: 

96 label: Reproduction Steps 

97 description: > 

98 What you did to make it happen. 

99 validations: 

100 required: true 

101 - type: textarea 

102 attributes: 

103 label: Minimal Reproducible Code 

104 description: > 

105 A short snippet of code that showcases the bug. 

106 render: python 

107 - type: textarea 

108 attributes: 

109 label: Expected Results 

110 description: > 

111 What did you expect to happen? 

112 validations: 

113 required: true 

114 - type: textarea 

115 attributes: 

116 label: Actual Results 

117 description: > 

118 What actually happened? 

119 validations: 

120 required: true 

121 - type: textarea 

122 attributes: 

123 label: Flow Launcher Version 

124 description: Go into your flow launcher settings, go into the about section, and the version should be at the top. 

125 validations: 

126 required: true 

127 - type: textarea 

128 attributes: 

129 label: Python Version/Path 

130 description: Go into your flow launcher settings, go to the general section, and scroll down until you find the `Python Path` field. Copy and paste the value here. 

131 validations: 

132 required: true 

133 - type: textarea 

134 attributes: 

135 label: If applicable, Flow Launcher Log File 

136 description: Use the `Open Log Location` command with the `System Commands` plugin to open the log file folder, and upload the newest file here. 

137 - type: textarea 

138 attributes: 

139 label: Flogin Log File 

140 description: Use the `Flow Launcher UserData Folder` command with the `System Commands` plugin to open your userdata folder, go into the `Plugins` folder, then find the plugin and go into it. If the `flogin.log` file exists, upload it here. Otherwise please state that it was not there. 

141 - type: checkboxes 

142 attributes: 

143 label: Checklist 

144 description: > 

145 Let's make sure you've properly done due diligence when reporting this issue! 

146 options: 

147 - label: I have searched the open issues for duplicates. 

148 required: true 

149 - label: I have shown the entire traceback, if possible. 

150 required: true 

151 - label: I have removed my token from display, if visible. 

152 required: true 

153 - type: textarea 

154 attributes: 

155 label: Additional Context 

156 description: If there is anything else to say, please do so here. 

157""" 

158_gh_pr_template = """ 

159## Summary 

160 

161<!-- What is this pull request for? Does it fix any issues? --> 

162 

163## Checklist 

164 

165<!-- Put an x inside [ ] to check it, like so: [x] --> 

166 

167- [ ] If code changes were made then they have been tested. 

168 - [ ] I have updated the documentation to reflect the changes. 

169- [ ] This PR fixes an issue. 

170- [ ] This PR adds something new (e.g. new method or parameters). 

171- [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed) 

172- [ ] This PR is **not** a code change (e.g. documentation, README, ...) 

173""" 

174_gh_publish_workflow = """ 

175name: Publish and Release 

176 

177on: 

178 workflow_dispatch: 

179 

180jobs: 

181 publish: 

182 runs-on: ubuntu-latest 

183 env: 

184 python_ver: 3.11 

185 permissions: 

186 contents: write 

187 

188 steps: 

189 - uses: actions/checkout@v2 

190 

191 - name: Set up Python ${{ env.python_ver }} 

192 uses: actions/setup-python@v2 

193 with: 

194 python-version: ${{ env.python_ver }} 

195 

196 - name: get version 

197 id: version 

198 uses: notiz-dev/github-action-json-property@release 

199 with: 

200 path: 'plugin.json' 

201 prop_path: 'Version' 

202 

203 - run: echo ${{steps.version.outputs.prop}} 

204 

205 - name: Install dependencies 

206 run: | 

207 python -m pip install --upgrade pip 

208 pip install -r ./requirements.txt -t ./lib 

209 zip -r ${{ github.event.repository.name }}.zip . -x '*.git*' 

210 

211 - name: Publish 

212 if: success() 

213 uses: softprops/action-gh-release@v2 

214 with: 

215 files: '${{ github.event.repository.name }}.zip' 

216 tag_name: "v${{steps.version.outputs.prop}}" 

217""" 

218_plugin_dot_py_template = """ 

219from flogin import Plugin 

220 

221from .handlers.root import RootHandler 

222from .settings import {plugin}Settings 

223 

224 

225class {plugin}Plugin(Plugin[{plugin}Settings]): 

226 def __init__(self) -> None: 

227 super().__init__() 

228 

229 self.register_search_handler(RootHandler()) 

230""" 

231_plugin_dot_py_template_no_settings = """ 

232from flogin import Plugin 

233 

234from .handlers.root import RootHandler 

235 

236 

237class {plugin}Plugin(Plugin): 

238 def __init__(self) -> None: 

239 super().__init__() 

240 

241 self.register_search_handler(RootHandler()) 

242""" 

243_settings_dot_py_template = """ 

244from flogin import Settings 

245 

246 

247class {plugin}Settings(Settings): 

248 ... 

249""" 

250_handler_template = """ 

251from __future__ import annotations 

252 

253from typing import TYPE_CHECKING 

254 

255from flogin import Query, Result, SearchHandler 

256 

257if TYPE_CHECKING: 

258 from ..plugin import {plugin}Plugin 

259 

260 

261class {name}Handler(SearchHandler["{plugin}Plugin"]): 

262 def condition(self, query: Query) -> bool: 

263 return True 

264 

265 async def callback(self, query: Query): 

266 return "Hello World!" 

267""" 

268_main_py_template = """ 

269import os 

270import sys 

271 

272parent_folder_path = os.path.abspath(os.path.dirname(__file__)) 

273sys.path.append(parent_folder_path) 

274sys.path.append(os.path.join(parent_folder_path, "lib")) 

275sys.path.append(os.path.join(parent_folder_path, "venv", "lib", "site-packages")) 

276 

277from plugin.plugin import {plugin}Plugin 

278 

279if __name__ == "__main__": 

280 {plugin}Plugin().run() 

281""" 

282 

283 

284def create_plugin_dot_json_file( 

285 parser: argparse.ArgumentParser, plugin_name: str 

286) -> None: 

287 data = { 

288 "ID": str(uuid.uuid4()), 

289 "ActionKeyword": "*", 

290 "Name": plugin_name, 

291 "Description": "", 

292 "Author": "", 

293 "Version": "0.0.1", 

294 "Language": "python_v2", 

295 "Website": "https://github.com/author/Flow.Launcher.Plugin.Name", 

296 "IcoPath": "Images/app.png", 

297 "ExecuteFileName": "main.py", 

298 } 

299 write_to_file(Path("plugin.json"), json.dumps(data, indent=4), parser) 

300 

301 

302def write_to_file(path: Path, content: str, parser: argparse.ArgumentParser) -> bool: 

303 path.parent.mkdir(parents=True, exist_ok=True) 

304 

305 try: 

306 with path.open("w") as f: 

307 f.write(content.strip()) 

308 except OSError as e: 

309 parser.error(f"Unable to write to {path}: {e}") 

310 return False 

311 else: 

312 print(f"Wrote to {path}") 

313 return True 

314 

315 

316def create_new_handler( 

317 parser: argparse.ArgumentParser, *, path: Path, name: str, plugin: str 

318) -> None: 

319 write_to_file(path, _handler_template.format(plugin=plugin, name=name), parser) 

320 

321 

322def create_plugin_directory( 

323 parser: argparse.ArgumentParser, args: argparse.Namespace 

324) -> None: 

325 plugin_dir = Path("plugin") 

326 plugin_name = args.plugin_name 

327 

328 main_file = Path("main.py") 

329 

330 write_to_file(main_file, _main_py_template.format(plugin=plugin_name), parser) 

331 

332 plugin_file = plugin_dir / "plugin.py" 

333 template = ( 

334 _plugin_dot_py_template_no_settings 

335 if args.no_settings 

336 else _plugin_dot_py_template 

337 ) 

338 write_to_file(plugin_file, template.format(plugin=plugin_name), parser) 

339 

340 if not args.no_settings: 

341 settings_file = plugin_dir / "settings.py" 

342 write_to_file( 

343 settings_file, _settings_dot_py_template.format(plugin=plugin_name), parser 

344 ) 

345 

346 handlers_dir = plugin_dir / "handlers" 

347 root_handler_file = handlers_dir / "root.py" 

348 

349 create_new_handler(parser, path=root_handler_file, name="Root", plugin=plugin_name) 

350 

351 

352def create_git_files(parser: argparse.ArgumentParser, args: argparse.Namespace) -> None: 

353 github_dir = Path(".github") 

354 issue_template_dir = github_dir / "ISSUE_TEMPLATE" 

355 workflows_dir = github_dir / "workflows" 

356 

357 bug_report_template_file = issue_template_dir / "bug_report.yml" 

358 write_to_file(bug_report_template_file, _gh_bug_report_issue_template, parser) 

359 

360 pr_template_file = github_dir / "PULL_REQUEST_TEMPLATE.md" 

361 write_to_file(pr_template_file, _gh_pr_template, parser) 

362 

363 publish_file = workflows_dir / "publish_release.yml" 

364 write_to_file(publish_file, _gh_publish_workflow, parser) 

365 

366 

367def init_command(parser: argparse.ArgumentParser, args: argparse.Namespace) -> None: 

368 plugin_name = args.plugin_name 

369 

370 if not args.no_manifest: 

371 create_plugin_dot_json_file(parser, plugin_name) 

372 

373 if not args.no_settings: 

374 settings_file = Path("SettingsTemplate.yaml") 

375 write_to_file(settings_file, _settings_template, parser) 

376 

377 if not args.no_git: 

378 create_git_files(parser, args) 

379 

380 if not args.no_plugin: 

381 create_plugin_directory(parser, args) 

382 

383 

384def add_init_args( 

385 subparser: "argparse._SubParsersAction[argparse.ArgumentParser]", 

386) -> None: 

387 parser = subparser.add_parser( 

388 "init", 

389 help="quickly sets up the environment for the development of a new plugin", 

390 ) 

391 parser.set_defaults(func=init_command) 

392 

393 parser.add_argument("plugin_name", help="the name of the plugin") 

394 parser.add_argument( 

395 "--no-git", help="whether or not to add git files", action="store_true" 

396 ) 

397 parser.add_argument( 

398 "--no-settings", help="whether or not to add setting files", action="store_true" 

399 ) 

400 parser.add_argument( 

401 "--no-plugin", help="Do not create an example plugin", action="store_true" 

402 ) 

403 parser.add_argument( 

404 "--no-manifest", 

405 help="Do not create a plugin.json manifest file", 

406 action="store_true", 

407 ) 

408 

409 

410def add_new_handler_command( 

411 parser: argparse.ArgumentParser, args: argparse.Namespace 

412) -> None: 

413 plugin_name: str = args.plugin_name or "" 

414 handler_name: str = args.name 

415 dir = Path("plugin") / (args.dir or "handlers") 

416 path = dir / handler_name.lower() 

417 

418 classname = handler_name.replace("_", " ").title().replace(" ", "") 

419 

420 create_new_handler( 

421 parser, path=path.with_suffix(".py"), name=classname, plugin=plugin_name 

422 ) 

423 

424 

425def add_handler_command_args( 

426 subparser: "argparse._SubParsersAction[argparse.ArgumentParser]", 

427) -> None: 

428 parser = subparser.add_parser( 

429 "new-handler", 

430 help="quickly set up a new handler using flogin's handler template.", 

431 ) 

432 parser.set_defaults(func=add_new_handler_command) 

433 

434 parser.add_argument( 

435 "name", help="the name of the handler in case snake format. ex: 'root'" 

436 ) 

437 parser.add_argument("--plugin-name", help="Sets the plugin name for importing") 

438 parser.add_argument("--dir", help="The handlers dir. Defaults to 'handlers'") 

439 

440 

441def parse_args() -> tuple[argparse.ArgumentParser, argparse.Namespace]: 

442 parser = argparse.ArgumentParser( 

443 prog="flogin", description="Tools for helping with plugin development" 

444 ) 

445 parser.add_argument( 

446 "-v", "--version", action="store_true", help="shows the library version" 

447 ) 

448 parser.set_defaults(func=core) 

449 

450 subparser = parser.add_subparsers(dest="subcommand", title="subcommands") 

451 add_init_args(subparser) 

452 add_handler_command_args(subparser) 

453 return parser, parser.parse_args() 

454 

455 

456def main() -> None: 

457 parser, args = parse_args() 

458 args.func(parser, args) 

459 

460 

461if __name__ == "__main__": 

462 main()