Measure the width of text in the terminal and build simple layouts!
0

Configure Feed

Select the types of activity you want to include in your feed.

WIP: implement limit (no tests)

+338 -7
+1 -1
probe/pop-improved.js
··· 1 - const str = "😀".repeat(100) 1 + const str = "😀".repeat(10) 2 2 3 3 const segmenter = new Intl.Segmenter(); 4 4 function fold_graphemes(str, state, fun) {
+1 -1
probe/pop-rust.mjs
··· 1 1 import { graphemeSegments } from 'unicode-segmenter/grapheme'; 2 2 3 - const str = "😀".repeat(100) 3 + const str = "😀".repeat(10) 4 4 5 5 function fold_graphemes(str, state, fun) { 6 6 for(const { segment: grapheme } of graphemeSegments(str)) {
+336 -5
src/string_width.gleam
··· 252 252 253 253 // -- TRUNCATE / PAD ----------------------------------------------------------- 254 254 255 - // "make lines be width" 256 - // line to short? align left/right/center/stretch 257 - // line to long? truncate/wrap 258 - // break at? shys/whitespace/unicode word boudnaries/grahpeme boundaries? this is hard 259 - // justify vs text align 255 + /// Limit the dimensions of a string, either by wrapping on white space 256 + /// characters, or by truncating the last line and appending an ellipsis. 257 + /// 258 + /// The lines of the resulting string are left aligned and are at most `columns` 259 + /// wide. If a single word is longer than the maximum allowed line width, the 260 + /// word is broken into 2 pieces at the grapheme level. If the string gets 261 + /// truncated, this function will keep collecting ANSI sequences to make sure 262 + /// all formatting is preserved. 263 + /// 264 + /// A commonly used ellipsis character is `"…"`, also known as 265 + /// U+2026 HORIZONTAL ELLIPSIS. 266 + /// 267 + /// ### Examples 268 + /// 269 + /// ```gleam 270 + /// limit("Hello World", Size(rows: 1, columns: 10), ellipsis: "...") 271 + /// // --> "Hello W..." 272 + /// 273 + /// limit("Hello World", Size(rows: 2, columns: 5), ellipsis: "...") 274 + /// // --> "Hello\nWorld" 275 + /// ``` 276 + pub fn limit( 277 + str: String, 278 + to max_size: Size, 279 + ellipsis ellipsis: String, 280 + ) -> String { 281 + limit_with(str, max_size, default_options, ellipsis) 282 + } 283 + 284 + type LimitState { 285 + LimitState( 286 + str: String, 287 + row: Int, 288 + col: Int, 289 + spaces: String, 290 + spaces_width: Int, 291 + non_spaces: String, 292 + non_spaces_width: Int, 293 + overflow: String, 294 + overflow_width: Int, 295 + ) 296 + } 297 + 298 + fn limit_state_col(state: LimitState) { 299 + state.col + state.spaces_width + state.non_spaces_width + state.overflow_width 300 + } 301 + 302 + /// Like `limit`, but also customise the options used for measuring. 303 + pub fn limit_with( 304 + str: String, 305 + to max_size: Size, 306 + using options: Options, 307 + ellipsis ellipsis: String, 308 + ) -> String { 309 + let ellipsis_width = line_with(ellipsis, options) 310 + 311 + let initial = 312 + LimitState( 313 + str: "", 314 + row: 0, 315 + col: 0, 316 + spaces: "", 317 + spaces_width: 0, 318 + non_spaces: "", 319 + non_spaces_width: 0, 320 + overflow: "", 321 + overflow_width: 0, 322 + ) 323 + 324 + // we encode a few states, but we do it this way such that spread works... 325 + // - if state.row == max_size.rows, we have reached the end and should do nothing 326 + // - if we have oveflow, we are at the last line and stuff doesn't fit. 327 + // - if we have non_spaces or overflow, we are collecting word characters. 328 + // - if we do not, we are collecting spaces _before_ a word. 329 + // - on the first space we collect, if we notice that we have word characters, 330 + // we try to push this word into the accumulator. 331 + // if it doesn't fit on the last line, we have overflow, and can append the ellipsis. 332 + // afterwards, we set row = max_sie.rows to indicate we are finished. 333 + let push_space = fn(state: LimitState, piece: String, width: Int) { 334 + let new_col = limit_state_col(state) 335 + 336 + // make sure we definitely have room for the ellipsis on the last line 337 + let max_col = case state.row + 1 >= max_size.rows { 338 + True -> max_size.columns - ellipsis_width 339 + False -> max_size.columns 340 + } 341 + 342 + case state.non_spaces == "" && state.overflow == "" { 343 + // we do not have non-spaces, so we are still collecting spaces. 344 + True -> 345 + case state.str { 346 + // skip spaces at the start, since we skip them at the end. 347 + "" -> state 348 + _ -> 349 + LimitState( 350 + ..state, 351 + spaces: state.spaces <> piece, 352 + spaces_width: state.spaces_width + width, 353 + ) 354 + } 355 + 356 + // we have non-spaces, so we want to push this word into the accumulator 357 + // and then start again collecting spaces. 358 + False -> 359 + // would it fit into the line just normally? 360 + case new_col <= max_col { 361 + True -> { 362 + // it fits normally, nothing special required! 363 + let str = 364 + state.str <> state.spaces <> state.non_spaces <> state.overflow 365 + // does our new space fit though? 366 + case state.row + 1 >= max_size.rows && new_col + width > max_col { 367 + False -> 368 + LimitState( 369 + str:, 370 + row: state.row, 371 + col: new_col, 372 + spaces: piece, 373 + spaces_width: width, 374 + non_spaces: "", 375 + non_spaces_width: 0, 376 + overflow: "", 377 + overflow_width: 0, 378 + ) 379 + 380 + True -> 381 + LimitState( 382 + str:, 383 + row: state.row, 384 + col: new_col, 385 + spaces: "", 386 + spaces_width: 0, 387 + non_spaces: "", 388 + non_spaces_width: 0, 389 + overflow: piece, 390 + overflow_width: width, 391 + ) 392 + } 393 + } 394 + 395 + False -> 396 + // it doesn't; if we are at the last line, truncate. else, wrap. 397 + case state.row + 1 >= max_size.rows { 398 + True -> 399 + // we are at the last line. 400 + LimitState( 401 + ..state, 402 + str: state.str <> state.spaces <> state.non_spaces <> ellipsis, 403 + row: state.row + 1, 404 + ) 405 + 406 + False -> 407 + LimitState( 408 + str: state.str <> "\n" <> state.non_spaces <> state.overflow, 409 + row: state.row + 1, 410 + col: state.non_spaces_width + state.overflow_width, 411 + spaces: piece, 412 + spaces_width: width, 413 + non_spaces: "", 414 + non_spaces_width: 0, 415 + overflow: "", 416 + overflow_width: 0, 417 + ) 418 + } 419 + } 420 + } 421 + } 422 + 423 + let state = { 424 + // TODO: this may be fold_raw-able 425 + use state, piece <- fold_with(str, options, initial) 426 + case state.row >= max_size.rows { 427 + // reached the end, but we still want to collect ansi sequences 428 + True -> 429 + case is_ansi_component(piece.piece, options) { 430 + True -> LimitState(..state, str: state.str <> piece.piece) 431 + False -> state 432 + } 433 + 434 + False -> 435 + case piece.piece { 436 + // Line break that we recognise as such 437 + "\n" | "\r\n" -> { 438 + let state = push_space(state, "", 0) 439 + LimitState( 440 + ..state, 441 + str: state.str <> "\n", 442 + row: state.row + 1, 443 + col: 0, 444 + ) 445 + } 446 + 447 + "\t" 448 + | // White_Space=Y, Non line-break 449 + " " 450 + | "\u{a0}" 451 + | "\u{1680}" 452 + | "\u{2000}" 453 + | "\u{2001}" 454 + | "\u{2002}" 455 + | "\u{2003}" 456 + | "\u{2004}" 457 + | "\u{2005}" 458 + | "\u{2006}" 459 + | "\u{2007}" 460 + | "\u{2008}" 461 + | "\u{2009}" 462 + | "\u{200a}" 463 + | "\u{202f}" 464 + | "\u{205f}" 465 + | "\u{3000}" -> push_space(state, piece.piece, piece.width) 466 + 467 + // not a space or a line break 468 + _ -> { 469 + let max_col = case state.row + 1 >= max_size.rows { 470 + True -> max_size.columns - ellipsis_width 471 + False -> max_size.columns 472 + } 473 + 474 + case limit_state_col(state) + piece.width > max_col { 475 + True -> { 476 + // these non_space characters no longer fit the line, 477 + // but what if overflow is too long as well? 478 + case 479 + state.non_spaces_width + state.overflow_width + piece.width 480 + > max_size.columns 481 + { 482 + True -> 483 + // it _is_! do we have room for another row? 484 + case state.row + 1 < max_size.rows { 485 + True -> 486 + LimitState( 487 + str: state.str 488 + <> state.spaces 489 + <> state.non_spaces 490 + <> "\n", 491 + col: 0, 492 + row: state.row + 1, 493 + non_spaces: state.overflow <> piece.piece, 494 + non_spaces_width: state.overflow_width + piece.width, 495 + spaces: "", 496 + spaces_width: 0, 497 + overflow: "", 498 + overflow_width: 0, 499 + ) 500 + 501 + False -> 502 + // no, word to long this is the last row, sorry. 503 + LimitState( 504 + ..state, 505 + str: state.str 506 + <> state.spaces 507 + <> state.non_spaces 508 + <> ellipsis, 509 + row: state.row + 1, 510 + ) 511 + } 512 + 513 + False -> 514 + LimitState( 515 + ..state, 516 + overflow: state.overflow <> piece.piece, 517 + overflow_width: state.overflow_width + piece.width, 518 + ) 519 + } 520 + } 521 + 522 + False -> 523 + // everything's fine and normal :) 524 + LimitState( 525 + ..state, 526 + non_spaces: state.non_spaces <> piece.piece, 527 + non_spaces_width: state.non_spaces_width + piece.width, 528 + ) 529 + } 530 + } 531 + } 532 + } 533 + } 534 + 535 + case 536 + state.row >= max_size.rows 537 + || { state.non_spaces == "" && state.overflow == "" } 538 + { 539 + // either reached the end, or we have no word characters left. 540 + True -> state.str 541 + False -> { 542 + let new_col = limit_state_col(state) 543 + // would it fit into the line just normally? 544 + case new_col <= max_size.columns { 545 + True -> state.str <> state.spaces <> state.non_spaces <> state.overflow 546 + 547 + False -> 548 + // it doesn't; if we are at the last line, truncate. else, wrap. 549 + case state.row + 1 >= max_size.rows { 550 + True -> state.str <> state.spaces <> state.non_spaces <> ellipsis 551 + False -> state.str <> "\n" <> state.non_spaces <> state.overflow 552 + } 553 + } 554 + } 555 + } 556 + } 557 + 558 + /// Replace all tab characters found in the string with the amount of spaces 559 + /// that this tab would have had otherwise. 560 + /// 561 + /// This makes it save to prepend to a line, without changing the spacing produced 562 + /// by tabs anymore. 563 + /// 564 + /// ### Examples 565 + /// 566 + /// ```gleam 567 + /// tabs_to_spaces("Hello\tWorld") 568 + /// // --> "Hello World" // 3 spaces 569 + /// ``` 570 + pub fn tabs_to_spaces(str: String) -> String { 571 + tabs_to_spaces_with(str, default_options) 572 + } 573 + 574 + /// Like `tabs_to_spaces`, but customise the opttions as well. 575 + /// 576 + /// **NB:** You can use `tab_offset` and `tab_width` to change the measure of 577 + /// tab characters inside the string! 578 + /// 579 + /// ```gleam 580 + /// let options = new() |> tab_width(4) 581 + /// tabs_to_spaces_with("hi\tcutie~", options) 582 + /// // --> "hi cutie~" 583 + /// ``` 584 + pub fn tabs_to_spaces_with(str: String, using options: Options) -> String { 585 + use acc, piece <- fold_with(str, options, from: "") 586 + case piece.piece { 587 + "\t" -> acc <> string.repeat(" ", piece.width) 588 + _ -> acc <> piece.piece 589 + } 590 + } 260 591 261 592 // fold over words? (handle shy hyphens/soft breaks etc) 262 593 // I do not want to implement full unicode word segmentation here though