Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+1
View File
@@ -0,0 +1 @@
<img src='https://raw.githubusercontent.com/vlang/v/master/examples/term.ui/screenshot_pong.png' width=300>
+98
View File
@@ -0,0 +1,98 @@
import term.ui as tui
const colors = [
tui.Color{33, 150, 243},
tui.Color{0, 150, 136},
tui.Color{205, 220, 57},
tui.Color{255, 152, 0},
tui.Color{244, 67, 54},
tui.Color{156, 39, 176},
]
struct Point {
x int
y int
}
struct App {
mut:
tui &tui.Context = unsafe { nil }
points []Point
color tui.Color = colors[0]
color_idx int
cut_rate f64 = 5
}
fn frame(mut app App) {
app.tui.clear()
if app.points.len > 0 {
app.tui.set_bg_color(app.color)
mut last := app.points[0]
for point in app.points {
// if the cursor moves quickly enough, different events are not
// necessarily touching, so we need to draw a line between them
app.tui.draw_line(last.x, last.y, point.x, point.y)
last = point
}
app.tui.reset()
l := int(app.points.len / app.cut_rate) + 1
app.points = app.points[l..].clone()
}
ww := app.tui.window_width
app.tui.bold()
app.tui.draw_text(ww / 6, 2, 'V term.input: cursor chaser demo')
app.tui.draw_text((ww - ww / 6) - 14, 2, 'cut rate: ${(100 / app.cut_rate):3.0f}%')
app.tui.horizontal_separator(3)
app.tui.reset()
app.tui.flush()
}
fn event(e &tui.Event, mut app App) {
match e.typ {
.key_down {
match e.code {
.escape {
exit(0)
}
.space, .enter {
app.color_idx++
if app.color_idx == colors.len {
app.color_idx = 0
}
app.color = colors[app.color_idx]
}
else {}
}
}
.mouse_move, .mouse_drag, .mouse_down {
app.points << Point{e.x, e.y}
}
.mouse_scroll {
d := if e.direction == .up { 0.1 } else { -0.1 }
app.cut_rate += d
if app.cut_rate < 1 {
app.cut_rate = 1
}
}
else {}
}
}
type EventFn = fn (&tui.Event, voidptr)
type FrameFn = fn (voidptr)
fn main() {
mut app := &App{}
app.tui = tui.init(
user_data: app
frame_fn: FrameFn(frame)
event_fn: EventFn(event)
hide_cursor: true
)
app.tui.run()!
}
+53
View File
@@ -0,0 +1,53 @@
import term.ui as tui
struct App {
mut:
tui &tui.Context = unsafe { nil }
}
fn (mut app App) show_header() {
app.tui.clear()
app.tui.set_cursor_position(0, 0)
app.tui.write('V term.input event viewer (press `esc` to exit)\n\n')
app.tui.flush()
}
fn event(e &tui.Event, mut app App) {
app.show_header()
app.tui.write('${e}')
app.tui.write('\n\nRaw event bytes: "${e.utf8.bytes().hex()}" = ${e.utf8.bytes()}')
if !e.modifiers.is_empty() {
app.tui.write('\nModifiers: ${e.modifiers} = ')
if e.modifiers.has(.ctrl) {
app.tui.write('ctrl. ')
}
if e.modifiers.has(.shift) {
app.tui.write('shift ')
}
if e.modifiers.has(.alt) {
app.tui.write('alt. ')
}
}
app.tui.flush()
if e.typ == .key_down && e.code == .escape {
println('\nGood bye.')
exit(0)
}
}
type EventFn = fn (&tui.Event, voidptr)
fn main() {
mut app := &App{}
app.tui = tui.init(
user_data: app
event_fn: EventFn(event)
window_title: 'V term.ui event viewer'
hide_cursor: true
capture_events: true
frame_rate: 60
use_alternate_buffer: false
)
app.show_header()
app.tui.run()!
}
+504
View File
@@ -0,0 +1,504 @@
// Copyright (c) 2020 Lars Pontoppidan. All rights reserved.
// Use of this source code is governed by the MIT license distributed with this software.
import term
import term.ui
import time
enum Mode {
menu
game
}
const player_one = 1 // Human control this racket
const player_two = 0 // Take over this AI controller
const white = ui.Color{255, 255, 255}
@[heap]
struct App {
mut:
tui &ui.Context = unsafe { nil }
mode Mode = Mode.menu
width int
height int
game &Game = unsafe { nil }
dt f32
ticks i64
}
fn (mut a App) init() {
a.game = &Game{
app: a
}
w, h := a.tui.window_width, a.tui.window_height
a.width = w
a.height = h
term.erase_del_clear()
term.set_cursor_position(
x: 0
y: 0
)
}
fn (mut a App) start_game() {
if a.mode != .game {
a.mode = .game
}
a.game.init()
}
fn (mut a App) frame() {
ticks := time.ticks()
a.dt = f32(ticks - a.ticks) / 1000.0
a.width, a.height = a.tui.window_width, a.tui.window_height
if a.mode == .game {
a.game.update()
}
a.tui.clear()
a.render()
a.tui.flush()
a.ticks = ticks
}
fn (mut a App) quit() {
if a.mode != .menu {
a.game.quit()
return
}
term.set_cursor_position(
x: 0
y: 0
)
exit(0)
}
fn (mut a App) event(e &ui.Event) {
match e.typ {
.mouse_move {
if a.mode != .game {
return
}
// TODO: mouse movement for real Pong sharks
// a.game.move_player(player_one, 0, -1)
}
.key_down {
match e.code {
.escape, .q {
a.quit()
}
.w {
if a.mode != .game {
return
}
a.game.move_player(player_one, 0, -1)
}
.a {
if a.mode != .game {
return
}
a.game.move_player(player_one, 0, -1)
}
.s {
if a.mode != .game {
return
}
a.game.move_player(player_one, 0, 1)
}
.d {
if a.mode != .game {
return
}
a.game.move_player(player_one, 0, 1)
}
.left {
if a.mode != .game {
return
}
a.game.move_player(player_two, 0, -1)
}
.right {
if a.mode != .game {
return
}
a.game.move_player(player_two, 0, 1)
}
.up {
if a.mode != .game {
return
}
a.game.move_player(player_two, 0, -1)
}
.down {
if a.mode != .game {
return
}
a.game.move_player(player_two, 0, 1)
}
.enter, .space {
if a.mode == .menu {
a.start_game()
}
}
else {}
}
}
else {}
}
}
fn (mut a App) free() {
unsafe {
a.game.free()
free(a.game)
}
}
fn (mut a App) render() {
match a.mode {
.menu { a.draw_menu() }
else { a.draw_game() }
}
}
fn (mut a App) draw_menu() {
cx := int(f32(a.width) * 0.5)
y025 := int(f32(a.height) * 0.25)
y075 := int(f32(a.height) * 0.75)
cy := int(f32(a.height) * 0.5)
a.tui.set_color(white)
a.tui.bold()
a.tui.draw_text(cx - 2, y025, 'VONG')
a.tui.reset()
a.tui.draw_text(cx - 13, y025 + 1, '(A game of Pong written in V)')
a.tui.set_color(white)
a.tui.bold()
a.tui.draw_text(cx - 3, cy + 1, 'START')
a.tui.reset()
a.tui.draw_text(cx - 9, y075 + 1, 'Press SPACE to start')
a.tui.reset()
a.tui.draw_text(cx - 5, y075 + 3, 'ESC to Quit')
a.tui.reset()
}
fn (mut a App) draw_game() {
a.game.draw()
}
struct Player {
mut:
game &Game = unsafe { nil }
pos Vec
racket_size int = 4
score int
ai bool
}
fn (mut p Player) move(x f32, y f32) {
p.pos.x += x
p.pos.y += y
}
fn (mut p Player) update() {
if !p.ai {
return
}
if isnil(p.game) {
return
}
// dt := p.game.app.dt
ball := unsafe { &p.game.ball }
// Evil AI that eventually will take over the world
p.pos.y = ball.pos.y - f32(p.racket_size) * 0.5
}
struct Vec {
mut:
x f32
y f32
}
fn (mut v Vec) set(x f32, y f32) {
v.x = x
v.y = y
}
struct Ball {
mut:
pos Vec
vel Vec
acc Vec
}
fn (mut b Ball) update(dt f32) {
b.pos.x += b.vel.x * b.acc.x * dt
b.pos.y += b.vel.y * b.acc.y * dt
}
@[heap]
struct Game {
mut:
app &App = unsafe { nil }
players []Player
ball Ball
}
fn (mut g Game) move_player(id int, x int, y int) {
mut p := unsafe { &g.players[id] }
if p.ai { // disable AI when moved
p.ai = false
}
p.move(x, y)
}
fn (mut g Game) init() {
if g.players.len == 0 {
g.players = []Player{len: 2, init: Player{ // <- BUG omitting the init will result in smaller racket sizes???
game: g
}}
}
g.reset()
}
fn (mut g Game) reset() {
mut i := 0
for mut p in g.players {
p.score = 0
if i != player_one {
p.ai = true
}
i++
}
g.new_round()
}
fn (mut g Game) new_round() {
mut i := 0
for mut p in g.players {
p.pos.x = if i == 0 { f32(3) } else { f32(g.app.width - 2) }
p.pos.y = f32(g.app.height) * 0.5 - f32(p.racket_size) * 0.5
i++
}
g.ball.pos.set(f32(g.app.width) * 0.5, f32(g.app.height) * 0.5)
g.ball.vel.set(-8, -15)
g.ball.acc.set(2.0, 1.0)
}
fn (mut g Game) update() {
dt := g.app.dt
mut b := unsafe { &g.ball }
for mut p in g.players {
p.update()
// Keep rackets within the game area
if p.pos.y <= 0 {
p.pos.y = 1.0
}
if p.pos.y + p.racket_size >= g.app.height {
p.pos.y = f32(g.app.height - p.racket_size - 1)
}
// Check ball collision
// Player left side
if p.pos.x < f32(g.app.width) * 0.5 {
// Racket collision
if b.pos.x <= p.pos.x + 1 {
if b.pos.y >= p.pos.y && b.pos.y <= p.pos.y + p.racket_size {
b.vel.x *= -1
}
}
// Behind racket
if b.pos.x < p.pos.x {
g.players[1].score++
g.new_round()
}
} else {
// Player right side
if b.pos.x >= p.pos.x - 1 {
if b.pos.y >= p.pos.y && b.pos.y <= p.pos.y + p.racket_size {
b.vel.x *= -1
}
}
if b.pos.x > p.pos.x {
g.players[0].score++
g.new_round()
}
}
}
if b.pos.x <= 1 || b.pos.x >= g.app.width {
b.vel.x *= -1
}
if b.pos.y <= 2 || b.pos.y >= g.app.height {
b.vel.y *= -1
}
b.update(dt)
}
fn (mut g Game) quit() {
if g.app.mode != .game {
return
}
g.app.mode = .menu
}
fn (mut g Game) draw_big_digit(px f32, py f32, digit int) {
// TODO: use draw_line or draw_point to fix tearing with non-monospaced terminal fonts
mut gfx := g.app.tui
x, y := int(px), int(py)
match digit {
0 {
gfx.draw_text(x, y + 0, '█████')
gfx.draw_text(x, y + 1, '█ █')
gfx.draw_text(x, y + 2, '█ █')
gfx.draw_text(x, y + 3, '█ █')
gfx.draw_text(x, y + 4, '█████')
}
1 {
gfx.draw_text(x + 3, y + 0, '█')
gfx.draw_text(x + 3, y + 1, '█')
gfx.draw_text(x + 3, y + 2, '█')
gfx.draw_text(x + 3, y + 3, '█')
gfx.draw_text(x + 3, y + 4, '█')
}
2 {
gfx.draw_text(x, y + 0, '█████')
gfx.draw_text(x, y + 1, ' █')
gfx.draw_text(x, y + 2, '█████')
gfx.draw_text(x, y + 3, '█')
gfx.draw_text(x, y + 4, '█████')
}
3 {
gfx.draw_text(x, y + 0, '█████')
gfx.draw_text(x, y + 1, ' ██')
gfx.draw_text(x, y + 2, ' ████')
gfx.draw_text(x, y + 3, ' ██')
gfx.draw_text(x, y + 4, '█████')
}
4 {
gfx.draw_text(x, y + 0, '█ █')
gfx.draw_text(x, y + 1, '█ █')
gfx.draw_text(x, y + 2, '█████')
gfx.draw_text(x, y + 3, ' █')
gfx.draw_text(x, y + 4, ' █')
}
5 {
gfx.draw_text(x, y + 0, '█████')
gfx.draw_text(x, y + 1, '█')
gfx.draw_text(x, y + 2, '█████')
gfx.draw_text(x, y + 3, ' █')
gfx.draw_text(x, y + 4, '█████')
}
6 {
gfx.draw_text(x, y + 0, '█████')
gfx.draw_text(x, y + 1, '█')
gfx.draw_text(x, y + 2, '█████')
gfx.draw_text(x, y + 3, '█ █')
gfx.draw_text(x, y + 4, '█████')
}
7 {
gfx.draw_text(x, y + 0, '█████')
gfx.draw_text(x, y + 1, ' █')
gfx.draw_text(x, y + 2, ' █')
gfx.draw_text(x, y + 3, ' █')
gfx.draw_text(x, y + 4, ' █')
}
8 {
gfx.draw_text(x, y + 0, '█████')
gfx.draw_text(x, y + 1, '█ █')
gfx.draw_text(x, y + 2, '█████')
gfx.draw_text(x, y + 3, '█ █')
gfx.draw_text(x, y + 4, '█████')
}
9 {
gfx.draw_text(x, y + 0, '█████')
gfx.draw_text(x, y + 1, '█ █')
gfx.draw_text(x, y + 2, '█████')
gfx.draw_text(x, y + 3, ' █')
gfx.draw_text(x, y + 4, '█████')
}
else {}
}
}
fn (mut g Game) draw() {
mut gfx := g.app.tui
gfx.set_bg_color(white)
// Border
gfx.draw_empty_rect(1, 1, g.app.width, g.app.height)
// Center line
gfx.draw_dashed_line(int(f32(g.app.width) * 0.5), 0, int(f32(g.app.width) * 0.5),
int(g.app.height))
border := 1
mut y, mut x := 0, 0
for p in g.players {
x = int(p.pos.x)
y = int(p.pos.y)
gfx.reset_bg_color()
gfx.set_color(white)
if x < f32(g.app.width) * 0.5 {
g.draw_big_digit(f32(g.app.width) * 0.25, 3, p.score)
} else {
g.draw_big_digit(f32(g.app.width) * 0.75, 3, p.score)
}
gfx.reset_color()
gfx.set_bg_color(white)
// Racket
gfx.draw_line(x, y + border, x, y + p.racket_size)
}
// Ball
gfx.draw_point(int(g.ball.pos.x), int(g.ball.pos.y))
// gfx.draw_text(22,2,'${g.ball.pos}')
gfx.reset_bg_color()
}
fn (mut g Game) free() {
g.players.clear()
}
// TODO: Remove these wrapper functions when we can assign methods as callbacks
fn init(mut app App) {
app.init()
}
fn frame(mut app App) {
app.frame()
}
fn cleanup(mut app App) {
unsafe {
app.free()
}
}
fn fail(error string) {
eprintln(error)
}
fn event(e &ui.Event, mut app App) {
app.event(e)
}
type InitFn = fn (voidptr)
type EventFn = fn (&ui.Event, voidptr)
type FrameFn = fn (voidptr)
type CleanupFn = fn (voidptr)
fn main() {
mut app := &App{}
app.tui = ui.init(
user_data: app
init_fn: InitFn(init)
frame_fn: FrameFn(frame)
cleanup_fn: CleanupFn(cleanup)
event_fn: EventFn(event)
fail_fn: fail
capture_events: true
hide_cursor: true
frame_rate: 60
)
app.tui.run()!
}
+100
View File
@@ -0,0 +1,100 @@
import term.ui as tui
import rand
struct Rect {
mut:
c tui.Color
x int
y int
x2 int
y2 int
}
struct App {
mut:
tui &tui.Context = unsafe { nil }
rects []Rect
cur_rect Rect
is_drag bool
redraw bool
}
fn random_color() tui.Color {
return tui.Color{
r: rand.u8()
g: rand.u8()
b: rand.u8()
}
}
fn event(e &tui.Event, mut app App) {
match e.typ {
.mouse_down {
app.is_drag = true
app.cur_rect = Rect{
c: random_color()
x: e.x
y: e.y
x2: e.x
y2: e.y
}
}
.mouse_drag {
app.cur_rect.x2 = e.x
app.cur_rect.y2 = e.y
}
.mouse_up {
app.rects << app.cur_rect
app.is_drag = false
}
.key_down {
if e.code == .c {
app.rects.clear()
} else if e.code == .escape {
exit(0)
}
}
else {}
}
app.redraw = true
}
fn frame(mut app App) {
if !app.redraw {
return
}
app.tui.clear()
for rect in app.rects {
app.tui.set_bg_color(rect.c)
app.tui.draw_rect(rect.x, rect.y, rect.x2, rect.y2)
}
if app.is_drag {
r := app.cur_rect
app.tui.set_bg_color(r.c)
app.tui.draw_empty_rect(r.x, r.y, r.x2, r.y2)
}
app.tui.reset_bg_color()
app.tui.flush()
app.redraw = false
}
type EventFn = fn (&tui.Event, voidptr)
type FrameFn = fn (voidptr)
fn main() {
mut app := &App{}
app.tui = tui.init(
user_data: app
event_fn: EventFn(event)
frame_fn: FrameFn(frame)
hide_cursor: true
frame_rate: 60
)
app.tui.run()!
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

+508
View File
@@ -0,0 +1,508 @@
// Copyright (c) 2020 Raúl Hernández. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module main
import term.ui
// The color palette, taken from Google's Material design
const colors = [
[
ui.Color{239, 154, 154},
ui.Color{244, 143, 177},
ui.Color{206, 147, 216},
ui.Color{179, 157, 219},
ui.Color{159, 168, 218},
ui.Color{144, 202, 249},
ui.Color{129, 212, 250},
ui.Color{128, 222, 234},
ui.Color{128, 203, 196},
ui.Color{165, 214, 167},
ui.Color{197, 225, 165},
ui.Color{230, 238, 156},
ui.Color{255, 245, 157},
ui.Color{255, 224, 130},
ui.Color{255, 204, 128},
ui.Color{255, 171, 145},
ui.Color{188, 170, 164},
ui.Color{238, 238, 238},
ui.Color{176, 190, 197},
],
[
ui.Color{244, 67, 54},
ui.Color{233, 30, 99},
ui.Color{156, 39, 176},
ui.Color{103, 58, 183},
ui.Color{63, 81, 181},
ui.Color{33, 150, 243},
ui.Color{3, 169, 244},
ui.Color{0, 188, 212},
ui.Color{0, 150, 136},
ui.Color{76, 175, 80},
ui.Color{139, 195, 74},
ui.Color{205, 220, 57},
ui.Color{255, 235, 59},
ui.Color{255, 193, 7},
ui.Color{255, 152, 0},
ui.Color{255, 87, 34},
ui.Color{121, 85, 72},
ui.Color{120, 120, 120},
ui.Color{96, 125, 139},
],
[
ui.Color{198, 40, 40},
ui.Color{173, 20, 87},
ui.Color{106, 27, 154},
ui.Color{69, 39, 160},
ui.Color{40, 53, 147},
ui.Color{21, 101, 192},
ui.Color{2, 119, 189},
ui.Color{0, 131, 143},
ui.Color{0, 105, 92},
ui.Color{46, 125, 50},
ui.Color{85, 139, 47},
ui.Color{158, 157, 36},
ui.Color{249, 168, 37},
ui.Color{255, 143, 0},
ui.Color{239, 108, 0},
ui.Color{216, 67, 21},
ui.Color{78, 52, 46},
ui.Color{33, 33, 33},
ui.Color{55, 71, 79},
],
]
const frame_rate = 30 // fps
const w = 200
const h = 100
const space = ' '
const spaces = ' '
const select_color = 'Select color: '
const select_size = 'Size: + -'
struct App {
mut:
ui &ui.Context = unsafe { nil }
header_text []string
mouse_pos Point
msg string
msg_hide_tick int
primary_color ui.Color = colors[1][6]
secondary_color ui.Color = colors[1][9]
primary_color_idx int = 25
secondary_color_idx int = 28
bg_color ui.Color = ui.Color{0, 0, 0}
drawing [][]ui.Color = [][]ui.Color{len: h, init: []ui.Color{len: w}}
size int = 1
should_redraw bool = true
is_dragging bool
}
struct Point {
mut:
x int
y int
}
type EventFn = fn (&ui.Event, voidptr)
type FrameFn = fn (voidptr)
fn main() {
mut app := &App{}
app.ui = ui.init(
user_data: app
frame_fn: FrameFn(frame)
event_fn: EventFn(event)
frame_rate: frame_rate
hide_cursor: true
window_title: 'V terminal pixelart drawing app'
)
app.mouse_pos.x = 40
app.mouse_pos.y = 15
app.ui.clear()
app.ui.run()!
}
fn frame(mut app App) {
mut redraw := app.should_redraw
if app.msg != '' && app.ui.frame_count >= app.msg_hide_tick {
app.msg = ''
redraw = true
}
if redraw {
app.render(false)
app.should_redraw = false
}
}
fn event(event &ui.Event, mut app App) {
match event.typ {
.mouse_down {
app.is_dragging = true
if app.ui.window_height - event.y < 5 {
app.footer_click(event)
} else {
app.paint(event)
}
}
.mouse_up {
app.is_dragging = false
}
.mouse_drag {
app.mouse_pos = Point{
x: event.x
y: event.y
}
app.paint(event)
}
.mouse_move {
app.mouse_pos = Point{
x: event.x
y: event.y
}
}
.mouse_scroll {
app.mouse_pos = Point{
x: event.x
y: event.y
}
d := event.direction == .down
if event.modifiers.has(.ctrl) {
p := !event.modifiers.has(.shift)
c := if d {
if p { app.primary_color_idx - 1 } else { app.secondary_color_idx - 1 }
} else {
if p { app.primary_color_idx + 1 } else { app.secondary_color_idx + 1 }
}
app.select_color(p, c)
} else {
if d {
app.inc_size()
} else {
app.dec_size()
}
}
}
.key_down {
match event.code {
.f1, ._1 {
oevent := *event
nevent := ui.Event{
...oevent
button: ui.MouseButton.left
x: app.mouse_pos.x
y: app.mouse_pos.y
}
app.paint(nevent)
}
.f2, ._2 {
oevent := *event
nevent := ui.Event{
...oevent
button: ui.MouseButton.right
x: app.mouse_pos.x
y: app.mouse_pos.y
}
app.paint(nevent)
}
.space, .enter {
oevent := *event
nevent := ui.Event{
...oevent
button: .left
x: app.mouse_pos.x
y: app.mouse_pos.y
}
app.paint(nevent)
}
.delete, .backspace {
oevent := *event
nevent := ui.Event{
...oevent
button: .middle
x: app.mouse_pos.x
y: app.mouse_pos.y
}
app.paint(nevent)
}
.j, .down {
if event.modifiers.has(.shift) {
app.set_pixel((1 + app.mouse_pos.x) / 2, app.mouse_pos.y, app.primary_color)
}
app.mouse_pos.y++
}
.k, .up {
if event.modifiers.has(.shift) {
app.set_pixel((1 + app.mouse_pos.x) / 2, app.mouse_pos.y, app.primary_color)
}
app.mouse_pos.y--
}
.h, .left {
if event.modifiers.has(.shift) {
app.set_pixel((1 + app.mouse_pos.x) / 2, app.mouse_pos.y, app.primary_color)
}
app.mouse_pos.x -= 2
}
.l, .right {
if event.modifiers.has(.shift) {
app.set_pixel((1 + app.mouse_pos.x) / 2, app.mouse_pos.y, app.primary_color)
}
app.mouse_pos.x += 2
}
.t {
p := !event.modifiers.has(.alt)
c := if event.modifiers.has(.shift) {
if p { app.primary_color_idx - 19 } else { app.secondary_color_idx - 19 }
} else {
if p { app.primary_color_idx + 19 } else { app.secondary_color_idx + 19 }
}
app.select_color(p, c)
}
.r {
p := !event.modifiers.has(.alt)
c := if event.modifiers.has(.shift) {
if p { app.primary_color_idx - 1 } else { app.secondary_color_idx - 1 }
} else {
if p { app.primary_color_idx + 1 } else { app.secondary_color_idx + 1 }
}
app.select_color(p, c)
}
.plus {
app.inc_size()
}
.minus {
app.dec_size()
}
.c {
app.drawing = [][]ui.Color{len: h, init: []ui.Color{len: w}}
}
.q, .escape {
app.render(true)
exit(0)
}
else {}
}
}
else {}
}
app.should_redraw = true
}
fn (mut app App) render(paint_only bool) {
app.ui.clear()
app.draw_header()
app.draw_content()
if !paint_only {
app.draw_footer()
app.draw_cursor()
}
app.ui.flush()
}
fn (mut app App) select_color(primary bool, idx int) {
c := (idx + 57) % 57
cx := c % 19
cy := (c / 19) % 3
color := colors[cy][cx]
if primary {
app.primary_color_idx = c % (19 * 3)
app.primary_color = color
} else {
app.secondary_color_idx = c % (19 * 3)
app.secondary_color = color
}
c_str := if primary { 'primary' } else { 'secondary' }
app.show_msg('set ${c_str} color idx: ${idx}', 1)
}
fn (mut app App) set_pixel(x_ int, y_ int, c ui.Color) {
// Term coords start at 1, and adjust for the header
x, y := x_ - 1, y_ - 4
if y < 0 || app.ui.window_height - y < 3 {
return
}
if y >= app.drawing.len || x < 0 || x >= app.drawing[0].len {
return
}
app.drawing[y][x] = c
}
fn (mut app App) paint(event &ui.Event) {
if event.y < 4 || app.ui.window_height - event.y < 4 {
return
}
x_start, y_start := int(f32((event.x - 1) / 2) - app.size / 2 + 1), event.y - app.size / 2
color := match event.button {
.left { app.primary_color }
.right { app.secondary_color }
else { app.bg_color }
}
for x in x_start .. x_start + app.size {
for y in y_start .. y_start + app.size {
app.set_pixel(x, y, color)
}
}
}
fn (mut app App) draw_content() {
w_, mut h_ := app.ui.window_width / 2, app.ui.window_height - 8
if h_ > app.drawing.len {
h_ = app.drawing.len
}
for row_idx, row in app.drawing[..h_] {
app.ui.set_cursor_position(0, row_idx + 4)
mut last := ui.Color{0, 0, 0}
for cell in row[..w_] {
if cell.r == 0 && cell.g == 0 && cell.b == 0 {
if !(cell.r == last.r && cell.g == last.g && cell.b == last.b) {
app.ui.reset()
}
} else {
if !(cell.r == last.r && cell.g == last.g && cell.b == last.b) {
app.ui.set_bg_color(cell)
}
}
app.ui.write(spaces)
last = cell
}
app.ui.reset()
}
}
fn (mut app App) draw_cursor() {
if app.mouse_pos.y in [3, app.ui.window_height - 5] {
// inside the horizontal separators
return
}
cursor_color := if app.is_dragging { ui.Color{220, 220, 220} } else { ui.Color{160, 160, 160} }
app.ui.set_bg_color(cursor_color)
if app.mouse_pos.y >= 3 && app.mouse_pos.y <= app.ui.window_height - 4 {
// inside the main content
mut x_start := int(f32((app.mouse_pos.x - 1) / 2) - app.size / 2 + 1) * 2 - 1
mut y_start := app.mouse_pos.y - app.size / 2
mut x_end := x_start + app.size * 2 - 1
mut y_end := y_start + app.size - 1
if x_start < 1 {
x_start = 1
}
if y_start < 4 {
y_start = 4
}
if x_end > app.ui.window_width {
x_end = app.ui.window_width
}
if y_end > app.ui.window_height - 5 {
y_end = app.ui.window_height - 5
}
app.ui.draw_rect(x_start, y_start, x_end, y_end)
} else {
app.ui.draw_text(app.mouse_pos.x, app.mouse_pos.y, space)
}
app.ui.reset()
}
fn (mut app App) draw_header() {
if app.msg != '' {
app.ui.set_color(
r: 0
g: 0
b: 0
)
app.ui.set_bg_color(
r: 220
g: 220
b: 220
)
app.ui.draw_text(0, 0, ' ${app.msg} ')
app.ui.reset()
}
//'tick: ${app.ui.frame_count} | ' +
app.ui.draw_text(3, 2,
'terminal size: (${app.ui.window_width}, ${app.ui.window_height}) | primary color: ${app.primary_color.hex()} | secondary color: ${app.secondary_color.hex()}')
app.ui.horizontal_separator(3)
}
fn (mut app App) draw_footer() {
_, wh := app.ui.window_width, app.ui.window_height
app.ui.horizontal_separator(wh - 4)
for i, color_row in colors {
for j, color in color_row {
x := j * 3 + 19
y := wh - 3 + i
app.ui.set_bg_color(color)
if app.primary_color_idx == j + (i * 19) {
app.ui.set_color(r: 0, g: 0, b: 0)
app.ui.draw_text(x, y, '><')
app.ui.reset_color()
} else if app.secondary_color_idx == j + (i * 19) {
app.ui.set_color(r: 255, g: 255, b: 255)
app.ui.draw_text(x, y, '><')
app.ui.reset_color()
} else {
app.ui.draw_rect(x, y, x + 1, y)
}
}
}
app.ui.reset_bg_color()
app.ui.draw_text(3, wh - 3, select_color)
app.ui.bold()
app.ui.draw_text(3, wh - 1, '${select_size} ${app.size}')
app.ui.reset()
// TODO: help button
// if ww >= 90 {
// app.ui.draw_text(80, wh - 3, help_1)
// app.ui.draw_text(80, wh - 2, help_2)
// app.ui.draw_text(80, wh - 1, help_3)
// }
}
@[inline]
fn (mut app App) inc_size() {
if app.size < 30 {
app.size++
}
app.show_msg('inc. size: ${app.size}', 1)
}
@[inline]
fn (mut app App) dec_size() {
if app.size > 1 {
app.size--
}
app.show_msg('dec. size: ${app.size}', 1)
}
fn (mut app App) footer_click(event &ui.Event) {
footer_y := 3 - (app.ui.window_height - event.y)
match event.x {
8...11 {
app.inc_size()
}
12...15 {
app.dec_size()
}
18...75 {
if (event.x % 3) == 0 {
// Inside the gap between tiles
return
}
idx := footer_y * 19 - 6 + event.x / 3
if idx < 0 || idx > 56 {
return
}
app.select_color(event.button == .left, idx)
}
else {}
}
}
fn (mut app App) show_msg(text string, time int) {
frames := time * frame_rate
app.msg_hide_tick = if time > 0 { int(app.ui.frame_count) + frames } else { -1 }
app.msg = text
}
+654
View File
@@ -0,0 +1,654 @@
// Copyright (c) 2020 Lars Pontoppidan. All rights reserved.
// Use of this source code is governed by the MIT license distributed with this software.
// Don't use this editor for any serious work.
// A lot of functionality is missing compared to your favourite editor :)
import strings
import os
import math
import term.ui as tui
import encoding.utf8
import encoding.utf8.east_asian
const rune_digits = [`0`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`]
const zero_width_unicode = [
`\u034f`, // U+034F COMBINING GRAPHEME JOINER
`\u061c`, // U+061C ARABIC LETTER MARK
`\u17b4`, // U+17B4 KHMER VOWEL INHERENT AQ
`\u17b5`, // U+17B5 KHMER VOWEL INHERENT AA
`\u200a`, // U+200A HAIR SPACE
`\u200b`, // U+200B ZERO WIDTH SPACE
`\u200c`, // U+200C ZERO WIDTH NON-JOINER
`\u200d`, // U+200D ZERO WIDTH JOINER
`\u200e`, // U+200E LEFT-TO-RIGHT MARK
`\u200f`, // U+200F RIGHT-TO-LEFT MARK
`\u2060`, // U+2060 WORD JOINER
`\u2061`, // U+2061 FUNCTION APPLICATION
`\u2062`, // U+2062 INVISIBLE TIMES
`\u2063`, // U+2063 INVISIBLE SEPARATOR
`\u2064`, // U+2064 INVISIBLE PLUS
`\u206a`, // U+206A INHIBIT SYMMETRIC SWAPPING
`\u206b`, // U+206B ACTIVATE SYMMETRIC SWAPPING
`\u206c`, // U+206C INHIBIT ARABIC FORM SHAPING
`\u206d`, // U+206D ACTIVATE ARABIC FORM SHAPING
`\u206e`, // U+206E NATIONAL DIGIT SHAPES
`\u206f`, // U+206F NOMINAL DIGIT SHAPES
`\ufeff`, // U+FEFF ZERO WIDTH NO-BREAK SPACE
]
enum Movement {
up
down
left
right
home
end
page_up
page_down
}
struct View {
pub:
raw string
cursor Cursor
}
struct App {
mut:
tui &tui.Context = unsafe { nil }
ed &Buffer = unsafe { nil }
current_file int
files []string
status string
t int
magnet_x int
footer_height int = 2
viewport int
}
fn (mut a App) set_status(msg string, duration_ms int) {
a.status = msg
a.t = duration_ms
}
fn (mut a App) save() {
if a.cfile().len > 0 {
b := a.ed
os.write_file(a.cfile(), b.raw()) or { panic(err) }
a.set_status('Saved', 2000)
} else {
a.set_status('No file loaded', 4000)
}
}
fn (mut a App) cfile() string {
if a.files.len == 0 {
return ''
}
if a.current_file >= a.files.len {
return ''
}
return a.files[a.current_file]
}
fn (mut a App) visit_prev_file() {
if a.files.len == 0 {
a.current_file = 0
} else {
a.current_file = (a.current_file + a.files.len - 1) % a.files.len
}
a.init_file()
}
fn (mut a App) visit_next_file() {
if a.files.len == 0 {
a.current_file = 0
} else {
a.current_file = (a.current_file + a.files.len + 1) % a.files.len
}
a.init_file()
}
fn (mut a App) footer() {
w, h := a.tui.window_width, a.tui.window_height
mut b := a.ed
// flat := b.flat()
// snip := if flat.len > 19 { flat[..20] } else { flat }
finfo := if a.cfile().len > 0 { ' (' + os.file_name(a.cfile()) + ')' } else { '' }
mut status := a.status
a.tui.draw_text(0, h - 1, '─'.repeat(w))
footer := '${finfo} Line ${b.cursor.pos_y + 1:4}/${b.lines.len:-4}, Column ${b.cursor.pos_x + 1:3}/${b.cur_line().len:-3} index: ${b.cursor_index():5} (ESC = quit, Ctrl+s = save)'
if footer.len < w {
a.tui.draw_text((w - footer.len) / 2, h, footer)
} else if footer.len == w {
a.tui.draw_text(0, h, footer)
} else {
a.tui.draw_text(0, h, footer[..w])
}
if a.t <= 0 {
status = ''
} else {
a.tui.set_bg_color(
r: 200
g: 200
b: 200
)
a.tui.set_color(
r: 0
g: 0
b: 0
)
a.tui.draw_text((w + 4 - status.len) / 2, h - 1, ' ${status} ')
a.tui.reset()
a.t -= 33
}
}
struct Buffer {
tab_width int = 4
pub mut:
lines []string
cursor Cursor
}
fn (b Buffer) flat() string {
return b.raw().replace_each(['\n', r'\n', '\t', r'\t'])
}
fn (b Buffer) raw() string {
return b.lines.join('\n')
}
fn (b Buffer) view(from int, to int) View {
l := b.cur_line().runes()
mut x := 0
for i := 0; i < b.cursor.pos_x && i < l.len; i++ {
if l[i] == `\t` {
x += b.tab_width
continue
}
x++
}
mut lines := []string{}
for i, line in b.lines {
if i >= from && i <= to {
lines << line
}
}
raw := lines.join('\n')
return View{
raw: raw.replace('\t', strings.repeat(` `, b.tab_width))
cursor: Cursor{
pos_x: x
pos_y: b.cursor.pos_y
}
}
}
fn (b Buffer) line(i int) string {
if i < 0 || i >= b.lines.len {
return ''
}
return b.lines[i]
}
fn (b Buffer) cur_line() string {
return b.line(b.cursor.pos_y)
}
fn (b Buffer) cur_slice() string {
line := b.line(b.cursor.pos_y).runes()
if b.cursor.pos_x == 0 || b.cursor.pos_x > line.len {
return ''
}
return line[..b.cursor.pos_x].string()
}
fn (b Buffer) cursor_index() int {
mut i := 0
for y, line in b.lines {
if b.cursor.pos_y == y {
i += b.cursor.pos_x
break
}
i += line.runes().len + 1
}
return i
}
fn (mut b Buffer) put(s string) {
has_line_ending := s.contains('\n')
x, y := b.cursor.xy()
if b.lines.len == 0 {
b.lines.prepend('')
}
line := b.lines[y].runes()
l, r := line[..x].string(), line[x..].string()
if has_line_ending {
mut lines := s.split('\n')
lines[0] = l + lines[0]
lines[lines.len - 1] += r
b.lines.delete(y)
b.lines.insert(y, lines)
last := lines[lines.len - 1].runes()
b.cursor.set(last.len, y + lines.len - 1)
if s == '\n' {
b.cursor.set(0, b.cursor.pos_y)
}
} else {
b.lines[y] = l + s + r
b.cursor.set(x + s.runes().len, y)
}
$if debug {
flat := s.replace('\n', r'\n')
eprintln(@MOD + '.' + @STRUCT + '::' + @FN + ' "${flat}"')
}
}
fn (mut b Buffer) del(amount int) string {
if amount == 0 {
return ''
}
x, y := b.cursor.xy()
if amount < 0 { // don't delete left if we're at 0,0
if x == 0 && y == 0 {
return ''
}
} else if x >= b.cur_line().runes().len && y >= b.lines.len - 1 {
return ''
}
mut removed := ''
if amount < 0 { // backspace (backward)
i := b.cursor_index()
raw_runes := b.raw().runes()
removed = raw_runes[i + amount..i].string()
mut left := amount * -1
for li := y; li >= 0 && left > 0; li-- {
ln := b.lines[li].runes()
if left == ln.len + 1 { // All of the line + 1 - since we're going backwards the "+1" is the line break delimiter.
b.lines.delete(li)
left = 0
if y == 0 {
return ''
}
line_above := b.lines[li - 1].runes()
b.cursor.pos_x = line_above.len
b.cursor.pos_y--
break
} else if left > ln.len {
b.lines.delete(li)
if ln.len == 0 { // line break delimiter
left--
if y == 0 {
return ''
}
line_above := b.lines[li - 1].runes()
b.cursor.pos_x = line_above.len
} else {
left -= ln.len
}
b.cursor.pos_y--
} else {
if x == 0 {
if y == 0 {
return ''
}
line_above := b.lines[li - 1].runes()
if ln.len == 0 { // at line break
b.lines.delete(li)
b.cursor.pos_y--
b.cursor.pos_x = line_above.len
} else {
b.lines[li - 1] = line_above.string() + ln.string()
b.lines.delete(li)
b.cursor.pos_y--
b.cursor.pos_x = line_above.len
}
} else if x == 1 {
runes := b.lines[li].runes()
b.lines[li] = runes[left..].string()
b.cursor.pos_x = 0
} else {
b.lines[li] = ln[..x - left].string() + ln[x..].string()
b.cursor.pos_x -= left
}
left = 0
break
}
}
} else { // delete (forward)
i := b.cursor_index() + 1
raw_buffer := b.raw().runes()
from_i := i
mut to_i := i + amount
if to_i > raw_buffer.len {
to_i = raw_buffer.len
}
removed = raw_buffer[from_i..to_i].string()
mut left := amount
for li := y; li >= 0 && left > 0; li++ {
ln := b.lines[li].runes()
if x == ln.len { // at line end
if y + 1 <= b.lines.len {
b.lines[li] = ln.string() + b.lines[y + 1]
b.lines.delete(y + 1)
left--
b.del(left)
}
} else if left > ln.len {
b.lines.delete(li)
left -= ln.len
} else {
b.lines[li] = ln[..x].string() + ln[x + left..].string()
left = 0
}
}
}
$if debug {
flat := removed.replace('\n', r'\n')
eprintln(@MOD + '.' + @STRUCT + '::' + @FN + ' "${flat}"')
}
return removed
}
fn (mut b Buffer) free() {
$if debug {
eprintln(@MOD + '.' + @STRUCT + '::' + @FN)
}
for line in b.lines {
unsafe { line.free() }
}
unsafe { b.lines.free() }
}
fn (mut b Buffer) move_updown(amount int) {
b.cursor.move(0, amount)
// Check the move
line := b.cur_line().runes()
if b.cursor.pos_x > line.len {
b.cursor.set(line.len, b.cursor.pos_y)
}
}
// move_cursor will navigate the cursor within the buffer bounds
fn (mut b Buffer) move_cursor(amount int, movement Movement) {
cur_line := b.cur_line().runes()
match movement {
.up {
if b.cursor.pos_y - amount >= 0 {
b.move_updown(-amount)
}
}
.down {
if b.cursor.pos_y + amount < b.lines.len {
b.move_updown(amount)
}
}
.page_up {
dlines := math.min(b.cursor.pos_y, amount)
b.move_updown(-dlines)
}
.page_down {
dlines := math.min(b.lines.len - 1, b.cursor.pos_y + amount) - b.cursor.pos_y
b.move_updown(dlines)
}
.left {
if b.cursor.pos_x - amount >= 0 {
b.cursor.move(-amount, 0)
} else if b.cursor.pos_y > 0 {
b.cursor.set(b.line(b.cursor.pos_y - 1).runes().len, b.cursor.pos_y - 1)
}
}
.right {
if b.cursor.pos_x + amount <= cur_line.len {
b.cursor.move(amount, 0)
} else if b.cursor.pos_y + 1 < b.lines.len {
b.cursor.set(0, b.cursor.pos_y + 1)
}
}
.home {
b.cursor.set(0, b.cursor.pos_y)
}
.end {
b.cursor.set(cur_line.len, b.cursor.pos_y)
}
}
}
fn (mut b Buffer) move_to_word(movement Movement) {
a := if movement == .left { -1 } else { 1 }
mut line := b.cur_line().runes()
mut x, mut y := b.cursor.pos_x, b.cursor.pos_y
if x + a < 0 && y > 0 {
y--
line = b.line(b.cursor.pos_y - 1).runes()
x = line.len
} else if x + a >= line.len && y + 1 < b.lines.len {
y++
line = b.line(b.cursor.pos_y + 1).runes()
x = 0
}
// first, move past all non-`a-zA-Z0-9_` characters
for x + a >= 0 && x + a < line.len && !(utf8.is_letter(line[x + a])
|| line[x + a] in rune_digits || line[x + a] == `_`) {
x += a
}
// then, move past all the letters and numbers
for x + a >= 0 && x + a < line.len && (utf8.is_letter(line[x + a])
|| line[x + a] in rune_digits || line[x + a] == `_`) {
x += a
}
// if the cursor is out of bounds, move it to the next/previous line
if x + a >= 0 && x + a <= line.len {
x += a
} else if a < 0 && y + 1 > b.lines.len && y - 1 >= 0 {
y += a
x = 0
}
b.cursor.set(x, y)
}
struct Cursor {
pub mut:
pos_x int
pos_y int
}
fn (mut c Cursor) set(x int, y int) {
c.pos_x = x
c.pos_y = y
}
fn (mut c Cursor) move(x int, y int) {
c.pos_x += x
c.pos_y += y
}
fn (c Cursor) xy() (int, int) {
return c.pos_x, c.pos_y
}
// App callbacks
fn init(mut app App) {
app.init_file()
}
fn (mut a App) init_file() {
a.ed = &Buffer{}
mut init_y := 0
mut init_x := 0
if a.files.len > 0 && a.current_file < a.files.len && a.files[a.current_file].len > 0 {
if !os.is_file(a.files[a.current_file]) && a.files[a.current_file].contains(':') {
// support the file:line:col: format
fparts := a.files[a.current_file].split(':')
if fparts.len > 0 {
a.files[a.current_file] = fparts[0]
}
if fparts.len > 1 {
init_y = fparts[1].int() - 1
}
if fparts.len > 2 {
init_x = fparts[2].int() - 1
}
}
if os.is_file(a.files[a.current_file]) {
// 'vico: ' +
a.tui.set_window_title(a.files[a.current_file])
mut b := a.ed
content := os.read_file(a.files[a.current_file]) or { panic(err) }
b.put(content)
a.ed.cursor.pos_x = init_x
a.ed.cursor.pos_y = init_y
}
}
}
fn (a &App) view_height() int {
return a.tui.window_height - a.footer_height - 1
}
// magnet_cursor_x will place the cursor as close to it's last move left or right as possible
fn (mut a App) magnet_cursor_x() {
mut buffer := a.ed
if buffer.cursor.pos_x < a.magnet_x {
if a.magnet_x < buffer.cur_line().runes().len {
move_x := a.magnet_x - buffer.cursor.pos_x
buffer.move_cursor(move_x, .right)
}
}
}
fn frame(mut a App) {
mut ed := a.ed
a.tui.clear()
scroll_limit := a.view_height()
// scroll down
if ed.cursor.pos_y > a.viewport + scroll_limit { // scroll down
a.viewport = ed.cursor.pos_y - scroll_limit
} else if ed.cursor.pos_y < a.viewport { // scroll up
a.viewport = ed.cursor.pos_y
}
view := ed.view(a.viewport, scroll_limit + a.viewport)
a.tui.draw_text(0, 0, view.raw)
a.footer()
// Unicode: Handle correct mapping of cursor X position in terminal.
mut ch_x := view.cursor.pos_x
mut sl := ed.cur_slice().replace('\t', ' '.repeat(ed.tab_width))
if sl.len > 0 {
// Strip out any zero-width codepoints.
sl = sl.runes().filter(it !in zero_width_unicode).string()
ch_x = east_asian.display_width(sl, 1)
}
a.tui.set_cursor_position(ch_x + 1, ed.cursor.pos_y + 1 - a.viewport)
a.tui.flush()
}
fn event(e &tui.Event, mut a App) {
mut buffer := a.ed
if e.typ == .key_down {
match e.code {
.escape {
exit(0)
}
.enter {
buffer.put('\n')
}
.backspace {
buffer.del(-1)
}
.delete {
buffer.del(1)
}
.left {
if e.modifiers == .ctrl {
buffer.move_to_word(.left)
} else if e.modifiers.is_empty() {
buffer.move_cursor(1, .left)
}
a.magnet_x = buffer.cursor.pos_x
}
.right {
if e.modifiers == .ctrl {
buffer.move_to_word(.right)
} else if e.modifiers.is_empty() {
buffer.move_cursor(1, .right)
}
a.magnet_x = buffer.cursor.pos_x
}
.up {
buffer.move_cursor(1, .up)
a.magnet_cursor_x()
}
.down {
buffer.move_cursor(1, .down)
a.magnet_cursor_x()
}
.page_up {
buffer.move_cursor(a.view_height(), .page_up)
}
.page_down {
buffer.move_cursor(a.view_height(), .page_down)
}
.home {
buffer.move_cursor(1, .home)
}
.end {
buffer.move_cursor(1, .end)
}
48...57, 97...122 { // 0-9a-zA-Z
if e.modifiers == .ctrl {
if e.code == .s {
a.save()
}
} else if !(e.modifiers.has(.ctrl | .alt) || e.code == .null) {
buffer.put(e.ascii.ascii_str())
}
}
else {
if e.modifiers == .alt {
if e.code == .comma {
a.visit_prev_file()
return
}
if e.code == .period {
a.visit_next_file()
return
}
}
buffer.put(e.utf8)
}
}
} else if e.typ == .mouse_scroll {
direction := if e.direction == .up { Movement.down } else { Movement.up }
buffer.move_cursor(1, direction)
}
}
type InitFn = fn (voidptr)
type EventFn = fn (&tui.Event, voidptr)
type FrameFn = fn (voidptr)
fn main() {
mut files := []string{}
if os.args.len > 1 {
files << os.args[1..]
}
mut a := &App{
files: files
}
a.tui = tui.init(
user_data: a
init_fn: InitFn(init)
frame_fn: FrameFn(frame)
event_fn: EventFn(event)
capture_events: true
)
a.tui.run()!
}
+478
View File
@@ -0,0 +1,478 @@
// import modules for use in app
import term.ui as termui
import rand
import math.vec
// define some global constants
const block_size = 1
const buffer = 10
const green = termui.Color{0, 255, 0}
const grey = termui.Color{150, 150, 150}
const white = termui.Color{255, 255, 255}
const blue = termui.Color{0, 0, 255}
const red = termui.Color{255, 0, 0}
const black = termui.Color{0, 0, 0}
// what edge of the screen are you facing
enum Orientation {
top
right
bottom
left
}
// what's the current state of the game
enum GameState {
pause
gameover
game
oob // snake out-of-bounds
}
// simple 2d vector representation
type Vec = vec.Vec2[int]
// determine orientation from vector (hacky way to set facing from velocity)
fn (v Vec) facing() Orientation {
result := if v.x >= 0 {
Orientation.right
} else if v.x < 0 {
Orientation.left
} else if v.y >= 0 {
Orientation.bottom
} else {
Orientation.top
}
return result
}
// generate a random vector with x in [min_x, max_x] and y in [min_y, max_y]
fn (mut v Vec) randomize(min_x int, min_y int, max_x int, max_y int) {
v.x = rand.int_in_range(min_x, max_x) or { min_x }
v.y = rand.int_in_range(min_y, max_y) or { min_y }
}
// part of snake's body representation
struct BodyPart {
mut:
pos Vec = Vec{block_size, block_size}
color termui.Color = green
facing Orientation = .top
}
// snake representation
struct Snake {
mut:
app &App = unsafe { nil }
direction Orientation
body []BodyPart
velocity Vec
}
// length returns the snake's current length
fn (s Snake) length() int {
return s.body.len
}
// impulse provides a impulse to change the snake's direction
fn (mut s Snake) impulse(direction Orientation) {
mut vel := Vec{}
match direction {
.top {
vel.x = 0
vel.y = -1 * block_size
}
.right {
vel.x = 2 * block_size
vel.y = 0
}
.bottom {
vel.x = 0
vel.y = block_size
}
.left {
vel.x = -2 * block_size
vel.y = 0
}
}
s.direction = direction
s.velocity = vel
}
// move performs the calculations for the snake's movements
fn (mut s Snake) move() {
mut i := s.body.len - 1
width := s.app.width
height := s.app.height
// move the parts of the snake as appropriate
for i = s.body.len - 1; i >= 0; i-- {
mut piece := s.body[i]
if i > 0 { // just move the body of the snake up one position
piece.pos = s.body[i - 1].pos
piece.facing = s.body[i - 1].facing
} else { // verify that the move is valid and move the head if so
piece.facing = s.direction
new_x := piece.pos.x + s.velocity.x
new_y := piece.pos.y + s.velocity.y
piece.pos.x += if new_x > block_size && new_x < width - block_size {
s.velocity.x
} else {
0
}
piece.pos.y += if new_y > block_size && new_y < height - block_size {
s.velocity.y
} else {
0
}
}
s.body[i] = piece
}
}
// grow add another part to the snake when it catches the rat
fn (mut s Snake) grow() {
head := s.get_tail()
mut pos := Vec{}
// add the segment on the opposite side of the previous tail
match head.facing {
.bottom {
pos.x = head.pos.x
pos.y = head.pos.y - block_size
}
.left {
pos.x = head.pos.x + block_size
pos.y = head.pos.y
}
.top {
pos.x = head.pos.x
pos.y = head.pos.y + block_size
}
.right {
pos.x = head.pos.x - block_size
pos.y = head.pos.y
}
}
s.body << BodyPart{
pos: pos
facing: head.facing
}
}
// get_body gets the parts of the snakes body
fn (s Snake) get_body() []BodyPart {
return s.body
}
// get_head get snake's head
fn (s Snake) get_head() BodyPart {
return s.body[0]
}
// get_tail get snake's tail
fn (s Snake) get_tail() BodyPart {
return s.body[s.body.len - 1]
}
// randomize randomizes position and veolcity of snake
fn (mut s Snake) randomize() {
speeds := [-2, 0, 2]
mut pos := s.get_head().pos
pos.randomize(buffer, buffer, s.app.width - buffer, s.app.height - buffer)
for pos.x % 2 != 0 || (pos.x < buffer && pos.x > s.app.width - buffer) {
pos.randomize(buffer, buffer, s.app.width - buffer, s.app.height - buffer)
}
s.velocity.y = rand.int_in_range(-1 * block_size, block_size) or { 0 }
s.velocity.x = speeds[rand.intn(speeds.len) or { 0 }]
s.direction = s.velocity.facing()
s.body[0].pos = pos
}
// check_overlap determine if the snake's looped onto itself
fn (s Snake) check_overlap() bool {
h := s.get_head()
head_pos := h.pos
for i in 2 .. s.length() {
piece_pos := s.body[i].pos
if head_pos.x == piece_pos.x && head_pos.y == piece_pos.y {
return true
}
}
return false
}
fn (s Snake) check_out_of_bounds() bool {
h := s.get_head()
return h.pos.x + s.velocity.x <= block_size
|| h.pos.x + s.velocity.x > s.app.width - s.velocity.x
|| h.pos.y + s.velocity.y <= block_size
|| h.pos.y + s.velocity.y > s.app.height - block_size - s.velocity.y
}
// draw draws the parts of the snake
fn (s Snake) draw() {
mut a := s.app
for part in s.get_body() {
a.termui.set_bg_color(part.color)
a.termui.draw_rect(part.pos.x, part.pos.y, part.pos.x + block_size, part.pos.y + block_size)
$if verbose ? {
text := match part.facing {
.top { '^' }
.bottom { 'v' }
.right { '>' }
.left { '<' }
}
a.termui.set_color(white)
a.termui.draw_text(part.pos.x, part.pos.y, text)
}
}
}
// rat representation
struct Rat {
mut:
pos Vec = Vec{block_size, block_size}
captured bool
color termui.Color = grey
app &App = unsafe { nil }
}
// randomize spawn the rat in a new spot within the playable field
fn (mut r Rat) randomize() {
r.pos.randomize(2 * block_size + buffer, 2 * block_size + buffer, r.app.width - block_size -
buffer, r.app.height - block_size - buffer)
}
@[heap]
struct App {
mut:
termui &termui.Context = unsafe { nil }
snake Snake
rat Rat
width int
height int
redraw bool = true
state GameState = .game
}
// new_game setups the rat and snake for play
fn (mut a App) new_game() {
mut snake := Snake{
body: []BodyPart{len: 1, init: BodyPart{}}
app: a
}
snake.randomize()
mut rat := Rat{
app: a
}
rat.randomize()
a.snake = snake
a.rat = rat
a.state = .game
a.redraw = true
}
// initialize the app and record the width and height of the window
fn init(mut app App) {
w, h := app.termui.window_width, app.termui.window_height
app.width = w
app.height = h
app.new_game()
}
// event handles different events for the app as they occur
fn event(e &termui.Event, mut app App) {
match e.typ {
.mouse_down {}
.mouse_drag {}
.mouse_up {}
.key_down {
match e.code {
.up, .w { app.move_snake(.top) }
.down, .s { app.move_snake(.bottom) }
.left, .a { app.move_snake(.left) }
.right, .d { app.move_snake(.right) }
.r { app.new_game() }
.c {}
.p { app.state = if app.state == .game { GameState.pause } else { GameState.game } }
.escape, .q { exit(0) }
else { exit(0) }
}
if e.code == .c {
} else if e.code == .escape {
exit(0)
}
}
else {}
}
app.redraw = true
}
// frame perform actions on every tick
fn frame(mut app App) {
app.update()
app.draw()
}
// update perform any calculations that are needed before drawing
fn (mut a App) update() {
if a.state == .game {
a.snake.move()
if a.snake.check_out_of_bounds() {
$if verbose ? {
a.snake.body[0].color = red
} $else {
a.state = .oob
}
}
if a.snake.check_overlap() {
a.state = .gameover
return
}
if a.check_capture() {
a.rat.randomize()
a.snake.grow()
}
}
}
// draw write to the screen
fn (mut a App) draw() {
// reset screen
a.termui.clear()
a.termui.set_bg_color(white)
a.termui.draw_empty_rect(1, 1, a.width, a.height)
// determine if a special screen needs to be draw
match a.state {
.gameover {
a.draw_gameover()
a.redraw = false
}
.pause {
a.draw_pause()
}
else {
a.redraw = true
}
}
a.termui.set_color(blue)
a.termui.set_bg_color(white)
a.termui.draw_text(3 * block_size, a.height - (2 * block_size),
'p - (un)pause r - reset q - quit')
// draw the snake, rat, and score if appropriate
if a.redraw {
a.termui.set_bg_color(black)
a.draw_gamescreen()
if a.state == .oob {
a.state = .gameover
}
}
// write to the screen
a.termui.reset_bg_color()
a.termui.flush()
}
// move_snake move the snake in specified direction
fn (mut a App) move_snake(direction Orientation) {
a.snake.impulse(direction)
}
// check_capture determine if the snake overlaps with the rat
fn (a App) check_capture() bool {
snake_pos := a.snake.get_head().pos
rat_pos := a.rat.pos
return snake_pos.x <= rat_pos.x + block_size && snake_pos.x + block_size >= rat_pos.x
&& snake_pos.y <= rat_pos.y + block_size && snake_pos.y + block_size >= rat_pos.y
}
fn (mut a App) draw_snake() {
a.snake.draw()
}
fn (mut a App) draw_rat() {
a.termui.set_bg_color(a.rat.color)
a.termui.draw_rect(a.rat.pos.x, a.rat.pos.y, a.rat.pos.x + block_size, a.rat.pos.y + block_size)
}
fn (mut a App) draw_gamescreen() {
$if verbose ? {
a.draw_debug()
}
a.draw_score()
a.draw_rat()
a.draw_snake()
}
fn (mut a App) draw_score() {
a.termui.set_color(blue)
a.termui.set_bg_color(white)
score := a.snake.length() - 1
a.termui.draw_text(a.width - (2 * block_size), block_size, '${score:03d}')
}
fn (mut a App) draw_pause() {
a.termui.set_color(blue)
a.termui.draw_text((a.width / 2) - block_size, 3 * block_size, 'Paused!')
}
fn (mut a App) draw_debug() {
a.termui.set_color(blue)
a.termui.set_bg_color(white)
snake := a.snake
a.termui.draw_text(block_size, 1 * block_size,
'Display_width: ${a.width:04d} Display_height: ${a.height:04d}')
a.termui.draw_text(block_size, 2 * block_size,
'Vx: ${snake.velocity.x:+02d} Vy: ${snake.velocity.y:+02d}')
a.termui.draw_text(block_size, 3 * block_size, 'F: ${snake.direction}')
snake_head := snake.get_head()
rat := a.rat
a.termui.draw_text(block_size, 4 * block_size,
'Sx: ${snake_head.pos.x:+03d} Sy: ${snake_head.pos.y:+03d}')
a.termui.draw_text(block_size, 5 * block_size, 'Rx: ${rat.pos.x:+03d} Ry: ${rat.pos.y:+03d}')
}
fn (mut a App) draw_gameover() {
a.termui.set_bg_color(white)
a.termui.set_color(red)
a.rat.pos = Vec{-1, -1}
x_offset := ' ##### '.len // take half of a line from the game over text and store the length
start_x := (a.width / 2) - x_offset
a.termui.draw_text(start_x, (a.height / 2) - 3 * block_size,
' ##### ####### ')
a.termui.draw_text(start_x, (a.height / 2) - 2 * block_size,
' # # ## # # ###### # # # # ###### ##### ')
a.termui.draw_text(start_x, (a.height / 2) - 1 * block_size,
' # # # ## ## # # # # # # # # ')
a.termui.draw_text(start_x, (a.height / 2) - 0 * block_size,
' # #### # # # ## # ##### # # # # ##### # # ')
a.termui.draw_text(start_x, (a.height / 2) + 1 * block_size,
' # # ###### # # # # # # # # ##### ')
a.termui.draw_text(start_x, (a.height / 2) + 2 * block_size,
' # # # # # # # # # # # # # # ')
a.termui.draw_text(start_x, (a.height / 2) + 3 * block_size,
' ##### # # # # ###### ####### ## ###### # # ')
}
type InitFn = fn (voidptr)
type EventFn = fn (&termui.Event, voidptr)
type FrameFn = fn (voidptr)
fn main() {
mut app := &App{}
app.termui = termui.init(
user_data: app
event_fn: EventFn(event)
frame_fn: FrameFn(frame)
init_fn: InitFn(init)
hide_cursor: true
frame_rate: 10
)
app.termui.run()!
}