| 1 | // Copyright (c) 2020 Lars Pontoppidan. All rights reserved. |
| 2 | // Use of this source code is governed by the MIT license distributed with this software. |
| 3 | // Don't use this editor for any serious work. |
| 4 | // A lot of functionality is missing compared to your favourite editor :) |
| 5 | import strings |
| 6 | import os |
| 7 | import math |
| 8 | import term.ui as tui |
| 9 | import encoding.utf8 |
| 10 | import encoding.utf8.east_asian |
| 11 | |
| 12 | const rune_digits = [`0`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`] |
| 13 | |
| 14 | const zero_width_unicode = [ |
| 15 | `\u034f`, // U+034F COMBINING GRAPHEME JOINER |
| 16 | `\u061c`, // U+061C ARABIC LETTER MARK |
| 17 | `\u17b4`, // U+17B4 KHMER VOWEL INHERENT AQ |
| 18 | `\u17b5`, // U+17B5 KHMER VOWEL INHERENT AA |
| 19 | `\u200a`, // U+200A HAIR SPACE |
| 20 | `\u200b`, // U+200B ZERO WIDTH SPACE |
| 21 | `\u200c`, // U+200C ZERO WIDTH NON-JOINER |
| 22 | `\u200d`, // U+200D ZERO WIDTH JOINER |
| 23 | `\u200e`, // U+200E LEFT-TO-RIGHT MARK |
| 24 | `\u200f`, // U+200F RIGHT-TO-LEFT MARK |
| 25 | `\u2060`, // U+2060 WORD JOINER |
| 26 | `\u2061`, // U+2061 FUNCTION APPLICATION |
| 27 | `\u2062`, // U+2062 INVISIBLE TIMES |
| 28 | `\u2063`, // U+2063 INVISIBLE SEPARATOR |
| 29 | `\u2064`, // U+2064 INVISIBLE PLUS |
| 30 | `\u206a`, // U+206A INHIBIT SYMMETRIC SWAPPING |
| 31 | `\u206b`, // U+206B ACTIVATE SYMMETRIC SWAPPING |
| 32 | `\u206c`, // U+206C INHIBIT ARABIC FORM SHAPING |
| 33 | `\u206d`, // U+206D ACTIVATE ARABIC FORM SHAPING |
| 34 | `\u206e`, // U+206E NATIONAL DIGIT SHAPES |
| 35 | `\u206f`, // U+206F NOMINAL DIGIT SHAPES |
| 36 | `\ufeff`, // U+FEFF ZERO WIDTH NO-BREAK SPACE |
| 37 | ] |
| 38 | |
| 39 | enum Movement { |
| 40 | up |
| 41 | down |
| 42 | left |
| 43 | right |
| 44 | home |
| 45 | end |
| 46 | page_up |
| 47 | page_down |
| 48 | } |
| 49 | |
| 50 | struct View { |
| 51 | pub: |
| 52 | raw string |
| 53 | cursor Cursor |
| 54 | } |
| 55 | |
| 56 | struct App { |
| 57 | mut: |
| 58 | tui &tui.Context = unsafe { nil } |
| 59 | ed &Buffer = unsafe { nil } |
| 60 | current_file int |
| 61 | files []string |
| 62 | status string |
| 63 | t int |
| 64 | magnet_x int |
| 65 | footer_height int = 2 |
| 66 | viewport int |
| 67 | } |
| 68 | |
| 69 | fn (mut a App) set_status(msg string, duration_ms int) { |
| 70 | a.status = msg |
| 71 | a.t = duration_ms |
| 72 | } |
| 73 | |
| 74 | fn (mut a App) save() { |
| 75 | if a.cfile().len > 0 { |
| 76 | b := a.ed |
| 77 | os.write_file(a.cfile(), b.raw()) or { panic(err) } |
| 78 | a.set_status('Saved', 2000) |
| 79 | } else { |
| 80 | a.set_status('No file loaded', 4000) |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | fn (mut a App) cfile() string { |
| 85 | if a.files.len == 0 { |
| 86 | return '' |
| 87 | } |
| 88 | if a.current_file >= a.files.len { |
| 89 | return '' |
| 90 | } |
| 91 | return a.files[a.current_file] |
| 92 | } |
| 93 | |
| 94 | fn (mut a App) visit_prev_file() { |
| 95 | if a.files.len == 0 { |
| 96 | a.current_file = 0 |
| 97 | } else { |
| 98 | a.current_file = (a.current_file + a.files.len - 1) % a.files.len |
| 99 | } |
| 100 | a.init_file() |
| 101 | } |
| 102 | |
| 103 | fn (mut a App) visit_next_file() { |
| 104 | if a.files.len == 0 { |
| 105 | a.current_file = 0 |
| 106 | } else { |
| 107 | a.current_file = (a.current_file + a.files.len + 1) % a.files.len |
| 108 | } |
| 109 | a.init_file() |
| 110 | } |
| 111 | |
| 112 | fn (mut a App) footer() { |
| 113 | w, h := a.tui.window_width, a.tui.window_height |
| 114 | mut b := a.ed |
| 115 | // flat := b.flat() |
| 116 | // snip := if flat.len > 19 { flat[..20] } else { flat } |
| 117 | finfo := if a.cfile().len > 0 { ' (' + os.file_name(a.cfile()) + ')' } else { '' } |
| 118 | mut status := a.status |
| 119 | a.tui.draw_text(0, h - 1, '─'.repeat(w)) |
| 120 | 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)' |
| 121 | if footer.len < w { |
| 122 | a.tui.draw_text((w - footer.len) / 2, h, footer) |
| 123 | } else if footer.len == w { |
| 124 | a.tui.draw_text(0, h, footer) |
| 125 | } else { |
| 126 | a.tui.draw_text(0, h, footer[..w]) |
| 127 | } |
| 128 | if a.t <= 0 { |
| 129 | status = '' |
| 130 | } else { |
| 131 | a.tui.set_bg_color( |
| 132 | r: 200 |
| 133 | g: 200 |
| 134 | b: 200 |
| 135 | ) |
| 136 | a.tui.set_color( |
| 137 | r: 0 |
| 138 | g: 0 |
| 139 | b: 0 |
| 140 | ) |
| 141 | a.tui.draw_text((w + 4 - status.len) / 2, h - 1, ' ${status} ') |
| 142 | a.tui.reset() |
| 143 | a.t -= 33 |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | struct Buffer { |
| 148 | tab_width int = 4 |
| 149 | pub mut: |
| 150 | lines []string |
| 151 | cursor Cursor |
| 152 | } |
| 153 | |
| 154 | fn (b Buffer) flat() string { |
| 155 | return b.raw().replace_each(['\n', r'\n', '\t', r'\t']) |
| 156 | } |
| 157 | |
| 158 | fn (b Buffer) raw() string { |
| 159 | return b.lines.join('\n') |
| 160 | } |
| 161 | |
| 162 | fn (b Buffer) view(from int, to int) View { |
| 163 | l := b.cur_line().runes() |
| 164 | mut x := 0 |
| 165 | for i := 0; i < b.cursor.pos_x && i < l.len; i++ { |
| 166 | if l[i] == `\t` { |
| 167 | x += b.tab_width |
| 168 | continue |
| 169 | } |
| 170 | x++ |
| 171 | } |
| 172 | mut lines := []string{} |
| 173 | for i, line in b.lines { |
| 174 | if i >= from && i <= to { |
| 175 | lines << line |
| 176 | } |
| 177 | } |
| 178 | raw := lines.join('\n') |
| 179 | return View{ |
| 180 | raw: raw.replace('\t', strings.repeat(` `, b.tab_width)) |
| 181 | cursor: Cursor{ |
| 182 | pos_x: x |
| 183 | pos_y: b.cursor.pos_y |
| 184 | } |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | fn (b Buffer) line(i int) string { |
| 189 | if i < 0 || i >= b.lines.len { |
| 190 | return '' |
| 191 | } |
| 192 | return b.lines[i] |
| 193 | } |
| 194 | |
| 195 | fn (b Buffer) cur_line() string { |
| 196 | return b.line(b.cursor.pos_y) |
| 197 | } |
| 198 | |
| 199 | fn (b Buffer) cur_slice() string { |
| 200 | line := b.line(b.cursor.pos_y).runes() |
| 201 | if b.cursor.pos_x == 0 || b.cursor.pos_x > line.len { |
| 202 | return '' |
| 203 | } |
| 204 | return line[..b.cursor.pos_x].string() |
| 205 | } |
| 206 | |
| 207 | fn (b Buffer) cursor_index() int { |
| 208 | mut i := 0 |
| 209 | for y, line in b.lines { |
| 210 | if b.cursor.pos_y == y { |
| 211 | i += b.cursor.pos_x |
| 212 | break |
| 213 | } |
| 214 | i += line.runes().len + 1 |
| 215 | } |
| 216 | return i |
| 217 | } |
| 218 | |
| 219 | fn (mut b Buffer) put(s string) { |
| 220 | has_line_ending := s.contains('\n') |
| 221 | x, y := b.cursor.xy() |
| 222 | if b.lines.len == 0 { |
| 223 | b.lines.prepend('') |
| 224 | } |
| 225 | line := b.lines[y].runes() |
| 226 | l, r := line[..x].string(), line[x..].string() |
| 227 | if has_line_ending { |
| 228 | mut lines := s.split('\n') |
| 229 | lines[0] = l + lines[0] |
| 230 | lines[lines.len - 1] += r |
| 231 | b.lines.delete(y) |
| 232 | b.lines.insert(y, lines) |
| 233 | last := lines[lines.len - 1].runes() |
| 234 | b.cursor.set(last.len, y + lines.len - 1) |
| 235 | if s == '\n' { |
| 236 | b.cursor.set(0, b.cursor.pos_y) |
| 237 | } |
| 238 | } else { |
| 239 | b.lines[y] = l + s + r |
| 240 | b.cursor.set(x + s.runes().len, y) |
| 241 | } |
| 242 | $if debug { |
| 243 | flat := s.replace('\n', r'\n') |
| 244 | eprintln(@MOD + '.' + @STRUCT + '::' + @FN + ' "${flat}"') |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | fn (mut b Buffer) del(amount int) string { |
| 249 | if amount == 0 { |
| 250 | return '' |
| 251 | } |
| 252 | x, y := b.cursor.xy() |
| 253 | if amount < 0 { // don't delete left if we're at 0,0 |
| 254 | if x == 0 && y == 0 { |
| 255 | return '' |
| 256 | } |
| 257 | } else if x >= b.cur_line().runes().len && y >= b.lines.len - 1 { |
| 258 | return '' |
| 259 | } |
| 260 | mut removed := '' |
| 261 | if amount < 0 { // backspace (backward) |
| 262 | i := b.cursor_index() |
| 263 | raw_runes := b.raw().runes() |
| 264 | removed = raw_runes[i + amount..i].string() |
| 265 | mut left := amount * -1 |
| 266 | for li := y; li >= 0 && left > 0; li-- { |
| 267 | ln := b.lines[li].runes() |
| 268 | if left == ln.len + 1 { // All of the line + 1 - since we're going backwards the "+1" is the line break delimiter. |
| 269 | b.lines.delete(li) |
| 270 | left = 0 |
| 271 | if y == 0 { |
| 272 | return '' |
| 273 | } |
| 274 | line_above := b.lines[li - 1].runes() |
| 275 | b.cursor.pos_x = line_above.len |
| 276 | b.cursor.pos_y-- |
| 277 | break |
| 278 | } else if left > ln.len { |
| 279 | b.lines.delete(li) |
| 280 | if ln.len == 0 { // line break delimiter |
| 281 | left-- |
| 282 | if y == 0 { |
| 283 | return '' |
| 284 | } |
| 285 | line_above := b.lines[li - 1].runes() |
| 286 | b.cursor.pos_x = line_above.len |
| 287 | } else { |
| 288 | left -= ln.len |
| 289 | } |
| 290 | b.cursor.pos_y-- |
| 291 | } else { |
| 292 | if x == 0 { |
| 293 | if y == 0 { |
| 294 | return '' |
| 295 | } |
| 296 | line_above := b.lines[li - 1].runes() |
| 297 | if ln.len == 0 { // at line break |
| 298 | b.lines.delete(li) |
| 299 | b.cursor.pos_y-- |
| 300 | b.cursor.pos_x = line_above.len |
| 301 | } else { |
| 302 | b.lines[li - 1] = line_above.string() + ln.string() |
| 303 | b.lines.delete(li) |
| 304 | b.cursor.pos_y-- |
| 305 | b.cursor.pos_x = line_above.len |
| 306 | } |
| 307 | } else if x == 1 { |
| 308 | runes := b.lines[li].runes() |
| 309 | b.lines[li] = runes[left..].string() |
| 310 | b.cursor.pos_x = 0 |
| 311 | } else { |
| 312 | b.lines[li] = ln[..x - left].string() + ln[x..].string() |
| 313 | b.cursor.pos_x -= left |
| 314 | } |
| 315 | left = 0 |
| 316 | break |
| 317 | } |
| 318 | } |
| 319 | } else { // delete (forward) |
| 320 | i := b.cursor_index() + 1 |
| 321 | raw_buffer := b.raw().runes() |
| 322 | from_i := i |
| 323 | mut to_i := i + amount |
| 324 | |
| 325 | if to_i > raw_buffer.len { |
| 326 | to_i = raw_buffer.len |
| 327 | } |
| 328 | removed = raw_buffer[from_i..to_i].string() |
| 329 | mut left := amount |
| 330 | for li := y; li >= 0 && left > 0; li++ { |
| 331 | ln := b.lines[li].runes() |
| 332 | if x == ln.len { // at line end |
| 333 | if y + 1 <= b.lines.len { |
| 334 | b.lines[li] = ln.string() + b.lines[y + 1] |
| 335 | b.lines.delete(y + 1) |
| 336 | left-- |
| 337 | b.del(left) |
| 338 | } |
| 339 | } else if left > ln.len { |
| 340 | b.lines.delete(li) |
| 341 | left -= ln.len |
| 342 | } else { |
| 343 | b.lines[li] = ln[..x].string() + ln[x + left..].string() |
| 344 | left = 0 |
| 345 | } |
| 346 | } |
| 347 | } |
| 348 | $if debug { |
| 349 | flat := removed.replace('\n', r'\n') |
| 350 | eprintln(@MOD + '.' + @STRUCT + '::' + @FN + ' "${flat}"') |
| 351 | } |
| 352 | return removed |
| 353 | } |
| 354 | |
| 355 | fn (mut b Buffer) free() { |
| 356 | $if debug { |
| 357 | eprintln(@MOD + '.' + @STRUCT + '::' + @FN) |
| 358 | } |
| 359 | for line in b.lines { |
| 360 | unsafe { line.free() } |
| 361 | } |
| 362 | unsafe { b.lines.free() } |
| 363 | } |
| 364 | |
| 365 | fn (mut b Buffer) move_updown(amount int) { |
| 366 | b.cursor.move(0, amount) |
| 367 | // Check the move |
| 368 | line := b.cur_line().runes() |
| 369 | if b.cursor.pos_x > line.len { |
| 370 | b.cursor.set(line.len, b.cursor.pos_y) |
| 371 | } |
| 372 | } |
| 373 | |
| 374 | // move_cursor will navigate the cursor within the buffer bounds |
| 375 | fn (mut b Buffer) move_cursor(amount int, movement Movement) { |
| 376 | cur_line := b.cur_line().runes() |
| 377 | match movement { |
| 378 | .up { |
| 379 | if b.cursor.pos_y - amount >= 0 { |
| 380 | b.move_updown(-amount) |
| 381 | } |
| 382 | } |
| 383 | .down { |
| 384 | if b.cursor.pos_y + amount < b.lines.len { |
| 385 | b.move_updown(amount) |
| 386 | } |
| 387 | } |
| 388 | .page_up { |
| 389 | dlines := math.min(b.cursor.pos_y, amount) |
| 390 | b.move_updown(-dlines) |
| 391 | } |
| 392 | .page_down { |
| 393 | dlines := math.min(b.lines.len - 1, b.cursor.pos_y + amount) - b.cursor.pos_y |
| 394 | b.move_updown(dlines) |
| 395 | } |
| 396 | .left { |
| 397 | if b.cursor.pos_x - amount >= 0 { |
| 398 | b.cursor.move(-amount, 0) |
| 399 | } else if b.cursor.pos_y > 0 { |
| 400 | b.cursor.set(b.line(b.cursor.pos_y - 1).runes().len, b.cursor.pos_y - 1) |
| 401 | } |
| 402 | } |
| 403 | .right { |
| 404 | if b.cursor.pos_x + amount <= cur_line.len { |
| 405 | b.cursor.move(amount, 0) |
| 406 | } else if b.cursor.pos_y + 1 < b.lines.len { |
| 407 | b.cursor.set(0, b.cursor.pos_y + 1) |
| 408 | } |
| 409 | } |
| 410 | .home { |
| 411 | b.cursor.set(0, b.cursor.pos_y) |
| 412 | } |
| 413 | .end { |
| 414 | b.cursor.set(cur_line.len, b.cursor.pos_y) |
| 415 | } |
| 416 | } |
| 417 | } |
| 418 | |
| 419 | fn (mut b Buffer) move_to_word(movement Movement) { |
| 420 | a := if movement == .left { -1 } else { 1 } |
| 421 | |
| 422 | mut line := b.cur_line().runes() |
| 423 | mut x, mut y := b.cursor.pos_x, b.cursor.pos_y |
| 424 | if x + a < 0 && y > 0 { |
| 425 | y-- |
| 426 | line = b.line(b.cursor.pos_y - 1).runes() |
| 427 | x = line.len |
| 428 | } else if x + a >= line.len && y + 1 < b.lines.len { |
| 429 | y++ |
| 430 | line = b.line(b.cursor.pos_y + 1).runes() |
| 431 | x = 0 |
| 432 | } |
| 433 | // first, move past all non-`a-zA-Z0-9_` characters |
| 434 | for x + a >= 0 && x + a < line.len && !(utf8.is_letter(line[x + a]) |
| 435 | || line[x + a] in rune_digits || line[x + a] == `_`) { |
| 436 | x += a |
| 437 | } |
| 438 | // then, move past all the letters and numbers |
| 439 | for x + a >= 0 && x + a < line.len && (utf8.is_letter(line[x + a]) |
| 440 | || line[x + a] in rune_digits || line[x + a] == `_`) { |
| 441 | x += a |
| 442 | } |
| 443 | // if the cursor is out of bounds, move it to the next/previous line |
| 444 | if x + a >= 0 && x + a <= line.len { |
| 445 | x += a |
| 446 | } else if a < 0 && y + 1 > b.lines.len && y - 1 >= 0 { |
| 447 | y += a |
| 448 | x = 0 |
| 449 | } |
| 450 | b.cursor.set(x, y) |
| 451 | } |
| 452 | |
| 453 | struct Cursor { |
| 454 | pub mut: |
| 455 | pos_x int |
| 456 | pos_y int |
| 457 | } |
| 458 | |
| 459 | fn (mut c Cursor) set(x int, y int) { |
| 460 | c.pos_x = x |
| 461 | c.pos_y = y |
| 462 | } |
| 463 | |
| 464 | fn (mut c Cursor) move(x int, y int) { |
| 465 | c.pos_x += x |
| 466 | c.pos_y += y |
| 467 | } |
| 468 | |
| 469 | fn (c Cursor) xy() (int, int) { |
| 470 | return c.pos_x, c.pos_y |
| 471 | } |
| 472 | |
| 473 | // App callbacks |
| 474 | fn init(mut app App) { |
| 475 | app.init_file() |
| 476 | } |
| 477 | |
| 478 | fn (mut a App) init_file() { |
| 479 | a.ed = &Buffer{} |
| 480 | mut init_y := 0 |
| 481 | mut init_x := 0 |
| 482 | if a.files.len > 0 && a.current_file < a.files.len && a.files[a.current_file].len > 0 { |
| 483 | if !os.is_file(a.files[a.current_file]) && a.files[a.current_file].contains(':') { |
| 484 | // support the file:line:col: format |
| 485 | fparts := a.files[a.current_file].split(':') |
| 486 | if fparts.len > 0 { |
| 487 | a.files[a.current_file] = fparts[0] |
| 488 | } |
| 489 | if fparts.len > 1 { |
| 490 | init_y = fparts[1].int() - 1 |
| 491 | } |
| 492 | if fparts.len > 2 { |
| 493 | init_x = fparts[2].int() - 1 |
| 494 | } |
| 495 | } |
| 496 | if os.is_file(a.files[a.current_file]) { |
| 497 | // 'vico: ' + |
| 498 | a.tui.set_window_title(a.files[a.current_file]) |
| 499 | mut b := a.ed |
| 500 | content := os.read_file(a.files[a.current_file]) or { panic(err) } |
| 501 | b.put(content) |
| 502 | a.ed.cursor.pos_x = init_x |
| 503 | a.ed.cursor.pos_y = init_y |
| 504 | } |
| 505 | } |
| 506 | } |
| 507 | |
| 508 | fn (a &App) view_height() int { |
| 509 | return a.tui.window_height - a.footer_height - 1 |
| 510 | } |
| 511 | |
| 512 | // magnet_cursor_x will place the cursor as close to it's last move left or right as possible |
| 513 | fn (mut a App) magnet_cursor_x() { |
| 514 | mut buffer := a.ed |
| 515 | if buffer.cursor.pos_x < a.magnet_x { |
| 516 | if a.magnet_x < buffer.cur_line().runes().len { |
| 517 | move_x := a.magnet_x - buffer.cursor.pos_x |
| 518 | buffer.move_cursor(move_x, .right) |
| 519 | } |
| 520 | } |
| 521 | } |
| 522 | |
| 523 | fn frame(mut a App) { |
| 524 | mut ed := a.ed |
| 525 | a.tui.clear() |
| 526 | scroll_limit := a.view_height() |
| 527 | // scroll down |
| 528 | if ed.cursor.pos_y > a.viewport + scroll_limit { // scroll down |
| 529 | a.viewport = ed.cursor.pos_y - scroll_limit |
| 530 | } else if ed.cursor.pos_y < a.viewport { // scroll up |
| 531 | a.viewport = ed.cursor.pos_y |
| 532 | } |
| 533 | view := ed.view(a.viewport, scroll_limit + a.viewport) |
| 534 | a.tui.draw_text(0, 0, view.raw) |
| 535 | a.footer() |
| 536 | |
| 537 | // Unicode: Handle correct mapping of cursor X position in terminal. |
| 538 | mut ch_x := view.cursor.pos_x |
| 539 | mut sl := ed.cur_slice().replace('\t', ' '.repeat(ed.tab_width)) |
| 540 | if sl.len > 0 { |
| 541 | // Strip out any zero-width codepoints. |
| 542 | sl = sl.runes().filter(it !in zero_width_unicode).string() |
| 543 | ch_x = east_asian.display_width(sl, 1) |
| 544 | } |
| 545 | |
| 546 | a.tui.set_cursor_position(ch_x + 1, ed.cursor.pos_y + 1 - a.viewport) |
| 547 | a.tui.flush() |
| 548 | } |
| 549 | |
| 550 | fn event(e &tui.Event, mut a App) { |
| 551 | mut buffer := a.ed |
| 552 | if e.typ == .key_down { |
| 553 | match e.code { |
| 554 | .escape { |
| 555 | exit(0) |
| 556 | } |
| 557 | .enter { |
| 558 | buffer.put('\n') |
| 559 | } |
| 560 | .backspace { |
| 561 | buffer.del(-1) |
| 562 | } |
| 563 | .delete { |
| 564 | buffer.del(1) |
| 565 | } |
| 566 | .left { |
| 567 | if e.modifiers == .ctrl { |
| 568 | buffer.move_to_word(.left) |
| 569 | } else if e.modifiers.is_empty() { |
| 570 | buffer.move_cursor(1, .left) |
| 571 | } |
| 572 | a.magnet_x = buffer.cursor.pos_x |
| 573 | } |
| 574 | .right { |
| 575 | if e.modifiers == .ctrl { |
| 576 | buffer.move_to_word(.right) |
| 577 | } else if e.modifiers.is_empty() { |
| 578 | buffer.move_cursor(1, .right) |
| 579 | } |
| 580 | a.magnet_x = buffer.cursor.pos_x |
| 581 | } |
| 582 | .up { |
| 583 | buffer.move_cursor(1, .up) |
| 584 | a.magnet_cursor_x() |
| 585 | } |
| 586 | .down { |
| 587 | buffer.move_cursor(1, .down) |
| 588 | a.magnet_cursor_x() |
| 589 | } |
| 590 | .page_up { |
| 591 | buffer.move_cursor(a.view_height(), .page_up) |
| 592 | } |
| 593 | .page_down { |
| 594 | buffer.move_cursor(a.view_height(), .page_down) |
| 595 | } |
| 596 | .home { |
| 597 | buffer.move_cursor(1, .home) |
| 598 | } |
| 599 | .end { |
| 600 | buffer.move_cursor(1, .end) |
| 601 | } |
| 602 | 48...57, 97...122 { // 0-9a-zA-Z |
| 603 | if e.modifiers == .ctrl { |
| 604 | if e.code == .s { |
| 605 | a.save() |
| 606 | } |
| 607 | } else if !(e.modifiers.has(.ctrl | .alt) || e.code == .null) { |
| 608 | buffer.put(e.ascii.ascii_str()) |
| 609 | } |
| 610 | } |
| 611 | else { |
| 612 | if e.modifiers == .alt { |
| 613 | if e.code == .comma { |
| 614 | a.visit_prev_file() |
| 615 | return |
| 616 | } |
| 617 | if e.code == .period { |
| 618 | a.visit_next_file() |
| 619 | return |
| 620 | } |
| 621 | } |
| 622 | |
| 623 | buffer.put(e.utf8) |
| 624 | } |
| 625 | } |
| 626 | } else if e.typ == .mouse_scroll { |
| 627 | direction := if e.direction == .up { Movement.down } else { Movement.up } |
| 628 | buffer.move_cursor(1, direction) |
| 629 | } |
| 630 | } |
| 631 | |
| 632 | type InitFn = fn (voidptr) |
| 633 | |
| 634 | type EventFn = fn (&tui.Event, voidptr) |
| 635 | |
| 636 | type FrameFn = fn (voidptr) |
| 637 | |
| 638 | fn main() { |
| 639 | mut files := []string{} |
| 640 | if os.args.len > 1 { |
| 641 | files << os.args[1..] |
| 642 | } |
| 643 | mut a := &App{ |
| 644 | files: files |
| 645 | } |
| 646 | a.tui = tui.init( |
| 647 | user_data: a |
| 648 | init_fn: InitFn(init) |
| 649 | frame_fn: FrameFn(frame) |
| 650 | event_fn: EventFn(event) |
| 651 | capture_events: true |
| 652 | ) |
| 653 | a.tui.run()! |
| 654 | } |
| 655 | |