├── .github └── workflows │ └── go.yml ├── .gitignore ├── README.md ├── cfg.json ├── examples └── logout-manager │ └── cfg.json ├── fastlauncher ├── flake.lock ├── flake.nix ├── go.mod ├── go.sum ├── guides ├── arch_kde │ ├── images │ │ ├── add.png │ │ ├── add2.png │ │ ├── add3.png │ │ ├── flatpaklist.png │ │ ├── hotkey.png │ │ ├── install.png │ │ └── system_parameters.png │ └── readme.md └── screenshots │ ├── logout-manager.png │ └── main.png ├── log └── log.go ├── main.go ├── mode ├── configSourceApps.go └── osSourceApp.go ├── model └── app.go ├── package.nix ├── pkg ├── apprunner │ ├── main.go │ └── runner │ │ ├── linuxAppRunner.go │ │ ├── macOsAppRunner.go │ │ └── windowsAppRunner.go ├── finderallapps │ ├── finder │ │ ├── linuxFinder.go │ │ ├── macOsFinder.go │ │ └── windowsFinder.go │ ├── finder_test │ │ └── linuxFinder_test.go │ ├── main.go │ └── model │ │ └── app.go └── parsedesktopfile │ ├── main.go │ ├── model │ └── desktopfile.go │ └── test │ └── main_test.go └── ui └── model.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: '1.22.3' 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | 30 | - name: golangci-lint 31 | uses: golangci/golangci-lint-action@v6 32 | with: 33 | version: v1.60 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastLauncher 2 | 3 | TUI Application Launcher. Alternative to rofi/wofi 4 | 5 | ![main windows](https://github.com/probeldev/fastlauncher/blob/main/guides/screenshots/main.png?raw=true) 6 | 7 | ## Suport OS 8 | 9 | Linux - Done 10 | 11 | Windows - Work in progress 12 | 13 | Mac Os - Work in progress 14 | 15 | ## Examples 16 | 17 | ### Logout Manager 18 | 19 | [Example config](https://github.com/probeldev/fastlauncher/blob/main/examples/logout-manager/cfg.json) 20 | 21 | ![Logout manager](https://github.com/probeldev/fastlauncher/blob/main/guides/screenshots/logout-manager.png?raw=true) 22 | 23 | ## Installation 24 | 25 | [Full guide for Arch Linux with KDE](https://github.com/probeldev/fastlauncher/tree/main/guides/arch_kde/readme.md) 26 | 27 | ### Go 28 | Installation 29 | 30 | go install github.com/probeldev/fastlauncher@latest 31 | 32 | 33 | If you get an error claiming that lazygit cannot be found or is not defined, you 34 | may need to add `~/go/bin` to your $PATH (MacOS/Linux), or `%HOME%\go\bin` 35 | (Windows) 36 | 37 | Zsh 38 | 39 | echo "export PATH=\$PATH:~/go/bin" >> ~/.zshrc 40 | 41 | Bash 42 | 43 | echo "export PATH=\$PATH:~/go/bin" >> ~/.bashrc 44 | 45 | ### Nix 46 | 47 | nix profile install github:probeldev/fastlauncher 48 | 49 | 50 | ## Usage 51 | 52 | ### All apps from OS 53 | 54 | fastlauncher 55 | 56 | ### Apps from config 57 | 58 | fastlauncher --config ~/script/fast-launcher/cfg.json 59 | 60 | Example file [cfg.json](https://github.com/probeldev/fastlauncher/blob/main/cfg.json) 61 | 62 | It's launched with the help of window manager. Example hyprland.conf: 63 | 64 | $terminal = foot 65 | $menu = $terminal -T fast-launcher fastlauncher --config ~/script/fast-launcher/cfg.json 66 | bind = $mainMod, D, exec, $menu 67 | 68 | 69 | windowrulev2 = float,title:(fast-launcher) 70 | windowrulev2 = pin,title:(fast-launcher) 71 | windowrulev2 = size 1000 600,title:(fast-launcher) 72 | windowrulev2 = center(1), title:(fast-launcher) 73 | 74 | 75 | -------------------------------------------------------------------------------- /cfg.json: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | { 4 | "title": "Mozilla Firefox", 5 | "description": "web browser", 6 | "command": "firefox" 7 | }, 8 | { 9 | "title": "Mozilla Firefox Private", 10 | "description": "Private window", 11 | "command": "firefox --private-window" 12 | }, 13 | { 14 | "title": "DBGate", 15 | "description": "Database IDE", 16 | "command": "flatpak run org.dbgate.DbGate" 17 | }, 18 | { 19 | "title": "Telegram", 20 | "description": "Telegram Desktop", 21 | "command": "flatpak run org.telegram.desktop" 22 | }, 23 | { 24 | "title": "Nemo", 25 | "description": "File manager", 26 | "command": "nemo" 27 | }, 28 | { 29 | "title": "Project: FastLauncher", 30 | "description": "Project: FastLauncher", 31 | "command": "alacritty --working-directory ~/work/opensource/fast-launcher" 32 | }, 33 | { 34 | "title": "Project: FunnyVideo", 35 | "description": "Project: FunnyVideo", 36 | "command": "alacritty --working-directory ~/work/opensource/funny_video" 37 | }, 38 | { 39 | "title": "SSH: Mac", 40 | "description": "Connect to Macbook Air", 41 | "command": "alacritty -e ssh sergey@192.168.1.49" 42 | }, 43 | { 44 | "title": "Obsidian", 45 | "description": "Obsidian", 46 | "command": "flatpak run md.obsidian.Obsidian" 47 | }, 48 | { 49 | "title": "KWrite", 50 | "description": "text editor", 51 | "command": "flatpak run org.kde.kwrite" 52 | }, 53 | { 54 | "title": "Krita", 55 | "description": "Digital painting", 56 | "command": "flatpak run org.kde.krita" 57 | }, 58 | { 59 | "title": "Kate", 60 | "description": "text editor", 61 | "command": "kate" 62 | } 63 | ] 64 | -------------------------------------------------------------------------------- /examples/logout-manager/cfg.json: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | { 4 | "title": "Reboot", 5 | "description": "Reboot", 6 | "command": "systemctl reboot" 7 | }, 8 | { 9 | "title": "Logout", 10 | "description": "Logout", 11 | "command": "loginctl terminate-user $USER" 12 | }, 13 | { 14 | "title": "Suspend", 15 | "description": "Suspend", 16 | "command": "Suspend" 17 | }, 18 | { 19 | "title": "Shutdown", 20 | "description": "Shutdown", 21 | "command": "systemctl poweroff" 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /fastlauncher: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probeldev/fastlauncher/2fc581307b70b5325477576a83d5dc5c6207870b/fastlauncher -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1743703532, 24 | "narHash": "sha256-s1KLDALEeqy+ttrvqV3jx9mBZEvmthQErTVOAzbjHZs=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "bdb91860de2f719b57eef819b5617762f7120c70", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-24.11", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | }; 6 | outputs = { 7 | self, 8 | nixpkgs, 9 | flake-utils, 10 | }: 11 | flake-utils.lib.eachDefaultSystem (system: let 12 | pkgs = import nixpkgs { 13 | inherit system; 14 | }; 15 | fastlauncher-package = pkgs.callPackage ./package.nix {}; 16 | in { 17 | packages = rec { 18 | fastlauncher = fastlauncher-package; 19 | default = fastlauncher; 20 | }; 21 | 22 | apps = rec { 23 | fastlauncher = flake-utils.lib.mkApp { 24 | drv = self.packages.${system}.fastlauncher; 25 | }; 26 | default = fastlauncher; 27 | }; 28 | 29 | devShells.default = pkgs.mkShell { 30 | packages = (with pkgs; [ 31 | go 32 | ]); 33 | }; 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/probeldev/fastlauncher 2 | 3 | go 1.22.3 4 | 5 | require ( 6 | github.com/charmbracelet/bubbles v0.18.0 7 | github.com/charmbracelet/bubbletea v0.26.4 8 | github.com/charmbracelet/lipgloss v0.11.0 9 | ) 10 | 11 | require ( 12 | github.com/atotto/clipboard v0.1.4 // indirect 13 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 14 | github.com/charmbracelet/x/ansi v0.1.2 // indirect 15 | github.com/charmbracelet/x/input v0.1.0 // indirect 16 | github.com/charmbracelet/x/term v0.1.1 // indirect 17 | github.com/charmbracelet/x/windows v0.1.0 // indirect 18 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 19 | github.com/gdamore/encoding v1.0.0 // indirect 20 | github.com/gdamore/tcell/v2 v2.7.1 // indirect 21 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 22 | github.com/mattn/go-isatty v0.0.20 // indirect 23 | github.com/mattn/go-localereader v0.0.1 // indirect 24 | github.com/mattn/go-runewidth v0.0.15 // indirect 25 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 26 | github.com/muesli/cancelreader v0.2.2 // indirect 27 | github.com/muesli/reflow v0.3.0 // indirect 28 | github.com/muesli/termenv v0.15.2 // indirect 29 | github.com/rivo/tview v0.0.0-20250501113434-0c592cd31026 // indirect 30 | github.com/rivo/uniseg v0.4.7 // indirect 31 | github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect 32 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 33 | golang.org/x/sync v0.7.0 // indirect 34 | golang.org/x/sys v0.20.0 // indirect 35 | golang.org/x/term v0.17.0 // indirect 36 | golang.org/x/text v0.14.0 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 | github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= 6 | github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= 7 | github.com/charmbracelet/bubbletea v0.26.4 h1:2gDkkzLZaTjMl/dQBpNVtnvcCxsh/FCkimep7FC9c40= 8 | github.com/charmbracelet/bubbletea v0.26.4/go.mod h1:P+r+RRA5qtI1DOHNFn0otoNwB4rn+zNAzSj/EXz6xU0= 9 | github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= 10 | github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= 11 | github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= 12 | github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= 13 | github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY= 14 | github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= 15 | github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ= 16 | github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28= 17 | github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= 18 | github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= 19 | github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4= 20 | github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= 21 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 22 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 23 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 24 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 25 | github.com/gdamore/tcell/v2 v2.7.1 h1:TiCcmpWHiAU7F0rA2I3S2Y4mmLmO9KHxJ7E1QhYzQbc= 26 | github.com/gdamore/tcell/v2 v2.7.1/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= 27 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 28 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 29 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 30 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 31 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 32 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 33 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 34 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 35 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 36 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 37 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 38 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 39 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 40 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 41 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 42 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 43 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 44 | github.com/rivo/tview v0.0.0-20250501113434-0c592cd31026 h1:ij8h8B3psk3LdMlqkfPTKIzeGzTaZLOiyplILMlxPAM= 45 | github.com/rivo/tview v0.0.0-20250501113434-0c592cd31026/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= 46 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 47 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 48 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 49 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 50 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 51 | github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= 52 | github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 53 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 54 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 55 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 56 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 57 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 58 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 59 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 60 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 61 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 62 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 63 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 64 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 65 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 66 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 67 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 68 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 69 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 70 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 72 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 75 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 78 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 79 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 80 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 81 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 82 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 83 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 84 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 85 | golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= 86 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 87 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 88 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 89 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 90 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 91 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 92 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 93 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 94 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 95 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 96 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 97 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 98 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 99 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 100 | -------------------------------------------------------------------------------- /guides/arch_kde/images/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probeldev/fastlauncher/2fc581307b70b5325477576a83d5dc5c6207870b/guides/arch_kde/images/add.png -------------------------------------------------------------------------------- /guides/arch_kde/images/add2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probeldev/fastlauncher/2fc581307b70b5325477576a83d5dc5c6207870b/guides/arch_kde/images/add2.png -------------------------------------------------------------------------------- /guides/arch_kde/images/add3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probeldev/fastlauncher/2fc581307b70b5325477576a83d5dc5c6207870b/guides/arch_kde/images/add3.png -------------------------------------------------------------------------------- /guides/arch_kde/images/flatpaklist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probeldev/fastlauncher/2fc581307b70b5325477576a83d5dc5c6207870b/guides/arch_kde/images/flatpaklist.png -------------------------------------------------------------------------------- /guides/arch_kde/images/hotkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probeldev/fastlauncher/2fc581307b70b5325477576a83d5dc5c6207870b/guides/arch_kde/images/hotkey.png -------------------------------------------------------------------------------- /guides/arch_kde/images/install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probeldev/fastlauncher/2fc581307b70b5325477576a83d5dc5c6207870b/guides/arch_kde/images/install.png -------------------------------------------------------------------------------- /guides/arch_kde/images/system_parameters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probeldev/fastlauncher/2fc581307b70b5325477576a83d5dc5c6207870b/guides/arch_kde/images/system_parameters.png -------------------------------------------------------------------------------- /guides/arch_kde/readme.md: -------------------------------------------------------------------------------- 1 | ## Installation for Arch KDE 2 | 3 | System parameters 4 | 5 |
6 | 7 | ![system_parameters](images/system_parameters.png) 8 | 9 |
10 | 11 | 12 | 13 | 1. Install Golang. Open the terminal (Konsole) and insert the following command: 14 | 15 | ```bash 16 | sudo pacman -S go 17 | ``` 18 | Check if it's installed: 19 | ``` 20 | go version 21 | ``` 22 | 23 | If's Ok, you'll see: 24 | `go version go1.23.4 linux/amd64` 25 | 26 | 27 | 2. Install the launcher 28 | 29 | ``` 30 | go install github.com/probeldev/fastlauncher@latest 31 | ``` 32 | 33 |
34 | 35 | ![system_parameters](images/install.png) 36 | 37 |
38 | 39 | 3. Creating Your Configuration File 40 | 41 | 1. Create a new folder on your computer called `fast-launcher` 42 | For example: 43 | 44 | ```bash 45 | mkdir -p ~/scripts/fast-launcher 46 | ``` 47 | 2. Inside this folder, create a new text file named `fast-launcher` 48 | 49 | ```bash 50 | touch cfg.json 51 | ``` 52 | 53 | 3. Open file 54 | 55 | ```bash 56 | nano ~/script/fast-launcher/cfg.json 57 | ``` 58 | 3. Copy and paste this template into the file: 59 | 60 | 61 | ``` 62 | [ 63 | 64 | { 65 | "title": "PROGRAM_NAME", 66 | "description": "DESCRIPTION", 67 | "command": "CALLING_COMMAND" 68 | }, 69 | 70 | { 71 | "title": "Krita", 72 | "description": "Digital painting", 73 | "command": "flatpak run org.kde.krita" 74 | }, 75 | 76 | { 77 | "title": "Kate", 78 | "description": "text editor", 79 | "command": "kate" 80 | } 81 | 82 | ] 83 | 84 | ``` 85 | - Replace PROGRAM_NAME with the program name 86 | - Replace DESCRIPTION with your own description program 87 | - Replace CALLING_COMMAND with command that used to call the program 88 | 89 | If the program is installed with Flatpak, use this template `flatpak run APPLICATION_ID`. 90 | 91 | Find APPLICATION_ID this way: 92 | 93 | ``` 94 | flatpak list 95 | ``` 96 | 97 | APPLICATION_ID is in the second column 98 | 99 |
100 | 101 | ![system_parameters](images/flatpaklist.png) 102 | 103 |
104 | 105 | 106 | ## USAGE 107 | 108 | ### with shortcuts 109 | 110 | Add Shorcuts to call the launcher 111 | 112 |
113 | 114 | ![system_parameters](images/hotkey.png) 115 | 116 |
117 | 118 | 1. System settings -> Keyboard -> Shortcuts -> Add New -> Application... 119 | 120 | 2. Enter the following in the field and save: 121 | 122 | ``` 123 | konsole --fullscreen -e 'bash -c "~/go/bin/fastlauncher --config ~/script/fast-launcher/cfg.json" ' 124 | ``` 125 |
126 | 127 | ![system_parameters](images/add.png) 128 | 129 |
130 | 131 | 3. Add shortcuts and apply: 132 | 133 |
134 | 135 | ![system_parameters](images/add2.png) 136 | 137 |
138 | 139 |
140 | 141 | ![system_parameters](images/add3.png) 142 | 143 |
144 | 145 | ### with terminal 146 | 147 | 1. Open the terminal 148 | 149 | 150 | ```bash 151 | echo "export PATH=\$PATH:~/go/bin" >> ~/.bashrc 152 | ``` 153 | 154 | 2. Close and reopen the terminal 155 | 156 | 157 | ```bash 158 | fastlauncher --config ~/script/fast-launcher/cfg.json 159 | ``` 160 | 161 | 162 | If you get an error `deserialization json`, repeat step 3. `Creating Your Configuration File` 163 | 164 | ## Unistallation 165 | 166 | ``` 167 | rm ~/go/bin/fastlauncher 168 | ``` 169 | -------------------------------------------------------------------------------- /guides/screenshots/logout-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probeldev/fastlauncher/2fc581307b70b5325477576a83d5dc5c6207870b/guides/screenshots/logout-manager.png -------------------------------------------------------------------------------- /guides/screenshots/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probeldev/fastlauncher/2fc581307b70b5325477576a83d5dc5c6207870b/guides/screenshots/main.png -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | baselog "log" 5 | "time" 6 | ) 7 | 8 | func Println(params ...any) { 9 | baselog.Println(params...) 10 | time.Sleep(time.Second * 15) 11 | } 12 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/probeldev/fastlauncher/log" 7 | "github.com/probeldev/fastlauncher/mode" 8 | "github.com/probeldev/fastlauncher/ui" 9 | ) 10 | 11 | func main() { 12 | 13 | cfgPath := flag.String("config", "", "Path to config file") 14 | flag.Parse() 15 | 16 | if cfgPath != nil && *cfgPath != "" { 17 | ca := mode.ConfigMode{} 18 | apps := ca.GetFromFile(*cfgPath) 19 | ui.StartUi(apps) 20 | return 21 | } 22 | 23 | oa := mode.OsMode{} 24 | apps, err := oa.GetAll() 25 | if err != nil { 26 | // TODO 27 | log.Println(err) 28 | } 29 | 30 | ui.StartUi(apps) 31 | 32 | } 33 | -------------------------------------------------------------------------------- /mode/configSourceApps.go: -------------------------------------------------------------------------------- 1 | package mode 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/probeldev/fastlauncher/log" 7 | "github.com/probeldev/fastlauncher/model" 8 | ) 9 | 10 | type ConfigMode struct{} 11 | 12 | func (cw *ConfigMode) GetFromFile(cfgPath string) []model.App { 13 | fn := "ConfigMode:GetFromFile" 14 | 15 | if cfgPath == "" { 16 | log.Println(fn, "cfg path not found") 17 | return nil 18 | } 19 | 20 | file, err := os.ReadFile(cfgPath) 21 | if err != nil { 22 | log.Println(fn, err) 23 | return nil 24 | } 25 | 26 | response, err := model.NewAppListFromJson(file) 27 | if err != nil { 28 | log.Println(fn, err) 29 | return nil 30 | } 31 | 32 | return response 33 | } 34 | -------------------------------------------------------------------------------- /mode/osSourceApp.go: -------------------------------------------------------------------------------- 1 | package mode 2 | 3 | import ( 4 | "github.com/probeldev/fastlauncher/model" 5 | "github.com/probeldev/fastlauncher/pkg/finderallapps" 6 | ) 7 | 8 | type OsMode struct{} 9 | 10 | func (o *OsMode) GetAll() ([]model.App, error) { 11 | os := o.getOs() 12 | finder, err := finderallapps.GetFinder(os) 13 | 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | osApps, err := finder.GetAllApp() 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | apps := []model.App{} 24 | 25 | for _, oa := range osApps { 26 | apps = append(apps, model.App{ 27 | Title: oa.Name, 28 | Description: oa.Description, 29 | Command: oa.Command, 30 | Keywords: oa.Keywords, 31 | }) 32 | } 33 | 34 | return apps, nil 35 | 36 | } 37 | 38 | func (o *OsMode) getOs() string { 39 | // TODO change 40 | return finderallapps.OsLinux 41 | } 42 | -------------------------------------------------------------------------------- /model/app.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type App struct { 8 | Title string `json:"title"` 9 | Description string `json:"description"` 10 | Command string `json:"command"` 11 | Keywords string `json:"keywords"` 12 | } 13 | 14 | func NewAppListFromJson( 15 | j []byte, 16 | ) ( 17 | []App, 18 | error, 19 | ) { 20 | response := []App{} 21 | 22 | if err := json.Unmarshal(j, &response); err != nil { 23 | return response, err 24 | } 25 | 26 | return response, nil 27 | } 28 | 29 | func AppListToJson(apps []App) ( 30 | []byte, 31 | error, 32 | ) { 33 | return json.Marshal(apps) 34 | } 35 | -------------------------------------------------------------------------------- /package.nix: -------------------------------------------------------------------------------- 1 | { 2 | buildGoModule 3 | }: 4 | buildGoModule { 5 | name = "fastlauncher"; 6 | src = ./.; 7 | vendorHash = "sha256-a39ZeJcyt+1iqnKbhcBiLScP5InAJa0jsYB0ZXlpCVM="; 8 | } 9 | -------------------------------------------------------------------------------- /pkg/apprunner/main.go: -------------------------------------------------------------------------------- 1 | package apprunner 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/probeldev/fastlauncher/pkg/apprunner/runner" 7 | ) 8 | 9 | const ( 10 | OsLinux = "Linux" 11 | OsMacOs = "MacOs" 12 | OsWindows = "Windows" 13 | ) 14 | 15 | type AppRunnerInterface interface { 16 | Run(string) error 17 | } 18 | 19 | func GetAppRunner(operatingSystem string) (AppRunnerInterface, error) { 20 | 21 | switch operatingSystem { 22 | case OsLinux: 23 | linuxAppRunner := runner.GetLinuxAppRunner() 24 | return &linuxAppRunner, nil 25 | case OsMacOs: 26 | macOsAppRunner := runner.GetMacOsAppRunner() 27 | return &macOsAppRunner, nil 28 | case OsWindows: 29 | windowsAppRunner := runner.GetWindowsAppRunner() 30 | return &windowsAppRunner, nil 31 | } 32 | 33 | return nil, errors.New("Operating System not suport") 34 | } 35 | -------------------------------------------------------------------------------- /pkg/apprunner/runner/linuxAppRunner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "os/exec" 5 | "syscall" 6 | ) 7 | 8 | type linuxAppRunner struct{} 9 | 10 | func GetLinuxAppRunner() linuxAppRunner { 11 | f := linuxAppRunner{} 12 | 13 | return f 14 | } 15 | 16 | func (lr *linuxAppRunner) Run(command string) error { 17 | cmd := exec.Command("bash", "-c", command) 18 | cmd.SysProcAttr = &syscall.SysProcAttr{ 19 | Setpgid: true, 20 | } 21 | err := cmd.Start() 22 | 23 | return err 24 | } 25 | -------------------------------------------------------------------------------- /pkg/apprunner/runner/macOsAppRunner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import "errors" 4 | 5 | type macOsAppRunner struct{} 6 | 7 | func GetMacOsAppRunner() macOsAppRunner { 8 | f := macOsAppRunner{} 9 | 10 | return f 11 | } 12 | 13 | func (lr *macOsAppRunner) Run(command string) error { 14 | // TODO: 15 | 16 | return errors.New("MacOs is not suport") 17 | } 18 | -------------------------------------------------------------------------------- /pkg/apprunner/runner/windowsAppRunner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "os/exec" 5 | "syscall" 6 | ) 7 | 8 | type windowsAppRunner struct{} 9 | 10 | func GetWindowsAppRunner() windowsAppRunner { 11 | f := windowsAppRunner{} 12 | 13 | return f 14 | } 15 | 16 | func (lr *windowsAppRunner) Run(command string) error { 17 | cmd := exec.Command("bash", "-c", command) 18 | cmd.SysProcAttr = &syscall.SysProcAttr{ 19 | Setpgid: true, 20 | } 21 | err := cmd.Start() 22 | 23 | return err 24 | } 25 | -------------------------------------------------------------------------------- /pkg/finderallapps/finder/linuxFinder.go: -------------------------------------------------------------------------------- 1 | package finder 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/probeldev/fastlauncher/pkg/finderallapps/model" 8 | "github.com/probeldev/fastlauncher/pkg/parsedesktopfile" 9 | ) 10 | 11 | type linuxFinder struct{} 12 | 13 | func GetLinuxFinder() linuxFinder { 14 | f := linuxFinder{} 15 | 16 | return f 17 | } 18 | 19 | func (lf *linuxFinder) GetAllApp() ([]model.App, error) { 20 | apps := []model.App{} 21 | 22 | foldersApps := lf.GetAllAppsFolders() 23 | 24 | for _, folder := range foldersApps { 25 | appsFromFolder, err := lf.GetFromFolder(folder) 26 | if err != nil { 27 | return apps, err 28 | } 29 | 30 | apps = append(apps, appsFromFolder...) 31 | } 32 | 33 | return apps, nil 34 | } 35 | 36 | func (lf *linuxFinder) GetFromFolder(folder string) ([]model.App, error) { 37 | 38 | files, err := lf.getAllDesktopListFromFolder(folder) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | apps := []model.App{} 44 | 45 | parser := parsedesktopfile.GetParseDesktopFile() 46 | for _, file := range files { 47 | desktop, err := parser.ParseFromFile(file) 48 | if err != nil { 49 | return apps, err 50 | } 51 | 52 | if desktop.Name == "" || desktop.Exec == "" { 53 | continue 54 | } 55 | 56 | apps = append(apps, model.App{ 57 | Name: desktop.Name, 58 | Command: desktop.Exec, 59 | Description: desktop.Comment, 60 | Keywords: desktop.Keywords, 61 | }) 62 | } 63 | 64 | return apps, nil 65 | } 66 | 67 | func (lf *linuxFinder) getAllDesktopListFromFolder(folder string) ( 68 | []string, 69 | error, 70 | ) { 71 | desktopFiles := []string{} 72 | entries, err := os.ReadDir(folder) 73 | if err != nil { 74 | return desktopFiles, nil 75 | } 76 | for _, e := range entries { 77 | name := e.Name() 78 | 79 | if strings.HasSuffix(name, ".desktop") { 80 | desktopFiles = append(desktopFiles, folder+name) 81 | } 82 | } 83 | 84 | return desktopFiles, nil 85 | } 86 | 87 | func (lf *linuxFinder) GetAllAppsFolders() []string { 88 | folders := []string{} 89 | 90 | foldersFromXdg := lf.GetAppFoldersFromXdg() 91 | folders = append(folders, foldersFromXdg...) 92 | 93 | foldersDefault := lf.GetDefaultAppFolders() 94 | folders = append(folders, foldersDefault...) 95 | 96 | folders = lf.RemoveDuplicateAppFolders(folders) 97 | 98 | return folders 99 | } 100 | 101 | func (lf *linuxFinder) GetAppFoldersFromXdg() []string { 102 | xdg := os.Getenv("XDG_DATA_DIRS") 103 | 104 | folders := strings.Split(xdg, ":") 105 | 106 | for i := 0; i < len(folders); i++ { 107 | folders[i] = folders[i] + "/applications/" 108 | } 109 | 110 | return folders 111 | } 112 | 113 | func (lf *linuxFinder) GetDefaultAppFolders() []string { 114 | return []string{ 115 | "/usr/share/applications/", 116 | "/usr/local/share/applications/", 117 | "/var/lib/flatpak/exports/share/applications/", 118 | "~/.local/share/flatpak/exports/share/application/", 119 | } 120 | } 121 | 122 | func (lf *linuxFinder) RemoveDuplicateAppFolders(folders []string) []string { 123 | mapFolders := map[string]bool{} 124 | 125 | responseFolder := []string{} 126 | for _, folder := range folders { 127 | if _, ok := mapFolders[folder]; ok { 128 | continue 129 | } 130 | 131 | mapFolders[folder] = true 132 | responseFolder = append(responseFolder, folder) 133 | } 134 | 135 | return responseFolder 136 | } 137 | -------------------------------------------------------------------------------- /pkg/finderallapps/finder/macOsFinder.go: -------------------------------------------------------------------------------- 1 | package finder 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/probeldev/fastlauncher/pkg/finderallapps/model" 7 | ) 8 | 9 | type macOsFinder struct{} 10 | 11 | func GetMacOsFinder() macOsFinder { 12 | f := macOsFinder{} 13 | 14 | return f 15 | } 16 | 17 | func (mf *macOsFinder) GetAllApp() ([]model.App, error) { 18 | apps := []model.App{} 19 | // TODO: 20 | 21 | return apps, errors.New("MacOs is not suport") 22 | } 23 | -------------------------------------------------------------------------------- /pkg/finderallapps/finder/windowsFinder.go: -------------------------------------------------------------------------------- 1 | package finder 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/probeldev/fastlauncher/pkg/finderallapps/model" 7 | ) 8 | 9 | type windowsFinder struct{} 10 | 11 | func GetWindowsFinder() windowsFinder { 12 | f := windowsFinder{} 13 | 14 | return f 15 | } 16 | 17 | func (lf *windowsFinder) GetAllApp() ([]model.App, error) { 18 | apps := []model.App{} 19 | // TODO: 20 | 21 | return apps, errors.New("Windows is not suport") 22 | } 23 | -------------------------------------------------------------------------------- /pkg/finderallapps/finder_test/linuxFinder_test.go: -------------------------------------------------------------------------------- 1 | package findertest_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/probeldev/fastlauncher/pkg/finderallapps/finder" 7 | ) 8 | 9 | func TestGetFromFolderNotFoundFolder(t *testing.T) { 10 | folder := "/not/found/folder" 11 | 12 | linuxFinder := finder.GetLinuxFinder() 13 | apps, err := linuxFinder.GetFromFolder(folder) 14 | 15 | if err != nil { 16 | t.Error(err.Error()) 17 | } 18 | 19 | if len(apps) != 0 { 20 | t.Error("len(apps)!=0") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/finderallapps/main.go: -------------------------------------------------------------------------------- 1 | package finderallapps 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/probeldev/fastlauncher/pkg/finderallapps/finder" 7 | "github.com/probeldev/fastlauncher/pkg/finderallapps/model" 8 | ) 9 | 10 | const ( 11 | OsLinux = "Linux" 12 | OsMacOs = "MacOs" 13 | OsWindows = "Windows" 14 | ) 15 | 16 | type FinderInterface interface { 17 | GetAllApp() ([]model.App, error) 18 | } 19 | 20 | func GetFinder(operatingSystem string) (FinderInterface, error) { 21 | 22 | switch operatingSystem { 23 | case OsLinux: 24 | linuxFinder := finder.GetLinuxFinder() 25 | return &linuxFinder, nil 26 | case OsMacOs: 27 | macOsFinder := finder.GetMacOsFinder() 28 | return &macOsFinder, nil 29 | case OsWindows: 30 | windowsFinder := finder.GetWindowsFinder() 31 | return &windowsFinder, nil 32 | } 33 | 34 | return nil, errors.New("Operating System not suport") 35 | } 36 | -------------------------------------------------------------------------------- /pkg/finderallapps/model/app.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type App struct { 4 | Name string 5 | Description string 6 | Command string 7 | Keywords string 8 | } 9 | -------------------------------------------------------------------------------- /pkg/parsedesktopfile/main.go: -------------------------------------------------------------------------------- 1 | package parsedesktopfile 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strings" 7 | 8 | "github.com/probeldev/fastlauncher/pkg/parsedesktopfile/model" 9 | ) 10 | 11 | type parseDesktopFile struct{} 12 | 13 | func GetParseDesktopFile() parseDesktopFile { 14 | p := parseDesktopFile{} 15 | 16 | return p 17 | } 18 | 19 | func (p *parseDesktopFile) ParseFromString(desktop string) ( 20 | model.Desktop, 21 | error, 22 | ) { 23 | return p.parse(desktop) 24 | } 25 | 26 | func (p *parseDesktopFile) ParseFromFile(desktopFile string) ( 27 | model.Desktop, 28 | error, 29 | ) { 30 | response := model.Desktop{} 31 | body, err := os.ReadFile(desktopFile) 32 | 33 | if err != nil { 34 | return response, err 35 | } 36 | 37 | desktop, err := p.parse(string(body)) 38 | if err != nil { 39 | return response, 40 | errors.New("Error parse, file: " + desktopFile + " error: " + err.Error()) 41 | } 42 | 43 | return desktop, err 44 | } 45 | 46 | func (p *parseDesktopFile) parse(body string) ( 47 | model.Desktop, 48 | error, 49 | ) { 50 | response := model.Desktop{} 51 | 52 | mapLines, err := p.GetDesktopEntry(body) 53 | if err != nil { 54 | return response, err 55 | } 56 | 57 | if exec, ok := mapLines["Exec"]; ok { 58 | response.Exec = strings.Trim(exec, " ") 59 | } 60 | if name, ok := mapLines["Name"]; ok { 61 | response.Name = strings.Trim(name, " ") 62 | } 63 | if typeDesk, ok := mapLines["Type"]; ok { 64 | response.Type = typeDesk 65 | } 66 | if comment, ok := mapLines["Comment"]; ok { 67 | response.Comment = comment 68 | } 69 | if keywords, ok := mapLines["Keywords"]; ok { 70 | response.Keywords = keywords 71 | } 72 | if terminal, ok := mapLines["Terminal"]; ok { 73 | response.Terminal = terminal == "true" 74 | } 75 | 76 | return response, nil 77 | } 78 | 79 | func (p *parseDesktopFile) GetDesktopEntry(body string) (map[string]string, error) { 80 | 81 | bodyLines := strings.Split(body, "\n") 82 | 83 | isSetDesktopEntry := true 84 | responseMap := map[string]string{} 85 | for _, line := range bodyLines { 86 | line = strings.Trim(line, " ") 87 | line = strings.Trim(line, "\t") 88 | if line == "" { 89 | continue 90 | } 91 | 92 | if strings.HasPrefix(line, "#") { 93 | continue 94 | } 95 | 96 | lineArr := strings.Split(line, "=") 97 | 98 | if strings.HasPrefix(line, "[Desktop Entry]") { 99 | isSetDesktopEntry = true 100 | continue 101 | } else if strings.HasPrefix(line, "[") { 102 | if isSetDesktopEntry { 103 | return responseMap, nil 104 | } 105 | continue 106 | } 107 | 108 | if isSetDesktopEntry { 109 | if len(lineArr) < 2 { 110 | return responseMap, errors.New("Parse error, line: " + line) 111 | } else if len(lineArr) == 2 { 112 | responseMap[lineArr[0]] = lineArr[1] 113 | } else { 114 | value := lineArr[1] 115 | 116 | for i := 2; i < len(lineArr); i++ { 117 | value += "=" + lineArr[i] 118 | } 119 | responseMap[lineArr[0]] = value 120 | } 121 | } 122 | } 123 | 124 | if !isSetDesktopEntry { 125 | return responseMap, errors.New("Desktop Entry not found") 126 | } 127 | 128 | return responseMap, nil 129 | } 130 | -------------------------------------------------------------------------------- /pkg/parsedesktopfile/model/desktopfile.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Desktop struct { 4 | Type string 5 | Exec string 6 | 7 | Terminal bool 8 | Keywords string 9 | 10 | Name string 11 | Comment string 12 | } 13 | -------------------------------------------------------------------------------- /pkg/parsedesktopfile/test/main_test.go: -------------------------------------------------------------------------------- 1 | package parsedesktopfile_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/probeldev/fastlauncher/pkg/parsedesktopfile" 7 | "github.com/probeldev/fastlauncher/pkg/parsedesktopfile/model" 8 | ) 9 | 10 | func TestParseFromString(t *testing.T) { 11 | body := ` 12 | [Desktop Entry] 13 | Type=Application 14 | Exec=foot 15 | Icon=foot 16 | Terminal=false 17 | Categories=System;TerminalEmulator; 18 | Keywords=shell;prompt;command;commandline; 19 | 20 | # comment 21 | 22 | Name=ImageMagick (color depth=q16) 23 | GenericName=Terminal 24 | Comment=A wayland native terminal emulator 25 | ` 26 | expected := model.Desktop{ 27 | Type: "Application", 28 | Exec: "foot", 29 | 30 | Terminal: false, 31 | Keywords: "shell;prompt;command;commandline;", 32 | 33 | Name: "ImageMagick (color depth=q16)", 34 | Comment: "A wayland native terminal emulator", 35 | } 36 | 37 | parse := parsedesktopfile.GetParseDesktopFile() 38 | 39 | actual, err := parse.ParseFromString(body) 40 | 41 | if err != nil { 42 | t.Error(err.Error()) 43 | } 44 | 45 | if actual != expected { 46 | t.Error("not equal", actual, parse) 47 | } 48 | 49 | } 50 | 51 | func TestGetDesktopEntry(t *testing.T) { 52 | body := ` 53 | [Desktop Entry] 54 | Version=1.0 55 | Terminal=false 56 | NoDisplay=false 57 | Icon=org.libreoffice.LibreOffice.startcenter 58 | Type=Application 59 | 60 | [Desktop Action Writer] 61 | Name=Writer 62 | Exec=/usr/bin/flatpak run --branch=stable --arch=x86_64 --command=libreoffice org.libreoffice.LibreOffice --writer 63 | 64 | [Desktop Action Calc] 65 | Name=Calc 66 | Exec=/usr/bin/flatpak run --branch=stable --arch=x86_64 --command=libreoffice org.libreoffice.LibreOffice --calc 67 | 68 | [Desktop Action Impress] 69 | Name=Impress 70 | Exec=/usr/bin/flatpak run --branch=stable --arch=x86_64 --command=libreoffice org.libreoffice.LibreOffice --impress 71 | 72 | [Desktop Action Draw] 73 | Name=Draw 74 | Exec=/usr/bin/flatpak run --branch=stable --arch=x86_64 --command=libreoffice org.libreoffice.LibreOffice --draw 75 | ` 76 | 77 | expected := map[string]string{} 78 | expected["Version"] = "1.0" 79 | expected["Terminal"] = "false" 80 | expected["NoDisplay"] = "false" 81 | expected["Icon"] = "org.libreoffice.LibreOffice.startcenter" 82 | expected["Type"] = "Application" 83 | 84 | parser := parsedesktopfile.GetParseDesktopFile() 85 | 86 | actual, err := parser.GetDesktopEntry(body) 87 | 88 | if err != nil { 89 | t.Error(err.Error()) 90 | return 91 | } 92 | 93 | if len(expected) != len(actual) { 94 | t.Error("not equal", actual, expected) 95 | return 96 | } 97 | 98 | for key, value := range expected { 99 | valueActual, ok := actual[key] 100 | if !ok { 101 | t.Error("not equal", actual, expected) 102 | return 103 | } 104 | 105 | if value != valueActual { 106 | t.Error("not equal", actual, expected) 107 | return 108 | } 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /ui/model.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/probeldev/fastlauncher/model" 10 | "github.com/probeldev/fastlauncher/pkg/apprunner" 11 | 12 | "github.com/gdamore/tcell/v2" 13 | "github.com/rivo/tview" 14 | ) 15 | 16 | type item struct { 17 | title string 18 | command string 19 | } 20 | 21 | type uiModel struct { 22 | items []item 23 | list *tview.List 24 | input *tview.InputField 25 | pages *tview.TextView 26 | currentPage int 27 | itemsPerPage int 28 | currentItem int 29 | lastWidth int 30 | lastHeight int 31 | } 32 | 33 | // filterItems фильтрует элементы по запросу 34 | func (m *uiModel) filterItems(query string) []item { 35 | if query == "" { 36 | return m.items 37 | } 38 | var filtered []item 39 | query = strings.ToLower(query) 40 | for _, item := range m.items { 41 | if strings.Contains(strings.ToLower(item.title), query) { 42 | filtered = append(filtered, item) 43 | } 44 | } 45 | return filtered 46 | } 47 | 48 | // updateList обновляет содержимое списка 49 | func (m *uiModel) updateList() { 50 | current := m.list.GetCurrentItem() // Сохраняем текущий элемент 51 | m.list.Clear() 52 | filtered := m.filterItems(m.input.GetText()) 53 | totalItems := len(filtered) 54 | totalPages := (totalItems + m.itemsPerPage - 1) / m.itemsPerPage 55 | if m.currentPage >= totalPages { 56 | m.currentPage = totalPages - 1 57 | } 58 | if m.currentPage < 0 { 59 | m.currentPage = 0 60 | } 61 | 62 | start := m.currentPage * m.itemsPerPage 63 | end := start + m.itemsPerPage 64 | if end > totalItems { 65 | end = totalItems 66 | } 67 | 68 | for i := start; i < end; i++ { 69 | m.list.AddItem(filtered[i].title, "", 0, nil) 70 | } 71 | 72 | // Восстанавливаем текущий элемент, если он в пределах нового списка 73 | if current >= 0 && current < end-start { 74 | m.list.SetCurrentItem(current) 75 | } else if m.list.GetItemCount() > 0 { 76 | m.list.SetCurrentItem(0) 77 | } 78 | 79 | // Обновляем индикатор пагинации 80 | pageText := fmt.Sprintf("Страница %d/%d (←/→)", m.currentPage+1, totalPages) 81 | m.pages.SetText(pageText) 82 | } 83 | 84 | // updateItemsPerPage вычисляет количество элементов на странице 85 | func (m *uiModel) updateItemsPerPage(height int) { 86 | // Высота всего экрана минус фиксированные элементы (поле ввода, пагинация и padding) 87 | m.itemsPerPage = height - 4 // 1 строка для ввода, 1 строка для пагинации, 2 строки для padding сверху и снизу 88 | if m.itemsPerPage < 1 { 89 | m.itemsPerPage = 1 90 | } 91 | } 92 | 93 | func StartUi(apps []model.App) { 94 | // Настраиваем стили tview для использования цветов терминала 95 | tview.Styles = tview.Theme{ 96 | PrimitiveBackgroundColor: tcell.ColorDefault, 97 | ContrastBackgroundColor: tcell.ColorDefault, 98 | MoreContrastBackgroundColor: tcell.ColorDefault, 99 | BorderColor: tcell.ColorDefault, 100 | TitleColor: tcell.ColorDefault, 101 | GraphicsColor: tcell.ColorDefault, 102 | PrimaryTextColor: tcell.ColorDefault, 103 | SecondaryTextColor: tcell.ColorDefault, 104 | TertiaryTextColor: tcell.ColorDefault, 105 | InverseTextColor: tcell.ColorDefault, 106 | ContrastSecondaryTextColor: tcell.ColorDefault, 107 | } 108 | 109 | // Создаём модель 110 | m := &uiModel{ 111 | items: make([]item, len(apps)), 112 | currentPage: 0, 113 | currentItem: 0, 114 | } 115 | 116 | // Заполняем элементы 117 | for i, a := range apps { 118 | m.items[i] = item{ 119 | title: a.Title, 120 | command: a.Command, 121 | } 122 | } 123 | 124 | // Создаём приложение 125 | app := tview.NewApplication() 126 | 127 | // Создаём список 128 | m.list = tview.NewList(). 129 | ShowSecondaryText(false). 130 | SetMainTextStyle(tcell.StyleDefault.Foreground(tcell.ColorDefault).Background(tcell.ColorDefault)). 131 | SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorDefault).Background(tcell.ColorDefault).Reverse(true)). 132 | SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) { 133 | // Запускаем команду 134 | actualIndex := m.currentPage*m.itemsPerPage + index 135 | filtered := m.filterItems(m.input.GetText()) 136 | if actualIndex < len(filtered) { 137 | runner, err := apprunner.GetAppRunner(apprunner.OsLinux) 138 | if err != nil { 139 | log.Println("GetAppRunner error:", err) 140 | return 141 | } 142 | err = runner.Run(filtered[actualIndex].command) 143 | if err != nil { 144 | log.Println("Run error:", err) 145 | return 146 | } 147 | } else { 148 | log.Println("Invalid index:", actualIndex, "Filtered length:", len(filtered)) 149 | } 150 | app.Stop() 151 | }) 152 | 153 | // Создаём поле ввода с рамкой 154 | m.input = tview.NewInputField() 155 | m.input.SetLabel("Поиск: "). 156 | SetLabelStyle(tcell.StyleDefault.Foreground(tcell.ColorDefault)). 157 | SetFieldStyle(tcell.StyleDefault.Foreground(tcell.ColorDefault).Background(tcell.ColorDefault)). 158 | SetBorder(true). 159 | SetBorderStyle(tcell.StyleDefault.Foreground(tcell.ColorDefault)) 160 | m.input.SetChangedFunc(func(text string) { 161 | m.currentPage = 0 // Сбрасываем страницу при изменении поиска 162 | m.updateList() 163 | }) 164 | 165 | // Создаём индикатор пагинации 166 | m.pages = tview.NewTextView(). 167 | SetTextAlign(tview.AlignLeft). 168 | SetTextStyle(tcell.StyleDefault.Foreground(tcell.ColorDefault).Background(tcell.ColorDefault)) 169 | 170 | // Компоновка с padding 171 | innerFlex := tview.NewFlex().SetDirection(tview.FlexRow). 172 | AddItem(m.input, 3, 1, true). // Увеличиваем высоту для рамки (1 строка текста + 2 строки рамки) 173 | AddItem(m.list, 0, 1, false). 174 | AddItem(m.pages, 1, 1, false) 175 | 176 | // Добавляем padding (1 строка сверху и снизу, 2 столбца слева и справа) 177 | outerFlex := tview.NewFlex().SetDirection(tview.FlexRow). 178 | AddItem(tview.NewBox(), 1, 0, false). // Padding сверху 179 | AddItem(tview.NewFlex().SetDirection(tview.FlexColumn). 180 | AddItem(tview.NewBox(), 2, 0, false). // Padding слева 181 | AddItem(innerFlex, 0, 1, true). 182 | AddItem(tview.NewBox(), 2, 0, false), // Padding справа 183 | 0, 1, true). 184 | AddItem(tview.NewBox(), 1, 0, false) // Padding снизу 185 | 186 | // Настраиваем обработку изменения размера через SetDrawFunc 187 | outerFlex.SetDrawFunc(func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) { 188 | // Проверяем, изменились ли размеры 189 | if m.lastWidth != width || m.lastHeight != height { 190 | m.lastWidth = width 191 | m.lastHeight = height 192 | m.updateItemsPerPage(height) // Используем полную высоту экрана 193 | m.updateList() 194 | } 195 | return x, y, width, height 196 | }) 197 | 198 | // Настраиваем клавиши 199 | app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 200 | switch event.Key() { 201 | case tcell.KeyLeft: 202 | if m.currentPage > 0 { 203 | m.currentPage-- 204 | m.updateList() 205 | } 206 | return nil 207 | case tcell.KeyRight: 208 | filtered := m.filterItems(m.input.GetText()) 209 | totalPages := (len(filtered) + m.itemsPerPage - 1) / m.itemsPerPage 210 | if m.currentPage < totalPages-1 { 211 | m.currentPage++ 212 | m.updateList() 213 | } 214 | return nil 215 | case tcell.KeyUp, tcell.KeyDown: 216 | // Обрабатываем навигацию напрямую 217 | current := m.list.GetCurrentItem() 218 | if event.Key() == tcell.KeyUp { 219 | if current > 0 { 220 | m.list.SetCurrentItem(current - 1) 221 | } 222 | } else if event.Key() == tcell.KeyDown { 223 | if current < m.list.GetItemCount()-1 { 224 | m.list.SetCurrentItem(current + 1) 225 | } 226 | } 227 | return nil 228 | case tcell.KeyCtrlC, tcell.KeyEscape: 229 | app.Stop() 230 | return nil 231 | case tcell.KeyEnter: 232 | if m.list.GetItemCount() == 0 { 233 | return nil 234 | } 235 | if app.GetFocus() == m.input { 236 | // Переключаем фокус на список и выбираем первый элемент 237 | app.SetFocus(m.list) 238 | if m.list.GetItemCount() > 0 { 239 | current := m.list.GetCurrentItem() 240 | if current < 0 { 241 | current = 0 242 | m.list.SetCurrentItem(current) 243 | } 244 | mainText, secondaryText := m.list.GetItemText(current) 245 | m.list.GetSelectedFunc()(current, mainText, secondaryText, 0) 246 | } 247 | return nil 248 | } 249 | // Позволяем списку обработать Enter 250 | return event 251 | } 252 | return event 253 | }) 254 | 255 | // Инициализируем itemsPerPage и список 256 | m.itemsPerPage = 10 // Начальное значение, будет обновлено при первом вызове SetDrawFunc 257 | m.updateList() 258 | 259 | // Запускаем приложение 260 | if err := app.SetRoot(outerFlex, true).Run(); err != nil { 261 | log.Println("Error running program:", err) 262 | os.Exit(1) 263 | } 264 | } 265 | --------------------------------------------------------------------------------