Skip to content

Instantly share code, notes, and snippets.

@Hammer2900
Created December 31, 2025 22:18
Show Gist options
  • Select an option

  • Save Hammer2900/670808279583237d18c38fd07eaac934 to your computer and use it in GitHub Desktop.

Select an option

Save Hammer2900/670808279583237d18c38fd07eaac934 to your computer and use it in GitHub Desktop.
wifi linux scaner
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