diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f91e24e5..38a45a63 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -67,7 +67,7 @@ In order to contribute to the source of the project I really appreciate contributions in the form of new themes to the project. If you'd like to add a new theme, please follow these steps: 1. Create a new `json` file in the `public/themes` folder using the theme name as the file name. Please use `kebab-case` for the file name. You can use the default theme file as a starting point. -2. Add the theme name to the array in the `public/themes.json` file. Please use the same name as the file and use alphabetical order. +2. Add the theme name to the array in the `src/utils/themes.js` file. Please use the same name as the file and use alphabetical order. 3. Test your theme by running the project locally by running `yarn dev` command 1. Use `set theme` command to see your theme on the list 2. Use `set theme ` to see your theme in action diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 6d10711c..bb795848 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -6,6 +6,7 @@ labels: '' assignees: excalith --- + **Describe the bug** A clear and concise description of what the bug is. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index acb51e1c..97573d27 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -6,6 +6,7 @@ labels: enhancement assignees: excalith --- + **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] diff --git a/.github/startpage.gif b/.github/startpage.gif index 2b5894a1..58c008c2 100644 Binary files a/.github/startpage.gif and b/.github/startpage.gif differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 576fe46b..b51765f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,19 @@ All upcoming and notable changes to this project will be documented in this file. > **Warning** -> Before updating, please be sure to check versions for possible **breaking changes**. If you are using preview version (online) and have issues, please check out the [troubleshooting](https://github.com/excalith/excalith-start-page/wiki/Troubleshooting) page how to fix it and learn why you shouldn't use preview version. +> Before updating, please be sure to check previous versions for possible **breaking changes**. If you are using preview (online) version and having issues, please refer to [troubleshooting](https://github.com/excalith/excalith-start-page/wiki/Troubleshooting) page to find possible solutions and learn why you shouldn't use preview version. ## Unreleased ### Added - Error handling for client-side issues by showing possible solutions. +- Using auto-completes the suggestion if any. (#25) +- Using TAB and SHIFT + TAB cycles through filtered links. (#25) + +### Improved +- `Prompt` and `Search` component layouts +- `help` output with keybindings +- Colorized `config help` output ## Previous Versions [![Latest Release](https://img.shields.io/github/v/release/excalith/excalith-start-page)](https://github.com/excalith/excalith-start-page/releases) diff --git a/README.md b/README.md index a304d79a..7e1fef11 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ You can check the working version from [here](https://excalith-start-page.vercel - Search websites with custom commands. For example, type `s some weird bug` to search StackOverflow for `some weird bug` - Wallpaper support through URL with blur and fade effects - Customizable Fetch UI for fetching browser and system data, including custom image support +- Autosuggest and Autocomplete support just like `zsh` and `fish` +- Cycle through filtered links back and forth - Multiple theme support (check all [available themes](./public/themes/)) - Built-in configuration editor to easily edit and save your configuration @@ -46,8 +48,10 @@ Please refer to [configuration](https://github.com/excalith/excalith-start-page/ ### Key Bindings +- Use to auto-complete the suggestion +- Cycle through filtered links using TAB and SHIFT + TAB - Clear the prompt quickly with CTRL + C -- Close window with ESC +- Close windows with ESC ## Using @@ -102,7 +106,7 @@ If you still prefer to use the online version, I would recommend you to use the ## Customization -There are multiple ways of customizing the start page to making it yours! Please refer to [configuration](https://github.com/excalith/excalith-start-page/wiki/Configuration) and [themes](https://github.com/excalith/excalith-start-page/wiki/Themes) pages for more information. +You can pretty much customize everything! Please refer to [configuration](https://github.com/excalith/excalith-start-page/wiki/Configuration) and [themes](https://github.com/excalith/excalith-start-page/wiki/Themes) pages for more information regarding themes and configuration options. ## How To Contribute @@ -110,4 +114,4 @@ Please feel free to contribute any way you can. Just keep in mind that you shoul ## License -The code is available under the [MIT license](LICENSE). +The code is available under the [MIT license](LICENSE). Feel free to copy, modify, and distribute the code as you wish, but please keep the original license in the files. Attribution is appreciated and will definetely help improving this project. diff --git a/package.json b/package.json index 8f1b5237..98f7ac08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "start-page", - "version": "2.2.0", + "version": "2.3.0", "private": true, "scripts": { "dev": "next dev", diff --git a/src/components/Config.js b/src/components/Config.js index ce730615..6aa10b83 100644 --- a/src/components/Config.js +++ b/src/components/Config.js @@ -3,8 +3,9 @@ import Prompt from "@/components/Prompt" import { isURL } from "@/utils/isURL" import { useSettings } from "@/context/settings" import dynamic from "next/dynamic" +import { themes } from "@/utils/themes" -async function getTheme(themeName) { +async function fetchTheme(themeName) { try { const theme = await fetch("/themes/" + themeName + ".json").then((res) => res.json()) return theme @@ -13,16 +14,6 @@ async function getTheme(themeName) { } } -async function getThemes() { - try { - const themes = await fetch("/themes.json").then((res) => res.json()) - return themes - } catch { - console.log("Error fetching themes") - return null - } -} - const Config = ({ commands, closeCallback }) => { const [command] = useState(commands.join(" ")) const [consoleLog, setConsoleLog] = useState([]) @@ -46,7 +37,7 @@ const Config = ({ commands, closeCallback }) => { } else if (cmd === "theme") { if (commands.length === 3) { const themeName = commands[2] - getTheme(themeName).then((theme) => { + fetchTheme(themeName).then((theme) => { if (theme === null) { invalidTheme(theme) } else { @@ -54,13 +45,10 @@ const Config = ({ commands, closeCallback }) => { } }) } else { - getThemes().then((themes) => { - appendToLog("Available themes:", "title") - for (let i in themes) { - appendToLog(themes[i]) - } - setDone(true) + themes.map((theme) => { + appendToLog(theme) }) + setDone(true) } } else if (cmd === "help") { usageExample() @@ -105,20 +93,20 @@ const Config = ({ commands, closeCallback }) => { const usageExample = () => { appendToLog("Usage:", "title") - appendToLog("config help: Show usage examples") - appendToLog("config theme: List available themes") - appendToLog("config theme : Switch theme") - appendToLog("config import : Import remote config") - appendToLog("config edit: Edit local config") - appendToLog("config reset: Reset to default config") + appendToLog(["config help", "Show usage examples"], "help") + appendToLog(["config theme", "List available themes"], "help") + appendToLog(["config theme ", "Switch theme"], "help") + appendToLog(["config import ", "Import remote config"], "help") + appendToLog(["config edit", "Edit local config"], "help") + appendToLog(["config reset", "Reset to default config"], "help") setDone(true) } const invalidTheme = (themeName) => { appendToLog("Invalid theme: " + commands[2], "error") appendToLog("Usage:", "title") - appendToLog("config theme: Show available themes") - appendToLog("config theme : Set theme") + appendToLog(["config theme", "Show available themes"], "help") + appendToLog(["config theme ", "Set theme"], "help") setDone(true) } @@ -146,8 +134,7 @@ const Config = ({ commands, closeCallback }) => {
  • - - {command} +
{isEditMode ? ( @@ -174,11 +161,21 @@ const Config = ({ commands, closeCallback }) => { {data.type === "title" && (

{data.text}

)} + {data.type === "help" && ( +

+ {data.text[0]}{" "} + {data.text[1]} +

+ )} {data.type === undefined &&

{data.text}

} ) })} - {isDone &&
  • Press ESC to continue...
  • } + {isDone && ( +
  • + Press ESC to continue... +
  • + )} )}
    diff --git a/src/components/Editor.js b/src/components/Editor.js index c7303349..42508521 100644 --- a/src/components/Editor.js +++ b/src/components/Editor.js @@ -52,7 +52,10 @@ const Editor = () => { setOptions={{ showLineNumbers: true, tabSize: 2, - useWorker: false + useWorker: false, + highlightActiveLine: false, + highlightSelectedWord: false, + highlightGutterLine: false }} ref={editor} /> diff --git a/src/components/Fetch.js b/src/components/Fetch.js index cf72996e..4d487da4 100644 --- a/src/components/Fetch.js +++ b/src/components/Fetch.js @@ -79,7 +79,9 @@ const Fetch = ({ closeCallback }) => {
    - v{fetchData.version} + + v{fetchData.version} +
      {settings.fetch.data.map((item, index) => { diff --git a/src/components/Help.js b/src/components/Help.js index bbd69caf..d998cd8e 100644 --- a/src/components/Help.js +++ b/src/components/Help.js @@ -16,8 +16,23 @@ const Help = ({ closeCallback }) => {
    • - Filter links by typing in the prompt
    • - Unfiltered prompt will search using default search engine
    • - Launch URL's directly from prompt
    • -
    • - Use CTRL+C to clear prompt
    • -
    • - Use ESC to exit windows
    • +
    + + Key Bindings +
      +
    • + - Use key to complete suggestion +
    • +
    • + - Use TAB and{" "} + SHIFT+TAB to cycle through filtered links +
    • +
    • + - Use CTRL+C to clear prompt +
    • +
    • + - Use ESC to exit windows +
    Built-in Commands @@ -58,7 +73,7 @@ const Help = ({ closeCallback }) => { - Custom Commands + Search Aliases
      {settings.search.shortcuts.map((cmd, index) => { return ( @@ -69,7 +84,9 @@ const Help = ({ closeCallback }) => { })}
      -
    • Press ESC to continue...
    • +
    • + Press ESC to continue... +
    diff --git a/src/components/Link.js b/src/components/Link.js index 680c5b20..a4c6e5a4 100644 --- a/src/components/Link.js +++ b/src/components/Link.js @@ -1,8 +1,9 @@ import React, { useEffect, useState } from "react" import { Icon } from "@iconify/react" -const Link = ({ linkData, filter }) => { +const Link = ({ linkData, filter, selection }) => { const [isHidden, setHidden] = useState(false) + const [isSelected, setSelected] = useState(false) const name = linkData.name const lower_name = linkData.name.toLowerCase() @@ -14,25 +15,33 @@ const Link = ({ linkData, filter }) => { const lower_command = filter.toLowerCase() if (lower_command) { - const isSelected = lower_name.includes(lower_command) - setHidden(!isSelected) + const isFiltered = lower_name.startsWith(lower_command) + setHidden(!isFiltered) } else { setHidden(false) } }, [filter, lower_name, target, url]), [filter] + useEffect(() => { + setSelected(lower_name === selection) + }, [selection]) + return ( - - - - - {name} - +
  • + + + + + {name} + +
  • ) } diff --git a/src/components/List.js b/src/components/List.js new file mode 100644 index 00000000..e1e3005e --- /dev/null +++ b/src/components/List.js @@ -0,0 +1,65 @@ +import React, { use, useEffect, useState } from "react" +import Link from "@/components/Link" +import Search from "@/components/Search" +import { useSettings } from "@/context/settings" + +const Section = ({ section, filter, selection }) => { + const alignment = section.align || "left" + return ( +
    +

    + {section.title} +

    + +
      + {section.links.map((link, index) => { + { + return ( + + ) + } + })} +
    +
    + ) +} + +const List = () => { + const { settings } = useSettings() + const [command, setCommand] = useState("") + const [selection, setSelection] = useState("") + + const handleCommandChange = (str) => { + setCommand(str) + } + + const handleSelectionChange = (sel) => { + setSelection(sel) + } + + return ( +
    +
    + {settings.sections.list.map((section, index) => { + return ( +
    + ) + })} +
    + +
    + ) +} + +export default List diff --git a/src/components/Prompt.js b/src/components/Prompt.js index 8861ff46..62064999 100644 --- a/src/components/Prompt.js +++ b/src/components/Prompt.js @@ -8,17 +8,17 @@ const Prompt = ({ command, showSymbol = true }) => { const promptSettings = settings.prompt return ( - + {lower_username} @ {browserData.browserLower} {showSymbol && ( - + {" "} {promptSettings.promptSymbol}{" "} )} - {command && {command}} + {command && {command}} ) } diff --git a/src/components/Search.js b/src/components/Search.js index 9f116740..94c83374 100644 --- a/src/components/Search.js +++ b/src/components/Search.js @@ -3,50 +3,146 @@ import { RunCommand } from "@/utils/command" import Prompt from "@/components/Prompt" import { useSettings } from "@/context/settings" -const Search = ({ prompt, commandChange }) => { - const [focus, setFocus] = useState(false) - const { settings } = useSettings() - const input = useRef(null) +const Search = ({ commandChange, selectionChange }) => { + const inputRef = useRef(null) + const suggestionRef = useRef(null) + const { settings, items } = useSettings() + const [inputFocus, setInputFocus] = useState(false) + + const [command, setCommand] = useState("") + const [filteredItems, setFilteredItems] = useState([]) + const [selection, setSelection] = useState("") + const [suggestion, setSuggestion] = useState("") + + // Focus on input useEffect(() => { - setTimeout(() => input.current.focus(), 0) - }, [focus]) + setTimeout(() => inputRef.current.focus(), 0) + }, [inputFocus]) + // Key Down useEffect(() => { - const handleKeyDown = (event) => { - if (event.key === "Enter") { - RunCommand(input.current.value, settings) - } else if (settings.prompt.ctrlC) { - if ((event.metaKey || event.ctrlKey) && event.code === "KeyC") { - input.current.value = "" - commandChange({ target: { value: "" } }) + const handleKeyDown = (e) => { + // Submit prompt + if (e.key === "Enter") { + RunCommand(command, settings) + } + // Clear prompt + else if ((e.metaKey || e.ctrlKey) && e.code === "KeyC") { + if (settings.prompt.ctrlC) { + inputRef.current.value = "" + selectionChange("") + commandChange("") + setSuggestion("") } } + // Auto Complete + else if (e.key === "ArrowRight") { + if (suggestion !== "") { + e.preventDefault() + inputRef.current.value = suggestion + setCommand(suggestion) + commandChange(suggestion) + selectionChange("") + setSuggestion("") + } + } + // Previous Selection + else if (e.shiftKey && e.key === "Tab") { + e.preventDefault() + + if (command === "") return + if (filteredItems.length === 0) return + + let idx = -1 + if (selection && selection !== "") + idx = filteredItems.indexOf(selection.toLowerCase()) + + idx = (idx + filteredItems.length - 1) % filteredItems.length + const selectedItem = filteredItems[idx] + setSelection(selectedItem) + setSuggestion(selectedItem) + selectionChange(selectedItem) + } + // Next Selection + else if (e.key === "Tab") { + e.preventDefault() + + if (command === "") return + if (filteredItems.length === 0) return + + let idx = -1 + if (selection && selection !== "") + idx = filteredItems.indexOf(selection.toLowerCase()) + + idx = (idx + 1) % filteredItems.length + const selectedItem = filteredItems[idx] + setSelection(selectedItem) + setSuggestion(selectedItem) + selectionChange(selectedItem) + } } document.addEventListener("keydown", handleKeyDown) return () => { document.removeEventListener("keydown", handleKeyDown) } - }) + }, [command, suggestion, selection, filteredItems, settings]) + + // Filter possible items + useEffect(() => { + commandChange(command) + + // Set possible filtered items + setFilteredItems([]) + if (command === "") { + selectionChange("") + } else { + const filtered = items.filter((item) => item.startsWith(command)) + setFilteredItems(filtered) + } + }, [command, items]) + + // Set suggestions + useEffect(() => { + if (filteredItems.length <= 1) selectionChange("") + + // Set suggestion + if (filteredItems.length === 0) { + setSuggestion("") + } else { + setSuggestion(filteredItems[0]) + } + }, [filteredItems]) return ( ) } diff --git a/src/components/Sections.js b/src/components/Sections.js deleted file mode 100644 index cbe26532..00000000 --- a/src/components/Sections.js +++ /dev/null @@ -1,45 +0,0 @@ -import React, { useState } from "react" -import Link from "@/components/Link" -import Search from "@/components/Search" -import { useSettings } from "@/context/settings" - -const Section = ({ section, filter }) => { - const alignment = section.align || "left" - return ( -
    -

    - {section.title} -

    - - {section.links.map((link, index) => { - { - return ( - - ) - } - })} -
    - ) -} - -const Sections = () => { - const [command, setCommand] = useState("") - const { settings } = useSettings() - - const handleCommandChange = (e) => { - setCommand(e.target.value) - } - - return ( -
    -
    - {settings.sections.list.map((section, index) => { - return
    - })} -
    - -
    - ) -} - -export default Sections diff --git a/src/components/Terminal.js b/src/components/Terminal.js index d67f56b5..cb67e664 100644 --- a/src/components/Terminal.js +++ b/src/components/Terminal.js @@ -1,5 +1,5 @@ import React, { useEffect, useState, useRef } from "react" -import Sections from "@/components/Sections" +import List from "@/components/List" import Help from "@/components/Help" import Config from "@/components/Config" import Fetch from "@/components/Fetch" @@ -49,7 +49,7 @@ const Terminal = () => { } else if (cmd === "fetch") { return } else { - return + return } } diff --git a/src/context/settings.js b/src/context/settings.js index ac5a010c..e65345ec 100644 --- a/src/context/settings.js +++ b/src/context/settings.js @@ -1,5 +1,6 @@ import { createContext, useContext, useEffect, useState } from "react" import defaultConfig from "startpage.config" +import { themes } from "@/utils/themes" const SETTINGS_KEY = "settings" @@ -12,6 +13,7 @@ export const useSettings = () => useContext(SettingsContext) export const SettingsProvider = ({ children }) => { const [settings, setSettings] = useState() + const [items, setItems] = useState([]) useEffect(() => { const settings = localStorage.getItem(SETTINGS_KEY) @@ -30,6 +32,30 @@ export const SettingsProvider = ({ children }) => { useEffect(() => { if (settings && settings !== "undefined") { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)) + + let filterArr = [ + "help", + "fetch", + "config", + "config help", + "config edit", + "config import", + "config reset", + "config theme" + ] + + themes.map((theme) => { + filterArr.push("config theme " + theme) + }) + + settings.sections.list.map((section) => { + section.links.map((link) => { + { + filterArr.push(link.name.toLowerCase()) + } + }) + }) + setItems(filterArr) } }, [settings]) @@ -43,7 +69,8 @@ export const SettingsProvider = ({ children }) => { } return ( - + {children} ) diff --git a/src/styles/globals.css b/src/styles/globals.css index 9af212fb..e2a9263b 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -49,7 +49,7 @@ body { a, a:active, a:visited { - color: var(--url-default) !important; + color: var(--url-default); text-decoration: none; transition: color 0.2s !important; } @@ -81,6 +81,11 @@ a:hover { text-shadow: 0px 0px 10px; } +.selected { + background-color: var(--selection-bg); + color: var(--selection-fg) !important; +} + .align-left { text-align: left; } .align-center { text-align: center; } .align-right { text-align: right; } diff --git a/src/utils/command.js b/src/utils/command.js index 304f723e..85d62e58 100644 --- a/src/utils/command.js +++ b/src/utils/command.js @@ -25,7 +25,7 @@ function openFilteredLinks(command, settings) { { section.links.map((link) => { { - if (link.name.toLowerCase().includes(command)) { + if (link.name.toLowerCase().startsWith(command)) { filteredUrls.push(link.url) } } diff --git a/public/themes.json b/src/utils/themes.js similarity index 82% rename from public/themes.json rename to src/utils/themes.js index 65b0178c..bd49b143 100644 --- a/public/themes.json +++ b/src/utils/themes.js @@ -1,4 +1,4 @@ -[ +export const themes = [ "default", "bushido", "catppuccin", @@ -9,4 +9,4 @@ "onedark", "synthwave", "tokyonight" -] \ No newline at end of file +] diff --git a/tailwind.config.js b/tailwind.config.js index 20e883e5..53466e89 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -17,7 +17,8 @@ module.exports = { "4xl": ["36px", "40px"] }, borderRadius: { - terminal: "0.625rem" + terminal: "0.625rem", + selection: "0.2rem" }, colors: { "background-color": "var(--background-color)", @@ -46,6 +47,9 @@ module.exports = { margin: { line: "1.4rem" }, + spacing: { + full: "100%" + }, animation: { fadeIn: "fadeIn 0.15s ease-in-out" },