Plot layout on a grid
Redraw all x,y coordinates on screen Instead of using ANSI codes, to be avoid clearing the screen which introduces flickering in TMUX
This commit is contained in:
@@ -52,7 +52,7 @@ pub fn new(
|
||||
handle_key(musicplayer, input_keys)
|
||||
})
|
||||
|
||||
process.spawn(fn() { update_playback_time_loop(mpv, ui, 1000) })
|
||||
process.spawn(fn() { update_playback_time_loop(mpv, ui, 250) })
|
||||
|
||||
Ok(pid)
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import gleam/list
|
||||
import gleam/string
|
||||
import gleam/string_tree
|
||||
|
||||
import musicplayer/ui/internal
|
||||
|
||||
pub fn text(chars: String, x: Int, y: Int) {
|
||||
internal.chars_at(chars, x, y)
|
||||
}
|
||||
|
||||
pub fn box(x: Int, y: Int, width: Int, height: Int) -> String {
|
||||
let box_chars = #("┌", "┐", "└", "┘", "─", "│")
|
||||
let #(tl, tr, bl, br, h, v) = box_chars
|
||||
|
||||
// Add top of box
|
||||
let tree =
|
||||
string_tree.new()
|
||||
|> string_tree.append(internal.chars_at(
|
||||
tl <> string.repeat(h, width - 2) <> tr,
|
||||
x,
|
||||
y,
|
||||
))
|
||||
|
||||
// Add sides of box
|
||||
let tree_with_sides =
|
||||
list.range(1, height - 2)
|
||||
|> list.map(fn(row) {
|
||||
tree
|
||||
|> string_tree.append(internal.chars_at(v, x, y + row))
|
||||
|> string_tree.append(internal.chars_at(v, x + width - 1, y + row))
|
||||
})
|
||||
|> string_tree.concat
|
||||
|
||||
// Add bottom of box
|
||||
tree_with_sides
|
||||
|> string_tree.append(internal.chars_at(
|
||||
bl <> string.repeat(h, width - 2) <> br,
|
||||
x,
|
||||
y + height - 1,
|
||||
))
|
||||
|> string_tree.to_string
|
||||
}
|
||||
@@ -3,6 +3,16 @@ import gleam/io
|
||||
|
||||
// https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
|
||||
|
||||
pub const move_to_home = "\u{001B}[H"
|
||||
|
||||
pub const disable_auto_wrap = "\u{001B}[?7l"
|
||||
|
||||
pub const enable_auto_wrap = "\u{001B}[?7h"
|
||||
|
||||
pub fn update(frame: String) -> Nil {
|
||||
io.print(disable_auto_wrap <> move_to_home <> frame <> enable_auto_wrap)
|
||||
}
|
||||
|
||||
pub fn clear_screen() -> Nil {
|
||||
io.print("\u{001B}[2J\u{001B}[H")
|
||||
}
|
||||
@@ -12,10 +22,6 @@ pub fn chars_at(chars: String, x: Int, y: Int) -> String {
|
||||
seq <> chars
|
||||
}
|
||||
|
||||
pub fn print(chars: String) -> Nil {
|
||||
io.print(chars)
|
||||
}
|
||||
|
||||
pub fn hide_cursor() -> Nil {
|
||||
io.print("\u{001B}[?25l")
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ import gleam/list
|
||||
import gleam/pair
|
||||
import gleam/set
|
||||
import gleam/string
|
||||
import gleam/string_tree
|
||||
|
||||
import musicplayer/logging/logging
|
||||
import musicplayer/ui/ansi
|
||||
import musicplayer/ui/internal
|
||||
import musicplayer/ui/plot.{type Buffer}
|
||||
|
||||
pub const root_section = "reserved_root_section"
|
||||
|
||||
@@ -93,8 +92,6 @@ pub fn update_dimensions(layout: Layout, columns: Int, rows: Int) -> Layout {
|
||||
}
|
||||
|
||||
pub fn render(layout: Layout) -> Nil {
|
||||
internal.clear_screen()
|
||||
|
||||
[layout.columns, layout.rows]
|
||||
|> list.map(int.to_string)
|
||||
|> string.join(" ")
|
||||
@@ -110,29 +107,11 @@ pub fn render(layout: Layout) -> Nil {
|
||||
position_index: 0,
|
||||
)
|
||||
|
||||
let ansi_renders =
|
||||
Renders(
|
||||
text: fn(tree, chars, x, y) {
|
||||
string_tree.append(tree, ansi.text(chars, x, y))
|
||||
},
|
||||
box: fn(tree, x, y, w, h) {
|
||||
string_tree.append(tree, ansi.box(x, y, w, h))
|
||||
},
|
||||
)
|
||||
let buffer: Buffer = dict.new()
|
||||
|
||||
string_tree.new()
|
||||
|> render_generic(layout, context, Section(root_section), _, ansi_renders)
|
||||
|> string_tree.to_string
|
||||
|> internal.print
|
||||
}
|
||||
|
||||
pub type Renders(into) {
|
||||
Renders(
|
||||
// into, chars, x, y
|
||||
text: fn(into, String, Int, Int) -> into,
|
||||
// into, x, y, width, height
|
||||
box: fn(into, Int, Int, Int, Int) -> into,
|
||||
)
|
||||
render_loop(layout, context, Section(root_section), buffer)
|
||||
|> plot.flush_buffer(layout.columns, layout.rows)
|
||||
|> internal.update
|
||||
}
|
||||
|
||||
pub type RenderContext {
|
||||
@@ -145,17 +124,16 @@ pub type RenderContext {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_generic(
|
||||
pub fn render_loop(
|
||||
layout: Layout,
|
||||
context: RenderContext,
|
||||
from: Section,
|
||||
render_into: into,
|
||||
renders: Renders(into),
|
||||
) -> into {
|
||||
buffer: Buffer,
|
||||
) -> Buffer {
|
||||
case dict.get(layout.nodes, from) {
|
||||
Error(_) -> render_into
|
||||
Error(_) -> buffer
|
||||
Ok(node) -> {
|
||||
// Margin between container and the node being rendere
|
||||
// Margin between container and the node being rendered
|
||||
let margin = 2
|
||||
|
||||
let #(node_width, node_height) = case node.style.dimensions {
|
||||
@@ -180,20 +158,20 @@ pub fn render_generic(
|
||||
}
|
||||
|
||||
let parent =
|
||||
render_into
|
||||
|> renders.box(
|
||||
plot.box(
|
||||
buffer,
|
||||
node_top_left_x,
|
||||
node_top_left_y,
|
||||
node_width,
|
||||
node_height,
|
||||
)
|
||||
|> renders.text(node.content, node_top_left_x, node_top_left_y)
|
||||
|> plot.text(node.content, node_top_left_x, node_top_left_y)
|
||||
|
||||
case node {
|
||||
Cell(..) -> parent
|
||||
Row(children:, ..) -> {
|
||||
list.index_map(children, fn(child, i) { #(i, child) })
|
||||
|> list.fold(parent, fn(acc_into, ic) {
|
||||
|> list.fold(parent, fn(acc_buffer, ic) {
|
||||
let #(i, child) = ic
|
||||
|
||||
let #(child_width, child_height) = case node.style.dimensions {
|
||||
@@ -215,7 +193,7 @@ pub fn render_generic(
|
||||
position_index: i,
|
||||
)
|
||||
|
||||
render_generic(layout, context, child, acc_into, renders)
|
||||
render_loop(layout, context, child, acc_buffer)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
71
src/musicplayer/ui/plot.gleam
Normal file
71
src/musicplayer/ui/plot.gleam
Normal file
@@ -0,0 +1,71 @@
|
||||
import gleam/dict.{type Dict}
|
||||
import gleam/list
|
||||
import gleam/string
|
||||
|
||||
pub type Buffer =
|
||||
Dict(#(Int, Int), String)
|
||||
|
||||
pub fn flush_buffer(buffer: Buffer, columns: Int, rows: Int) -> String {
|
||||
list.range(1, rows)
|
||||
|> list.map(fn(y) {
|
||||
list.range(1, columns)
|
||||
|> list.map(fn(x) {
|
||||
case dict.get(buffer, #(x, y)) {
|
||||
Ok(char) -> char
|
||||
Error(_) -> " "
|
||||
}
|
||||
})
|
||||
|> string.join("")
|
||||
})
|
||||
|> string.join("\r\n")
|
||||
}
|
||||
|
||||
pub fn text(buffer: Buffer, text: String, x: Int, y: Int) -> Buffer {
|
||||
text
|
||||
|> string.to_graphemes
|
||||
|> list.index_fold(buffer, fn(acc, char, i) {
|
||||
dict.insert(acc, #(x + i, y), char)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn box(buffer: Buffer, x: Int, y: Int, width: Int, height: Int) -> Buffer {
|
||||
let box_chars = #("┌", "┐", "└", "┘", "─", "│")
|
||||
let #(tl, tr, bl, br, hor, ver) = box_chars
|
||||
|
||||
case width < 2 || height < 2 {
|
||||
True -> buffer
|
||||
False -> {
|
||||
buffer
|
||||
|> dict.insert(#(x, y), tl)
|
||||
|> dict.insert(#(x + width - 1, y), tr)
|
||||
|> dict.insert(#(x, y + height - 1), bl)
|
||||
|> dict.insert(#(x + width - 1, y + height - 1), br)
|
||||
|> horizontal_line(x + 1, y, width - 2, hor)
|
||||
|> horizontal_line(x + 1, y + height - 1, width - 2, hor)
|
||||
|> vertical_line(x, y + 1, height - 2, ver)
|
||||
|> vertical_line(x + width - 1, y + 1, height - 2, ver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn horizontal_line(
|
||||
buffer: Buffer,
|
||||
x: Int,
|
||||
y: Int,
|
||||
len: Int,
|
||||
char: String,
|
||||
) -> Buffer {
|
||||
list.range(0, len - 1)
|
||||
|> list.fold(buffer, fn(acc, i) { dict.insert(acc, #(x + i, y), char) })
|
||||
}
|
||||
|
||||
fn vertical_line(
|
||||
buffer: Buffer,
|
||||
x: Int,
|
||||
y: Int,
|
||||
len: Int,
|
||||
char: String,
|
||||
) -> Buffer {
|
||||
list.range(0, len - 1)
|
||||
|> list.fold(buffer, fn(acc, i) { dict.insert(acc, #(x, y + i), char) })
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import gleam/dict
|
||||
import gleam/io
|
||||
import gleam/string
|
||||
import gleeunit
|
||||
import gleeunit/should
|
||||
import musicplayer/ui/virtual_ansi
|
||||
|
||||
import musicplayer/ui/layout.{Percent, Section, Style}
|
||||
import musicplayer/ui/layout.{Percent, RenderContext, Section, Style}
|
||||
import musicplayer/ui/plot
|
||||
|
||||
pub fn main() -> Nil {
|
||||
gleeunit.main()
|
||||
@@ -74,9 +75,28 @@ pub fn percent_layout_test() {
|
||||
│└────────────────────────────────────────────────────────────────────────────┘│
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
"
|
||||
|> string.replace(each: "\n", with: "\r\n")
|
||||
|> string.trim
|
||||
|
||||
let visual = virtual_ansi.render(layout)
|
||||
case visual == string.trim(expected) {
|
||||
let context =
|
||||
RenderContext(
|
||||
parent_width: layout.columns,
|
||||
parent_height: layout.rows,
|
||||
parent_top_left_x: 1,
|
||||
parent_top_left_y: 1,
|
||||
position_index: 0,
|
||||
)
|
||||
|
||||
let flushed =
|
||||
layout.render_loop(
|
||||
layout,
|
||||
context,
|
||||
Section(layout.root_section),
|
||||
dict.new(),
|
||||
)
|
||||
|> plot.flush_buffer(layout.columns, layout.rows)
|
||||
|
||||
case flushed == expected {
|
||||
True -> Nil
|
||||
False -> {
|
||||
io.println("Test failed")
|
||||
@@ -84,9 +104,9 @@ pub fn percent_layout_test() {
|
||||
io.println(string.trim(expected))
|
||||
|
||||
io.println("Got:")
|
||||
io.println(visual)
|
||||
io.println(flushed)
|
||||
|
||||
should.equal(visual, expected)
|
||||
should.equal(flushed, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import gleam/dict
|
||||
import gleam/int
|
||||
import gleam/list
|
||||
import gleam/string
|
||||
|
||||
import musicplayer/ui/layout.{type Layout, RenderContext, Renders}
|
||||
|
||||
pub type Screen =
|
||||
dict.Dict(#(Int, Int), String)
|
||||
|
||||
pub fn render(layout: Layout) -> String {
|
||||
let test_renders =
|
||||
Renders(
|
||||
text: fn(screen, chars, x, y) { text(screen, chars, x, y) },
|
||||
box: fn(screen, x, y, w, h) { box(screen, x, y, w, h) },
|
||||
)
|
||||
|
||||
let context =
|
||||
RenderContext(
|
||||
parent_width: layout.columns,
|
||||
parent_height: layout.rows,
|
||||
parent_top_left_x: 1,
|
||||
parent_top_left_y: 1,
|
||||
position_index: 0,
|
||||
)
|
||||
|
||||
let screen =
|
||||
layout.render_generic(
|
||||
layout,
|
||||
context,
|
||||
layout.Section(layout.root_section),
|
||||
dict.new(),
|
||||
test_renders,
|
||||
)
|
||||
|
||||
screen_to_string(screen)
|
||||
}
|
||||
|
||||
pub fn screen_to_string(screen: Screen) -> String {
|
||||
let keys = dict.keys(screen)
|
||||
|
||||
// Find the bounding box of the drawing
|
||||
let max_x = list.fold(keys, 0, fn(m, k) { int.max(m, k.0) })
|
||||
let max_y = list.fold(keys, 0, fn(m, k) { int.max(m, k.1) })
|
||||
|
||||
// We start from 1 because ANSI is 1-based
|
||||
let min_y = list.fold(keys, 1000, fn(m, k) { int.min(m, k.1) })
|
||||
|
||||
list.range(min_y, max_y)
|
||||
|> list.map(fn(y) {
|
||||
list.range(1, max_x)
|
||||
|> list.map(fn(x) {
|
||||
case dict.get(screen, #(x, y)) {
|
||||
Ok(char) -> char
|
||||
Error(_) -> " "
|
||||
// Fill gaps with space
|
||||
}
|
||||
})
|
||||
|> string.join("")
|
||||
})
|
||||
|> string.join("\n")
|
||||
}
|
||||
|
||||
pub fn text(screen: Screen, text: String, start_x: Int, y: Int) -> Screen {
|
||||
// We use to_graphemes to ensure Unicode characters (like emoji or box lines)
|
||||
// are treated as single visual units
|
||||
text
|
||||
|> string.to_graphemes
|
||||
|> list.index_fold(screen, fn(acc, char, i) {
|
||||
dict.insert(acc, #(start_x + i, y), char)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn box(screen: Screen, x: Int, y: Int, w: Int, h: Int) -> Screen {
|
||||
let box_chars = #("┌", "┐", "└", "┘", "─", "│")
|
||||
let #(tl, tr, bl, br, hor, ver) = box_chars
|
||||
|
||||
// Don't draw impossible boxes
|
||||
case w < 2 || h < 2 {
|
||||
True -> screen
|
||||
False -> {
|
||||
screen
|
||||
// 1. Corners
|
||||
|> dict.insert(#(x, y), tl)
|
||||
|> dict.insert(#(x + w - 1, y), tr)
|
||||
|> dict.insert(#(x, y + h - 1), bl)
|
||||
|> dict.insert(#(x + w - 1, y + h - 1), br)
|
||||
// 2. Top and Bottom edges
|
||||
|> horizontal_line(x + 1, y, w - 2, hor)
|
||||
|> horizontal_line(x + 1, y + h - 1, w - 2, hor)
|
||||
// 3. Side edges
|
||||
|> vertical_line(x, y + 1, h - 2, ver)
|
||||
|> vertical_line(x + w - 1, y + 1, h - 2, ver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn horizontal_line(
|
||||
screen: Screen,
|
||||
x: Int,
|
||||
y: Int,
|
||||
len: Int,
|
||||
char: String,
|
||||
) -> Screen {
|
||||
list.range(0, len - 1)
|
||||
|> list.fold(screen, fn(acc, i) { dict.insert(acc, #(x + i, y), char) })
|
||||
}
|
||||
|
||||
fn vertical_line(
|
||||
screen: Screen,
|
||||
x: Int,
|
||||
y: Int,
|
||||
len: Int,
|
||||
char: String,
|
||||
) -> Screen {
|
||||
list.range(0, len - 1)
|
||||
|> list.fold(screen, fn(acc, i) { dict.insert(acc, #(x, y + i), char) })
|
||||
}
|
||||
Reference in New Issue
Block a user