├── .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 | 
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 | 
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 | 
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 | 
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 | 
102 |
103 |
104 |
105 |
106 | ## USAGE
107 |
108 | ### with shortcuts
109 |
110 | Add Shorcuts to call the launcher
111 |
112 |
113 |
114 | 
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 | 
128 |
129 |
130 |
131 | 3. Add shortcuts and apply:
132 |
133 |
134 |
135 | 
136 |
137 |
138 |
139 |
140 |
141 | 
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 |
--------------------------------------------------------------------------------