Skip to content

Commit

Permalink
🚧 Re-write the workflow in Dart
Browse files Browse the repository at this point in the history
  • Loading branch information
techouse committed Mar 6, 2022
1 parent 13a5478 commit 8e48f13
Show file tree
Hide file tree
Showing 22 changed files with 600 additions and 352 deletions.
127 changes: 6 additions & 121 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,127 +1,12 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Files and directories created by pub.
.dart_tool/
.packages

# C extensions
*.so

# Distribution / packaging
.Python
# Conventional directory for build output.
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# IDE specific
.idea
.idea/

# macOS specific
.DS_Store

# Package specific
src/algoliasearch
src/certifi
src/chardet
src/idna
src/requests
src/urllib3
src/workflow
src/typing.py
.DS_Store
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2019 Klemen Tušar
Copyright (c) 2022 Klemen Tušar

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,4 @@ twd flex v2

### Note

Built using [Alfred-Workflow](https://github.com/deanishe/alfred-workflow).
The lightning fast search is powered by [Algolia](https://www.algolia.com) using the _same_ index as the official [Tailwind CSS](https://tailwindcss.com/) website.
26 changes: 26 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
analyzer:
plugins:
- dart_code_metrics

dart_code_metrics:
metrics:
cyclomatic-complexity: 20
number-of-arguments: 4
maximum-nesting-level: 5
metrics-exclude:
- test/**
rules:
- newline-before-return
- no-boolean-literal-compare
- no-empty-block
- prefer-trailing-comma
- prefer-conditional-expressions
- no-equal-then-else
anti-patterns:
- long-method
- long-parameter-list

linter:
rules:
avoid_print: false
prefer_single_quotes: true
File renamed without changes
File renamed without changes
135 changes: 135 additions & 0 deletions bin/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import 'dart:io' show exitCode, stdout;

import 'package:alfred_workflow/alfred_workflow.dart'
show AlfredItem, AlfredItemIcon, AlfredItemText, AlfredWorkflow;
import 'package:algolia/algolia.dart' show AlgoliaQuerySnapshot;
import 'package:args/args.dart' show ArgParser, ArgResults;
import 'package:easy_debounce/easy_debounce.dart' show EasyDebounce;
import 'package:html_unescape/html_unescape.dart' show HtmlUnescape;

import 'src/constants/config.dart' show Config;
import 'src/models/search_result.dart' show SearchResult;
import 'src/services/algolia_search.dart' show AlgoliaSearch;

void _showPlaceholder() {
workflow.addItem(
const AlfredItem(
title: 'Search the Tailwind CSS docs...',
icon: AlfredItemIcon(path: 'icon.png'),
),
);
}

Future<void> _performSearch(String query, {String? version}) async {
final AlgoliaQuerySnapshot snapshot = await AlgoliaSearch.query(
query,
version: version,
);

if (snapshot.nbHits > 0) {
final HtmlUnescape unescape = HtmlUnescape();

workflow.addItems(
snapshot.hits.map((snapshot) => SearchResult.fromJson(snapshot.data)).map(
(result) {
final int level = int.tryParse(result.type.substring(3)) ?? 0;
final String? title = result.hierarchy.getLevel(level);
final Map<String, String?> hierarchy = result.hierarchy.toJson()
..removeWhere((_, value) => value == null);

return AlfredItem(
uid: result.objectID,
title: title!,
subtitle: level > 0
? unescape.convert(hierarchy.values.join(' > '))
: null,
arg: result.url,
text: AlfredItemText(
largeType: title,
copy: result.url,
),
quickLookUrl: result.url,
icon: AlfredItemIcon(path: 'icon.png'),
valid: true,
);
},
),
);
} else {
final Uri url =
Uri.https('www.google.com', '/search', {'q': 'Tailwind CSS $query'});

workflow.addItem(
AlfredItem(
title: 'No matching answers found',
subtitle: 'Shall I try and search Google?',
arg: url.toString(),
text: AlfredItemText(
copy: url.toString(),
),
quickLookUrl: url.toString(),
icon: AlfredItemIcon(path: 'google.png'),
valid: true,
),
);
}
}

final AlfredWorkflow workflow = AlfredWorkflow();
bool verbose = false;

void main(List<String> arguments) async {
try {
exitCode = 0;

workflow.clearItems();

final ArgParser parser = ArgParser()
..addOption('query', abbr: 'q', mandatory: true)
..addFlag('verbose', abbr: 'v', defaultsTo: false);

final ArgResults args = parser.parse(arguments);

final String query = args['query'].replaceAll(RegExp(r'\s+'), ' ').trim();
final String version = query.split(' ').firstWhere(
(el) => Config.supportedVersions.contains(el),
orElse: () => Config.supportedVersions.last,
);

if (args['verbose']) {
verbose = true;
}

if (verbose) {
stdout.writeln('Query: "$query"');
}

EasyDebounce.debounce(
'search',
Duration(milliseconds: 250),
() async {
if (query.isEmpty) {
_showPlaceholder();
} else {
await _performSearch(
query,
version: version,
);
}

workflow.run();
},
);
} on FormatException catch (err) {
exitCode = 2;
workflow.addItem(AlfredItem(title: err.toString()));
workflow.run();
} catch (err) {
exitCode = 1;
workflow.addItem(AlfredItem(title: err.toString()));
workflow.run();
if (verbose) {
rethrow;
}
}
}
9 changes: 9 additions & 0 deletions bin/src/constants/config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Config {
Config._();

static const String algoliaApplicationId = 'KNPXZI5B0M';
static const String algoliaSearchOnlyApiKey =
'5fc87cef58bb80203d2207578309fab6';
static const String algoliaSearchIndex = 'tailwindcss';
static const List<String> supportedVersions = ['v0', 'v1', 'v2', 'v3'];
}
59 changes: 59 additions & 0 deletions bin/src/models/search_result.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'search_result_hierarchy.dart';

class SearchResult {
const SearchResult({
required this.objectID,
required this.type,
required this.url,
required this.hierarchy,
this.content,
});

final String objectID;
final String type;
final String url;
final SearchResultHierarchy hierarchy;
final String? content;

static const List<String> attributesToRetrieve = [
'hierarchy.lvl0',
'hierarchy.lvl1',
'hierarchy.lvl2',
'hierarchy.lvl3',
'hierarchy.lvl4',
'hierarchy.lvl5',
'hierarchy.lvl6',
'content',
'type',
'url',
];

static const List<String> attributesToSnippet = [
'hierarchy.lvl1:10',
'hierarchy.lvl2:10',
'hierarchy.lvl3:10',
'hierarchy.lvl4:10',
'hierarchy.lvl5:10',
'hierarchy.lvl6:10',
'content:10',
];

static const String snippetEllipsisText = '...';

SearchResult.fromJson(Map<String, dynamic> json)
: objectID = json['objectID'] as String,
type = json['type'] as String,
url = json['url'] as String,
hierarchy = SearchResultHierarchy.fromJson(
json['hierarchy'] as Map<String, dynamic>,
),
content = json['content'] as String?;

Map<String, dynamic> toJson() => {
'objectID': objectID,
'type': type,
'url': url,
'content': content,
'hierarchy': hierarchy.toJson(),
};
}
Loading

0 comments on commit 8e48f13

Please sign in to comment.