├── .gitignore ├── LICENSE.md ├── app ├── config.go ├── dbus.go ├── utils.go └── wifi.go ├── go.mod ├── go.sum ├── main.go └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | go.mod 2 | go.sum 3 | ./bin/ewc 4 | .saved_networks.json 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ``` 2 | MIT License 3 | 4 | Copyright (c) [2025] [Vistahm] 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | - The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ``` 24 | -------------------------------------------------------------------------------- /app/config.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "slices" 10 | ) 11 | 12 | var savedNetworksFile = GetConfigFilePath() 13 | 14 | // Saves the password of the ssid if the connection was successful 15 | func SavePassword(ssid, password string) error { 16 | var savedNetworks []SavedNetwork 17 | 18 | data, err := os.ReadFile(savedNetworksFile) 19 | if err == nil { 20 | json.Unmarshal(data, &savedNetworks) 21 | } 22 | 23 | for i, network := range savedNetworks { 24 | if network.SSID == ssid { 25 | savedNetworks[i].Password = password 26 | file, err := json.MarshalIndent(savedNetworks, "", " ") 27 | if err != nil { 28 | return err 29 | } 30 | return os.WriteFile(savedNetworksFile, file, 0644) 31 | } 32 | } 33 | 34 | savedNetworks = append(savedNetworks, SavedNetwork{SSID: ssid, Password: password}) 35 | 36 | file, err := json.MarshalIndent(savedNetworks, "", " ") 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return os.WriteFile(savedNetworksFile, file, 0644) 42 | } 43 | 44 | // Loads the password of ssid 45 | func LoadPassword(ssid string) (string, bool) { 46 | data, err := os.ReadFile(savedNetworksFile) 47 | if err != nil { 48 | return "", false 49 | } 50 | 51 | var saved_networks []SavedNetwork 52 | json.Unmarshal(data, &saved_networks) 53 | 54 | for _, network := range saved_networks { 55 | if network.SSID == ssid { 56 | return network.Password, true 57 | } 58 | } 59 | 60 | return "", false 61 | } 62 | 63 | // Specifies the full path to save the json file in the user's home directory 64 | func GetConfigFilePath() string { 65 | homeDir, err := os.UserHomeDir() 66 | if err != nil { 67 | panic("Unable to find the home directory") 68 | } 69 | 70 | return filepath.Join(homeDir, ".saved_networks.json") 71 | } 72 | 73 | // Forgets the network based on the ssid 74 | func ForgetNetwork(ssid string) error { 75 | data, err := os.ReadFile(savedNetworksFile) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | var savedNetworks []SavedNetwork 81 | json.Unmarshal(data, &savedNetworks) 82 | 83 | // remove the network 84 | found := false 85 | for i, network := range savedNetworks { 86 | if network.SSID == ssid { 87 | savedNetworks = slices.Delete(savedNetworks, i, i+1) 88 | found = true 89 | break 90 | } 91 | } 92 | 93 | if !found { 94 | return fmt.Errorf("SSID '%s' not found as a saved network.", ssid) 95 | } 96 | 97 | // save back to file 98 | file, err := json.MarshalIndent(savedNetworks, "", " ") 99 | if err != nil { 100 | return err 101 | } 102 | 103 | return os.WriteFile(savedNetworksFile, file, 0644) 104 | } 105 | 106 | // Turns the system's wifi on 107 | func TurnOnWifi() error { 108 | cmd := exec.Command("nmcli", "radio", "wifi", "on") 109 | output, err := cmd.CombinedOutput() 110 | if err != nil { 111 | return fmt.Errorf("failed to enable wifi: %w, output: %s", err, output) 112 | } 113 | 114 | return nil 115 | } 116 | 117 | // Turns the system's wifi off 118 | func TurnOffWifi() error { 119 | cmd := exec.Command("nmcli", "radio", "wifi", "off") 120 | output, err := cmd.CombinedOutput() 121 | if err != nil { 122 | return fmt.Errorf("failed to enable wifi: %w, output: %s", err, output) 123 | } 124 | 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /app/dbus.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/godbus/dbus/v5" 7 | ) 8 | 9 | // Creates a d-bus object and activates a connection 10 | func ConnectToNetwork(conn *dbus.Conn, settings map[string]map[string]dbus.Variant, wifiDevicePath dbus.ObjectPath, apPath dbus.ObjectPath) (dbus.ObjectPath, error) { 11 | obj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") 12 | 13 | var newConnectionPath, activeConnectionPath dbus.ObjectPath 14 | err := obj.Call("org.freedesktop.NetworkManager.AddAndActivateConnection", 0, settings, wifiDevicePath, apPath).Store(&newConnectionPath, &activeConnectionPath) 15 | if err != nil { 16 | return "", fmt.Errorf("AddAndActivateConnection failed: %w", err) 17 | } 18 | 19 | return activeConnectionPath, nil 20 | } 21 | 22 | // Checks to see if the connection attempt was successful or not 23 | func CheckConnectionState(conn *dbus.Conn, activeConnectionPath dbus.ObjectPath) (bool, error) { 24 | activeConnectionObj := conn.Object("org.freedesktop.NetworkManager", activeConnectionPath) 25 | 26 | activeVariant, err := activeConnectionObj.GetProperty("org.freedesktop.NetworkManager.Connection.Active.State") 27 | state, ok := activeVariant.Value().(uint32) 28 | if !ok { 29 | return false, fmt.Errorf("State is not uint32: %w", err) 30 | } 31 | 32 | if err != nil { 33 | return false, fmt.Errorf("failed to get connection state: %w", err) 34 | } 35 | 36 | // 0: NM_ACTIVE_CONNECTION_STATE_UNKNOWN 37 | // 1: NM_ACTIVE_CONNECTION_STATE_ACTIVATING 38 | // 2: NM_ACTIVE_CONNECTION_STATE_ACTIVATED 39 | // 3: NM_ACTIVE_CONNECTION_STATE_DEACTIVATING 40 | // 4: NM_ACTIVE_CONNECTION_STATE_DEACTIVATED 41 | if state == 2 { 42 | return true, nil 43 | } 44 | 45 | return false, nil 46 | } 47 | 48 | func ForceWifiScan(conn *dbus.Conn, wifiDevicePath dbus.ObjectPath) error { 49 | deviceObj := conn.Object("org.freedesktop.NetworkManager", wifiDevicePath) 50 | options := map[string]dbus.Variant{} 51 | err := deviceObj.Call("org.freedesktop.NetworkManager.Device.Wireless.RequestScan", 0, options).Store() 52 | return err 53 | } 54 | -------------------------------------------------------------------------------- /app/utils.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "slices" 7 | "time" 8 | 9 | "github.com/charmbracelet/huh" 10 | "github.com/charmbracelet/huh/spinner" 11 | "github.com/godbus/dbus/v5" 12 | ) 13 | 14 | // ANSI escape color codes 15 | var Reset = "\033[0m" 16 | var Orange = "\033[38;5;208m" 17 | var Yellow = "\033[33m" 18 | var Cyan = "\033[36m" 19 | 20 | // Prints out the help message for the help argument 21 | func ShowHelpMessage() { 22 | fmt.Println("Usage: ewc | ewc [Option]") 23 | fmt.Println("Options:") 24 | fmt.Println(" con : directly connects to SSID without scanning") 25 | fmt.Println(" forget : forgets the SSID") 26 | fmt.Println(" on: turns on the wifi") 27 | fmt.Println(" off: turns off the wifi") 28 | fmt.Println(" help: shows this message") 29 | } 30 | 31 | // Handles the error as a helper function 32 | func HandleError(err error, message string) { 33 | if err != nil { 34 | fmt.Printf("%s: %s\n", err, message) 35 | os.Exit(1) 36 | } 37 | } 38 | 39 | // Waits for scanning the nerby networks based on the given timeoutSeconds 40 | func WaitForScan(timeoutSeconds int) { 41 | action := func() { 42 | time.Sleep(time.Duration(timeoutSeconds) * time.Second) 43 | } 44 | if err := spinner.New().Title("Scanning the nearby networks...").Action(action).Run(); err != nil { 45 | fmt.Println("Spinner scan failed:", err) 46 | } 47 | } 48 | 49 | // Waits to establish connection based on the given timeoutSeconds 50 | func WaitForConnection(timeoutSeconds int) { 51 | action := func() { 52 | time.Sleep(time.Duration(timeoutSeconds) * time.Second) 53 | } 54 | if err := spinner.New().Title("Establishing connection...").Action(action).Run(); err != nil { 55 | fmt.Println("Spinner establish failed:", err) 56 | } 57 | } 58 | 59 | // Handles the command-line arguments 60 | func HandleArguments(args []string) { 61 | 62 | if !slices.Equal(args, nil) { 63 | switch args[0] { 64 | case "con": 65 | if len(args) < 2 { 66 | fmt.Println("Please provide a SSID to connect.") 67 | os.Exit(1) 68 | } 69 | 70 | ssidToConnect := args[1] 71 | DirectConnection(ssidToConnect) 72 | os.Exit(0) 73 | 74 | case "forget": 75 | if len(args) < 2 { 76 | fmt.Println("Please provide a SSID to forget.") 77 | os.Exit(1) 78 | } 79 | 80 | ssidToForget := args[1] 81 | if err := ForgetNetwork(ssidToForget); err != nil { 82 | fmt.Println(err) 83 | } else { 84 | fmt.Printf("Success.\nForgotten network: %s\n", ssidToForget) 85 | } 86 | os.Exit(0) 87 | 88 | case "on": 89 | if err := TurnOnWifi(); err != nil { 90 | fmt.Println(err) 91 | } else { 92 | fmt.Println("Wi-Fi Enabled.") 93 | } 94 | os.Exit(0) 95 | 96 | case "off": 97 | if err := TurnOffWifi(); err != nil { 98 | fmt.Println(err) 99 | } else { 100 | fmt.Println("Wi-Fi Disabled.") 101 | } 102 | os.Exit(0) 103 | 104 | case "help": 105 | ShowHelpMessage() 106 | os.Exit(0) 107 | 108 | default: 109 | fmt.Println("Unknown command. Use 'help' for a list of commands.") 110 | os.Exit(0) 111 | } 112 | } 113 | } 114 | 115 | // Checks the system's NetworkManager state 116 | func GetNetworkManagerState(obj dbus.BusObject) error { 117 | var state uint32 118 | err := obj.Call("org.freedesktop.NetworkManager.state", 0).Store(&state) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | switch state { 124 | case 70: 125 | fmt.Println("NetworkManager is connected globally.") 126 | case 60: 127 | fmt.Println("NetworkManager is connected to a local Network.") 128 | case 50: 129 | fmt.Println("NetworkManager is connecting.") 130 | case 40: 131 | fmt.Println("NetworkManager is disconnected.") 132 | case 20: 133 | fmt.Println("NetworkManager is sleeping.") 134 | case 10: 135 | fmt.Println("NetworkManager is unavailable.") 136 | case 0: 137 | fmt.Println("NetworkManager's status is unknown.") 138 | default: 139 | fmt.Println("unknown NetworkManager state.") 140 | } 141 | 142 | return nil 143 | } 144 | 145 | // Creates a huh form to accept an access point from user 146 | func SelectAccessPoint(accessPoints []AccessPoint) AccessPoint { 147 | var selectedAP AccessPoint 148 | var ssidOptions []huh.Option[AccessPoint] 149 | 150 | for _, ap := range accessPoints { 151 | ssidOptions = append(ssidOptions, huh.NewOption(fmt.Sprintf("%s (Strength: %d)", ap.SSID, ap.Strength), ap)) 152 | } 153 | 154 | selectForm := huh.NewSelect[AccessPoint](). 155 | Title("Select Wi-Fi Network"). 156 | Options(ssidOptions...). 157 | Value(&selectedAP) 158 | 159 | HandleError(selectForm.Run(), "Error with form") 160 | return selectedAP 161 | } 162 | 163 | // If a saved password was found for the selectedAP it will load it, otherwise it will prompt the user for a password. Also if the the access point is not protected with WPA/WPA2, ignore the password prompt. 164 | func GetPasswordForAccessPoint(selectedAP AccessPoint) string { 165 | var password string 166 | savedPassword, found := LoadPassword(selectedAP.SSID) 167 | 168 | if (selectedAP.Flags & 0x1) > 0 { 169 | if found { 170 | password = savedPassword 171 | fmt.Println("Using saved password for:", selectedAP.SSID) 172 | } else { 173 | password = PromptForPassword() 174 | } 175 | } else { 176 | fmt.Println("The selected network is not encrypted.") 177 | } 178 | 179 | return password 180 | } 181 | 182 | // Creates a huh form to accept password for the selected ssid 183 | func PromptForPassword() string { 184 | var passwordInput string 185 | var showPassword bool 186 | var passwordForm *huh.Input 187 | 188 | showPasswordToggle := huh.NewConfirm(). 189 | Title("Show Password?"). 190 | Value(&showPassword). 191 | Run() 192 | if showPasswordToggle != nil { 193 | fmt.Println("failed to showPasswordToggle") 194 | os.Exit(1) 195 | } 196 | 197 | if showPassword { 198 | passwordForm = huh.NewInput().Title("Enter Password:").EchoMode(huh.EchoModeNormal).Value(&passwordInput) 199 | } else { 200 | passwordForm = huh.NewInput().Title("Enter Password:").EchoMode(huh.EchoModePassword).Value(&passwordInput) 201 | } 202 | 203 | HandleError(passwordForm.Run(), "failed to load form") 204 | 205 | return passwordInput 206 | } 207 | -------------------------------------------------------------------------------- /app/wifi.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/godbus/dbus/v5" 9 | ) 10 | 11 | type AccessPoint struct { 12 | Path dbus.ObjectPath 13 | SSID string 14 | Strength uint8 15 | Flags uint32 16 | } 17 | 18 | type SavedNetwork struct { 19 | SSID string `json:"ssid"` 20 | Password string `json:"password"` 21 | } 22 | 23 | func GetWifiDevicePath(conn *dbus.Conn) (dbus.ObjectPath, error) { 24 | obj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") 25 | 26 | var devicePaths []dbus.ObjectPath 27 | err := obj.Call("org.freedesktop.NetworkManager.GetAllDevices", 0).Store(&devicePaths) 28 | 29 | if err != nil { 30 | return "", fmt.Errorf("GetAllDevices failed: %w", err) 31 | } 32 | 33 | for _, path := range devicePaths { 34 | deviceObj := conn.Object("org.freedesktop.NetworkManager", path) 35 | variant, err := deviceObj.GetProperty("org.freedesktop.NetworkManager.Device.DeviceType") 36 | if err != nil { 37 | return "", fmt.Errorf("GetProperty failed: %w", err) 38 | } 39 | 40 | deviceType, ok := variant.Value().(uint32) 41 | if !ok { 42 | return "", fmt.Errorf("type assertion failed. DeviceType is not uint32") 43 | } 44 | 45 | if deviceType == 2 { 46 | return path, nil 47 | } 48 | } 49 | 50 | return "", fmt.Errorf("No wifi device found.") 51 | } 52 | 53 | func GetAccessPoints(conn *dbus.Conn, wifiDevicePath dbus.ObjectPath) ([]AccessPoint, error) { 54 | wifiDeviceObj := conn.Object("org.freedesktop.NetworkManager", wifiDevicePath) 55 | 56 | var accessPointsPaths []dbus.ObjectPath 57 | err := wifiDeviceObj.Call("org.freedesktop.NetworkManager.Device.Wireless.GetAllAccessPoints", 0).Store(&accessPointsPaths) 58 | if err != nil { 59 | return nil, fmt.Errorf("GetAllAccessPoints failed: %w", err) 60 | } 61 | 62 | var accessPoints []AccessPoint 63 | for _, apPath := range accessPointsPaths { 64 | apObj := conn.Object("org.freedesktop.NetworkManager", apPath) 65 | ssidVariant, err := apObj.GetProperty("org.freedesktop.NetworkManager.AccessPoint.Ssid") 66 | if err != nil { 67 | fmt.Printf("Failed to get SSID: %s\n", err) 68 | continue 69 | } 70 | 71 | ssidBytes, ok := ssidVariant.Value().([]byte) 72 | if !ok { 73 | fmt.Println("type assertion failed. SSID is not []byte") 74 | continue 75 | } 76 | ssid := string(ssidBytes) 77 | 78 | strengthVariant, err := apObj.GetProperty("org.freedesktop.NetworkManager.AccessPoint.Strength") 79 | if err != nil { 80 | fmt.Printf("failed to get Strength: %s\n", err) 81 | continue 82 | } 83 | 84 | strength, ok := strengthVariant.Value().(uint8) 85 | if !ok { 86 | fmt.Println("type assertion failed. Strength is not uint8") 87 | continue 88 | } 89 | 90 | flagsVariant, err := apObj.GetProperty("org.freedesktop.NetworkManager.AccessPoint.Flags") 91 | if err != nil { 92 | fmt.Printf("failed tp get Flags: %s\n", err) 93 | continue 94 | } 95 | 96 | flags, ok := flagsVariant.Value().(uint32) 97 | if !ok { 98 | fmt.Println("type assertion failed. Flags is not uint32") 99 | continue 100 | } 101 | 102 | accessPoints = append(accessPoints, AccessPoint{ 103 | Path: apPath, 104 | SSID: ssid, 105 | Strength: strength, 106 | Flags: flags, 107 | }) 108 | } 109 | 110 | return accessPoints, nil 111 | } 112 | 113 | func CreateConnectionSettings(ap AccessPoint, password string) map[string]map[string]dbus.Variant { 114 | settings := map[string]map[string]dbus.Variant{ 115 | "802-11-wireless": { 116 | "mode": dbus.MakeVariant("infrastructure"), 117 | "ssid": dbus.MakeVariant([]byte(ap.SSID)), 118 | "security": dbus.MakeVariant("802-11-wireless-security"), 119 | }, 120 | "connection": { 121 | "id": dbus.MakeVariant(ap.SSID), 122 | "type": dbus.MakeVariant("802-11-wireless"), 123 | "autoconnect": dbus.MakeVariant(true), 124 | }, 125 | "802-11-wireless-security": { 126 | "key-mgmt": dbus.MakeVariant("wpa-psk"), 127 | "psk": dbus.MakeVariant(password), 128 | }, 129 | "ipv4": { 130 | "method": dbus.MakeVariant("auto"), 131 | }, 132 | "ipv6": { 133 | "method": dbus.MakeVariant("auto"), 134 | }, 135 | } 136 | 137 | // Add password if needed 138 | //if ap.Flags == 0 || (ap.Flags&0x1) > 0 { 139 | // settings["802-11-wireless-security"] = map[string]dbus.Variant{ 140 | // "key-mgmt": dbus.MakeVariant("wpa-psk"), 141 | // "psk": dbus.MakeVariant(password), 142 | // } 143 | //} else { 144 | // // If the network is not encrypted, don't include security settings 145 | // // No additional settings needer here 146 | //} 147 | 148 | // remove security settings if the network is unencrypted 149 | if password == "" { 150 | delete(settings, "802-11-wireless-security") 151 | wirelessSettings := settings["802-11-wireless"] 152 | delete(wirelessSettings, "security") 153 | settings["802-11-wireless"] = wirelessSettings 154 | } 155 | 156 | return settings 157 | } 158 | 159 | // Directly connects to ssid without scanning 160 | func DirectConnection(ssid string) { 161 | // print the hint 162 | fmt.Println(Yellow+"  You're using direct connection and the program doesn't scan your nearby networks.", "\n", 163 | " So you should be aware of the SSID that you're trying to connect to."+Reset+"\n") 164 | 165 | // Create a new dbus object 166 | conn, err := dbus.SystemBus() 167 | HandleError(err, "SystemBus failed (DirectConnection)") 168 | 169 | wifiDevicePath, err := GetWifiDevicePath(conn) 170 | if err != nil { 171 | fmt.Println(err) 172 | os.Exit(1) 173 | } 174 | 175 | // laod saved password 176 | password, found := LoadPassword(ssid) 177 | 178 | var settings map[string]map[string]dbus.Variant 179 | if found { 180 | // Load password if already saved 181 | fmt.Println("Using saved password for:", ssid) 182 | ap := AccessPoint{SSID: ssid} 183 | settings = CreateConnectionSettings(ap, password) 184 | } else { 185 | // Prompt the user for password if not found 186 | password = PromptForPassword() 187 | ap := AccessPoint{SSID: ssid} 188 | settings = CreateConnectionSettings(ap, password) 189 | } 190 | 191 | // if no password entered, treat the network as unencrypted 192 | if password == "" { 193 | fmt.Println(Cyan + "No password entered. The selected network will be treated as unencrypted." + Reset) 194 | ap := AccessPoint{SSID: ssid} 195 | settings = CreateConnectionSettings(ap, password) 196 | } 197 | 198 | activeConnectionPath, err := ConnectToNetwork(conn, settings, wifiDevicePath, "/") 199 | if err != nil { 200 | fmt.Println(err) 201 | os.Exit(1) 202 | } 203 | 204 | WaitForConnection(5) 205 | 206 | // checking the connection state repeatedly for a certain duration 207 | timeout := 5 * time.Second 208 | startTime := time.Now() 209 | connected := false 210 | 211 | for time.Since(startTime) < timeout { 212 | connected, err = CheckConnectionState(conn, activeConnectionPath) 213 | if err != nil { 214 | // consider logging the error or taking other actions 215 | time.Sleep(1 * time.Second) 216 | continue 217 | } 218 | 219 | if connected { 220 | break // Successfull connection 221 | } 222 | } 223 | 224 | if connected { 225 | fmt.Println("Successfully connected to:", ssid) 226 | 227 | // save password 228 | if password != "" { 229 | if err := SavePassword(ssid, password); err != nil { 230 | fmt.Println("failed to save password:", err) 231 | } 232 | } 233 | } else { 234 | fmt.Println("Connection was not established. Wrong password maybe?") 235 | os.Exit(1) 236 | } 237 | 238 | } 239 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Vistahm/ewc 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | github.com/charmbracelet/huh v0.6.0 7 | github.com/charmbracelet/huh/spinner v0.0.0-20250327021907-684a9cf11922 8 | github.com/godbus/dbus/v5 v5.1.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/catppuccin/go v0.2.0 // indirect 15 | github.com/charmbracelet/bubbles v0.20.1-0.20250320170029-54f28b650198 // indirect 16 | github.com/charmbracelet/bubbletea v1.3.4 // indirect 17 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 18 | github.com/charmbracelet/lipgloss v1.1.0 // indirect 19 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 20 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect 21 | github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect 22 | github.com/charmbracelet/x/term v0.2.1 // indirect 23 | github.com/dustin/go-humanize v1.0.1 // indirect 24 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 25 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/mattn/go-localereader v0.0.1 // indirect 28 | github.com/mattn/go-runewidth v0.0.16 // indirect 29 | github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect 30 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 31 | github.com/muesli/cancelreader v0.2.2 // indirect 32 | github.com/muesli/termenv v0.16.0 // indirect 33 | github.com/rivo/uniseg v0.4.7 // indirect 34 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 35 | golang.org/x/sync v0.11.0 // indirect 36 | golang.org/x/sys v0.30.0 // indirect 37 | golang.org/x/text v0.18.0 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= 2 | github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= 3 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 4 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 5 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 6 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 7 | github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 8 | github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 9 | github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= 10 | github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= 11 | github.com/charmbracelet/bubbles v0.20.1-0.20250320170029-54f28b650198 h1:f3Y3wAWtPIl4v+HdC1ahy2vJ1yjaRHS1jJ9rZjpqUcQ= 12 | github.com/charmbracelet/bubbles v0.20.1-0.20250320170029-54f28b650198/go.mod h1:Qn5ilIoStd+mC+ZomW1Mo0ZyhDsr6qrqjC75d8i8bUY= 13 | github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= 14 | github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= 15 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= 16 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= 17 | github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= 18 | github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= 19 | github.com/charmbracelet/huh/spinner v0.0.0-20250327021907-684a9cf11922 h1:ZtaivJNoRsJaWkR19jrvOt8a7ZHVk+8kyK/Tt/dEJFM= 20 | github.com/charmbracelet/huh/spinner v0.0.0-20250327021907-684a9cf11922/go.mod h1:6O/Z+rCko0coy/tNKoTVPqatPE/0RN+1geO8Iii2h9g= 21 | github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 22 | github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 23 | github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 24 | github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 25 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= 26 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 27 | github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= 28 | github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 29 | github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= 30 | github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= 31 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 32 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 33 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 34 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 35 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 36 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 37 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 38 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 39 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 40 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 41 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 42 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 43 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 44 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 45 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 46 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 47 | github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= 48 | github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= 49 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 50 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 51 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 52 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 53 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 54 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 55 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 56 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 57 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 58 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 59 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 60 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 61 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 62 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 63 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 64 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 67 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 68 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 69 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 70 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/Vistahm/ewc/app" 8 | "github.com/godbus/dbus/v5" 9 | ) 10 | 11 | func main() { 12 | 13 | // Create a system bus 14 | conn, err := dbus.SystemBus() 15 | app.HandleError(err, "SystemBus failed") 16 | 17 | // Collect the arguments and handle them 18 | args := os.Args[1:] 19 | app.HandleArguments(args) 20 | 21 | obj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") 22 | 23 | // NetworkManager state 24 | app.GetNetworkManagerState(obj) 25 | 26 | wifiDevicePath, err := app.GetWifiDevicePath(conn) 27 | if err != nil { 28 | fmt.Println(err) 29 | os.Exit(1) 30 | } 31 | 32 | // Force scan for available networks 33 | if err := app.ForceWifiScan(conn, wifiDevicePath); err != nil { 34 | fmt.Println("failed to initiate wifi scan:", err) 35 | os.Exit(1) 36 | } 37 | 38 | // Wait to scan all the access points 39 | app.WaitForScan(10) 40 | 41 | accessPoints, err := app.GetAccessPoints(conn, wifiDevicePath) 42 | if err != nil { 43 | fmt.Println(err) 44 | os.Exit(1) 45 | } 46 | 47 | // Selecting an access point 48 | selectedAP := app.SelectAccessPoint(accessPoints) 49 | password := app.GetPasswordForAccessPoint(selectedAP) 50 | 51 | settings := app.CreateConnectionSettings(selectedAP, password) 52 | 53 | activeConnectionPath, err := app.ConnectToNetwork(conn, settings, wifiDevicePath, selectedAP.Path) 54 | if err != nil { 55 | fmt.Println(err) 56 | os.Exit(1) 57 | } 58 | 59 | // Wait 60 | app.WaitForConnection(6) 61 | 62 | connected, err := app.CheckConnectionState(conn, activeConnectionPath) 63 | if err != nil { 64 | fmt.Println("failed to verify connection: please check your password again") 65 | os.Exit(1) 66 | } 67 | 68 | if connected { 69 | fmt.Printf("Successfully connected to: %s\n", selectedAP.SSID) 70 | 71 | // Save the password for ssid 72 | if password != "" { 73 | if err := app.SavePassword(selectedAP.SSID, password); err != nil { 74 | fmt.Println("failed to save password:", err) 75 | } 76 | } 77 | } else { 78 | fmt.Println("Failed to connect to the network. Please check your password.") 79 | os.Exit(1) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # EWC (Easy Wifi Connection) 2 | 3 | Perhaps we all agree that connecting or switching wifi network in Linux can be a pain. (if you're a terminal dude!) 4 | 5 | Recently I became really frustrated with `nmcli` and its over-complicated process. So I decided to build something that I can use more easily to switch access points. 6 | 7 | I don't know if there's already such a program that can help you to do so, I didn't research and honestly I don't care! I wanted to build it myself and also have some fun. 8 | 9 | So here it is! A program that helps you to control you wireless connection on your Linux machine. Most important goal of this project was to be genuinely easy to use. 10 | 11 | It looks cool (uses `huh?` library from [charmbracelet](https://github.com/charmbracelet)), it's easy to use and fast enough. 12 | 13 | ## Examples 14 | Connecting and forgetting: 15 | 16 | ![001](https://github.com/user-attachments/assets/fbbbc235-333e-4aba-9e47-50f48b8db108) 17 | 18 | Direct connection: 19 | 20 | ![002](https://github.com/user-attachments/assets/ac2e2aae-55ff-479a-946d-398aefdbbe2a) 21 | 22 | 23 | Turning Wi-Fi on/off: 24 | 25 | ![002](https://github.com/user-attachments/assets/d7741139-8daf-42c9-970e-18ff198dca96) 26 | 27 | 28 | ## Features 29 | - **Connect and Switch between Wi-Fi networks:** easily connect the available Wi-Fi networks with a simple and interactive interface 30 | - **Save Passwords:** The program is able to save the passwords of each access point so you don't have to re-enter them every time. 31 | - **Forget Networks:** Remove saved networks if you want to try another password. 32 | - **Direct connection without scanning:** You can use direct connection to connect to a SSID that you already know. 33 | - **Disable/Enable Wi-Fi:** Switch the Wi-Fi on/off with less typing! 34 | - **User-Friendly Interface:** Built with `huh?` library, providing an intuitive and visually appealing user experience. 35 | - **Fast and Lightweight:** It is fast enough to not waste your time; if you don't believe you can give it a shot! 36 | 37 | ## Installation 38 | ### Manual (recommended) 39 | 40 | 1. **Clone the repository:** 41 | ``` 42 | git clone https://github.com/Vistahm/ewc.git 43 | cd ewc 44 | ``` 45 | 46 | 2. **Build the program:** 47 | (Make sure you have [Go](https://go.dev/) installed) Run: 48 | ``` 49 | go build -o ewc *.go 50 | ``` 51 | 52 | 3. **Run the program:** 53 | `./ewc` 54 | 55 | You can also move the executable file to your `/usr/local/bin` directory to use it globally on your machine. 56 | ``` 57 | mv ./ewc /usr/local/bin 58 | ``` 59 | 60 | ### Auto 61 | 62 | For auto installation just enter the following line in your terminal: 63 | ``` 64 | bash -c "$(curl -sLo- https://gist.githubusercontent.com/Vistahm/9a0d968f1e20057e534559e8e016adc6/raw/8766da4ffe7ef9c58733716319f865e69007a428/install.sh)" 65 | ``` 66 | 67 | ## Dependencies 68 | 69 | This project requires the following Go libraries: 70 | 71 | - [huh?](https://github.com/charmbracelet/huh) - For the user interface. 72 | - [godbus/dbus](https://github.com/godbus/dbus) - For D-Bus communication. 73 | 74 | When you build the project, [Go](https://go.dev/) will automatically download and install these dependencies for you. 75 | 76 | ## Contributing 77 | This program is still under development. Bugs can appear. If you encounter any problem feel free to open an issue under this repository. 78 | 79 | Any contributions are welcome! 80 | --------------------------------------------------------------------------------