Skip to content
This repository was archived by the owner on Jul 28, 2021. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d390a5c
Adding basic foundation for search command
chrisforrette Nov 6, 2018
da1c070
Trying to JSX the search
chrisforrette Nov 6, 2018
aea8899
Fleshing out a bunch of the interactive search component composition …
chrisforrette Nov 7, 2018
799c11d
Getting fake install flow working
chrisforrette Nov 7, 2018
fc84e7c
Customizing package listing details and select components
chrisforrette Nov 7, 2018
7204636
Fixing inline search term search
chrisforrette Nov 7, 2018
a009788
deps: Adding figures@2.0.0
chrisforrette Nov 14, 2018
4a0bd88
fix(search): making search command JSX-ish and removing ink 'h' funct…
chrisforrette Nov 14, 2018
64c7e71
fix(search): adding ink 'h' import back to search command, ink gets m…
chrisforrette Nov 14, 2018
0588526
fix(search): changing search component export to be the search compon…
chrisforrette Nov 14, 2018
4dca41e
fix(search): added maintenance/popularity/quality legend to results t…
chrisforrette Nov 14, 2018
c45cbbd
feat(search): adding options config object with almost all libnpm sea…
chrisforrette Nov 14, 2018
096c2a0
fix(search): removing special character stuff: package selector indic…
chrisforrette Nov 14, 2018
6842d04
deps: removing figures and ink-spinner
chrisforrette Nov 14, 2018
989b967
feat(search): worked out a decent control flow, removing focus on sel…
chrisforrette Nov 27, 2018
ead33b5
feat(search): making it so that the search input prompt doesn't displ…
chrisforrette Nov 27, 2018
c521af8
feat(search): fixing issue with focus remaining on search results whe…
chrisforrette Dec 4, 2018
45fa41a
feat(search): linting
chrisforrette Dec 4, 2018
ff28c51
feat(search): removing stray todo note
chrisforrette Dec 4, 2018
6e583e9
feat(search): updating search command, moving it into yargs-modules, …
chrisforrette Dec 4, 2018
60091f5
Updating package-lock.json, removing unnecessary eslint comment
chrisforrette Jan 5, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bin/tink.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@ const CMDS = new Set([
'prepare',
'profile',
'rm',
'search',
'shell',
'team',
'view',
'whoami'
])

const ALIASES = new Map([
['find', 'search'],
['prep', 'prepare'],
['s', 'search'],
['se', 'search'],
['sh', 'shell']
])

Expand Down
247 changes: 247 additions & 0 deletions lib/components/search.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
'use strict'

const {
h, // eslint-disable-line no-unused-vars
Color,
Component,
Fragment
} = require('ink')
const SelectInput = require('ink-select-input')
const TextInput = require('ink-text-input')
const libnpm = require('libnpm')

const FOCUS_SEARCH = 'FOCUS_SEARCH'
const FOCUS_RESULTS = 'FOCUS_RESULTS'

class Search extends Component {
constructor (props) {
super(props)
const { terms } = this.props
this.state = {
isInstalling: false,
isLoading: null,
focusedOn: FOCUS_SEARCH,
matches: [],
selectedPackage: null,
terms: terms || ''
}

this.onChangeTerms = this.onChangeTerms.bind(this)
this.onKeyPress = this.onKeyPress.bind(this)
this.onSubmit = this.onSubmit.bind(this)
this.onSelectPackage = this.onSelectPackage.bind(this)
}

componentDidMount () {
const { terms } = this.props
process.stdin.on('keypress', this.onKeyPress)

if (terms) {
this.search(terms)
}
}

componentWillUnmount () {
process.stdin.removeListener('keypress', this.onKeyPress)
}

render () {
const {
focusedOn,
isInstalling,
isLoading,
matches,
selectedPackage,
terms
} = this.state

return <div>
{ !isInstalling && <Fragment>
<SearchInput
isFocused={focusedOn === FOCUS_SEARCH}
terms={terms}
onChange={this.onChangeTerms}
onSubmit={this.onSubmit} />
<SearchResults
isFocused={focusedOn === FOCUS_RESULTS}
isLoading={isLoading}
terms={terms}
matches={matches}
onSelect={this.onSelectPackage} />
</Fragment> }
{ isInstalling && selectedPackage
? <InstallingPackage isInstalling={isInstalling} pkg={selectedPackage} />
: null
}
</div>
}

async search (terms) {
const { options } = this.props
try {
this.setState({
focusedOn: FOCUS_SEARCH,
isLoading: true,
matches: []
})
const matches = await libnpm.search(terms, {
detailed: true,
...options
})
this.setState({
focusedOn: matches && matches.length ? FOCUS_RESULTS : FOCUS_SEARCH,
isLoading: false,
matches
})
} catch (err) {
// @todo Show error message
this.setState({
isLoading: false,
matches: []
})
}
}

async install (pkg) {
try {
this.setState({
isInstalling: true,
selectedPackage: pkg
})
// @todo Implement real install
await new Promise((resolve, reject) => setTimeout(resolve, 1000))
this.setState({ isInstalling: false })
this.props.onExit()
} catch (err) {
// @todo Show error message
this.setState({ isInstalling: false })
}
}

onChangeTerms (terms) {
this.setState({ terms })
}

onKeyPress (chunk, key) {
const {
focusedOn,
matches,
selectedPackage
} = this.state

// If up/down arrows are pressed, make sure we're focused on search results

if (['up', 'down'].includes(key.name)) {
if (focusedOn !== FOCUS_RESULTS && matches && matches.length) {
return this.setState({ focusedOn: FOCUS_RESULTS })
}
return
}

// If enter is pressed and we're focused on search results, install

if (key.name === 'enter' && focusedOn === FOCUS_RESULTS && selectedPackage) {
return this.install(selectedPackage)
}

// If non up/down/enter keys were pressed and we're not focused on search
// input, focus on search input

if (focusedOn !== FOCUS_SEARCH) {
return this.setState({ focusedOn: FOCUS_SEARCH })
}
}

onSubmit (terms) {
this.search(terms)
}

onSelectPackage (pkg) {
this.install(pkg)
}
}

const SearchInput = ({ isFocused, onChange, onSubmit, terms }) => {
return <div>
<Color grey bold>Find a package: </Color>
<TextInput focused={isFocused} value={terms} onChange={onChange} onSubmit={onSubmit} />
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason, if I erase the textinput data and press "Enter" it does BOTH a search and an install (and closes the process). Or was I testing an older version?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea! This is that weird input focus thing I mentioned in my Todos list. I saw that the ink-select-input has an explicit focus boolean prop, and I just realized ink-text-input has the same prop undocumented. I just need to add some logic to juggle these intuitively...

}

const SearchResults = ({ isFocused, isLoading, matches, onSelect, terms }) => {
if (isLoading) {
return <div><Color green>Searching...</Color></div>
}

if (isLoading === false && terms && (!matches || !matches.length)) {
return <Color grey>No matches found</Color>
}

if (isLoading === false && matches && matches.length) {
return <div>
<Color grey bold>Results (maintenance, popularity, quality):</Color><br />
<PackageSelector isFocused={isFocused} matches={matches} onSelect={onSelect} />
</div>
}

return null
}

const PackageSelectIndicator = ({ isSelected }) => {
if (!isSelected) {
return ' '
}

return <Color green>{ '> ' }</Color>
}

const formatPackageScore = num => `${Math.round(num * 100)}%`

const PackageItem = ({ isSelected, value }) => {
const {
package: {
name,
publisher: { username }
},
score: {
detail: {
maintenance,
popularity,
quality
}
}
} = value

const m = formatPackageScore(maintenance)
const p = formatPackageScore(popularity)
const q = formatPackageScore(quality)

return <Color green={isSelected}>{name} @{username} ({m} / {p} / {q})</Color>
}

const PackageSelector = ({ isFocused, matches, onSelect }) => {
const items = matches.map(match => ({
value: match,
label: match
}))

if (isFocused) {
return <SelectInput
indicatorComponent={PackageSelectIndicator}
items={items}
itemComponent={PackageItem}
onSelect={({ value }) => onSelect(value)} />
} else if (items && items.length) {
return items.map(({ value }) => <div>{ ' ' }<PackageItem value={value} /></div>)
}
}

const InstallingPackage = ({ isInstalling, pkg }) => {
if (!isInstalling) {
return null
}

return <div><Color green>Installing {pkg.package.name}...</Color></div>
}

module.exports = Search
78 changes: 78 additions & 0 deletions lib/yargs-modules/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use strict'

const { h, render } = require('ink')
const Search = require('../components/search.jsx')

const DEFAULT_LIMIT = 10

const SearchCommand = module.exports = {
command: 'search',
aliases: ['s', 'se', 'find'],
describe: 'Search the NPM registry',
builder (y) {
return y.help().alias('help', 'h')
.options(SearchCommand.options)
},
options: {
limit: {
alias: 'l',
default: DEFAULT_LIMIT,
description: 'Number of results to limit the query to',
type: 'number'
},
maintenance: {
alias: 'm',
description: 'Decimal number between `0` and `1` that defines the weight of `maintenance` metrics when scoring and sorting packages',
type: 'number'
},
offset: {
alias: 'o',
description: 'Offset number for results. Used with `--limit` for pagination',
type: 'number'
},
popularity: {
alias: 'p',
description: 'Decimal number between `0` and `1` that defines the weight of `popularity` metrics when scoring and sorting packages',
type: 'number'
},
quality: {
alias: 'q',
description: 'Decimal number between `0` and `1` that defines the weight of `quality` metrics when scoring and sorting packages',
type: 'number'
},
sortBy: {
alias: 's',
choices: [
'maintenance',
'optimal',
'popularity',
'quality'
],
description: 'Used as a shorthand to set `--quality`, `--maintenance`, and `--popularity` with values that prioritize each one'
}
},
handler: search
}

async function search (argv) {
let unmount

const onError = () => {
unmount()
process.exit(1)
}

const onExit = () => {
unmount()
process.exit()
}

const terms = argv._[1]

unmount = render(h(Search, {
onError,
onExit,
options: argv,
terms
}))
}
Loading