Created
December 31, 2025 22:18
-
-
Save Hammer2900/670808279583237d18c38fd07eaac934 to your computer and use it in GitHub Desktop.
wifi linux scaner
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package wifi_viz | |
| import "core:fmt" | |
| import "core:strings" | |
| import "core:strconv" | |
| import "core:math" | |
| import "core:math/rand" | |
| import "core:c" | |
| import "core:thread" | |
| import "core:sync" | |
| import "core:slice" | |
| import rl "vendor:raylib" | |
| foreign import libc "system:c" | |
| foreign libc { | |
| popen :: proc(command: cstring, type: cstring) -> ^c.FILE --- | |
| pclose :: proc(stream: ^c.FILE) -> c.int --- | |
| fgets :: proc(s: [^]u8, n: c.int, stream: ^c.FILE) -> [^]u8 --- | |
| } | |
| ease_elastic_out :: proc(x: f32) -> f32 { | |
| c4 :: (2.0 * math.PI) / 3.0 | |
| if x == 0 { return 0 } | |
| if x == 1 { return 1 } | |
| return math.pow(2.0, -10.0 * x) * math.sin_f32((x * 10.0 - 0.75) * c4) + 1.0 | |
| } | |
| // --- СТРУКТУРЫ --- | |
| NetworkNode :: struct { | |
| ssid: string, | |
| signal: int, | |
| security: string, | |
| angle: f32, | |
| speed: f32, | |
| dist: f32, | |
| color: rl.Color, | |
| base_radius: f32, | |
| pop_timer: f32, | |
| } | |
| ScanContext :: struct { | |
| mutex: sync.Mutex, | |
| is_scanning: bool, | |
| has_new_data: bool, | |
| buffer: [dynamic]NetworkNode, | |
| } | |
| AppState :: struct { | |
| networks: [dynamic]NetworkNode, | |
| scan_ctx: ScanContext, | |
| timer: f32, | |
| selected_idx: int, | |
| visual_sel_y: f32, | |
| global_speed: f32, | |
| } | |
| // --- ПОТОКИ --- | |
| scan_thread_proc :: proc(t: ^thread.Thread) { | |
| ctx := cast(^ScanContext)t.data | |
| results := perform_scan_logic() | |
| sync.lock(&ctx.mutex) | |
| ctx.buffer = results | |
| ctx.has_new_data = true | |
| ctx.is_scanning = false | |
| sync.unlock(&ctx.mutex) | |
| } | |
| perform_scan_logic :: proc() -> [dynamic]NetworkNode { | |
| result: [dynamic]NetworkNode | |
| cmd_str := strings.clone_to_cstring("nmcli -t -f SSID,SIGNAL,SECURITY device wifi list --rescan yes") | |
| defer delete(cmd_str) | |
| mode_str := strings.clone_to_cstring("r") | |
| defer delete(mode_str) | |
| fp := popen(cmd_str, mode_str) | |
| if fp == nil { return result } | |
| defer pclose(fp) | |
| buffer: [1024]u8 | |
| for fgets(&buffer[0], 1024, fp) != nil { | |
| line_len := 0 | |
| for buffer[line_len] != 0 && buffer[line_len] != '\n' { line_len += 1 } | |
| line := string(buffer[:line_len]) | |
| parts := strings.split(line, ":") | |
| defer delete(parts) | |
| if len(parts) >= 3 { | |
| ssid := parts[0] | |
| if ssid == "" { ssid = "<Hidden>" } | |
| sig_str := parts[1] | |
| signal, ok := strconv.parse_int(sig_str) | |
| if !ok { continue } | |
| sec := parts[2] | |
| exists := false | |
| for i in 0..<len(result) { | |
| if result[i].ssid == ssid { | |
| exists = true | |
| if signal > result[i].signal { result[i].signal = signal } | |
| break | |
| } | |
| } | |
| if !exists { | |
| inv_signal := 100.0 - f32(signal) | |
| orbit_dist := 120.0 + (inv_signal * 3.5) | |
| orbit_speed := 20.0 / math.sqrt(orbit_dist) | |
| if rand.float32() > 0.5 { orbit_speed = -orbit_speed } | |
| node_color := rl.GREEN | |
| if signal < 70 { node_color = rl.YELLOW } | |
| if signal < 40 { node_color = rl.RED } | |
| if ssid == "<Hidden>" { node_color = rl.GRAY } | |
| append(&result, NetworkNode{ | |
| ssid = strings.clone(ssid), | |
| signal = signal, | |
| security = strings.clone(sec), | |
| dist = orbit_dist, | |
| angle = rand.float32_range(0, 360), | |
| speed = orbit_speed, | |
| color = node_color, | |
| base_radius = f32(signal) / 5.0 + 5.0, | |
| pop_timer = 0.0, | |
| }) | |
| } | |
| } | |
| } | |
| return result | |
| } | |
| start_scan :: proc(ctx: ^ScanContext) { | |
| ctx.is_scanning = true | |
| t := thread.create(scan_thread_proc) | |
| if t != nil { | |
| t.data = ctx | |
| thread.start(t) | |
| } | |
| } | |
| merge_networks :: proc(current_list: ^[dynamic]NetworkNode, new_list: [dynamic]NetworkNode) { | |
| final_list: [dynamic]NetworkNode | |
| for new_node in new_list { | |
| node_to_add := new_node | |
| found_idx := -1 | |
| for i in 0..<len(current_list) { | |
| if current_list[i].ssid == node_to_add.ssid { | |
| found_idx = i | |
| break | |
| } | |
| } | |
| if found_idx != -1 { | |
| old_node := current_list[found_idx] | |
| node_to_add.angle = old_node.angle | |
| node_to_add.pop_timer = 1.0 | |
| delete(old_node.ssid) | |
| delete(old_node.security) | |
| unordered_remove(current_list, found_idx) | |
| } else { | |
| node_to_add.pop_timer = 1.0 | |
| } | |
| append(&final_list, node_to_add) | |
| } | |
| for node in current_list { | |
| delete(node.ssid) | |
| delete(node.security) | |
| } | |
| delete(current_list^) | |
| current_list^ = final_list | |
| slice.sort_by(current_list[:], proc(i, j: NetworkNode) -> bool { | |
| return i.signal > j.signal | |
| }) | |
| } | |
| // --- MAIN --- | |
| main :: proc() { | |
| width :: 1200 | |
| height :: 800 | |
| rl.InitWindow(width, height, "Odin WiFi Orbit Visualizer") | |
| rl.SetTargetFPS(60) | |
| defer rl.CloseWindow() | |
| app := AppState{ | |
| scan_ctx = ScanContext{}, | |
| global_speed = 1.0, | |
| selected_idx = 0, | |
| } | |
| start_scan(&app.scan_ctx) | |
| camera := rl.Camera2D{ | |
| offset = {width/2 + 100, height/2}, | |
| target = {0, 0}, | |
| zoom = 1.0, | |
| } | |
| for !rl.WindowShouldClose() { | |
| dt := rl.GetFrameTime() | |
| app.timer += dt | |
| // 1. ПОТОК | |
| if sync.try_lock(&app.scan_ctx.mutex) { | |
| if app.scan_ctx.has_new_data { | |
| merge_networks(&app.networks, app.scan_ctx.buffer) | |
| app.scan_ctx.buffer = nil | |
| app.scan_ctx.has_new_data = false | |
| if app.selected_idx >= len(app.networks) { | |
| app.selected_idx = 0 | |
| } | |
| } | |
| sync.unlock(&app.scan_ctx.mutex) | |
| } | |
| // 2. INPUT | |
| wheel := rl.GetMouseWheelMove() | |
| if wheel != 0 { | |
| camera.zoom += wheel * 0.1 | |
| if camera.zoom < 0.1 { camera.zoom = 0.1 } | |
| } | |
| if rl.IsKeyPressed(.R) && !app.scan_ctx.is_scanning { | |
| start_scan(&app.scan_ctx) | |
| } | |
| if len(app.networks) > 0 { | |
| if rl.IsKeyPressed(.UP) { | |
| app.selected_idx -= 1 | |
| if app.selected_idx < 0 { app.selected_idx = len(app.networks) - 1 } | |
| } | |
| if rl.IsKeyPressed(.DOWN) { | |
| app.selected_idx += 1 | |
| if app.selected_idx >= len(app.networks) { app.selected_idx = 0 } | |
| } | |
| } | |
| if rl.IsKeyPressed(.RIGHT) { app.global_speed += 0.5 } | |
| if rl.IsKeyPressed(.LEFT) { | |
| app.global_speed -= 0.5 | |
| if app.global_speed < 0 { app.global_speed = 0 } | |
| } | |
| target_y := f32(app.selected_idx * 40) | |
| app.visual_sel_y = math.lerp(app.visual_sel_y, target_y, dt * 10.0) | |
| // 3. UPDATE | |
| for i in 0..<len(app.networks) { | |
| node := &app.networks[i] | |
| if !app.scan_ctx.is_scanning { | |
| node.angle += node.speed * app.global_speed * dt * 20.0 | |
| } | |
| if node.pop_timer > 0 { | |
| node.pop_timer -= dt * 1.5 | |
| if node.pop_timer < 0 { node.pop_timer = 0 } | |
| } | |
| } | |
| // 4. DRAW | |
| rl.BeginDrawing() | |
| rl.ClearBackground({15, 15, 20, 255}) | |
| rl.BeginMode2D(camera) | |
| for n in app.networks { | |
| rl.DrawCircleLines(0, 0, n.dist, {255, 255, 255, 10}) | |
| } | |
| pulse_speed: f32 = app.scan_ctx.is_scanning ? 15.0 : 2.0 | |
| pulse_col := app.scan_ctx.is_scanning ? rl.ORANGE : rl.SKYBLUE | |
| pulse := math.sin_f32(app.timer * pulse_speed) * 5.0 | |
| rl.DrawCircle(0, 0, 40 + pulse, {0, 100, 255, 40}) | |
| rl.DrawCircle(0, 0, 30, pulse_col) | |
| rl.DrawCircleLines(0, 0, 30, rl.WHITE) | |
| rl.DrawText("YOU", -10, -5, 10, rl.WHITE) | |
| for n in app.networks { | |
| rad := math.to_radians(n.angle) | |
| x := math.cos(rad) * n.dist | |
| y := math.sin(rad) * n.dist | |
| pop_scale := ease_elastic_out(n.pop_timer) * 0.8 | |
| current_radius := n.base_radius * (1.0 + pop_scale) | |
| rl.DrawCircleV({x, y}, current_radius, n.color) | |
| rl.DrawCircleV({x - current_radius*0.3, y - current_radius*0.3}, current_radius*0.2, {255,255,255,150}) | |
| if n.signal > 80 { rl.DrawLineEx({0,0}, {x,y}, 1.0, {0, 255, 0, 30}) } | |
| // Используем fmt.ctprintf("%s", ...) для безопасного перевода string -> cstring без утечек | |
| rl.DrawText(fmt.ctprintf("%s", n.ssid), i32(x) + 15, i32(y) - 5, 14, rl.WHITE) | |
| info := fmt.ctprintf("%d%%", n.signal) | |
| rl.DrawText(info, i32(x) + 15, i32(y) + 10, 10, rl.GRAY) | |
| } | |
| rl.EndMode2D() | |
| // ЛИНИЯ-КОННЕКТОР | |
| if len(app.networks) > 0 { | |
| selected_node := app.networks[app.selected_idx] | |
| rad := math.to_radians(selected_node.angle) | |
| world_x := math.cos(rad) * selected_node.dist | |
| world_y := math.sin(rad) * selected_node.dist | |
| screen_pos := rl.GetWorldToScreen2D({world_x, world_y}, camera) | |
| menu_x: f32 = 260.0 | |
| menu_y: f32 = 80.0 + app.visual_sel_y + 15.0 | |
| rl.DrawLineEx({menu_x, menu_y}, screen_pos, 2.0, {255, 255, 255, 100}) | |
| rl.DrawCircleV(screen_pos, 5.0, rl.WHITE) | |
| rl.DrawCircleV({menu_x, menu_y}, 3.0, rl.WHITE) | |
| } | |
| // БОКОВАЯ ПАНЕЛЬ | |
| rl.DrawRectangle(0, 0, 260, height, {20, 20, 25, 240}) | |
| rl.DrawRectangleLines(0, 0, 260, height, {50, 50, 60, 255}) | |
| rl.DrawText("NETWORKS", 20, 20, 20, rl.GREEN) | |
| rl.DrawText(fmt.ctprintf("Found: %d", len(app.networks)), 140, 22, 10, rl.GRAY) | |
| speed_text := fmt.ctprintf("Speed: %.1fx", app.global_speed) | |
| rl.DrawText(speed_text, 20, 50, 20, rl.ORANGE) | |
| list_start_y: f32 = 80.0 | |
| if len(app.networks) > 0 { | |
| rl.DrawRectangle(10, i32(list_start_y + app.visual_sel_y), 240, 30, {255, 255, 255, 20}) | |
| rl.DrawRectangleLines(10, i32(list_start_y + app.visual_sel_y), 240, 30, {255, 255, 255, 50}) | |
| } | |
| for i in 0..<len(app.networks) { | |
| n := app.networks[i] | |
| y_pos := i32(list_start_y) + i32(i * 40) | |
| rl.DrawCircle(30, y_pos + 15, 6, n.color) | |
| text_col := (i == app.selected_idx) ? rl.WHITE : rl.GRAY | |
| // --- ИСПРАВЛЕНИЕ: ИСПОЛЬЗУЕМ tprintf --- | |
| ssid_disp := n.ssid | |
| if len(ssid_disp) > 18 { | |
| // tprintf возвращает string, что совместимо с типом переменной | |
| ssid_disp = fmt.tprintf("%.15s...", n.ssid) | |
| } | |
| // Рисуем через ctprintf, чтобы не было утечек памяти | |
| rl.DrawText(fmt.ctprintf("%s", ssid_disp), 50, y_pos + 8, 16, text_col) | |
| sig_text := fmt.ctprintf("%d", n.signal) | |
| rl.DrawText(sig_text, 210, y_pos + 8, 16, n.color) | |
| } | |
| if app.scan_ctx.is_scanning { | |
| alpha := u8(math.abs(math.sin_f32(app.timer * 5.0)) * 255.0) | |
| rl.DrawText("SCANNING...", 20, height - 80, 20, {255, 200, 0, alpha}) | |
| } else { | |
| rl.DrawText("Arrows: Nav/Speed", 20, height - 80, 10, rl.GRAY) | |
| rl.DrawText("'R': Rescan", 20, height - 60, 10, rl.GRAY) | |
| rl.DrawText("Scroll: Zoom", 20, height - 40, 10, rl.GRAY) | |
| } | |
| rl.EndDrawing() | |
| } | |
| for n in app.networks { delete(n.ssid); delete(n.security) } | |
| delete(app.networks) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment