🖨️ esc/pos implementation in gleam

feat: document api

okk.moe dc1231a4 cefa8edd

verified
+387 -103
+17 -60
src/escpos.gleam
··· 1 + import escpos/printer.{type CommandBuffer, CommandBuffer} 1 2 import escpos/protocol 2 3 import gleam/bit_array 3 4 import gleam/bool 4 - 5 - pub opaque type CommandBuffer { 6 - CommandBuffer(data: BitArray) 7 - } 8 5 9 6 pub fn new() -> CommandBuffer { 10 7 CommandBuffer(<<>>) ··· 19 16 |> append(protocol.lf) 20 17 } 21 18 22 - pub fn bold( 23 - cb: CommandBuffer, 24 - commands: fn(CommandBuffer) -> CommandBuffer, 25 - ) -> CommandBuffer { 26 - append(cb, protocol.bold(True)) 27 - |> commands 28 - |> append(protocol.bold(False)) 19 + pub fn bold(cb: CommandBuffer, b: Bool) -> CommandBuffer { 20 + append(cb, protocol.bold(b)) 29 21 } 30 22 31 - pub fn underline( 32 - cb: CommandBuffer, 33 - commands: fn(CommandBuffer) -> CommandBuffer, 34 - ) -> CommandBuffer { 35 - append(cb, protocol.underline(True)) 36 - |> commands 37 - |> append(protocol.underline(False)) 23 + pub fn underline(cb: CommandBuffer, b: Bool) -> CommandBuffer { 24 + append(cb, protocol.underline(b)) 38 25 } 39 26 40 - pub fn double_strike( 41 - cb: CommandBuffer, 42 - commands: fn(CommandBuffer) -> CommandBuffer, 43 - ) -> CommandBuffer { 44 - append(cb, protocol.double_strike(True)) 45 - |> commands 46 - |> append(protocol.double_strike(False)) 27 + pub fn double_strike(cb: CommandBuffer, b: Bool) -> CommandBuffer { 28 + append(cb, protocol.double_strike(b)) 47 29 } 48 30 49 - pub fn reverse( 50 - cb: CommandBuffer, 51 - commands: fn(CommandBuffer) -> CommandBuffer, 52 - ) -> CommandBuffer { 53 - append(cb, protocol.reverse(True)) 54 - |> commands 55 - |> append(protocol.reverse(False)) 31 + pub fn reverse(cb: CommandBuffer, b: Bool) -> CommandBuffer { 32 + append(cb, protocol.reverse(b)) 56 33 } 57 34 58 - pub fn upside_down( 59 - cb: CommandBuffer, 60 - commands: fn(CommandBuffer) -> CommandBuffer, 61 - ) -> CommandBuffer { 62 - append(cb, protocol.upside_down(True)) 63 - |> commands 64 - |> ensure_new_line 65 - |> append(protocol.upside_down(False)) 35 + pub fn upside_down(cb: CommandBuffer, b: Bool) -> CommandBuffer { 36 + append(cb, protocol.upside_down(b)) 66 37 } 67 38 68 - pub fn smooth( 69 - cb: CommandBuffer, 70 - commands: fn(CommandBuffer) -> CommandBuffer, 71 - ) -> CommandBuffer { 72 - append(cb, protocol.smooth(True)) 73 - |> commands 74 - |> append(protocol.smooth(False)) 39 + pub fn smooth(cb: CommandBuffer, b: Bool) -> CommandBuffer { 40 + append(cb, protocol.smooth(b)) 75 41 } 76 42 77 - pub fn flip( 78 - cb: CommandBuffer, 79 - commands: fn(CommandBuffer) -> CommandBuffer, 80 - ) -> CommandBuffer { 81 - append(cb, protocol.flip(True)) 82 - |> commands 83 - |> append(protocol.flip(False)) 43 + pub fn flip(cb: CommandBuffer, b: Bool) -> CommandBuffer { 44 + append(cb, protocol.flip(b)) 84 45 } 85 46 86 47 pub fn set_font(cb: CommandBuffer, font: protocol.Font) -> CommandBuffer { ··· 110 71 111 72 pub fn cut(cb: CommandBuffer) -> CommandBuffer { 112 73 line_feed(cb, 3) 113 - |> append(protocol.full_cut) 74 + |> append(protocol.cut(protocol.Full)) 114 75 } 115 76 116 77 pub fn partial_cut(cb: CommandBuffer) -> CommandBuffer { 117 78 line_feed(cb, 3) 118 - |> append(protocol.partial_cut) 79 + |> append(protocol.cut(protocol.Partial)) 119 80 } 120 81 121 82 pub fn new_line(cb: CommandBuffer) -> CommandBuffer { ··· 137 98 fn append(cb: CommandBuffer, data: BitArray) -> CommandBuffer { 138 99 bit_array.append(cb.data, data) 139 100 |> CommandBuffer 140 - } 141 - 142 - pub fn get_payload(cb: CommandBuffer) -> BitArray { 143 - ensure_new_line(cb).data 144 101 } 145 102 146 103 fn ensure_new_line(cb: CommandBuffer) -> CommandBuffer {
+352
src/escpos/document.gleam
··· 1 + import escpos/printer.{type CommandBuffer, CommandBuffer} 2 + import escpos/protocol 3 + import gleam/bit_array 4 + import gleam/list 5 + import gleam/option.{type Option, None, Some} 6 + import gleam/set.{type Set} 7 + 8 + pub opaque type Command { 9 + Write(String) 10 + Writeln(String) 11 + LineFeed(Int) 12 + Cut(protocol.Cut) 13 + Styled(modifiers: Set(Modifier), commands: List(Command)) 14 + Custom(BitArray) 15 + } 16 + 17 + pub opaque type Modifier { 18 + Bold 19 + Underline 20 + DoubleStrike 21 + Reverse 22 + UpsideDown 23 + Justify(protocol.Justify) 24 + Font(protocol.Font) 25 + } 26 + 27 + pub opaque type AST { 28 + Init 29 + DoWrite(String) 30 + DoLineFeed(Int) 31 + DoCut(protocol.Cut) 32 + DoCustom(BitArray) 33 + SetBold(Bool) 34 + SetUnderline(Bool) 35 + SetDoubleStrike(Bool) 36 + SetReverse(Bool) 37 + SetUpsideDown(Bool) 38 + SetJustify(protocol.Justify) 39 + SetFont(protocol.Font) 40 + } 41 + 42 + type State { 43 + State( 44 + new_line: Bool, 45 + bold: Bool, 46 + underline: Bool, 47 + double_strike: Bool, 48 + reverse: Bool, 49 + upside_down: Bool, 50 + justify: protocol.Justify, 51 + font: protocol.Font, 52 + ) 53 + } 54 + 55 + fn default_state() -> State { 56 + State( 57 + new_line: True, 58 + bold: False, 59 + underline: False, 60 + double_strike: False, 61 + reverse: False, 62 + upside_down: False, 63 + justify: protocol.Left, 64 + font: protocol.FontA, 65 + ) 66 + } 67 + 68 + pub fn build(document: List(Command)) -> CommandBuffer { 69 + build_ast(document) 70 + |> compile_ast 71 + |> CommandBuffer 72 + } 73 + 74 + pub fn styled(modifiers: List(Modifier), commands: List(Command)) -> Command { 75 + Styled(set.from_list(modifiers), commands) 76 + } 77 + 78 + pub fn write(text: String) -> Command { 79 + Write(text) 80 + } 81 + 82 + pub fn writeln(text: String) -> Command { 83 + Writeln(text) 84 + } 85 + 86 + pub fn line_feed(lines: Int) -> Command { 87 + LineFeed(lines) 88 + } 89 + 90 + pub fn cut() -> Command { 91 + Cut(protocol.Full) 92 + } 93 + 94 + pub fn partial_cut() -> Command { 95 + Cut(protocol.Partial) 96 + } 97 + 98 + pub fn bold() -> Modifier { 99 + Bold 100 + } 101 + 102 + pub fn underline() -> Modifier { 103 + Underline 104 + } 105 + 106 + pub fn double_strike() -> Modifier { 107 + DoubleStrike 108 + } 109 + 110 + pub fn reverse() -> Modifier { 111 + Reverse 112 + } 113 + 114 + pub fn upside_down() -> Modifier { 115 + UpsideDown 116 + } 117 + 118 + pub fn justify(j: protocol.Justify) -> Modifier { 119 + Justify(j) 120 + } 121 + 122 + pub fn font(f: protocol.Font) -> Modifier { 123 + Font(f) 124 + } 125 + 126 + fn build_ast(commands: List(Command)) -> List(AST) { 127 + do_build_ast(commands, [Init], default_state()) 128 + |> list.reverse 129 + } 130 + 131 + fn do_build_ast( 132 + commands: List(Command), 133 + acc: List(AST), 134 + state: State, 135 + ) -> List(AST) { 136 + case commands { 137 + [] -> acc 138 + [cmd, ..rest] -> { 139 + case cmd { 140 + Styled(modifiers, commands) -> { 141 + let #(acc_with_mods, nested_state) = 142 + do_apply_modifiers(set.to_list(modifiers), acc, state) 143 + let acc_with_commands = 144 + do_build_ast(commands, acc_with_mods, nested_state) 145 + let #(acc_with_reset_styles, reset_state) = 146 + revert_styles(state, nested_state, acc_with_commands) 147 + do_build_ast( 148 + rest, 149 + acc_with_reset_styles, 150 + State(..state, new_line: reset_state.new_line), 151 + ) 152 + } 153 + Cut(c) -> { 154 + let new_acc = 155 + list.prepend(acc, DoLineFeed(3)) |> list.prepend(DoCut(c)) 156 + do_build_ast(rest, new_acc, State(..state, new_line: True)) 157 + } 158 + LineFeed(n) -> 159 + do_build_ast( 160 + rest, 161 + [DoLineFeed(n), ..acc], 162 + State(..state, new_line: True), 163 + ) 164 + Write(x) -> 165 + do_build_ast( 166 + rest, 167 + [DoWrite(x), ..acc], 168 + State(..state, new_line: False), 169 + ) 170 + Writeln(x) -> { 171 + let new_acc = 172 + list.prepend(acc, DoWrite(x)) |> list.prepend(DoLineFeed(1)) 173 + do_build_ast(rest, new_acc, State(..state, new_line: True)) 174 + } 175 + Custom(b) -> 176 + do_build_ast( 177 + rest, 178 + [DoCustom(b), ..acc], 179 + State(..state, new_line: False), 180 + ) 181 + } 182 + } 183 + } 184 + } 185 + 186 + fn revert_styles( 187 + state: State, 188 + nested_state: State, 189 + acc: List(AST), 190 + ) -> #(List(AST), State) { 191 + let changes = 192 + [ 193 + changed(state.bold != nested_state.bold, SetBold(state.bold)), 194 + changed( 195 + state.underline != nested_state.underline, 196 + SetUnderline(state.underline), 197 + ), 198 + changed( 199 + state.double_strike != nested_state.double_strike, 200 + SetDoubleStrike(state.double_strike), 201 + ), 202 + changed(state.reverse != nested_state.reverse, SetReverse(state.reverse)), 203 + changed( 204 + state.upside_down != nested_state.upside_down, 205 + SetUpsideDown(state.upside_down), 206 + ), 207 + changed(state.justify != nested_state.justify, SetJustify(state.justify)), 208 + changed(state.font != nested_state.font, SetFont(state.font)), 209 + ] 210 + |> option.values 211 + 212 + do_revert_styles(changes, acc, state) 213 + } 214 + 215 + fn do_revert_styles( 216 + changes: List(AST), 217 + acc: List(AST), 218 + state: State, 219 + ) -> #(List(AST), State) { 220 + case changes { 221 + [] -> #(acc, state) 222 + [style, ..rest] -> 223 + case style { 224 + SetJustify(_) | SetUpsideDown(_) -> 225 + case state.new_line { 226 + True -> do_revert_styles(rest, [style, ..acc], state) 227 + False -> 228 + do_revert_styles( 229 + rest, 230 + list.prepend(acc, DoLineFeed(1)) |> list.prepend(style), 231 + State(..state, new_line: True), 232 + ) 233 + } 234 + _ -> do_revert_styles(rest, [style, ..acc], state) 235 + } 236 + } 237 + } 238 + 239 + fn changed(change: Bool, ast: AST) -> Option(AST) { 240 + case change { 241 + True -> Some(ast) 242 + False -> None 243 + } 244 + } 245 + 246 + fn do_apply_modifiers( 247 + modifiers: List(Modifier), 248 + acc: List(AST), 249 + state: State, 250 + ) -> #(List(AST), State) { 251 + case modifiers { 252 + [] -> #(acc, state) 253 + [mod, ..rest] -> { 254 + case mod { 255 + Bold -> 256 + do_apply_modifiers( 257 + rest, 258 + [SetBold(True), ..acc], 259 + State(..state, bold: True), 260 + ) 261 + DoubleStrike -> 262 + do_apply_modifiers( 263 + rest, 264 + [SetDoubleStrike(True), ..acc], 265 + State(..state, double_strike: True), 266 + ) 267 + Underline -> 268 + do_apply_modifiers( 269 + rest, 270 + [SetUnderline(True), ..acc], 271 + State(..state, underline: True), 272 + ) 273 + Reverse -> 274 + do_apply_modifiers( 275 + rest, 276 + [SetReverse(True), ..acc], 277 + State(..state, reverse: True), 278 + ) 279 + UpsideDown -> 280 + case state.new_line { 281 + True -> 282 + do_apply_modifiers( 283 + rest, 284 + [SetUpsideDown(True), ..acc], 285 + State(..state, upside_down: True, new_line: True), 286 + ) 287 + False -> 288 + do_apply_modifiers( 289 + rest, 290 + list.prepend(acc, DoLineFeed(1)) 291 + |> list.prepend(SetUpsideDown(True)), 292 + State(..state, upside_down: True), 293 + ) 294 + } 295 + Justify(j) -> 296 + case state.new_line { 297 + True -> 298 + do_apply_modifiers( 299 + rest, 300 + [SetJustify(j), ..acc], 301 + State(..state, justify: j, new_line: True), 302 + ) 303 + False -> 304 + do_apply_modifiers( 305 + rest, 306 + list.prepend(acc, DoLineFeed(1)) |> list.prepend(SetJustify(j)), 307 + State(..state, justify: j), 308 + ) 309 + } 310 + Font(f) -> 311 + do_apply_modifiers(rest, [SetFont(f), ..acc], State(..state, font: f)) 312 + } 313 + } 314 + } 315 + } 316 + 317 + fn compile_ast(ast: List(AST)) -> BitArray { 318 + do_compile_ast(ast, <<>>) 319 + } 320 + 321 + fn do_compile_ast(ast: List(AST), acc: BitArray) -> BitArray { 322 + case ast { 323 + [] -> acc 324 + [cmd, ..rest] -> 325 + case cmd { 326 + Init -> do_compile_ast(rest, bit_array.append(acc, protocol.init)) 327 + DoWrite(x) -> 328 + do_compile_ast(rest, bit_array.append(acc, bit_array.from_string(x))) 329 + DoLineFeed(n) -> 330 + do_compile_ast(rest, bit_array.append(acc, protocol.line_feed(n))) 331 + DoCut(c) -> do_compile_ast(rest, bit_array.append(acc, protocol.cut(c))) 332 + DoCustom(b) -> do_compile_ast(rest, bit_array.append(acc, b)) 333 + SetBold(on) -> 334 + do_compile_ast(rest, bit_array.append(acc, protocol.bold(on))) 335 + SetUnderline(on) -> 336 + do_compile_ast(rest, bit_array.append(acc, protocol.underline(on))) 337 + SetDoubleStrike(on) -> 338 + do_compile_ast( 339 + rest, 340 + bit_array.append(acc, protocol.double_strike(on)), 341 + ) 342 + SetReverse(on) -> 343 + do_compile_ast(rest, bit_array.append(acc, protocol.reverse(on))) 344 + SetUpsideDown(on) -> 345 + do_compile_ast(rest, bit_array.append(acc, protocol.upside_down(on))) 346 + SetJustify(j) -> 347 + do_compile_ast(rest, bit_array.append(acc, protocol.justify(j))) 348 + SetFont(f) -> 349 + do_compile_ast(rest, bit_array.append(acc, protocol.font(f))) 350 + } 351 + } 352 + }
+6 -4
src/escpos/printer.gleam
··· 1 - import escpos.{type CommandBuffer} 2 1 import escpos/protocol 3 2 import gleam/result 4 3 import mug 4 + 5 + @internal 6 + pub type CommandBuffer { 7 + CommandBuffer(data: BitArray) 8 + } 5 9 6 10 pub opaque type Printer { 7 11 Printer(socket: mug.Socket) ··· 32 36 33 37 /// Sends the CommandBuffer to the printer 34 38 pub fn print(cb: CommandBuffer, printer: Printer) -> Result(Nil, PrinterError) { 35 - let data = escpos.get_payload(cb) 36 - 37 - mug.send(printer.socket, data) 39 + mug.send(printer.socket, cb.data) 38 40 |> result.map_error(PrintError) 39 41 } 40 42
+11 -3
src/escpos/protocol.gleam
··· 6 6 Right 7 7 } 8 8 9 + pub type Cut { 10 + Partial 11 + Full 12 + } 13 + 9 14 pub type Font { 10 15 FontA 11 16 FontB ··· 24 29 25 30 pub const lf = <<10>> 26 31 27 - pub const full_cut = <<gs, "V", 0>> 28 - 29 - pub const partial_cut = <<gs, "V", 1>> 32 + pub fn cut(cut: Cut) -> BitArray { 33 + case cut { 34 + Full -> <<gs, "V", 0>> 35 + Partial -> <<gs, "V", 1>> 36 + } 37 + } 30 38 31 39 pub fn line_feed(lines: Int) -> BitArray { 32 40 case lines {
+1 -36
test/escpos_test.gleam
··· 8 8 } 9 9 10 10 fn setup_printer() -> Result(printer.Printer, printer.PrinterError) { 11 - printer.connect("10.255.8.62", 9100) 11 + printer.connect("localhost", 9100) 12 12 } 13 13 14 14 fn test_print( ··· 28 28 29 29 let assert Ok(Nil) = 30 30 test_print(printer, "writeln", escpos.write(_, "Hello, World!")) 31 - 32 - let assert Ok(Nil) = 33 - test_print( 34 - printer, 35 - "write bold", 36 - escpos.bold(_, escpos.write(_, "Hello, World!")), 37 - ) 38 - 39 - let assert Ok(Nil) = 40 - test_print( 41 - printer, 42 - "write underline", 43 - escpos.underline(_, escpos.write(_, "Hello, World!")), 44 - ) 45 - 46 - let assert Ok(Nil) = 47 - test_print( 48 - printer, 49 - "write double strike", 50 - escpos.double_strike(_, escpos.write(_, "Hello, World!")), 51 - ) 52 - 53 - let assert Ok(Nil) = 54 - test_print( 55 - printer, 56 - "write reverse", 57 - escpos.reverse(_, escpos.write(_, "Hello, World!")), 58 - ) 59 - 60 - let assert Ok(Nil) = 61 - test_print( 62 - printer, 63 - "write upside down", 64 - escpos.upside_down(_, escpos.write(_, "Hello, World!")), 65 - ) 66 31 67 32 let assert Ok(Nil) = 68 33 test_print(printer, "font B", fn(b) {