├── .github
└── workflows
│ ├── build.yml
│ ├── gradle-wrapper-validation.yml
│ └── release.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── build.gradle.kts
├── buildSrc
├── build.gradle.kts
├── settings.gradle.kts
└── src
│ └── main
│ └── kotlin
│ ├── mordant-js-conventions.gradle.kts
│ ├── mordant-js-sample-conventions.gradle.kts
│ ├── mordant-jvm-sample-conventions.gradle.kts
│ ├── mordant-kotlin-conventions.gradle.kts
│ ├── mordant-mpp-conventions.gradle.kts
│ ├── mordant-mpp-sample-conventions.gradle.kts
│ ├── mordant-native-conventions.gradle.kts
│ ├── mordant-native-core-conventions.gradle.kts
│ ├── mordant-native-sample-conventions.gradle.kts
│ └── mordant-publishing-conventions.gradle.kts
├── docs
├── css
│ └── logo-styles.css
├── guide.md
├── img
│ ├── animation.svg
│ ├── animation_text.gif
│ ├── gradient_black_36dp.svg
│ ├── gradient_white_24dp.svg
│ ├── progess_simple.gif
│ ├── progress_cells.gif
│ ├── progress_context.gif
│ ├── progress_multi.gif
│ ├── select_list.gif
│ └── tour.png
├── input.md
└── progress.md
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── mkdocs.yml
├── mordant-coroutines
├── README.md
├── api
│ └── mordant-coroutines.api
├── build.gradle.kts
├── gradle.properties
└── src
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ ├── animation
│ │ └── coroutines
│ │ │ └── CoroutineAnimator.kt
│ │ └── input
│ │ └── coroutines
│ │ └── ReceiveEventsFlow.kt
│ └── commonTest
│ └── kotlin
│ └── com
│ └── github
│ └── ajalt
│ └── mordant
│ └── animation
│ └── coroutines
│ └── CoroutinesAnimatorTest.kt
├── mordant-jvm-ffm
├── README.md
├── api
│ └── mordant-jvm-ffm.api
├── build.gradle.kts
├── gradle.properties
└── src
│ └── jvmMain
│ ├── kotlin
│ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── terminal
│ │ └── terminalinterface
│ │ └── ffm
│ │ ├── FfmLayouts.kt
│ │ ├── TerminalInterface.ffm.linux.kt
│ │ ├── TerminalInterface.ffm.macos.kt
│ │ ├── TerminalInterface.ffm.windows.kt
│ │ └── TerminalInterfaceProvider.ffm.kt
│ └── resources
│ └── META-INF
│ ├── native-image
│ └── com.github.ajalt.mordant
│ │ └── mordant-jvm-ffm
│ │ ├── reflection-config.json
│ │ └── resource-config.json
│ └── services
│ └── com.github.ajalt.mordant.terminal.TerminalInterfaceProvider
├── mordant-jvm-graal-ffi
├── README.md
├── api
│ └── mordant-jvm-graal-ffi.api
├── build.gradle.kts
├── gradle.properties
└── src
│ └── jvmMain
│ ├── kotlin
│ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── terminal
│ │ └── terminalinterface
│ │ └── nativeimage
│ │ ├── TerminalInterface.nativeimage.linux.kt
│ │ ├── TerminalInterface.nativeimage.macos.kt
│ │ ├── TerminalInterface.nativeimage.windows.kt
│ │ └── TerminalInterfaceProvider.nativeimage.kt
│ └── resources
│ └── META-INF
│ ├── native-image
│ └── com.github.ajalt.mordant
│ │ └── mordant-jvm-graal-ffi
│ │ ├── reflection-config.json
│ │ └── resource-config.json
│ ├── proguard
│ └── mordant-jvm-graal-ffi.pro
│ └── services
│ └── com.github.ajalt.mordant.terminal.TerminalInterfaceProvider
├── mordant-jvm-jna
├── README.md
├── api
│ └── mordant-jvm-jna.api
├── build.gradle.kts
├── gradle.properties
└── src
│ └── jvmMain
│ ├── kotlin
│ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── terminal
│ │ └── terminalinterface
│ │ └── jna
│ │ ├── TerminalInterface.jna.linux.kt
│ │ ├── TerminalInterface.jna.macos.kt
│ │ ├── TerminalInterface.jna.windows.kt
│ │ └── TerminalInterfaceProvider.jna.kt
│ └── resources
│ └── META-INF
│ ├── native-image
│ └── com.github.ajalt.mordant
│ │ └── mordant-jvm-jna
│ │ ├── reflection-config.json
│ │ └── resource-config.json
│ ├── proguard
│ └── mordant-jvm-jna.pro
│ └── services
│ └── com.github.ajalt.mordant.terminal.TerminalInterfaceProvider
├── mordant-markdown
├── README.md
├── api
│ └── mordant-markdown.api
├── build.gradle.kts
├── gradle.properties
└── src
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── markdown
│ │ ├── BlockQuote.kt
│ │ ├── Markdown.kt
│ │ └── MarkdownRenderer.kt
│ └── commonTest
│ └── kotlin
│ └── com
│ └── github
│ └── ajalt
│ └── mordant
│ └── markdown
│ └── MarkdownTest.kt
├── mordant-omnibus
├── README.md
├── api
│ └── mordant-omnibus.api
├── build.gradle.kts
├── gradle.properties
└── src
│ └── commonMain
│ └── kotlin
│ └── com
│ └── github
│ └── ajalt
│ └── mordant
│ └── internal
│ └── OmibusInternal.kt
├── mordant
├── README.md
├── api
│ └── mordant.api
├── build.gradle.kts
├── gradle.properties
└── src
│ ├── appleNonDesktopMain
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── internal
│ │ └── MppInternal.appleNonDesktop.kt
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ ├── animation
│ │ ├── Animation.kt
│ │ ├── RefreshableAnimation.kt
│ │ ├── StoppableAnimation.kt
│ │ └── progress
│ │ │ ├── MultiProgressBarAnimation.kt
│ │ │ └── ProgressBarAnimation.kt
│ │ ├── input
│ │ ├── InputEvent.kt
│ │ ├── InputReceiver.kt
│ │ ├── InteractiveSelectList.kt
│ │ ├── MouseTracking.kt
│ │ ├── RawMode.kt
│ │ ├── ReceiveEvents.kt
│ │ └── SelectListAnimation.kt
│ │ ├── internal
│ │ ├── AnsiCodes.kt
│ │ ├── AnsiRender.kt
│ │ ├── BlankWidgetWrapper.kt
│ │ ├── Constants.kt
│ │ ├── Formatting.kt
│ │ ├── HyperlinkIds.kt
│ │ ├── MppInternal.kt
│ │ ├── Parsing.kt
│ │ ├── ThemeValue.kt
│ │ ├── Utf8.kt
│ │ ├── cellwidth.kt
│ │ └── gen
│ │ │ ├── ZwjSequences1.kt
│ │ │ ├── ZwjSequences2.kt
│ │ │ ├── ZwjSequences3.kt
│ │ │ ├── ZwjSequences4.kt
│ │ │ ├── cellwidthtable.kt
│ │ │ └── emojiseqtable.kt
│ │ ├── platform
│ │ └── MultiplatformSystem.kt
│ │ ├── rendering
│ │ ├── Align.kt
│ │ ├── BorderType.kt
│ │ ├── Lines.kt
│ │ ├── OverflowWrap.kt
│ │ ├── Size.kt
│ │ ├── Span.kt
│ │ ├── TextColors.kt
│ │ ├── TextStyle.kt
│ │ ├── Theme.kt
│ │ ├── Whitespace.kt
│ │ ├── Widget.kt
│ │ └── WidthRange.kt
│ │ ├── table
│ │ ├── Borders.kt
│ │ ├── Table.kt
│ │ ├── TableCsv.kt
│ │ ├── TableDsl.kt
│ │ ├── TableDslInstances.kt
│ │ ├── TableLayout.kt
│ │ └── VerticalLayout.kt
│ │ ├── terminal
│ │ ├── HtmlRenderer.kt
│ │ ├── Prompt.kt
│ │ ├── StandardTerminalInterface.kt
│ │ ├── Terminal.kt
│ │ ├── TerminalCursor.kt
│ │ ├── TerminalDetection.kt
│ │ ├── TerminalExtensions.kt
│ │ ├── TerminalInfo.kt
│ │ ├── TerminalInterceptor.kt
│ │ ├── TerminalInterface.kt
│ │ ├── TerminalInterfaceProvider.kt
│ │ ├── TerminalRecorder.kt
│ │ └── terminalinterface
│ │ │ ├── PosixEventParser.kt
│ │ │ ├── TerminalInterface.posix.kt
│ │ │ ├── TerminalInterface.windows.kt
│ │ │ └── WindowsVirtualKeyCodeToKeyEvent.kt
│ │ └── widgets
│ │ ├── Caption.kt
│ │ ├── DefinitionList.kt
│ │ ├── EmptyWidget.kt
│ │ ├── HorizontalRule.kt
│ │ ├── OrderedList.kt
│ │ ├── Padding.kt
│ │ ├── Panel.kt
│ │ ├── ProgressBar.kt
│ │ ├── SelectList.kt
│ │ ├── Spinner.kt
│ │ ├── Text.kt
│ │ ├── UnorderedList.kt
│ │ ├── Viewport.kt
│ │ └── progress
│ │ ├── CachedProgressBarDefinition.kt
│ │ ├── ProgressBarWidgetMaker.kt
│ │ ├── ProgressLayoutCells.kt
│ │ ├── ProgressLayoutScope.kt
│ │ └── ProgressState.kt
│ ├── commonTest
│ ├── kotlin
│ │ └── com
│ │ │ └── github
│ │ │ └── ajalt
│ │ │ └── mordant
│ │ │ ├── animation
│ │ │ ├── AnimationTest.kt
│ │ │ └── progress
│ │ │ │ └── BaseProgressAnimationTest.kt
│ │ │ ├── input
│ │ │ ├── InteractiveSelectListTest.kt
│ │ │ └── SelectListAnimationTest.kt
│ │ │ ├── platform
│ │ │ └── MultiplatformSystemTest.kt
│ │ │ ├── rendering
│ │ │ ├── TextAlignmentTest.kt
│ │ │ ├── TextOverflowWrapTest.kt
│ │ │ ├── TextStyleOscTest.kt
│ │ │ ├── TextWhitespaceTest.kt
│ │ │ ├── ThemeTest.kt
│ │ │ └── internal
│ │ │ │ └── CellWidthTest.kt
│ │ │ ├── table
│ │ │ ├── LinearLayoutTest.kt
│ │ │ ├── TableAlignmentTest.kt
│ │ │ ├── TableBorderStyleTest.kt
│ │ │ ├── TableBorderTest.kt
│ │ │ ├── TableColumnWidthTest.kt
│ │ │ ├── TableCsvTest.kt
│ │ │ └── TableTest.kt
│ │ │ ├── terminal
│ │ │ ├── HtmlRendererTest.kt
│ │ │ ├── PromptTest.kt
│ │ │ ├── TerminalCursorTest.kt
│ │ │ └── TerminalTest.kt
│ │ │ ├── test
│ │ │ ├── RenderingTest.kt
│ │ │ └── TestUtils.kt
│ │ │ └── widgets
│ │ │ ├── DefinitionListTest.kt
│ │ │ ├── HorizontalRuleTest.kt
│ │ │ ├── MultiProgressLayoutTest.kt
│ │ │ ├── OrderedListTest.kt
│ │ │ ├── PaddingTest.kt
│ │ │ ├── PanelTest.kt
│ │ │ ├── ProgressBarTest.kt
│ │ │ ├── ProgressLayoutTest.kt
│ │ │ ├── SelectListTest.kt
│ │ │ ├── SpinnerTest.kt
│ │ │ ├── TextTest.kt
│ │ │ ├── UnorderedListTest.kt
│ │ │ └── ViewportTest.kt
│ └── resources
│ │ └── multiplatform_system_test.txt
│ ├── iosMain
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ ├── internal
│ │ └── MppInternal.ios.kt
│ │ └── terminal
│ │ └── terminalinterface
│ │ └── TerminalInterface.native.ios.kt
│ ├── jsCommonMain
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ ├── internal
│ │ └── MppInternal.jsCommon.kt
│ │ └── terminal
│ │ └── terminalinterface
│ │ └── TerminalInterface.jsCommon.kt
│ ├── jsMain
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ ├── internal
│ │ ├── JsCompat.kt
│ │ └── MppInternal.js.kt
│ │ └── terminal
│ │ └── terminalinterface
│ │ └── TerminalInterface.js.kt
│ ├── jvmMain
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ ├── animation
│ │ └── progress
│ │ │ └── ThreadAnimator.kt
│ │ ├── internal
│ │ └── MppInternal.jvm.kt
│ │ └── terminal
│ │ └── terminalinterface
│ │ └── TerminalInterface.jvm.posix.kt
│ ├── jvmTest
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ ├── animation
│ │ └── progress
│ │ │ └── ThreadAnimatorTest.kt
│ │ └── terminal
│ │ └── StderrTerminalTest.kt
│ ├── linuxMain
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── internal
│ │ └── MppInternal.linux.kt
│ ├── macosMain
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── internal
│ │ └── MppInternal.macos.kt
│ ├── mingwMain
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ ├── internal
│ │ └── MppInternal.mingw.kt
│ │ └── terminal
│ │ └── terminalinterface
│ │ └── TerminalInterface.native.windows.kt
│ ├── nativeMain
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── internal
│ │ └── MppInternal.native.kt
│ ├── posixMain
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ ├── internal
│ │ └── tty.kt
│ │ └── terminal
│ │ └── terminalinterface
│ │ └── TerminalInterface.native.posix.kt
│ ├── posixSharedMain
│ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ ├── internal
│ │ └── MppInternal.native.posixshared.kt
│ │ └── terminal
│ │ └── terminalinterface
│ │ └── TerminalInterface.native.shared.kt
│ └── wasmJsMain
│ └── kotlin
│ └── com
│ └── github
│ └── ajalt
│ └── mordant
│ ├── internal
│ └── MppInternal.wasmJs.kt
│ └── terminal
│ └── terminalinterface
│ └── TerminalInterface.wasm.kt
├── prepare_docs.sh
├── runsample
├── runsample.bat
├── samples
├── detection
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── samples
│ │ └── main.kt
├── drawing
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── samples
│ │ └── main.kt
├── hexviewer
│ ├── README.md
│ ├── build.gradle.kts
│ ├── example.png
│ └── src
│ │ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── samples
│ │ └── main.kt
├── markdown
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── samples
│ │ └── main.kt
├── progress
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── jvmMain
│ │ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── samples
│ │ └── main.kt
├── select
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── samples
│ │ └── main.kt
├── table
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── ajalt
│ │ └── mordant
│ │ └── samples
│ │ └── main.kt
└── tour
│ ├── build.gradle.kts
│ └── src
│ └── commonMain
│ └── kotlin
│ └── com
│ └── github
│ └── ajalt
│ └── mordant
│ └── samples
│ └── main.kt
├── scripts
├── generate_cellwidth_table.py
└── generate_emoji_sequence_table.py
├── settings.gradle.kts
└── test
├── graalvm
├── build.gradle.kts
└── src
│ └── test
│ └── kotlin
│ └── GraalSmokeTest.kt
└── proguard
├── build.gradle.kts
└── src
└── main
├── kotlin
└── R8SmokeTest.kt
└── rules.pro
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | pull_request:
5 | paths-ignore:
6 | - 'docs/**'
7 | - 'samples/**'
8 | - '*.md'
9 | push:
10 | branches:
11 | - 'master'
12 | paths-ignore:
13 | - 'docs/**'
14 | - 'samples/**'
15 | - '*.md'
16 | jobs:
17 | test:
18 | strategy:
19 | matrix:
20 | os: [macos-latest, windows-latest, ubuntu-latest]
21 | include:
22 | - os: ubuntu-latest
23 | EXTRA_GRADLE_ARGS: apiCheck :test:proguard:r8jar
24 | - os: macos-latest
25 | EXTRA_GRADLE_ARGS: >-
26 | :mordant:compileNativeMainKotlinMetadata
27 | :mordant:compilePosixMainKotlinMetadata
28 | :mordant:compileAppleMainKotlinMetadata
29 | :mordant:compileMacosMainKotlinMetadata
30 | :mordant:compileWatchosMainKotlinMetadata
31 | :mordant:compileTvosMainKotlinMetadata
32 | runs-on: ${{matrix.os}}
33 | steps:
34 | - uses: actions/checkout@v4
35 | - uses: graalvm/setup-graalvm@v1
36 | with:
37 | java-version: 22
38 | distribution: 'graalvm-community'
39 | set-java-home: false
40 | - uses: actions/setup-java@v4
41 | with:
42 | distribution: 'zulu'
43 | java-version: 22
44 | - uses: gradle/actions/setup-gradle@v3
45 | - name: Run tests
46 | run: >-
47 | ./gradlew
48 | ${{matrix.EXTRA_GRADLE_ARGS}}
49 | :mordant:check
50 | :mordant-coroutines:check
51 | :mordant-markdown:check
52 | :test:graalvm:nativeTest
53 | --stacktrace
54 | - name: Run R8 Jar
55 | if: ${{ matrix.os == 'ubuntu-latest' }}
56 | run: java -jar test/proguard/build/libs/main-r8.jar
57 | - name: Upload the build report
58 | if: failure()
59 | uses: actions/upload-artifact@master
60 | with:
61 | name: build-report-${{ matrix.os }}
62 | path: '**/build/reports'
63 | publish-snapshot:
64 | needs: test
65 | runs-on: macos-latest
66 | if: ${{ github.ref == 'refs/heads/master' && github.repository == 'ajalt/mordant' }}
67 | steps:
68 | - uses: actions/checkout@v4
69 | - uses: actions/setup-java@v4
70 | with:
71 | distribution: 'zulu'
72 | java-version: 22
73 | - uses: gradle/actions/setup-gradle@v3
74 | - name: Deploy to sonatype
75 | # disable configuration cache due to https://github.com/gradle/gradle/issues/22779
76 | run: ./gradlew publishToMavenCentral -PsnapshotVersion=true --no-configuration-cache
77 | env:
78 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralPassword }}
79 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_signingInMemoryKey }}
80 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralUsername }}
81 | env:
82 | GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx12g -Dfile.encoding=UTF-8" -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.project.kotlin.incremental.multiplatform=false -Dorg.gradle.project.kotlin.native.disableCompilerDaemon=true
83 |
--------------------------------------------------------------------------------
/.github/workflows/gradle-wrapper-validation.yml:
--------------------------------------------------------------------------------
1 | name: "Validate Gradle Wrapper"
2 | on:
3 | pull_request:
4 | paths:
5 | - 'gradlew'
6 | - 'gradlew.bat'
7 | - 'gradle/wrapper/'
8 |
9 |
10 | jobs:
11 | validation:
12 | name: "Validation"
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - uses: gradle/actions/wrapper-validation@v3
17 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '**'
7 |
8 | jobs:
9 | release:
10 | runs-on: macos-latest
11 | if: ${{ github.repository == 'ajalt/mordant' }}
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions/setup-java@v4
15 | with:
16 | distribution: 'zulu'
17 | java-version: 22
18 | - uses: actions/setup-python@v5
19 | with:
20 | python-version: '3.12'
21 | - uses: gradle/actions/setup-gradle@v3
22 | - run: ./gradlew publishToMavenCentral --no-configuration-cache
23 | env:
24 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralPassword }}
25 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_signingInMemoryKey }}
26 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralUsername }}
27 | # Disabled due to https://github.com/ffurrer2/extract-release-notes/issues/339
28 | # - name: Extract release notes
29 | # id: extract-release-notes
30 | # uses: ffurrer2/extract-release-notes@v2
31 | # - name: Create release
32 | # uses: ncipollo/release-action@v1
33 | # with:
34 | # body: ${{ steps.extract-release-notes.outputs.release_notes }}
35 | # - name: Dokka
36 | # uses: gradle/actions/setup-gradle@v3
37 | # with:
38 | # arguments: dokkaHtmlMultiModule
39 | # - run : ./prepare_docs.sh
40 | # - name: Build mkdocs
41 | # run: |
42 | # pip install mkdocs-material
43 | # mkdocs build
44 | # - name: Deploy docs to website
45 | # uses: JamesIves/github-pages-deploy-action@v4
46 | # with:
47 | # branch: gh-pages
48 | # folder: site
49 | env:
50 | # macos-latest is now macos-14 and has less than half as much memory available as other runners
51 | GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -Dfile.encoding=UTF-8" -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.project.kotlin.incremental.multiplatform=false -Dorg.gradle.project.kotlin.native.disableCompilerDaemon=true
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/
5 | .DS_Store
6 | build/
7 | captures/
8 | .externalNativeBuild
9 | out/
10 | docs/api/
11 | docs/changelog.md
12 | docs/index.md
13 | site/
14 | kotlin-js-store/
15 | .kotlin/
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Mordant is a multiplatform library for rendering styled text in the terminal. You can use it to
6 | add color and style to text, create tables, draw animations, and more.
7 |
8 | Mordant has:
9 |
10 | * Easy colorful ANSI output with automatic detection of terminal capabilities
11 | * Markdown rendering directly to the terminal
12 | * Widgets for laying out terminal output, including lists, tables, panels, and more
13 | * Support for animating any widget, like progress bars and dashboards
14 |
15 | ## Documentation
16 |
17 | The full documentation can be found on [the website](https://ajalt.github.io/mordant/).
18 |
19 | ## Installation
20 |
21 | Mordant is distributed through Maven Central.
22 |
23 | ```groovy
24 | dependencies {
25 | implementation("com.github.ajalt.mordant:mordant:3.0.2")
26 |
27 | // optional extensions for running animations with coroutines
28 | implementation("com.github.ajalt.mordant:mordant-coroutines:3.0.2")
29 |
30 | // optional widget for rendering Markdown
31 | implementation("com.github.ajalt.mordant:mordant-markdown:3.0.2")
32 | }
33 | ```
34 |
35 | On JVM, there are more granular dependencies available.
36 | [See the docs for details](https://ajalt.github.io/mordant/guide/).
37 |
38 | ###### If you're using Maven instead of Gradle, use `mordant-jvm `
39 |
40 | #### Snapshots
41 |
42 |
43 | Snapshot builds are also available
44 |
45 |
46 |
47 |
48 | You'll need to add the Sonatype snapshots repository:
49 |
50 | ```kotlin
51 | repositories {
52 | maven {
53 | url = uri("https://oss.sonatype.org/content/repositories/snapshots/")
54 | }
55 | }
56 | ```
57 |
58 |
59 |
60 | ## License
61 |
62 | Copyright 2018 AJ Alt
63 |
64 | Licensed under the Apache License, Version 2.0 (the "License");
65 | you may not use this file except in compliance with the License.
66 | You may obtain a copy of the License at
67 |
68 | http://www.apache.org/licenses/LICENSE-2.0
69 |
70 | Unless required by applicable law or agreed to in writing, software
71 | distributed under the License is distributed on an "AS IS" BASIS,
72 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
73 | See the License for the specific language governing permissions and
74 | limitations under the License.
75 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.dokka.gradle.DokkaMultiModuleTask
2 | import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask
3 |
4 | plugins {
5 | alias(libs.plugins.kotlinBinaryCompatibilityValidator)
6 | id("org.jetbrains.dokka")
7 | }
8 |
9 | apiValidation {
10 | // https://github.com/Kotlin/binary-compatibility-validator/issues/3
11 | project("samples").subprojects.mapTo(ignoredProjects) { it.name }
12 | project("test").subprojects.mapTo(ignoredProjects) { it.name }
13 | }
14 |
15 | tasks.withType().configureEach {
16 | outputDirectory.set(rootProject.rootDir.resolve("docs/api"))
17 | pluginsMapConfiguration.set(
18 | mapOf(
19 | "org.jetbrains.dokka.base.DokkaBase" to """{
20 | "footerMessage": "Copyright © 2017 AJ Alt"
21 | }"""
22 | )
23 | )
24 | }
25 |
26 | // https://youtrack.jetbrains.com/issue/KT-63014
27 | tasks.withType().configureEach {
28 | args.add("--ignore-engines")
29 | }
30 |
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | mavenCentral()
7 | gradlePluginPortal()
8 | }
9 |
10 | dependencies {
11 | implementation(libs.kotlin.gradle.plugin)
12 | implementation(libs.publish)
13 | implementation(libs.dokka)
14 | }
15 |
--------------------------------------------------------------------------------
/buildSrc/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "buildSrc"
2 |
3 | dependencyResolutionManagement {
4 | versionCatalogs {
5 | create("libs") {
6 | from(files("../gradle/libs.versions.toml"))
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/mordant-js-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
2 | import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
3 |
4 | plugins {
5 | kotlin("multiplatform")
6 | }
7 |
8 | kotlin {
9 | js {
10 | // We have different code paths on browsers and node, so we run tests on both
11 | nodejs()
12 | browser()
13 | }
14 | @OptIn(ExperimentalWasmDsl::class)
15 | wasmJs {
16 | nodejs()
17 | browser()
18 | }
19 |
20 | sourceSets {
21 | val jsCommonMain by creating { dependsOn(commonMain.get()) }
22 | jsMain.get().dependsOn(jsCommonMain)
23 | wasmJsMain.get().dependsOn(jsCommonMain)
24 | }
25 | }
26 |
27 | // Need to compile using a canary version of Node due to
28 | // https://youtrack.jetbrains.com/issue/KT-63014
29 | rootProject.the().apply {
30 | version = "21.0.0-v8-canary2023091837d0630120"
31 | downloadBaseUrl = "https://nodejs.org/download/v8-canary"
32 | }
33 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/mordant-js-sample-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
2 |
3 | plugins {
4 | kotlin("multiplatform")
5 | }
6 |
7 | kotlin {
8 | js {
9 | nodejs()
10 | binaries.executable()
11 | }
12 | @OptIn(ExperimentalWasmDsl::class)
13 | wasmJs {
14 | nodejs()
15 | binaries.executable()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/mordant-jvm-sample-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-kotlin-conventions")
3 | application
4 | }
5 |
6 | kotlin {
7 | // For some reason MainKt isn't present in the jar unless we add withJava
8 | jvm { withJava() }
9 |
10 | sourceSets {
11 | jvmMain.dependencies {
12 | implementation(project(":mordant-omnibus"))
13 | }
14 | }
15 | }
16 |
17 | application {
18 | mainClass.set("com.github.ajalt.mordant.samples.MainKt")
19 | applicationDefaultJvmArgs = listOf(
20 | "-Dfile.encoding=utf-8",
21 | "--enable-native-access=ALL-UNNAMED"
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/mordant-kotlin-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2 | import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
3 |
4 | plugins {
5 | kotlin("multiplatform")
6 | }
7 |
8 | tasks.withType().configureEach {
9 | compilerOptions {
10 | freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn")
11 | jvmTarget.set(JvmTarget.JVM_1_8)
12 | }
13 | }
14 |
15 | tasks.withType().configureEach {
16 | options.release.set(8)
17 | }
18 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/mordant-mpp-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-kotlin-conventions")
3 | id("mordant-native-conventions")
4 | id("mordant-js-conventions")
5 | }
6 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/mordant-mpp-sample-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-jvm-sample-conventions")
3 | id("mordant-native-sample-conventions")
4 | id("mordant-js-sample-conventions")
5 | }
6 |
7 | kotlin {
8 | sourceSets {
9 | commonMain.dependencies {
10 | implementation(project(":mordant"))
11 | }
12 | jvmMain.dependencies {
13 | implementation(project(":mordant-omnibus"))
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/mordant-native-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-native-core-conventions")
3 | }
4 |
5 | kotlin {
6 | // Add targets not supported by the markdown library
7 | tvosX64()
8 | tvosArm64()
9 | tvosSimulatorArm64()
10 | watchosArm32()
11 | watchosArm64()
12 | watchosX64()
13 | watchosSimulatorArm64()
14 | }
15 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/mordant-native-core-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | }
4 |
5 | kotlin {
6 | applyDefaultHierarchyTemplate()
7 |
8 | linuxX64()
9 | linuxArm64()
10 | macosX64()
11 | macosArm64()
12 | mingwX64()
13 |
14 | iosX64()
15 | iosArm64()
16 | iosSimulatorArm64()
17 | // Not all targets are supported by the markdown library
18 | // tvosX64()
19 | // tvosArm64()
20 | // tvosSimulatorArm64()
21 | // watchosArm32()
22 | // watchosArm64()
23 | // watchosDeviceArm64()
24 | // watchosX64()
25 | // watchosSimulatorArm64()
26 | }
27 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/mordant-native-sample-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
2 |
3 | plugins {
4 | id("mordant-native-conventions")
5 | }
6 |
7 | kotlin.targets.filterIsInstance().forEach { target ->
8 | target.binaries.executable {
9 | entryPoint = "com.github.ajalt.mordant.samples.main"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/mordant-publishing-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("PropertyName")
2 |
3 | import com.vanniktech.maven.publish.JavadocJar
4 | import com.vanniktech.maven.publish.KotlinMultiplatform
5 | import com.vanniktech.maven.publish.SonatypeHost
6 | import org.jetbrains.dokka.gradle.DokkaTaskPartial
7 |
8 | plugins {
9 | id("com.vanniktech.maven.publish.base")
10 | id("org.jetbrains.dokka")
11 | }
12 |
13 | fun getPublishVersion(): String {
14 | val version = project.property("VERSION_NAME").toString()
15 | // Call gradle with -PsnapshotVersion to set the version as a snapshot.
16 | if (!project.hasProperty("snapshotVersion")) return version
17 | val buildNumber = System.getenv("GITHUB_RUN_NUMBER") ?: "0"
18 | return "$version.$buildNumber-SNAPSHOT"
19 | }
20 |
21 | // Since we want to set the version name dynamically, we have to use the base plugin
22 | @Suppress("UnstableApiUsage")
23 | mavenPublishing {
24 | project.setProperty("VERSION_NAME", getPublishVersion())
25 | pomFromGradleProperties()
26 | configure(KotlinMultiplatform(JavadocJar.Empty()))
27 | publishToMavenCentral(SonatypeHost.DEFAULT)
28 | signAllPublications()
29 | }
30 |
31 | tasks.withType().configureEach {
32 | dokkaSourceSets.configureEach {
33 | skipDeprecated.set(true)
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/docs/css/logo-styles.css:
--------------------------------------------------------------------------------
1 | /* Custom CSS for Dokka docs */
2 |
3 | #logo {
4 | background-image: url('../images/gradient_black_36dp.svg');
5 | }
6 |
7 | /* Add the project name to the logo */
8 | #logo::before {
9 | content: "Mordant";
10 | margin-left: 80px;
11 | color: black;
12 | font-size: 20px;
13 | font-weight: 600;
14 | }
15 |
16 | /* Remove the "What's on this Page" tab that covers up the scroll bar */
17 | .page-summary {
18 | display: none;
19 | }
20 |
--------------------------------------------------------------------------------
/docs/img/animation_text.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ajalt/mordant/f98b0f055f45f32e4a6595ea328e3d9cb2a5fd2d/docs/img/animation_text.gif
--------------------------------------------------------------------------------
/docs/img/gradient_black_36dp.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/img/gradient_white_24dp.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/img/progess_simple.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ajalt/mordant/f98b0f055f45f32e4a6595ea328e3d9cb2a5fd2d/docs/img/progess_simple.gif
--------------------------------------------------------------------------------
/docs/img/progress_cells.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ajalt/mordant/f98b0f055f45f32e4a6595ea328e3d9cb2a5fd2d/docs/img/progress_cells.gif
--------------------------------------------------------------------------------
/docs/img/progress_context.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ajalt/mordant/f98b0f055f45f32e4a6595ea328e3d9cb2a5fd2d/docs/img/progress_context.gif
--------------------------------------------------------------------------------
/docs/img/progress_multi.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ajalt/mordant/f98b0f055f45f32e4a6595ea328e3d9cb2a5fd2d/docs/img/progress_multi.gif
--------------------------------------------------------------------------------
/docs/img/select_list.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ajalt/mordant/f98b0f055f45f32e4a6595ea328e3d9cb2a5fd2d/docs/img/select_list.gif
--------------------------------------------------------------------------------
/docs/img/tour.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ajalt/mordant/f98b0f055f45f32e4a6595ea328e3d9cb2a5fd2d/docs/img/tour.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | VERSION_NAME=3.0.2
2 |
3 | kotlin.mpp.stability.nowarn=true
4 |
5 | # gradle-maven-publish configuration
6 | SONATYPE_HOST=DEFAULT
7 | RELEASE_SIGNING_ENABLED=true
8 | GROUP=com.github.ajalt.mordant
9 | POM_DESCRIPTION=Colorful multiplatform styling Kotlin for command-line applications
10 | POM_INCEPTION_YEAR=2018
11 | POM_URL=https://github.com/ajalt/mordant/
12 | POM_LICENSE_NAME=Apache-2.0
13 | POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0
14 | POM_LICENSE_DIST=repo
15 | POM_SCM_URL=https://github.com/ajalt/
16 | POM_SCM_CONNECTION=scm:git:git://github.com/ajalt/mordant.git
17 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/ajalt/mordant.git
18 | POM_DEVELOPER_ID=ajalt
19 | POM_DEVELOPER_NAME=AJ Alt
20 | POM_DEVELOPER_URL=https://github.com/ajalt/
21 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlin = "2.0.10"
3 | coroutines = "1.8.1"
4 |
5 | [libraries]
6 | colormath = "com.github.ajalt.colormath:colormath:3.6.0"
7 | markdown = "org.jetbrains:markdown:0.7.3"
8 | jna-core = "net.java.dev.jna:jna:5.14.0"
9 |
10 | # compileOnly
11 | graalvm-svm = "org.graalvm.nativeimage:svm:23.1.0"
12 |
13 | # used in extensions
14 | coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
15 |
16 | # used in tests
17 | kotest = "io.kotest:kotest-assertions-core:5.9.1"
18 | systemrules = "com.github.stefanbirkner:system-rules:1.19.0"
19 | r8 = "com.android.tools:r8:8.3.37"
20 | coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
21 |
22 | # build logic
23 | kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
24 | dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "1.9.20" }
25 | publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version = "0.28.0" }
26 |
27 | [plugins]
28 | graalvm-nativeimage = "org.graalvm.buildtools.native:0.9.28"
29 | kotlinBinaryCompatibilityValidator = "org.jetbrains.kotlinx.binary-compatibility-validator:0.14.0"
30 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ajalt/mordant/f98b0f055f45f32e4a6595ea328e3d9cb2a5fd2d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo. 1>&2
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
48 | echo. 1>&2
49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
50 | echo location of your Java installation. 1>&2
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo. 1>&2
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
62 | echo. 1>&2
63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
64 | echo location of your Java installation. 1>&2
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Mordant
2 | repo_name: Mordant
3 | repo_url: https://github.com/ajalt/mordant
4 | site_description: "Mordant: Multiplatform text styling for Kotlin command-line applications"
5 | site_author: AJ Alt
6 | remote_branch: gh-pages
7 |
8 | copyright: 'Copyright © 2018 AJ Alt'
9 |
10 | theme:
11 | name: 'material'
12 | logo: img/gradient_white_24dp.svg
13 | favicon: img/favicon.ico
14 | icon:
15 | repo: fontawesome/brands/github
16 | palette:
17 | - media: "(prefers-color-scheme: light)"
18 | scheme: default
19 | primary: indigo
20 | accent: indigo
21 | toggle:
22 | icon: material/brightness-7
23 | name: Switch to dark mode
24 |
25 | - media: "(prefers-color-scheme: dark)"
26 | scheme: slate
27 | primary: indigo
28 | accent: indigo
29 | toggle:
30 | icon: material/brightness-4
31 | name: Switch to light mode
32 |
33 | markdown_extensions:
34 | - smarty
35 | - codehilite:
36 | guess_lang: false
37 | - footnotes
38 | - meta
39 | - toc:
40 | permalink: true
41 | - admonition
42 | - pymdownx.betterem:
43 | smart_enable: all
44 | - pymdownx.details
45 | - pymdownx.caret
46 | - pymdownx.details
47 | - pymdownx.inlinehilite
48 | - pymdownx.magiclink
49 | - pymdownx.smartsymbols
50 | - pymdownx.superfences
51 | - pymdownx.tabbed:
52 | alternate_style: true
53 | - tables
54 | - admonition
55 |
56 | nav:
57 | - 'Getting Started': guide.md
58 | - 'Progress Bars': progress.md
59 | - 'Keyboard and Mouse Input': input.md
60 | - 'API Reference': api/index.html
61 | - 'Releases': changelog.md
62 |
--------------------------------------------------------------------------------
/mordant-coroutines/README.md:
--------------------------------------------------------------------------------
1 | # Mordant Coroutines
2 |
3 | This module adds extensions to running animations with coroutines.
4 |
5 | ```kotlin
6 | implementation("com.github.ajalt.mordant:mordant-coroutines:$mordantVersion")
7 | ```
8 |
--------------------------------------------------------------------------------
/mordant-coroutines/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-mpp-conventions")
3 | id("mordant-publishing-conventions")
4 | }
5 |
6 | kotlin {
7 | jvm()
8 | sourceSets {
9 | commonMain.dependencies {
10 | api(project(":mordant"))
11 | api(libs.coroutines.core)
12 | }
13 | commonTest.dependencies {
14 | implementation(kotlin("test"))
15 | implementation(libs.kotest)
16 | implementation(libs.coroutines.test)
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/mordant-coroutines/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=mordant-coroutines
2 | POM_NAME=Mordant Coroutines Extensions
3 |
--------------------------------------------------------------------------------
/mordant-coroutines/src/commonMain/kotlin/com/github/ajalt/mordant/input/coroutines/ReceiveEventsFlow.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.input.coroutines
2 |
3 | import com.github.ajalt.mordant.input.*
4 | import com.github.ajalt.mordant.terminal.Terminal
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.filterIsInstance
7 | import kotlinx.coroutines.flow.flow
8 |
9 | /**
10 | * Enter raw mode, emit input events until the flow in cancelled, then exit raw mode.
11 | *
12 | * @param mouseTracking The type of mouse tracking to enable.
13 | */
14 | fun Terminal.receiveEventsFlow(
15 | mouseTracking: MouseTracking = MouseTracking.Normal,
16 | ): Flow = flow {
17 | enterRawMode(mouseTracking).use {
18 | while (true) emit(it.readEvent())
19 | }
20 | }
21 |
22 | /**
23 | * Enter raw mode, emit [KeyboardEvent]s until the flow in cancelled, then exit raw mode.
24 | */
25 | fun Terminal.receiveKeyEventsFlow(
26 | ): Flow = receiveEventsFlow(MouseTracking.Off).filterIsInstance()
27 |
28 | /**
29 | * Enter raw mode, emit [MouseEvent]s until the flow in cancelled, then exit raw mode.
30 | *
31 | * @param mouseTracking The type of mouse tracking to enable.
32 | */
33 | fun Terminal.receiveMouseEventsFlow(
34 | mouseTracking: MouseTracking = MouseTracking.Normal,
35 | ): Flow {
36 | require(mouseTracking != MouseTracking.Off) {
37 | "Mouse tracking must be enabled to receive mouse events"
38 | }
39 | return receiveEventsFlow(mouseTracking).filterIsInstance()
40 | }
41 |
--------------------------------------------------------------------------------
/mordant-jvm-ffm/README.md:
--------------------------------------------------------------------------------
1 | # Mordant JVM FFM
2 |
3 | This is a JVM-only module that adds a `TerminalInterface` implementation that uses the Java Foreign
4 | Functions and Memory APIs. It requires JDK 22 or newer, and, like all usages of FFM, requires that
5 | you add `--enable-native-access=ALL-UNNAMED` to your `java` command line arguments or
6 | `Enable-Native-Access: ALL-UNNAMED` to the manifest of your executable JAR.
7 |
8 | ```kotlin
9 | implementation("com.github.ajalt.mordant:mordant-jvm-ffm:$mordantVersion")
10 | ```
11 |
--------------------------------------------------------------------------------
/mordant-jvm-ffm/api/mordant-jvm-ffm.api:
--------------------------------------------------------------------------------
1 | public final class com/github/ajalt/mordant/terminal/terminalinterface/ffm/TerminalInterfaceProviderFfm : com/github/ajalt/mordant/terminal/TerminalInterfaceProvider {
2 | public fun ()V
3 | public fun load ()Lcom/github/ajalt/mordant/terminal/TerminalInterface;
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/mordant-jvm-ffm/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-kotlin-conventions")
3 | id("mordant-publishing-conventions")
4 | }
5 |
6 | kotlin {
7 | jvm()
8 | sourceSets {
9 | commonMain.dependencies {
10 | implementation(project(":mordant"))
11 | }
12 | jvmMain.dependencies {
13 | compileOnly(libs.graalvm.svm)
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/mordant-jvm-ffm/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=mordant-jvm-ffm
2 | POM_NAME=Mordant Foreign Function & Memory Interface
3 |
--------------------------------------------------------------------------------
/mordant-jvm-ffm/src/jvmMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/ffm/TerminalInterfaceProvider.ffm.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal.terminalinterface.ffm
2 |
3 | import com.github.ajalt.mordant.terminal.TerminalInterface
4 | import com.github.ajalt.mordant.terminal.TerminalInterfaceProvider
5 | import com.oracle.svm.core.annotate.Substitute
6 | import com.oracle.svm.core.annotate.TargetClass
7 |
8 | class TerminalInterfaceProviderFfm : TerminalInterfaceProvider {
9 | override fun load(): TerminalInterface? {
10 | try {
11 | if (!TerminalInterfaceProviderFfm::class.java.module.isNativeAccessEnabled()) {
12 | return null
13 | }
14 | } catch (e: NoSuchMethodError) {
15 | // isNativeAccessEnabled doesn't exist on old JDKs
16 | return null
17 | }
18 | val os = System.getProperty("os.name")
19 | return when {
20 | os.startsWith("Windows") -> TerminalInterfaceFfmWindows()
21 | os == "Linux" -> TerminalInterfaceFfmLinux()
22 | os == "Mac OS X" -> TerminalInterfaceFfmMacos()
23 | else -> null
24 | }
25 | }
26 | }
27 |
28 | @TargetClass(TerminalInterfaceProviderFfm::class)
29 | private class TerminalInterfaceProviderFfmNative {
30 |
31 | @Substitute
32 | fun load(): TerminalInterface? = null
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/mordant-jvm-ffm/src/jvmMain/resources/META-INF/native-image/com.github.ajalt.mordant/mordant-jvm-ffm/reflection-config.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name" : "com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceProviderFfm",
4 | "allDeclaredConstructors" : true,
5 | "allPublicConstructors" : true,
6 | "allDeclaredMethods" : false,
7 | "allPublicMethods" : false,
8 | "allDeclaredFields" : false,
9 | "allPublicFields" : false
10 | }
11 | ]
12 |
--------------------------------------------------------------------------------
/mordant-jvm-ffm/src/jvmMain/resources/META-INF/native-image/com.github.ajalt.mordant/mordant-jvm-ffm/resource-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "resources": [
3 | {"pattern": "META-INF/services/.*"}
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/mordant-jvm-ffm/src/jvmMain/resources/META-INF/services/com.github.ajalt.mordant.terminal.TerminalInterfaceProvider:
--------------------------------------------------------------------------------
1 | com.github.ajalt.mordant.terminal.terminalinterface.ffm.TerminalInterfaceProviderFfm
2 |
--------------------------------------------------------------------------------
/mordant-jvm-graal-ffi/README.md:
--------------------------------------------------------------------------------
1 | # Mordant JVM GraalVM FFI
2 |
3 | This is a JVM-only module that adds a `TerminalInterface` implementation for use with GraalVM Native
4 | Image. It doesn't support regular JRE runtimes.
5 |
6 | ```kotlin
7 | implementation("com.github.ajalt.mordant:mordant-jvm-graal-ffi:$mordantVersion")
8 | ```
9 |
--------------------------------------------------------------------------------
/mordant-jvm-graal-ffi/api/mordant-jvm-graal-ffi.api:
--------------------------------------------------------------------------------
1 | public final class com/github/ajalt/mordant/terminal/terminalinterface/nativeimage/TerminalInterfaceProviderNativeImage : com/github/ajalt/mordant/terminal/TerminalInterfaceProvider {
2 | public fun ()V
3 | public fun load ()Lcom/github/ajalt/mordant/terminal/TerminalInterface;
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/mordant-jvm-graal-ffi/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-kotlin-conventions")
3 | id("mordant-publishing-conventions")
4 | }
5 |
6 | kotlin {
7 | jvm()
8 | sourceSets {
9 | commonMain.dependencies {
10 | implementation(project(":mordant"))
11 | }
12 | jvmMain.dependencies {
13 | compileOnly(libs.graalvm.svm)
14 | }
15 | }
16 | compilerOptions {
17 | freeCompilerArgs.addAll(
18 | "-Xno-param-assertions",
19 | "-Xno-call-assertions",
20 | "-Xno-receiver-assertions",
21 | )
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/mordant-jvm-graal-ffi/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=mordant-jvm-graal-ffi
2 | POM_NAME=Mordant GraalVM Foreign Function Interface
3 |
--------------------------------------------------------------------------------
/mordant-jvm-graal-ffi/src/jvmMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/nativeimage/TerminalInterfaceProvider.nativeimage.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal.terminalinterface.nativeimage
2 |
3 | import com.github.ajalt.mordant.terminal.TerminalInterface
4 | import com.github.ajalt.mordant.terminal.TerminalInterfaceProvider
5 | import com.oracle.svm.core.annotate.Substitute
6 | import com.oracle.svm.core.annotate.TargetClass
7 | import org.graalvm.nativeimage.Platform
8 | import org.graalvm.nativeimage.Platforms
9 |
10 | class TerminalInterfaceProviderNativeImage : TerminalInterfaceProvider {
11 | override fun load(): TerminalInterface? = null
12 | }
13 |
14 | @Platforms(Platform.LINUX::class)
15 | @TargetClass(TerminalInterfaceProviderNativeImage::class)
16 | private class TerminalInterfaceProviderNativeImageLinux {
17 |
18 | @Substitute
19 | fun load(): TerminalInterface = TerminalInterfaceNativeImageLinux()
20 |
21 | }
22 |
23 | @Platforms(Platform.WINDOWS::class)
24 | @TargetClass(TerminalInterfaceProviderNativeImage::class)
25 | private class TerminalInterfaceProviderNativeImageWindows {
26 |
27 | @Substitute
28 | fun load(): TerminalInterface = TerminalInterfaceNativeImageWindows()
29 |
30 | }
31 |
32 | @Platforms(Platform.MACOS::class)
33 | @TargetClass(TerminalInterfaceProviderNativeImage::class)
34 | private class TerminalInterfaceProviderNativeImageMacos {
35 |
36 | @Substitute
37 | fun load(): TerminalInterface = TerminalInterfaceNativeImageMacos()
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/mordant-jvm-graal-ffi/src/jvmMain/resources/META-INF/native-image/com.github.ajalt.mordant/mordant-jvm-graal-ffi/reflection-config.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name" : "com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceProviderNativeImage",
4 | "allDeclaredConstructors" : true,
5 | "allPublicConstructors" : true,
6 | "allDeclaredMethods" : false,
7 | "allPublicMethods" : false,
8 | "allDeclaredFields" : false,
9 | "allPublicFields" : false
10 | }
11 | ]
12 |
--------------------------------------------------------------------------------
/mordant-jvm-graal-ffi/src/jvmMain/resources/META-INF/native-image/com.github.ajalt.mordant/mordant-jvm-graal-ffi/resource-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "resources": [
3 | {"pattern": "META-INF/services/.*"}
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/mordant-jvm-graal-ffi/src/jvmMain/resources/META-INF/proguard/mordant-jvm-graal-ffi.pro:
--------------------------------------------------------------------------------
1 | # Keep rules for those who are using ProGuard.
2 |
3 | -dontwarn org.graalvm.**
4 | -dontwarn com.oracle.svm.core.annotate.Delete
5 |
--------------------------------------------------------------------------------
/mordant-jvm-graal-ffi/src/jvmMain/resources/META-INF/services/com.github.ajalt.mordant.terminal.TerminalInterfaceProvider:
--------------------------------------------------------------------------------
1 | com.github.ajalt.mordant.terminal.terminalinterface.nativeimage.TerminalInterfaceProviderNativeImage
2 |
--------------------------------------------------------------------------------
/mordant-jvm-jna/README.md:
--------------------------------------------------------------------------------
1 | # Mordant JVM JNA
2 |
3 | This is a JVM-only module that adds a `TerminalInterface` implementation that uses [JNA]. This
4 | module supports all JDK versions, but links to a bundled native library, so it increases your JAR
5 | size.
6 |
7 | ```kotlin
8 | implementation("com.github.ajalt.mordant:mordant-jvm-jna:$mordantVersion")
9 | ```
10 |
11 | [JNA]: https://github.com/java-native-access/jna
12 |
--------------------------------------------------------------------------------
/mordant-jvm-jna/api/mordant-jvm-jna.api:
--------------------------------------------------------------------------------
1 | public final class com/github/ajalt/mordant/terminal/terminalinterface/jna/TerminalInterfaceProviderJna : com/github/ajalt/mordant/terminal/TerminalInterfaceProvider {
2 | public fun ()V
3 | public fun load ()Lcom/github/ajalt/mordant/terminal/TerminalInterface;
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/mordant-jvm-jna/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-kotlin-conventions")
3 | id("mordant-publishing-conventions")
4 | }
5 |
6 | kotlin {
7 | jvm()
8 | sourceSets {
9 | commonMain.dependencies {
10 | implementation(project(":mordant"))
11 | }
12 | jvmMain.dependencies {
13 | implementation(libs.jna.core)
14 | compileOnly(libs.graalvm.svm)
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/mordant-jvm-jna/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=mordant-jvm-jna
2 | POM_NAME=Mordant JNA Interface
3 |
--------------------------------------------------------------------------------
/mordant-jvm-jna/src/jvmMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/jna/TerminalInterfaceProvider.jna.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal.terminalinterface.jna
2 |
3 | import com.github.ajalt.mordant.terminal.TerminalInterface
4 | import com.github.ajalt.mordant.terminal.TerminalInterfaceProvider
5 | import com.oracle.svm.core.annotate.Substitute
6 | import com.oracle.svm.core.annotate.TargetClass
7 |
8 | class TerminalInterfaceProviderJna : TerminalInterfaceProvider {
9 | override fun load(): TerminalInterface? {
10 | // Inlined version of ImageInfo.inImageCode()
11 | val imageCode = System.getProperty("org.graalvm.nativeimage.imagecode")
12 | val isNativeImage = imageCode == "buildtime" || imageCode == "runtime"
13 | if (isNativeImage) return null
14 |
15 | val os = System.getProperty("os.name")
16 | return try {
17 | when {
18 | os.startsWith("Windows") -> TerminalInterfaceJnaWindows()
19 | os == "Linux" -> TerminalInterfaceJnaLinux()
20 | os == "Mac OS X" -> TerminalInterfaceJnaMacos()
21 | else -> null
22 | }
23 | } catch (e: UnsatisfiedLinkError) {
24 | null
25 | }
26 | }
27 | }
28 |
29 | @TargetClass(TerminalInterfaceProviderJna::class)
30 | private class TerminalInterfaceProviderJnaNative {
31 |
32 | @Substitute
33 | fun load(): TerminalInterface? = null
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/mordant-jvm-jna/src/jvmMain/resources/META-INF/native-image/com.github.ajalt.mordant/mordant-jvm-jna/reflection-config.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name" : "com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceProviderJna",
4 | "allDeclaredConstructors" : true,
5 | "allPublicConstructors" : true,
6 | "allDeclaredMethods" : false,
7 | "allPublicMethods" : false,
8 | "allDeclaredFields" : false,
9 | "allPublicFields" : false
10 | }
11 | ]
12 |
--------------------------------------------------------------------------------
/mordant-jvm-jna/src/jvmMain/resources/META-INF/native-image/com.github.ajalt.mordant/mordant-jvm-jna/resource-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "resources": [
3 | {"pattern": "META-INF/services/.*"}
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/mordant-jvm-jna/src/jvmMain/resources/META-INF/proguard/mordant-jvm-jna.pro:
--------------------------------------------------------------------------------
1 | # Keep rules for those who are using ProGuard.
2 |
3 | -dontwarn org.graalvm.**
4 | -dontwarn com.oracle.svm.core.annotate.**
5 | -keep class com.sun.jna.** { *; }
6 | -keep class * implements com.sun.jna.** { *; }
7 | -keepattributes RuntimeVisibleAnnotations,RuntimeVisibleParameterAnnotations,RuntimeVisibleTypeAnnotations,AnnotationDefault
8 |
--------------------------------------------------------------------------------
/mordant-jvm-jna/src/jvmMain/resources/META-INF/services/com.github.ajalt.mordant.terminal.TerminalInterfaceProvider:
--------------------------------------------------------------------------------
1 | com.github.ajalt.mordant.terminal.terminalinterface.jna.TerminalInterfaceProviderJna
2 |
--------------------------------------------------------------------------------
/mordant-markdown/README.md:
--------------------------------------------------------------------------------
1 | # Mordant Markdown
2 |
3 | This module adds a `Markdown` widget that renders markdown.
4 |
--------------------------------------------------------------------------------
/mordant-markdown/api/mordant-markdown.api:
--------------------------------------------------------------------------------
1 | public final class com/github/ajalt/mordant/markdown/Markdown : com/github/ajalt/mordant/rendering/Widget {
2 | public fun (Ljava/lang/String;ZLjava/lang/Boolean;)V
3 | public synthetic fun (Ljava/lang/String;ZLjava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
4 | public fun measure (Lcom/github/ajalt/mordant/terminal/Terminal;I)Lcom/github/ajalt/mordant/rendering/WidthRange;
5 | public fun render (Lcom/github/ajalt/mordant/terminal/Terminal;I)Lcom/github/ajalt/mordant/rendering/Lines;
6 | }
7 |
8 |
--------------------------------------------------------------------------------
/mordant-markdown/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-kotlin-conventions")
3 | id("mordant-js-conventions")
4 | // Need core here pending https://github.com/JetBrains/markdown/pull/159
5 | id("mordant-native-core-conventions")
6 | id("mordant-publishing-conventions")
7 | }
8 |
9 | kotlin {
10 | jvm() // need to list explicitly since we aren't using mpp-conventions for now
11 | sourceSets {
12 | commonMain.dependencies {
13 | api(project(":mordant"))
14 | implementation(libs.markdown)
15 | }
16 | commonTest.dependencies {
17 | implementation(kotlin("test"))
18 | implementation(libs.kotest)
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/mordant-markdown/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=mordant-markdown
2 | POM_NAME=Mordant Markdown Rendering
3 |
--------------------------------------------------------------------------------
/mordant-markdown/src/commonMain/kotlin/com/github/ajalt/mordant/markdown/BlockQuote.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.markdown
2 |
3 | import com.github.ajalt.mordant.rendering.*
4 | import com.github.ajalt.mordant.terminal.Terminal
5 |
6 | internal class BlockQuote(private val content: Widget) : Widget {
7 | override fun measure(t: Terminal, width: Int): WidthRange {
8 | return content.measure(t, width - 2) + 2
9 | }
10 |
11 | override fun render(t: Terminal, width: Int): Lines {
12 | val bar = Span.word(t.theme.string("markdown.blockquote.bar"), t.theme.style("markdown.blockquote"))
13 | val justBar = Line(listOf(bar))
14 | val paddedBar = listOf(bar, Span.space(style = t.theme.style("markdown.blockquote")))
15 | val lines = content.render(t, width).withStyle(t.theme.style("markdown.blockquote")).lines
16 | return Lines(lines.map { if (it.isEmpty()) justBar else Line(paddedBar + it) })
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/mordant-markdown/src/commonMain/kotlin/com/github/ajalt/mordant/markdown/Markdown.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.markdown
2 |
3 | import com.github.ajalt.mordant.rendering.Lines
4 | import com.github.ajalt.mordant.rendering.Widget
5 | import com.github.ajalt.mordant.rendering.WidthRange
6 | import com.github.ajalt.mordant.terminal.Terminal
7 |
8 | /**
9 | * A [Widget] that renders a GitHub Flavored [markdown] string.
10 | *
11 | * @property showHtml If `true`, any html tags in the [markdown] will be rendered verbatim to the
12 | * output. By default, html tags are skipped.
13 | * @property hyperlinks If `true`, links will always be rendered with ANSI hyperlinks. If `false`,
14 | * links will always print their targets instead. By default, hyperlinks are used if the current
15 | * terminal supports them.
16 | */
17 | class Markdown(
18 | private val markdown: String,
19 | private val showHtml: Boolean = false,
20 | private val hyperlinks: Boolean? = null,
21 | ) : Widget {
22 | private var document: Widget? = null
23 | private fun document(t: Terminal): Widget {
24 | if (document == null) {
25 | document = MarkdownRenderer(markdown, t.theme, showHtml, hyperlinks ?: t.terminalInfo.ansiHyperLinks).render()
26 | }
27 | return document!!
28 | }
29 |
30 | override fun measure(t: Terminal, width: Int): WidthRange = document(t).measure(t, width)
31 | override fun render(t: Terminal, width: Int): Lines = document(t).render(t, width)
32 | }
33 |
--------------------------------------------------------------------------------
/mordant-omnibus/README.md:
--------------------------------------------------------------------------------
1 | # Mordant Omnibus
2 |
3 | This module is a convenience for depending on `mordant-core` and all of the `mordant-jvm` modules.
4 |
5 | ```kotlin
6 | implementation("com.github.ajalt.mordant:mordant:$mordantVersion")
7 | ```
8 |
--------------------------------------------------------------------------------
/mordant-omnibus/api/mordant-omnibus.api:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ajalt/mordant/f98b0f055f45f32e4a6595ea328e3d9cb2a5fd2d/mordant-omnibus/api/mordant-omnibus.api
--------------------------------------------------------------------------------
/mordant-omnibus/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
2 |
3 | // We don't use all the conventions plugins here since applying the mpp=convention results in
4 | // "IllegalStateException: Configuration already finalized for previous property values"
5 | plugins {
6 | kotlin("multiplatform")
7 | id("mordant-native-conventions")
8 | id("mordant-publishing-conventions")
9 | }
10 |
11 | kotlin {
12 | jvm()
13 | js { nodejs() } // we don't have any code, but it's an error not to pick aa JS environment
14 | @OptIn(ExperimentalWasmDsl::class)
15 | wasmJs { nodejs() }
16 | sourceSets {
17 | commonMain.dependencies {
18 | api(project(":mordant"))
19 | }
20 | jvmMain.dependencies {
21 | implementation(project(":mordant-jvm-jna"))
22 | implementation(project(":mordant-jvm-ffm"))
23 | implementation(project(":mordant-jvm-graal-ffi"))
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/mordant-omnibus/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=mordant
2 | POM_NAME=Mordant
3 |
--------------------------------------------------------------------------------
/mordant-omnibus/src/commonMain/kotlin/com/github/ajalt/mordant/internal/OmibusInternal.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | // This intentionally empty file is a workaround for KT-52344, which fails to publish a target with
4 | // no source files
5 |
--------------------------------------------------------------------------------
/mordant/README.md:
--------------------------------------------------------------------------------
1 | # Mordant Core
2 |
3 | This is the core mordant module that implements most Mordant functionality. All other modules depend on this.
4 |
5 | ```kotlin
6 | implementation("com.github.ajalt.mordant:mordant-core:$mordantVersion")
7 | ```
8 |
--------------------------------------------------------------------------------
/mordant/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-mpp-conventions")
3 | id("mordant-publishing-conventions")
4 | }
5 |
6 | kotlin {
7 | jvm()
8 | sourceSets {
9 | all {
10 | languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi")
11 | languageSettings.optIn("kotlin.experimental.ExperimentalNativeApi")
12 | }
13 | commonMain.dependencies {
14 | api(libs.colormath)
15 | }
16 | commonTest.dependencies {
17 | implementation(kotlin("test"))
18 | implementation(libs.kotest)
19 | }
20 | jvmTest.dependencies {
21 | api(libs.systemrules)
22 | }
23 | // Kotlin 2.0 changed the way MPP is compiled, so instead of copying shared sources to each
24 | // target, it compiles intermediate sources separately. That means that code that previously
25 | // compiled is broken due to errors like "declaration is using numbers with different bit
26 | // widths". So we copy the shared sources to each target manually.
27 | sourceSets {
28 | // https://kotlinlang.org/docs/multiplatform-hierarchy.html#see-the-full-hierarchy-template
29 | val posixMain by creating { dependsOn(nativeMain.get()) }
30 | linuxMain.get().dependsOn(posixMain)
31 | appleMain.get().dependsOn(posixMain)
32 | val appleNonDesktopMain by creating { dependsOn(appleMain.get()) }
33 | for (target in listOf(iosMain, tvosMain, watchosMain)) {
34 | target.get().dependsOn(appleNonDesktopMain)
35 | }
36 | for (target in listOf(
37 | "linuxX64", "linuxArm64",
38 | "macosX64", "macosArm64",
39 | "tvosX64", "tvosArm64", "tvosSimulatorArm64",
40 | "watchosArm32", "watchosArm64", "watchosX64", "watchosSimulatorArm64",
41 | )) {
42 | sourceSets.getByName(target + "Main").kotlin.srcDirs("src/posixSharedMain/kotlin")
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/mordant/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=mordant-core
2 | POM_NAME=Mordant Core
3 |
--------------------------------------------------------------------------------
/mordant/src/appleNonDesktopMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.appleNonDesktop.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | internal actual fun testsHaveFileSystem(): Boolean = false
4 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/animation/RefreshableAnimation.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.animation
2 |
3 | import kotlin.time.Duration
4 | import kotlin.time.Duration.Companion.seconds
5 |
6 | interface Refreshable {
7 | /**
8 | * `true` if this animation has finished and should be stopped or cleared.
9 | */
10 | val finished: Boolean
11 |
12 | /**
13 | * Draw the animation to the screen.
14 | *
15 | * This is called automatically when the animation is running, so you don't usually need to call
16 | * it manually.
17 | *
18 | * @param refreshAll If `true`, refresh all contents, ignoring their fps.
19 | */
20 | fun refresh(refreshAll: Boolean = false)
21 | }
22 |
23 | /**
24 | * A version of [Animation] that has a parameterless [refresh] method instead of `update`.
25 | *
26 | * Implementations will need to handle concurrently updating their state.
27 | */
28 | interface RefreshableAnimation : Refreshable, StoppableAnimation {
29 |
30 | /**
31 | * Stop this animation and remove it from the screen.
32 | *
33 | * Future calls to [refresh] will cause the animation to resume.
34 | */
35 | override fun clear()
36 |
37 | /**
38 | * Stop this animation without removing it from the screen.
39 | *
40 | * Anything printed to the terminal after this call will be printed below this last frame of
41 | * this animation.
42 | *
43 | * Future calls to [refresh] will cause the animation to start again.
44 | */
45 | override fun stop()
46 |
47 | /**
48 | * The rate, in Hz, that this animation should be refreshed, or 0 if it should not be refreshed
49 | * automatically.
50 | */
51 | val fps: Int get() = 5
52 | }
53 |
54 | /** The time between refreshes. This is `1 / refreshRate` */
55 | val RefreshableAnimation.refreshPeriod: Duration
56 | get() = (1.0 / fps).seconds
57 |
58 | /**
59 | * Convert this [Animation] to a [RefreshableAnimation].
60 | *
61 | * ### Example
62 | * ```
63 | * terminal.animation {/*...*/}.asRefreshable().animateOnThread()
64 | * ```
65 | *
66 | * @param fps The rate at which the animation should be refreshed.
67 | * @param finished A function that returns `true` if the animation has finished.
68 | */
69 | inline fun Animation.asRefreshable(
70 | fps: Int = 5,
71 | crossinline finished: () -> Boolean = { false },
72 | ): RefreshableAnimation {
73 | return object : RefreshableAnimation {
74 | override fun refresh(refreshAll: Boolean) = update(Unit)
75 | override fun clear() = this@asRefreshable.clear()
76 | override fun stop() = this@asRefreshable.stop()
77 | override val finished: Boolean get() = finished()
78 | override val fps: Int get() = fps
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/animation/StoppableAnimation.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.animation
2 |
3 | interface StoppableAnimation {
4 | /**
5 | * Stop this animation without removing it from the screen.
6 | *
7 | * Anything printed to the terminal after this call will be printed below this last frame of
8 | * this animation.
9 | */
10 | fun stop()
11 |
12 | /**
13 | * Stop this animation and remove it from the screen.
14 | */
15 | fun clear()
16 | }
17 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/input/InputReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.input
2 |
3 | import com.github.ajalt.mordant.animation.StoppableAnimation
4 | import com.github.ajalt.mordant.terminal.Terminal
5 |
6 | /**
7 | * An object that can receive input events.
8 | */
9 | interface InputReceiver {
10 | sealed class Status {
11 | companion object {
12 | val Finished = Finished(Unit)
13 | }
14 |
15 | data object Continue : Status()
16 | data class Finished(val result: T) : Status()
17 | }
18 |
19 | /**
20 | * The terminal that this receiver is reading input from.
21 | */
22 | val terminal: Terminal
23 |
24 | /**
25 | * Receive an input event.
26 | *
27 | * @param event The input event to process
28 | * @return [Status.Continue] to continue receiving events, or [Status.Finished] to stop.
29 | */
30 | fun receiveEvent(event: InputEvent): Status
31 | }
32 |
33 | /**
34 | * An [InputReceiver] that is also an [StoppableAnimation].
35 | */
36 | interface InputReceiverAnimation : InputReceiver, StoppableAnimation
37 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/input/MouseTracking.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.input
2 |
3 | // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
4 | enum class MouseTracking {
5 | /**
6 | * Disable mouse tracking
7 | */
8 | Off,
9 |
10 | /**
11 | * Normal tracking mode sends an escape sequence on both button press and
12 | * release. Modifier key (shift, ctrl, meta) information is also sent.
13 | */
14 | Normal,
15 |
16 | /**
17 | * Button-event tracking is essentially the same as normal tracking, but
18 | * xterm also reports button-motion events. Motion events are reported
19 | * only if the mouse pointer has moved to a different character cell.
20 | */
21 | Button,
22 |
23 | /**
24 | * Any-event mode is the same as button-event mode, except that all motion
25 | * events are reported, even if no mouse button is down.
26 | */
27 | Any
28 | }
29 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/input/ReceiveEvents.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.input
2 |
3 | import com.github.ajalt.mordant.terminal.Terminal
4 |
5 | /**
6 | * Enter raw mode, read input from the terminal for this [InputReceiver] until it returns a
7 | * result, then exit raw mode.
8 | *
9 | * @param mouseTracking The type of mouse tracking to enable.
10 | * @return the result of the completed receiver.
11 | * @throws RuntimeException if the terminal is not interactive or the input could not be read.
12 | */
13 | fun InputReceiver.receiveEvents(
14 | mouseTracking: MouseTracking = MouseTracking.Normal,
15 | ): T {
16 | terminal.enterRawMode(mouseTracking).use { rawMode ->
17 | while (true) {
18 | val event = rawMode.readEvent()
19 | when (val status = receiveEvent(event)) {
20 | is InputReceiver.Status.Continue -> continue
21 | is InputReceiver.Status.Finished -> return status.result
22 | }
23 | }
24 | }
25 | }
26 |
27 | /**
28 | * Enter raw mode, read input and pass any [KeyboardEvent]s to [block] until it returns a
29 | * result, then exit raw mode.
30 | *
31 | * @return the result of the completed receiver.
32 | * @throws RuntimeException if the terminal is not interactive or the input could not be read.
33 | */
34 | inline fun Terminal.receiveKeyEvents(
35 | crossinline block: (KeyboardEvent) -> InputReceiver.Status,
36 | ): T {
37 | return receiveEvents(MouseTracking.Off) { event ->
38 | when (event) {
39 | is KeyboardEvent -> block(event)
40 | else -> InputReceiver.Status.Continue
41 | }
42 | }
43 | }
44 |
45 | /**
46 | * Enter raw mode, read input and pass any [MouseEvent]s to [block] until it returns a
47 | * result, then exit raw mode.
48 | *
49 | * @param mouseTracking The type of mouse tracking to enable.
50 | * @return the result of the completed receiver.
51 | * @throws RuntimeException if the terminal is not interactive or the input could not be read.
52 | */
53 | inline fun Terminal.receiveMouseEvents(
54 | mouseTracking: MouseTracking = MouseTracking.Normal,
55 | crossinline block: (MouseEvent) -> InputReceiver.Status,
56 | ): T {
57 | require(mouseTracking != MouseTracking.Off) {
58 | "Mouse tracking must be enabled to receive mouse events"
59 | }
60 | return receiveEvents(mouseTracking) { event ->
61 | when (event) {
62 | is MouseEvent -> block(event)
63 | else -> InputReceiver.Status.Continue
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Enter raw mode, read input and pass any [InputEvent]s to [block] until it returns a
70 | * result, then exit raw mode.
71 | *
72 | * @param mouseTracking The type of mouse tracking to enable.
73 | * @return the result of the completed receiver.
74 | * @throws RuntimeException if the terminal is not interactive or the input could not be read.
75 | */
76 | inline fun Terminal.receiveEvents(
77 | mouseTracking: MouseTracking = MouseTracking.Normal,
78 | crossinline block: (InputEvent) -> InputReceiver.Status,
79 | ): T {
80 | return object : InputReceiver {
81 | override val terminal: Terminal get() = this@receiveEvents
82 | override fun receiveEvent(event: InputEvent): InputReceiver.Status {
83 | return block(event)
84 | }
85 | }.receiveEvents(mouseTracking)
86 | }
87 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/AnsiCodes.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | @Suppress("ConstPropertyName")
4 | internal object AnsiCodes {
5 | val fg16Range = 30..37
6 | val fg16BrightRange = 90..97
7 | const val fgColorSelector = 38
8 | const val fgColorReset = 39
9 |
10 | const val fgBgOffset = 10
11 |
12 | val bg16Range = 40..47
13 | val bg16BrightRange = 100..107
14 | const val bgColorSelector = 48
15 | const val bgColorReset = 49
16 |
17 | const val selector256 = 5
18 | const val selectorRgb = 2
19 |
20 | const val underlineColorSelector = 58
21 |
22 | const val reset = 0
23 | const val boldOpen = 1
24 | const val boldAndDimClose = 22
25 | const val dimOpen = 2
26 | const val italicOpen = 3
27 | const val italicClose = 23
28 | const val underlineOpen = 4
29 | const val underlineClose = 24
30 | const val inverseOpen = 7
31 | const val inverseClose = 27
32 | const val strikethroughOpen = 9
33 | const val strikethroughClose = 29
34 | }
35 |
36 | internal const val ESC = "\u001B"
37 |
38 | /** Control Sequence Introducer */
39 | internal const val CSI = "$ESC["
40 |
41 | /** Operating System Command */
42 | internal const val OSC = "$ESC]"
43 |
44 | /** String Terminator */
45 | internal const val ST = "$ESC\\"
46 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/BlankWidgetWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | import com.github.ajalt.mordant.rendering.*
4 | import com.github.ajalt.mordant.terminal.Terminal
5 |
6 | /** A widget that displays blank space the same size as an [inner] widget */
7 | internal class BlankWidgetWrapper(private val inner: Widget) : Widget {
8 | override fun measure(t: Terminal, width: Int): WidthRange = inner.measure(t, width)
9 |
10 | override fun render(t: Terminal, width: Int): Lines {
11 | val orig = inner.render(t, width)
12 | return Lines(orig.lines.map { Line(listOf(Span.space(it.lineWidth))) })
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | import com.github.ajalt.mordant.rendering.Line
4 | import com.github.ajalt.mordant.rendering.Lines
5 | import com.github.ajalt.mordant.rendering.Span
6 | import com.github.ajalt.mordant.rendering.TextStyle
7 | import com.github.ajalt.mordant.widgets.Padding
8 |
9 | // All top-level vals are defined in this file to avoid initialization order issues on native.
10 |
11 | internal val DEFAULT_STYLE = TextStyle(
12 | color = null,
13 | bgColor = null,
14 | bold = false,
15 | italic = false,
16 | underline = false,
17 | dim = false,
18 | inverse = false,
19 | strikethrough = false,
20 | hyperlink = null,
21 | )
22 |
23 | internal val EMPTY_LINES = Lines(emptyList())
24 | internal val EMPTY_LINE: Line = Line(emptyList(), DEFAULT_STYLE)
25 | internal val SINGLE_SPACE = Span.space(1)
26 | internal val DEFAULT_PADDING = Padding(0)
27 |
28 | @Suppress("RegExpRedundantEscape") // JS requires escaping the lone `]` at the beginning of the pattern, so we can't use $OSC
29 | internal val ANSI_RE = Regex("""$ESC\][^$ESC]*$ESC\\|$ESC(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])""")
30 | internal const val HYPERLINK_RESET = "__mordant_reset__"
31 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/Formatting.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | import kotlin.math.pow
4 |
5 | private const val SI_PREFIXES = "KMGTEPZY"
6 |
7 | /**
8 | * Return a list of all numbers in [nums] formatted as a string, and the unit they were reduced with.
9 | *
10 | * All numbers will be formatted to the same unit.
11 | *
12 | * @param precision The number of decimal places to include in the formatted numbers
13 | * @param nums The numbers to format
14 | */
15 | internal fun formatMultipleWithSiSuffixes(
16 | precision: Int, truncateDecimals: Boolean, vararg nums: Double,
17 | ): Pair, String> {
18 | require(precision >= 0) { "precision must be >= 0" }
19 | val largest = nums.max()
20 | var divisor = 1
21 | var prefix = ""
22 | for (s in SI_PREFIXES) {
23 | if (largest / divisor < 1000) break
24 | divisor *= 1000
25 | prefix = s.toString()
26 | }
27 |
28 | val exp = 10.0.pow(precision)
29 | val formatted = nums.map {
30 | val n = it / divisor
31 | val i = n.toInt()
32 | val d = ((n - i) * exp).toInt()
33 | when {
34 | truncateDecimals && (precision == 0 || divisor == 1 && d == 0) -> i.toString()
35 | else -> "$i.${d.toString().padEnd(precision, '0')}"
36 | }
37 | }
38 | return formatted to prefix
39 | }
40 |
41 | /** Return this number formatted as a string, suffixed with its SI unit */
42 | internal fun Double.formatWithSiSuffix(precision: Int): String {
43 | return formatMultipleWithSiSuffixes(precision, false, this).let { it.first.first() + it.second }
44 | }
45 |
46 | /** Return the number of seconds represented by [nanos] as a `Double` */
47 | internal fun nanosToSeconds(nanos: Double): Double = nanos / 1_000_000_000
48 |
49 | /** Return the number of seconds represented by [nanos] as a `Double` */
50 | internal fun nanosToSeconds(nanos: Long): Double = nanosToSeconds(nanos.toDouble())
51 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/HyperlinkIds.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 |
4 | private val nextHyperlinkId = MppAtomicInt(1)
5 |
6 | internal fun generateHyperlinkId(): String {
7 | return nextHyperlinkId.getAndIncrement().toString()
8 | }
9 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | import com.github.ajalt.mordant.terminal.*
4 |
5 | internal interface MppAtomicInt {
6 | fun getAndIncrement(): Int
7 | fun get(): Int
8 | fun set(value: Int)
9 | }
10 |
11 | internal interface MppAtomicRef {
12 | val value: T
13 |
14 | /** @return true if the value was set */
15 | fun compareAndSet(expected: T, newValue: T): Boolean
16 | fun getAndSet(newValue: T): T
17 | }
18 |
19 | /** Update the reference via spin lock, spinning forever until it succeeds. */
20 | internal inline fun MppAtomicRef.update(block: T.() -> T): Pair {
21 | while (true) {
22 | val old = value
23 | val newValue = block(old)
24 | if (compareAndSet(old, newValue)) return old to newValue
25 | }
26 | }
27 |
28 | internal expect fun MppAtomicRef(value: T): MppAtomicRef
29 |
30 | internal expect fun MppAtomicInt(initial: Int): MppAtomicInt
31 |
32 | internal expect fun getEnv(key: String): String?
33 |
34 | internal expect fun runningInIdeaJavaAgent(): Boolean
35 |
36 | internal expect fun codepointSequence(string: String): Sequence
37 |
38 | internal expect fun makePrintingTerminalCursor(terminal: Terminal): TerminalCursor
39 |
40 | internal expect fun printStderr(message: String, newline: Boolean)
41 |
42 | internal expect fun readLineOrNullMpp(hideInput: Boolean): String?
43 |
44 | internal expect fun sendInterceptedPrintRequest(
45 | request: PrintRequest,
46 | terminalInterface: TerminalInterface,
47 | interceptors: List,
48 | )
49 |
50 | internal expect val CR_IMPLIES_LF: Boolean
51 |
52 | internal expect fun exitProcessMpp(status: Int)
53 |
54 | internal expect fun readFileIfExists(filename: String): String?
55 |
56 | /** Whether tests running on this platform can access the filesystem */
57 | internal expect fun testsHaveFileSystem(): Boolean
58 |
59 | internal expect fun getStandardTerminalInterface(): TerminalInterface
60 |
61 | internal val STANDARD_TERM_INTERFACE: TerminalInterface = getStandardTerminalInterface()
62 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/ThemeValue.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | import com.github.ajalt.mordant.rendering.TextStyle
4 | import com.github.ajalt.mordant.rendering.Theme
5 | import com.github.ajalt.mordant.terminal.Terminal
6 |
7 | internal sealed class ThemeStyle {
8 | companion object {
9 | fun of(key: String, explicit: TextStyle?, default: TextStyle = DEFAULT_STYLE): ThemeStyle {
10 | return when (explicit) {
11 | null -> Default(key, default)
12 | else -> Explicit(explicit)
13 | }
14 | }
15 | }
16 |
17 | abstract operator fun get(theme: Theme): TextStyle
18 | operator fun get(terminal: Terminal): TextStyle = get(terminal.theme)
19 |
20 | class Default(private val key: String, private val default: TextStyle) : ThemeStyle() {
21 | override fun get(theme: Theme): TextStyle = theme.style(key, default)
22 | }
23 |
24 | class Explicit(private val style: TextStyle) : ThemeStyle() {
25 | override fun get(theme: Theme): TextStyle = style
26 | }
27 | }
28 |
29 | internal sealed class ThemeString {
30 | companion object {
31 | fun of(key: String, explicit: String?, default: String = ""): ThemeString {
32 | return when (explicit) {
33 | null -> Default(key, default)
34 | else -> Explicit(explicit)
35 | }
36 | }
37 | }
38 |
39 | abstract operator fun get(theme: Theme): String
40 | operator fun get(terminal: Terminal) = get(terminal.theme)
41 |
42 | class Default(private val key: String, private val default: String) : ThemeString() {
43 | override fun get(theme: Theme): String = theme.string(key, default)
44 | }
45 |
46 | class Explicit(private val style: String) : ThemeString() {
47 | override fun get(theme: Theme): String = style
48 | }
49 | }
50 |
51 | internal sealed class ThemeFlag {
52 | companion object {
53 | fun of(key: String, explicit: Boolean?, default: Boolean = false): ThemeFlag {
54 | return when (explicit) {
55 | null -> Default(key, default)
56 | else -> Explicit(explicit)
57 | }
58 | }
59 | }
60 |
61 | abstract operator fun get(theme: Theme): Boolean
62 | operator fun get(terminal: Terminal) = get(terminal.theme)
63 |
64 | class Default(private val key: String, private val default: Boolean) : ThemeFlag() {
65 | override fun get(theme: Theme): Boolean = theme.flag(key, default)
66 | }
67 |
68 | class Explicit(private val style: Boolean) : ThemeFlag() {
69 | override fun get(theme: Theme): Boolean = style
70 | }
71 | }
72 |
73 | internal sealed class ThemeDimension {
74 | companion object {
75 | fun of(key: String, explicit: Int?, default: Int = 0): ThemeDimension {
76 | return when (explicit) {
77 | null -> Default(key, default)
78 | else -> Explicit(explicit)
79 | }
80 | }
81 | }
82 |
83 | abstract operator fun get(theme: Theme): Int
84 | operator fun get(terminal: Terminal) = get(terminal.theme)
85 |
86 | class Default(private val key: String, private val default: Int) : ThemeDimension() {
87 | override fun get(theme: Theme): Int = theme.dimension(key, default)
88 | }
89 |
90 | class Explicit(private val style: Int) : ThemeDimension() {
91 | override fun get(theme: Theme): Int = style
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/Utf8.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | /** Read bytes from a UTF-8 encoded stream, and return the next codepoint. */
4 | internal fun readBytesAsUtf8(readByte: () -> Int?): Int? {
5 | val byte = readByte() ?: return null
6 | val byteLength: Int
7 | var codepoint: Int
8 | when {
9 | byte and 0b1000_0000 == 0x00 -> {
10 | return byte // 1-byte character
11 | }
12 |
13 | byte and 0b1110_0000 == 0b1100_0000 -> {
14 | codepoint = byte and 0b11111
15 | byteLength = 2
16 | }
17 |
18 | byte and 0b1111_0000 == 0b1110_0000 -> {
19 | codepoint = byte and 0b1111
20 | byteLength = 3
21 | }
22 |
23 | byte and 0b1111_1000 == 0b1111_0000 -> {
24 | codepoint = byte and 0b111
25 | byteLength = 4
26 | }
27 |
28 | else -> error("Invalid UTF-8 byte")
29 | }
30 |
31 | repeat(byteLength - 1) {
32 | val next = readByte() ?: return null
33 | if (next and 0b1100_0000 != 0b1000_0000) error("Invalid UTF-8 byte")
34 | codepoint = codepoint shl 6 or (next and 0b0011_1111)
35 | }
36 | return codepoint
37 | }
38 |
39 | /** Convert a unicode codepoint to a String. */
40 | internal fun codepointToString(codePoint: Int): String {
41 | return when (codePoint) {
42 | in 0..0xFFFF -> {
43 | Char(codePoint).toString()
44 | }
45 | in 0x10000..0x10FFFF -> {
46 | val highSurrogate = Char(((codePoint - 0x10000) shr 10) or 0xD800)
47 | val lowSurrogate = Char(((codePoint - 0x10000) and 0x3FF) or 0xDC00)
48 | highSurrogate.toString() + lowSurrogate.toString()
49 | }
50 | else -> {
51 | throw IllegalArgumentException("Invalid code point: $codePoint")
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/cellwidth.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | import com.github.ajalt.mordant.internal.gen.CELL_WIDTH_TABLE
4 | import com.github.ajalt.mordant.internal.gen.EMOJI_SEQUENCES
5 | import com.github.ajalt.mordant.internal.gen.IntTrie
6 | import com.github.ajalt.mordant.internal.gen.couldStartEmojiSeq
7 |
8 |
9 | /*
10 | * This implementation uses a binary search of a lookup table, similar to Markus Kuhn's classic C
11 | * implementation of wcwidth. This function differs from his in a few ways. We generate the lookup
12 | * table from the latest unicode standard. wcwidth effectively requires two codespace searches for
13 | * each codepoint. We perform at most a single binary search, and for ASCII characters, we don't
14 | * perform any search at all. wcwidth also returns -1 for most control codes, which is wrong for all
15 | * the use cases you'd use wcwidth for. A BEL character does not suddenly make your line of text
16 | * shorter. We return 0 for control codes other than DEL and BS.
17 | */
18 |
19 | /**
20 | * Return the width, in terminal cells, of the given unicode [codepoint].
21 | */
22 | internal fun cellWidth(codepoint: Int): Int {
23 | if (codepoint in 0x20..0x7e) return 1 // fast path for printable ASCII
24 | if (codepoint == 0x08 || codepoint == 0x7f) return -1 // DEL and BS
25 |
26 | val table = CELL_WIDTH_TABLE
27 | var min = 0
28 | var mid: Int
29 | var max = table.lastIndex
30 |
31 | if (codepoint < table[0].low || codepoint > table[max].high) return 1
32 |
33 | while (max >= min) {
34 | mid = (min + max) / 2
35 | val entry = table[mid]
36 | when {
37 | codepoint > entry.high -> min = mid + 1
38 | codepoint < entry.low -> max = mid - 1
39 | else -> return table[mid].width.toInt()
40 | }
41 | }
42 |
43 | return 1
44 | }
45 |
46 | /** Return the width, in terminal cells, of the given [string]*/
47 | internal fun stringCellWidth(string: String): Int {
48 | var sum = 0
49 | var sumSinceZwj = 0
50 | var zwjSeq: IntTrie? = null
51 | for (codepoint in codepointSequence(string)) {
52 | val width = cellWidth(codepoint)
53 | if (zwjSeq != null) {
54 | sumSinceZwj += width
55 | if (codepoint in zwjSeq.values) {
56 | sumSinceZwj = 0
57 | }
58 | zwjSeq = zwjSeq.children[codepoint]
59 | if (zwjSeq == null) {
60 | // all ZWJ sequences combine to one glyph, which is always an emoji, so add 2 for the width of the
61 | // emoji, plus the width of any codepoints since the end of the last complete sequence. Unfortunately,
62 | // some of these emoji are wider than two cells, but given that their size is font-dependant and usually
63 | // not cell-aligned anyway, there's no perfect solution here. Thanks, unicode.
64 | sum += sumSinceZwj + 2
65 | sumSinceZwj = 0
66 | } else {
67 | sumSinceZwj += width
68 | }
69 | } else {
70 | // We do a fast range check to skip ZWJ sequence processing for most codepoints
71 | if (couldStartEmojiSeq(codepoint)) {
72 | zwjSeq = EMOJI_SEQUENCES.children[codepoint]
73 | }
74 | if (zwjSeq == null) {
75 | sum += width
76 | }
77 | }
78 | }
79 | // If we were in a zwj sequence at the end of the string, add whatever was left to the sum
80 | return sum + sumSinceZwj
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/gen/emojiseqtable.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal.gen
2 |
3 | internal fun couldStartEmojiSeq(codepoint: Int): Boolean {
4 | return codepoint in 0x261d..0x2764 || codepoint in 0x1f344..0x1faf8
5 | }
6 |
7 | internal data class IntTrie(
8 | val children: MutableMap,
9 | val values: MutableSet = mutableSetOf(),
10 | ) {
11 | constructor(vararg children: Pair, values: MutableSet = mutableSetOf())
12 | : this(mutableMapOf(*children), values)
13 | }
14 |
15 | internal val EMOJI_SEQUENCES: IntTrie = buildSeqTrie()
16 |
17 | private fun buildSeqTrie(): IntTrie {
18 | val root = IntTrie()
19 | for (sequences in arrayOf(sequences1(), sequences2(), sequences3(), sequences4())) {
20 | for (seq in sequences) {
21 | var node = root
22 | for (i in 0.."
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/rendering/Whitespace.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.rendering
2 |
3 | /**
4 | * Settings for handling of whitespace and line wrapping.
5 | *
6 | * These values correspond to the values of the CSS
7 | * [white-space](https://developer.mozilla.org/en-US/docs/Web/CSS/white-space) property.
8 | *
9 | * @property collapseNewlines If true, line breaks in the text will be replaced with spaces.
10 | * @property collapseSpaces If true, consecutive spaces will be replaced with a single space.
11 | * @property wrap If true, lines will be broken by replacing spaces with line breaks where necessary
12 | * to keep lines under the configured width.
13 | * @property trimEol If true, whitespace at the end of lines will be removed.
14 | * @see OverflowWrap
15 | */
16 | enum class Whitespace(
17 | val collapseNewlines: Boolean,
18 | val collapseSpaces: Boolean,
19 | val wrap: Boolean,
20 | val trimEol: Boolean,
21 | ) {
22 | /** Wrap text and collapse all whitespaces and line breaks */
23 | NORMAL(collapseNewlines = true, collapseSpaces = true, wrap = true, trimEol = true),
24 |
25 | /** Collapse spaces and line breaks, but don't wrap lines. This will effectively put all text on a single line. */
26 | NOWRAP(collapseNewlines = true, collapseSpaces = true, wrap = false, trimEol = true),
27 |
28 | /** Make no changes to the input text */
29 | PRE(collapseNewlines = false, collapseSpaces = false, wrap = false, trimEol = false),
30 |
31 | /** Like [PRE], but will break long lines */
32 | PRE_WRAP(collapseNewlines = false, collapseSpaces = false, wrap = true, trimEol = true),
33 |
34 | /** Like [NORMAL], but preserves any line breaks from the input */
35 | PRE_LINE(collapseNewlines = false, collapseSpaces = true, wrap = true, trimEol = true)
36 | }
37 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/rendering/Widget.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.rendering
2 |
3 | import com.github.ajalt.mordant.terminal.Terminal
4 |
5 | interface Widget {
6 | fun measure(t: Terminal, width: Int = t.size.width): WidthRange
7 | fun render(t: Terminal, width: Int = t.size.width): Lines
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/rendering/WidthRange.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.rendering
2 |
3 | import com.github.ajalt.mordant.terminal.Terminal
4 |
5 | /**
6 | * @property min The minimum width that a widget needs to render without truncation
7 | * @property max The width that a widget would use if given all available space
8 | */
9 | data class WidthRange(val min: Int, val max: Int) {
10 | init {
11 | require(min <= max) { "Range min cannot be larger than max" }
12 | }
13 |
14 | operator fun plus(extra: Int) = if (extra == 0) this else WidthRange(min + extra, max + extra)
15 | operator fun plus(other: WidthRange) = WidthRange(min + other.min, max + other.max)
16 | operator fun div(divisor: Int) =
17 | if (divisor == 1) this else WidthRange(min / divisor, max / divisor)
18 | }
19 |
20 | internal fun Iterable.maxWidthRange(
21 | t: Terminal,
22 | width: Int,
23 | paddingWidth: Int = 0,
24 | ): WidthRange {
25 | return maxWidthRange(paddingWidth) { it.measure(t, width - paddingWidth) }
26 | }
27 |
28 | internal inline fun Iterable.maxWidthRange(
29 | paddingWidth: Int = 0,
30 | mapping: (T) -> WidthRange?,
31 | ): WidthRange {
32 | var max = 0
33 | var min = 0
34 | for (it in this) {
35 | val range = mapping(it) ?: continue
36 | max = maxOf(max, range.max)
37 | min = maxOf(min, range.min)
38 | }
39 | return WidthRange(min + paddingWidth, max + paddingWidth)
40 | }
41 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/Borders.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.table
2 |
3 | enum class Borders(
4 | val left: Boolean,
5 | val top: Boolean,
6 | val right: Boolean,
7 | val bottom: Boolean,
8 | ) {
9 | NONE(left = false, top = false, right = false, bottom = false),
10 | BOTTOM(left = false, top = false, right = false, bottom = true),
11 | RIGHT(left = false, top = false, right = true, bottom = false),
12 | BOTTOM_RIGHT(left = false, top = false, right = true, bottom = true),
13 | TOP(left = false, top = true, right = false, bottom = false),
14 | TOP_BOTTOM(left = false, top = true, right = false, bottom = true),
15 | TOP_RIGHT(left = false, top = true, right = true, bottom = false),
16 | TOP_RIGHT_BOTTOM(left = false, top = true, right = true, bottom = true),
17 | LEFT(left = true, top = false, right = false, bottom = false),
18 | LEFT_BOTTOM(left = true, top = false, right = false, bottom = true),
19 | LEFT_RIGHT(left = true, top = false, right = true, bottom = false),
20 | LEFT_RIGHT_BOTTOM(left = true, top = false, right = true, bottom = true),
21 | LEFT_TOP(left = true, top = true, right = false, bottom = false),
22 | LEFT_TOP_BOTTOM(left = true, top = true, right = false, bottom = true),
23 | LEFT_TOP_RIGHT(left = true, top = true, right = true, bottom = false),
24 | ALL(left = true, top = true, right = true, bottom = true),
25 | }
26 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/VerticalLayout.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.table
2 |
3 | import com.github.ajalt.mordant.internal.DEFAULT_STYLE
4 | import com.github.ajalt.mordant.internal.EMPTY_LINE
5 | import com.github.ajalt.mordant.rendering.*
6 | import com.github.ajalt.mordant.rendering.TextAlign.NONE
7 | import com.github.ajalt.mordant.table.ColumnWidth.Companion.Auto
8 | import com.github.ajalt.mordant.terminal.Terminal
9 |
10 | private class VerticalLayoutCell(
11 | val content: Widget,
12 | val style: TextStyle?,
13 | val textAlign: TextAlign,
14 | )
15 |
16 | internal class VerticalLayout private constructor(
17 | private val cells: List,
18 | private val spacing: Int,
19 | private val columnWidth: ColumnWidth,
20 | private val textAlign: TextAlign,
21 | private val hasAlignedCells: Boolean,
22 | ) : Widget {
23 | companion object {
24 | fun fromTableBuilder(
25 | builder: TableBuilderInstance,
26 | spacing: Int,
27 | columnWidth: ColumnWidth,
28 | ): VerticalLayout {
29 | builder.padding(0)
30 | builder.cellBorders = Borders.NONE
31 | builder.tableBorders = Borders.NONE
32 | var aligned = false
33 | val cells = TableLayout(builder).buildTable().rows.map {
34 | check(it.size == 1)
35 | val cell = it[0] as Cell.Content
36 | aligned = aligned || (cell.textAlign !in listOf(null, NONE))
37 | VerticalLayoutCell(cell.content, cell.style, cell.textAlign)
38 | }
39 | return VerticalLayout(
40 | cells,
41 | spacing,
42 | columnWidth,
43 | builder.align ?: NONE,
44 | aligned,
45 | )
46 | }
47 | }
48 |
49 | init {
50 | require(spacing >= 0) { "layout spacing cannot be negative" }
51 | }
52 |
53 | override fun measure(t: Terminal, width: Int): WidthRange {
54 | return cells.maxWidthRange { it.content.measure(t, width) }
55 | }
56 |
57 | override fun render(t: Terminal, width: Int): Lines {
58 | val renderWidth = when {
59 | columnWidth.isExpand -> width
60 | hasAlignedCells -> measure(t, width).max
61 | else -> width
62 | }.coerceAtMost(width)
63 | val lines = mutableListOf()
64 | val spacingLine = when (textAlign) {
65 | NONE -> EMPTY_LINE
66 | else -> Line(listOf(Span.space(renderWidth)), DEFAULT_STYLE)
67 | }
68 | for ((i, cell) in cells.withIndex()) {
69 | if (i > 0) repeat(spacing) { lines += spacingLine }
70 | var rendered = cell.content.render(t, renderWidth).withStyle(cell.style)
71 |
72 | val w = columnWidth
73 | rendered = when {
74 | w.expandWeight != null -> rendered.setSize(width, textAlign = cell.textAlign)
75 | w.width != null -> rendered.setSize(w.width, textAlign = cell.textAlign)
76 | cell.textAlign != NONE -> rendered.setSize(renderWidth, textAlign = cell.textAlign)
77 | else -> rendered
78 | }
79 | // Cells always take up a line, even if empty
80 | lines += rendered.lines.ifEmpty { listOf(EMPTY_LINE) }
81 | }
82 | return Lines(lines)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/StandardTerminalInterface.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal
2 |
3 | import com.github.ajalt.mordant.internal.printStderr
4 | import com.github.ajalt.mordant.internal.readLineOrNullMpp
5 | import com.github.ajalt.mordant.rendering.AnsiLevel
6 |
7 | /**
8 | * A base class for terminal interfaces that prints using standard `kotlin.io` functions.
9 | */
10 | abstract class StandardTerminalInterface : TerminalInterface {
11 | override fun info(
12 | ansiLevel: AnsiLevel?,
13 | hyperlinks: Boolean?,
14 | outputInteractive: Boolean?,
15 | inputInteractive: Boolean?,
16 | ): TerminalInfo {
17 | return TerminalDetection.detectTerminal(
18 | ansiLevel = ansiLevel,
19 | hyperlinks = hyperlinks,
20 | forceInputInteractive = inputInteractive,
21 | forceOutputInteractive = outputInteractive,
22 | detectedStdinInteractive = stdinInteractive(),
23 | detectedStdoutInteractive = stdoutInteractive(),
24 | )
25 | }
26 |
27 | override fun completePrintRequest(request: PrintRequest) {
28 | when {
29 | request.stderr -> printStderr(request.text, request.trailingLinebreak)
30 | request.trailingLinebreak -> {
31 | if (request.text.isEmpty()) {
32 | println()
33 | } else {
34 | println(request.text)
35 | }
36 | }
37 |
38 | else -> print(request.text)
39 | }
40 | }
41 |
42 | override fun readLineOrNull(hideInput: Boolean): String? = readLineOrNullMpp(hideInput)
43 |
44 | /** Whether the output stream is detected as interactive. */
45 | open fun stdoutInteractive(): Boolean = true
46 |
47 | /** Whether the input stream is detected as interactive. */
48 | open fun stdinInteractive(): Boolean = true
49 | }
50 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/TerminalExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal
2 |
3 | import com.github.ajalt.mordant.rendering.OverflowWrap
4 | import com.github.ajalt.mordant.rendering.TextAlign
5 | import com.github.ajalt.mordant.rendering.Whitespace
6 |
7 | /**
8 | * Print a line styled with the theme's [danger][Theme.danger] style.
9 | */
10 | fun Terminal.danger(
11 | message: Any?,
12 | whitespace: Whitespace = Whitespace.PRE,
13 | align: TextAlign = TextAlign.NONE,
14 | overflowWrap: OverflowWrap = OverflowWrap.NORMAL,
15 | width: Int? = null,
16 | stderr: Boolean = false,
17 | ) {
18 | println(theme.danger(message.toString()), whitespace, align, overflowWrap, width, stderr)
19 | }
20 |
21 | /**
22 | * Print a line styled with the theme's [warning][Theme.warning] style.
23 | */
24 | fun Terminal.warning(
25 | message: Any?,
26 | whitespace: Whitespace = Whitespace.PRE,
27 | align: TextAlign = TextAlign.NONE,
28 | overflowWrap: OverflowWrap = OverflowWrap.NORMAL,
29 | width: Int? = null,
30 | stderr: Boolean = false,
31 | ) {
32 | println(theme.warning(message.toString()), whitespace, align, overflowWrap, width, stderr)
33 | }
34 |
35 | /**
36 | * Print a line styled with the theme's [info][Theme.info] style.
37 | */
38 | fun Terminal.info(
39 | message: Any?,
40 | whitespace: Whitespace = Whitespace.PRE,
41 | align: TextAlign = TextAlign.NONE,
42 | overflowWrap: OverflowWrap = OverflowWrap.NORMAL,
43 | width: Int? = null,
44 | stderr: Boolean = false,
45 | ) {
46 | println(theme.info(message.toString()), whitespace, align, overflowWrap, width, stderr)
47 | }
48 |
49 | /**
50 | * Print a line styled with the theme's [muted][Theme.muted] style.
51 | */
52 | fun Terminal.muted(
53 | message: Any?,
54 | whitespace: Whitespace = Whitespace.PRE,
55 | align: TextAlign = TextAlign.NONE,
56 | overflowWrap: OverflowWrap = OverflowWrap.NORMAL,
57 | width: Int? = null,
58 | stderr: Boolean = false,
59 | ) {
60 | println(theme.muted(message.toString()), whitespace, align, overflowWrap, width, stderr)
61 | }
62 |
63 | /**
64 | * Print a line styled with the theme's [success][Theme.success] style.
65 | */
66 | fun Terminal.success(
67 | message: Any?,
68 | whitespace: Whitespace = Whitespace.PRE,
69 | align: TextAlign = TextAlign.NONE,
70 | overflowWrap: OverflowWrap = OverflowWrap.NORMAL,
71 | width: Int? = null,
72 | stderr: Boolean = false,
73 | ) {
74 | println(theme.success(message.toString()), whitespace, align, overflowWrap, width, stderr)
75 | }
76 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/TerminalInfo.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal
2 |
3 | import com.github.ajalt.mordant.rendering.AnsiLevel
4 |
5 |
6 | /**
7 | * Information about the current terminal
8 | */
9 | data class TerminalInfo(
10 | /** The level of ANSI codes to use when printing to the terminal */
11 | var ansiLevel: AnsiLevel,
12 | /** If true, ANSI hyperlink codes can be used */
13 | var ansiHyperLinks: Boolean,
14 | /**
15 | * If false the output stream is not an interactive terminal, such as when it's redirected to a
16 | * file.
17 | */
18 | val outputInteractive: Boolean,
19 | /**
20 | * If false the intput stream is not an interactive terminal, such as when it's redirected from
21 | * a file
22 | */
23 | val inputInteractive: Boolean,
24 | /**
25 | * If false using ANSI cursor movement codes may not work.
26 | */
27 | val supportsAnsiCursor: Boolean,
28 | ) {
29 | /** Return true if both input and output are interactive */
30 | val interactive: Boolean get() = inputInteractive && outputInteractive
31 | }
32 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/TerminalInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal
2 |
3 | internal fun interface TerminalInterceptor {
4 | fun intercept(request: PrintRequest): PrintRequest
5 | }
6 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/TerminalInterfaceProvider.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal
2 |
3 | /**
4 | * A provider for a [TerminalInterface].
5 | *
6 | * Implementations of this interface are loaded via the `ServiceLoader` mechanism, and should be
7 | * declared in the `META-INF/services/com.github.ajalt.mordant.terminal.TerminalInterfaceProvider`
8 | * file.
9 | */
10 | interface TerminalInterfaceProvider {
11 | /**
12 | * Load the terminal interface.
13 | *
14 | * @return The terminal interface, or null if it could not be loaded.
15 | */
16 | fun load(): TerminalInterface?
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/Caption.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.widgets
2 |
3 | import com.github.ajalt.mordant.rendering.*
4 | import com.github.ajalt.mordant.terminal.Terminal
5 |
6 | /** Add a [top] and/or [bottom] caption to [content] */
7 | class Caption(
8 | val content: Widget,
9 | val top: Widget? = null,
10 | val bottom: Widget? = null,
11 | ) : Widget {
12 | constructor(
13 | content: Widget,
14 | top: String? = null,
15 | bottom: String? = null,
16 | topAlign: TextAlign = TextAlign.CENTER,
17 | bottomAlign: TextAlign = TextAlign.CENTER,
18 | ) : this(
19 | content,
20 | top?.let { Text(it, align = topAlign) },
21 | bottom?.let { Text(it, align = bottomAlign) },
22 | )
23 |
24 | override fun measure(t: Terminal, width: Int): WidthRange {
25 | return content.measure(t, width)
26 | }
27 |
28 | override fun render(t: Terminal, width: Int): Lines {
29 | val captionWidth = content.measure(t, width).max.coerceAtMost(width)
30 | val lines = mutableListOf()
31 | top?.let { lines.addAll(it.render(t, captionWidth).lines) }
32 | lines.addAll(content.render(t, width).lines)
33 | bottom?.let { lines.addAll(it.render(t, captionWidth).lines) }
34 | return Lines(lines)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/EmptyWidget.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.widgets
2 |
3 | import com.github.ajalt.mordant.internal.EMPTY_LINES
4 | import com.github.ajalt.mordant.rendering.Widget
5 | import com.github.ajalt.mordant.rendering.WidthRange
6 | import com.github.ajalt.mordant.terminal.Terminal
7 |
8 | /**
9 | * A widget with 0 width, 0 height, and no content.
10 | *
11 | * Can be used as a placeholder in layouts etc.
12 | */
13 | object EmptyWidget : Widget {
14 | override fun measure(t: Terminal, width: Int) = WidthRange(0, 0)
15 | override fun render(t: Terminal, width: Int) = EMPTY_LINES
16 | }
17 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/OrderedList.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.widgets
2 |
3 | import com.github.ajalt.mordant.internal.*
4 | import com.github.ajalt.mordant.rendering.*
5 | import com.github.ajalt.mordant.terminal.Terminal
6 | import kotlin.math.log10
7 |
8 | /**
9 | * A numbered list of widgets.
10 | *
11 | * @property listEntries The content of the list
12 | * @property numberStyle The style of the list numbers. Defaults to the theme value `list.number`.
13 | * @property numberSeparator The string to print between the list numbers and the widgets. Defaults to the theme value
14 | * `list.number.separator`
15 | */
16 | class OrderedList private constructor(
17 | private val listEntries: List,
18 | private val numberStyle: ThemeStyle,
19 | private val numberSeparator: ThemeString,
20 | ) : Widget {
21 | constructor(
22 | listEntries: List,
23 | numberStyle: TextStyle? = null,
24 | numberSeparator: String? = null,
25 | ) : this(
26 | listEntries,
27 | ThemeStyle.of("list.number", numberStyle),
28 | ThemeString.of("list.number.separator", numberSeparator)
29 | )
30 |
31 | private fun sep(t: Theme): Line {
32 | val text = numberSeparator[t]
33 | require("\n" !in text) { "number separator cannot contain newlines" }
34 | return parseText(
35 | text,
36 | numberStyle[t]
37 | ).lines.firstOrNull() ?: EMPTY_LINE
38 | }
39 |
40 | private fun continuationPadding(i: Int, sepWidth: Int): List {
41 | val n = bulletWidth(i, sepWidth)
42 | return listOf(Span.space(n))
43 | }
44 |
45 | private fun bulletWidth(i: Int, sepWidth: Int): Int {
46 | return (log10((i + 1).toDouble()).toInt() + 1 // number
47 | + 2 // padding
48 | + sepWidth
49 | )
50 | }
51 |
52 | private val maxBulletWidth = bulletWidth(listEntries.size, sep(Theme.Default).lineWidth)
53 |
54 | override fun measure(t: Terminal, width: Int): WidthRange {
55 | return listEntries.maxWidthRange(t, width, maxBulletWidth)
56 | }
57 |
58 | override fun render(t: Terminal, width: Int): Lines {
59 | val contentWidth = width - maxBulletWidth
60 | val lines = mutableListOf()
61 | val style = numberStyle[t]
62 | val sep = sep(t.theme)
63 | val sepWidth = sep.lineWidth
64 |
65 | for ((i, entry) in listEntries.withIndex()) {
66 | val bullet = flatLine(
67 | SINGLE_SPACE,
68 | Span.word("${i + 1}", style),
69 | sep,
70 | SINGLE_SPACE
71 | )
72 | for ((j, line) in entry.render(t, contentWidth).lines.withIndex()) {
73 | val start = if (j == 0) bullet else continuationPadding(i, sepWidth)
74 | lines += Line(start + line)
75 | }
76 | }
77 | return Lines(lines)
78 | }
79 | }
80 |
81 | fun OrderedList(
82 | vararg listEntries: String,
83 | numberStyle: TextStyle? = null,
84 | numberSeparator: String? = null,
85 | ): OrderedList {
86 | return OrderedList(listEntries.map { Text(it) }, numberStyle, numberSeparator)
87 | }
88 |
89 | fun OrderedList(
90 | vararg listEntries: Widget,
91 | numberStyle: TextStyle? = null,
92 | numberSeparator: String? = null,
93 | ): OrderedList {
94 | return OrderedList(listEntries.toList(), numberStyle, numberSeparator)
95 | }
96 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/Spinner.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.widgets
2 |
3 | import com.github.ajalt.mordant.internal.DEFAULT_STYLE
4 | import com.github.ajalt.mordant.internal.MppAtomicInt
5 | import com.github.ajalt.mordant.rendering.Lines
6 | import com.github.ajalt.mordant.rendering.TextStyle
7 | import com.github.ajalt.mordant.rendering.Widget
8 | import com.github.ajalt.mordant.rendering.WidthRange
9 | import com.github.ajalt.mordant.terminal.Terminal
10 |
11 | /**
12 | * A widget that will loop through a fixed list of frames.
13 | *
14 | * The widget will render the same frame until [tick] is set or [advanceTick] is called, at which point the next frame
15 | * will render. The animation will loop when [tick] is larger than the number frames.
16 | *
17 | * To reduce the speed of the animation, increase the [duration].
18 | *
19 | * @property duration the number of [ticks][tick] that each frame of the spinner should show for. This defaults to 1,
20 | * which will cause a new frame to display every time [advanceTick] is called.
21 | * @param initial the starting tick value.
22 | */
23 | class Spinner(
24 | private val frames: List,
25 | private val duration: Int = 1,
26 | initial: Int = 0,
27 | ) : Widget {
28 | companion object {
29 | /**
30 | * Create a spinner with the following frames:
31 | *
32 | * `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`
33 | */
34 | fun Dots(style: TextStyle = DEFAULT_STYLE, duration: Int = 1, initial: Int = 0): Spinner {
35 | return Spinner("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏", style, duration, initial)
36 | }
37 |
38 | /**
39 | * Create a spinner with the following frames:
40 | *
41 | * `|/-\`
42 | *
43 | * These frames only use ASCII characters.
44 | */
45 | fun Lines(style: TextStyle = DEFAULT_STYLE, duration: Int = 1, initial: Int = 0): Spinner {
46 | return Spinner("|/-\\", style, duration, initial)
47 | }
48 | }
49 |
50 | /**
51 | * Construct a [Spinner] from a string, where each character in the string is one frame.
52 | */
53 | constructor(
54 | frames: String,
55 | style: TextStyle = DEFAULT_STYLE,
56 | duration: Int = 1,
57 | initial: Int = 0,
58 | ) :
59 | this(frames.map { Text(style(it.toString())) }, duration, initial)
60 |
61 | private val _tick = MppAtomicInt(initial)
62 |
63 | /**
64 | * The current frame number.
65 | *
66 | * This may be larger than the number of frames, in which case the animation will loop.
67 | */
68 | var tick: Int
69 | get() = _tick.get()
70 | set(value) {
71 | _tick.set(value)
72 | }
73 |
74 | /**
75 | * Increment the [tick] value by one and return the new value.
76 | */
77 | fun advanceTick(): Int {
78 | return _tick.getAndIncrement() + 1
79 | }
80 |
81 | /** The current frame */
82 | val currentFrame: Widget get() = frames[(tick / duration) % frames.size]
83 |
84 | override fun measure(t: Terminal, width: Int): WidthRange = currentFrame.measure(t, width)
85 | override fun render(t: Terminal, width: Int): Lines = currentFrame.render(t, width)
86 | }
87 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/UnorderedList.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.widgets
2 |
3 | import com.github.ajalt.mordant.internal.*
4 | import com.github.ajalt.mordant.rendering.*
5 | import com.github.ajalt.mordant.terminal.Terminal
6 |
7 | class UnorderedList private constructor(
8 | private val listEntries: List,
9 | private val bulletText: ThemeString,
10 | private val bulletStyle: ThemeStyle,
11 | ) : Widget {
12 | constructor(
13 | listEntries: List,
14 | bulletText: String? = null,
15 | bulletStyle: TextStyle? = null,
16 | ) : this(
17 | listEntries,
18 | ThemeString.of("list.bullet.text", bulletText),
19 | ThemeStyle.of("list.bullet", bulletStyle)
20 | )
21 |
22 | private fun bullet(t: Theme): Line {
23 | val text = bulletText[t]
24 | require("\n" !in text) { "bullet text cannot contain newlines" }
25 | if (text.isEmpty()) return EMPTY_LINE
26 | return flatLine(
27 | SINGLE_SPACE,
28 | parseText(text, bulletStyle[t]).lines.firstOrNull() ?: EMPTY_LINE,
29 | SINGLE_SPACE
30 | )
31 | }
32 |
33 | override fun measure(t: Terminal, width: Int): WidthRange {
34 | val bulletWidth = bullet(t.theme).sumOf { it.cellWidth }
35 | return listEntries.maxWidthRange(t, width, bulletWidth)
36 | }
37 |
38 | override fun render(t: Terminal, width: Int): Lines {
39 | val bullet = bullet(t.theme)
40 | val bulletWidth = bullet.sumOf { it.cellWidth }
41 | val contentWidth = (width - bulletWidth).coerceAtLeast(0)
42 | val continuationPadding = when {
43 | bulletWidth > 0 -> listOf(Span.space(bulletWidth, bulletStyle[t.theme]))
44 | else -> EMPTY_LINE
45 | }
46 |
47 | val lines = mutableListOf()
48 |
49 | for (entry in listEntries) {
50 | for ((i, line) in entry.render(t, contentWidth).lines.withIndex()) {
51 | val start = if (i == 0) bullet else continuationPadding
52 | lines += Line(start + line)
53 | }
54 | }
55 | return Lines(lines)
56 | }
57 | }
58 |
59 | fun UnorderedList(
60 | vararg listEntries: String,
61 | bulletText: String? = null,
62 | bulletStyle: TextStyle? = null,
63 | ): UnorderedList = UnorderedList(
64 | listEntries.map { Text(it) },
65 | bulletText,
66 | bulletStyle,
67 | )
68 |
69 | fun UnorderedList(
70 | vararg listEntries: Widget,
71 | bulletText: String? = null,
72 | bulletStyle: TextStyle? = null,
73 | ): UnorderedList = UnorderedList(
74 | listEntries.toList(),
75 | bulletText,
76 | bulletStyle,
77 | )
78 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/Viewport.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.widgets
2 |
3 | import com.github.ajalt.mordant.rendering.Lines
4 | import com.github.ajalt.mordant.rendering.Widget
5 | import com.github.ajalt.mordant.rendering.WidthRange
6 | import com.github.ajalt.mordant.rendering.setSize
7 | import com.github.ajalt.mordant.terminal.Terminal
8 |
9 |
10 | /**
11 | * Crop or pad another [content] widget to a fixed size, and optionally scroll visible portion of
12 | * the widget within that size.
13 | *
14 | * If [width] or [height] are larger than the size of the [content] widget, the extra space will be
15 | * filled with spaces.
16 | *
17 | * You can specify [scrollRight] and [scrollDown] to scroll the viewport across the content.
18 | * Negative values will scroll the widget to the right or up, respectively.
19 | *
20 | * ### Example
21 | *
22 | * ```
23 | * val text = Text(
24 | * """
25 | * 123
26 | * 456
27 | * 7890
28 | * """.trimIndent()
29 | * )
30 | *
31 | * val viewport1 = Viewport(text, width = 2, height = 2, scrollRight = 1, scrollDown = 1)
32 | * terminal.println(Panel(viewport1))
33 | *
34 | * val viewport2 = Viewport(text, width = 2, height = 2, scrollRight = -1, scrollDown = -1)
35 | * terminal.println(Panel(viewport2))
36 | * ```
37 | *
38 | * Will print the following:
39 | *
40 | * ```
41 | * ╭──╮
42 | * │56│
43 | * │89│
44 | * ╰──╯
45 | * ╭──╮
46 | * │ │
47 | * │ 1│
48 | * ╰──╯
49 | * ```
50 | */
51 | class Viewport(
52 | /** The widget to crop. */
53 | private val content: Widget,
54 | /** The width to crop the widget to, or `null` to use the width of the longest line */
55 | private val width: Int?,
56 | /** The height to crop the widget to, or `null` to leave the height unchanged */
57 | private val height: Int? = null,
58 | /**
59 | * The number of characters to crop from the left of the [content] (or from the right, if
60 | * negative)
61 | */
62 | private val scrollRight: Int = 0,
63 | /**
64 | * The number of lines to crop from the top of the [content] (or from the bottom, if negative)
65 | */
66 | private val scrollDown: Int = 0,
67 | ) : Widget {
68 | override fun measure(t: Terminal, width: Int): WidthRange {
69 | return if (this.width != null) {
70 | WidthRange(this.width, this.width)
71 | } else {
72 | content.measure(t, width)
73 | }
74 | }
75 |
76 | override fun render(t: Terminal, width: Int): Lines {
77 | val lines = content.render(t, width)
78 | return lines.setSize(
79 | newWidth = this.width ?: lines.width,
80 | newHeight = this.height ?: lines.lines.size,
81 | scrollRight = scrollRight,
82 | scrollDown = scrollDown
83 | )
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/progress/CachedProgressBarDefinition.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.widgets.progress
2 |
3 | import com.github.ajalt.mordant.internal.MppAtomicRef
4 | import com.github.ajalt.mordant.internal.update
5 | import com.github.ajalt.mordant.rendering.Widget
6 | import kotlin.time.ComparableTimeMark
7 | import kotlin.time.Duration.Companion.seconds
8 | import kotlin.time.TimeSource
9 |
10 |
11 | /**
12 | * A [ProgressBarDefinition] that caches the widgets for each cell so that they only update as fast
13 | * as their [fps][ProgressBarCell.fps].
14 | */
15 | class CachedProgressBarDefinition(
16 | definition: ProgressBarDefinition,
17 | private val timeSource: TimeSource.WithComparableMarks,
18 | ) : ProgressBarDefinition {
19 | private val cache = MppAtomicRef>>(emptyMap())
20 | override val cells: List> =
21 | definition.cells.mapIndexed { i, it -> makeCell(i, it) }
22 | override val spacing: Int = definition.spacing
23 | override val alignColumns: Boolean = definition.alignColumns
24 |
25 | /**
26 | * Invalidate the cache for this definition.
27 | */
28 | fun invalidateCache() {
29 | cache.getAndSet(emptyMap())
30 | }
31 |
32 | /**
33 | * The refresh rate, Hz, that will satisfy the [fps][ProgressBarCell.fps] of
34 | * all this progress bar's cells.
35 | */
36 | val fps: Int = definition.cells.maxOfOrNull { it.fps } ?: 0
37 |
38 | // Wrap the cell builder in a block that caches the widget
39 | private fun makeCell(
40 | i: Int,
41 | cell: ProgressBarCell,
42 | ): ProgressBarCell {
43 | return ProgressBarCell(cell.columnWidth, cell.fps, cell.align, cell.verticalAlign) {
44 | val (_, new) = cache.update {
45 | when {
46 | isCacheValid(cell, this[i]?.first) -> this
47 | else -> {
48 | val content = cell.content(this@ProgressBarCell)
49 | this + (i to (timeSource.markNow() to content))
50 | }
51 | }
52 | }
53 |
54 | new[i]?.second ?: cell.content(this)
55 | }
56 | }
57 |
58 | private fun isCacheValid(
59 | cell: ProgressBarCell, lastFrameTime: ComparableTimeMark?,
60 | ): Boolean {
61 | if (lastFrameTime == null) return false
62 | val timeSinceLastFrame = lastFrameTime.elapsedNow()
63 | // if fps is 0 this will be Infinity, so it will be cached forever
64 | val maxCacheRetentionDuration = (1.0 / cell.fps).seconds
65 | return timeSinceLastFrame < maxCacheRetentionDuration
66 | }
67 | }
68 |
69 | /**
70 | * Cache this progress bar definition so that each cell only updates as often as its
71 | * [fps][ProgressBarCell.fps].
72 | */
73 | fun ProgressBarDefinition.cache(
74 | timeSource: TimeSource.WithComparableMarks = TimeSource.Monotonic,
75 | ): CachedProgressBarDefinition {
76 | return when (this) {
77 | is CachedProgressBarDefinition -> this
78 | else -> CachedProgressBarDefinition(this, timeSource)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/input/InteractiveSelectListTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.input
2 |
3 | import com.github.ajalt.mordant.rendering.AnsiLevel
4 | import com.github.ajalt.mordant.terminal.Terminal
5 | import com.github.ajalt.mordant.terminal.TerminalRecorder
6 | import com.github.ajalt.mordant.widgets.SelectList.Entry
7 | import io.kotest.matchers.shouldBe
8 | import io.kotest.matchers.string.shouldContain
9 | import kotlin.js.JsName
10 | import kotlin.test.Test
11 |
12 | class InteractiveSelectListTest {
13 | private val rec = TerminalRecorder(
14 | width = 24, ansiLevel = AnsiLevel.NONE, inputInteractive = true, outputInteractive = true
15 | )
16 | private val t = Terminal(terminalInterface = rec)
17 |
18 | @[Test JsName("single_select_strings")]
19 | fun `single select strings`() = doSingleSelectTest {
20 | t.interactiveSelectList(listOf("a", "b", "c"))
21 | }
22 |
23 | @[Test JsName("single_select_entries")]
24 | fun `single select entries`() = doSingleSelectTest {
25 | t.interactiveSelectList(listOf(Entry("a"), Entry("b"), Entry("c")))
26 | }
27 |
28 | @[Test JsName("multi_select_strings")]
29 | fun `multi select strings`() = doMultiSelectTest {
30 | t.interactiveMultiSelectList(listOf("a", "b", "c"))
31 | }
32 |
33 | @[Test JsName("multi_select_entries")]
34 | fun `multi select entries`() = doMultiSelectTest {
35 | t.interactiveMultiSelectList(listOf(Entry("a"), Entry("b"), Entry("c")))
36 | }
37 |
38 | private fun doSingleSelectTest(runList: () -> String?) {
39 | rec.inputEvents = mutableListOf(KeyboardEvent("ArrowDown"), KeyboardEvent("Enter"))
40 | runList() shouldBe "b"
41 | rec.stdout() shouldContain """
42 | ░❯ a
43 | ░ b
44 | ░ c
45 | """.trimMargin("░")
46 | }
47 |
48 | private fun doMultiSelectTest(runList: () -> List?) {
49 | rec.inputEvents = mutableListOf(
50 | KeyboardEvent("ArrowDown"),
51 | KeyboardEvent("x"),
52 | KeyboardEvent("ArrowDown"),
53 | KeyboardEvent("x"),
54 | KeyboardEvent("Enter"),
55 | )
56 | runList() shouldBe listOf("b", "c")
57 | rec.stdout() shouldContain """
58 | ░❯ • a
59 | ░ • b
60 | ░ • c
61 | """.trimMargin("░")
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/platform/MultiplatformSystemTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.platform
2 |
3 | import com.github.ajalt.mordant.internal.testsHaveFileSystem
4 | import io.kotest.matchers.nulls.shouldNotBeNull
5 | import io.kotest.matchers.shouldBe
6 | import io.kotest.matchers.string.shouldNotBeEmpty
7 | import kotlin.test.Test
8 |
9 |
10 | class MultiplatformSystemTest {
11 | @Test
12 | fun readEnvironmentVariable() {
13 | // The kotlin.test plugin doesn't provide a way to set environment variables that works on
14 | // all targets, so just pick a common one that should exist everywhere.
15 | val actual = MultiplatformSystem.readEnvironmentVariable("PATH")
16 | if (!testsHaveFileSystem()) return
17 | actual.shouldNotBeNull().shouldNotBeEmpty()
18 | }
19 |
20 | @Test
21 | fun readFileAsUtf8() {
22 | val actual = MultiplatformSystem.readFileAsUtf8(
23 | // Most targets have a cwd of $moduleDir
24 | "src/commonTest/resources/multiplatform_system_test.txt"
25 | ) ?: MultiplatformSystem.readFileAsUtf8(
26 | // js targets have a cwd of $projectDir/build/js/packages/mordant-mordant-test
27 | "../../../../mordant/src/commonTest/resources/multiplatform_system_test.txt"
28 | )
29 | if (!testsHaveFileSystem()) return
30 | actual?.trim() shouldBe "pass"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextAlignmentTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.rendering
2 |
3 | import com.github.ajalt.mordant.rendering.TextAlign.*
4 | import com.github.ajalt.mordant.rendering.TextColors.blue
5 | import com.github.ajalt.mordant.rendering.TextColors.white
6 | import com.github.ajalt.mordant.test.RenderingTest
7 | import com.github.ajalt.mordant.widgets.Text
8 | import kotlin.js.JsName
9 | import kotlin.test.Test
10 |
11 |
12 | class TextAlignmentTest : RenderingTest() {
13 | @[Test JsName("align_none")]
14 | fun `align none`() = doTest(
15 | NONE, 79, """
16 | ░one_word
17 | ░
18 | ░two words
19 | ░3 whole words
20 | ░four words 4 4
21 | ░5 5 5 5 5
22 | """
23 | )
24 |
25 | @[Test JsName("align_left")]
26 | fun `align left`() = doTest(
27 | LEFT, 15, """
28 | ░one_word ░
29 | ░ ░
30 | ░two words ░
31 | ░3 whole words ░
32 | ░four words 4 4 ░
33 | ░5 5 5 5 5 ░
34 | """
35 | )
36 |
37 | @[Test JsName("align_right")]
38 | fun `align right`() = doTest(
39 | RIGHT, 15, """
40 | ░ one_word░
41 | ░ ░
42 | ░ two words░
43 | ░ 3 whole words░
44 | ░ four words 4 4░
45 | ░ 5 5 5 5 5░
46 | """
47 | )
48 |
49 | @[Test JsName("align_center")]
50 | fun `align center`() = doTest(
51 | CENTER, 15, """
52 | ░ one_word ░
53 | ░ ░
54 | ░ two words ░
55 | ░ 3 whole words ░
56 | ░four words 4 4 ░
57 | ░ 5 5 5 5 5 ░
58 | """
59 | )
60 |
61 | @[Test JsName("align_justify")]
62 | fun `align justify`() = doTest(
63 | JUSTIFY, 15, """
64 | ░ one_word ░
65 | ░ ░
66 | ░two words░
67 | ░3 whole words░
68 | ░four words 4 4░
69 | ░5 5 5 5 5░
70 | """
71 | )
72 |
73 | @[Test JsName("align_justify_wide")]
74 | fun `align justify wide`() = doTest(
75 | JUSTIFY, 21, """
76 | ░ one_word ░
77 | ░ ░
78 | ░two words░
79 | ░3 whole words░
80 | ░four words 4 4░
81 | ░5 5 5 5 5░
82 | """
83 | )
84 |
85 | private fun doTest(align: TextAlign, width: Int, expected: String) {
86 | val ex = expected.trimMargin("░").lines().joinToString("\n") { (blue on white)(it) }
87 | val text = """
88 | ░one_word
89 | ░
90 | ░two words
91 | ░3 whole words
92 | ░four words 4 4
93 | ░5 5 5 5 5
94 | """.trimMargin("░")
95 | checkRender(
96 | Text(
97 | (blue on white)(text),
98 | whitespace = Whitespace.PRE_WRAP,
99 | align = align,
100 | ), ex, width = width
101 | )
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextOverflowWrapTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.rendering
2 |
3 | import com.github.ajalt.mordant.test.RenderingTest
4 | import com.github.ajalt.mordant.widgets.Text
5 | import kotlin.test.Test
6 |
7 |
8 | class TextOverflowWrapTest : RenderingTest() {
9 | @Test
10 | fun normal() = doTest(
11 | """
12 | ░The weather today is
13 | ░21°C in
14 | ░Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
15 | """, OverflowWrap.NORMAL
16 | )
17 |
18 | @Test
19 | fun break_word() = doTest(
20 | """
21 | ░The weather today is
22 | ░21°C in
23 | ░Llanfairpwllgwyngyllgog
24 | ░erychwyrndrobwllllantys
25 | ░iliogogogoch
26 | """, OverflowWrap.BREAK_WORD
27 | )
28 |
29 | @Test
30 | fun truncate() = doTest(
31 | """
32 | ░The weather today is
33 | ░21°C in
34 | ░Llanfairpwllgwyngyllgog
35 | """, OverflowWrap.TRUNCATE
36 | )
37 |
38 | @Test
39 | fun ellipses() = doTest(
40 | """
41 | ░The weather today is
42 | ░21°C in
43 | ░Llanfairpwllgwyngyllgo…
44 | """, OverflowWrap.ELLIPSES
45 | )
46 |
47 | @Test
48 | fun nowrap() = doTest(
49 | """
50 | ░The weather today is 21°C in Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
51 | """, OverflowWrap.ELLIPSES, Whitespace.NOWRAP
52 | )
53 |
54 | private fun doTest(
55 | expected: String,
56 | wrap: OverflowWrap,
57 | whitespace: Whitespace = Whitespace.NORMAL,
58 | width: Int = 23,
59 | ) {
60 | val text = """
61 | ░The weather today is 21°C in
62 | ░Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
63 | """.trimMargin("░")
64 | val widget = Text(
65 | text,
66 | whitespace = whitespace,
67 | align = TextAlign.NONE,
68 | overflowWrap = wrap
69 | )
70 | checkRender(widget, expected, width = width)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextStyleOscTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.rendering
2 |
3 | import com.github.ajalt.mordant.internal.OSC
4 | import com.github.ajalt.mordant.internal.ST
5 | import com.github.ajalt.mordant.rendering.TextStyles.Companion.hyperlink
6 | import com.github.ajalt.mordant.test.normalizeHyperlinks
7 | import io.kotest.matchers.shouldBe
8 | import kotlin.js.JsName
9 | import kotlin.test.Test
10 |
11 | class TextStyleOscTest {
12 | @[Test JsName("single_hyperlink")]
13 | fun `single hyperlink`() = doTest(
14 | hyperlink("foo.com")("bar"),
15 | "<;id=1;foo.com>bar<;;>"
16 | )
17 |
18 | @[Test JsName("nested_hyperlink")]
19 | fun `nested hyperlink`() = doTest(
20 | hyperlink("foo")("bar${hyperlink("baz")("qux")}qor"),
21 | "<;id=1;foo>bar<;id=2;baz>qux<;id=1;foo>qor<;;>"
22 | )
23 |
24 | private fun doTest(actual: String, expected: String) {
25 | val normalized = actual.replace("${OSC}8", "<").replace(ST, ">").normalizeHyperlinks()
26 | try {
27 | normalized shouldBe expected
28 | } catch (e: Throwable) {
29 | println(normalized)
30 | throw e
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextWhitespaceTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.rendering
2 |
3 | import com.github.ajalt.mordant.rendering.TextAlign.NONE
4 | import com.github.ajalt.mordant.rendering.Whitespace.*
5 | import com.github.ajalt.mordant.test.RenderingTest
6 | import com.github.ajalt.mordant.widgets.Text
7 | import kotlin.js.JsName
8 | import kotlin.test.Test
9 |
10 |
11 | class TextWhitespaceTest : RenderingTest() {
12 | @Test
13 | fun normal() = doTest(
14 | NORMAL, 18, """
15 | ░Lorem ipsum dolor░
16 | ░sit amet,░
17 | ░consectetur░
18 | ░adipiscing elit,
19 | ░sed░
20 | """
21 | )
22 |
23 | @Test
24 | fun nowrap() = doTest(
25 | NOWRAP, 18, """
26 | ░Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed░
27 | """
28 | )
29 |
30 | @Test
31 | fun pre() = doTest(
32 | PRE, 19, """
33 | ░Lorem ipsum dolor ░
34 | ░░
35 | ░sit amet, consectetur ░
36 | ░ adipiscing ░
37 | ░elit, sed░
38 | """
39 | )
40 |
41 | @Test
42 | fun pre_wrap() = doTest(
43 | PRE_WRAP, 19, """
44 | ░Lorem ipsum dolor░
45 | ░░
46 | ░sit amet,░
47 | ░consectetur░
48 | ░ adipiscing░
49 | ░elit, sed░
50 | """
51 | )
52 |
53 | @Test
54 | fun pre_line() = doTest(
55 | PRE_LINE, 19, """
56 | ░Lorem ipsum dolor░
57 | ░░
58 | ░sit amet,░
59 | ░consectetur░
60 | ░adipiscing░
61 | ░elit, sed░
62 | """
63 | )
64 |
65 | @[Test JsName("consecutive_whitespace_spans")]
66 | fun `consecutive whitespace spans`() {
67 | val line1 = Line(listOf("a", " ", " ").map { Span.word(it) })
68 | val line2 = Line(listOf(" ", "b").map { Span.word(it) })
69 | checkRender(
70 | Text(Lines(listOf(line1, line2)), whitespace = PRE_WRAP),
71 | """
72 | ░a
73 | ░ b
74 | """,
75 | width = 2
76 | )
77 | }
78 |
79 | private fun doTest(ws: Whitespace, width: Int, expected: String) {
80 | val text = """
81 | ░Lorem ipsum dolor ░
82 | ░░
83 | ░sit amet, consectetur ░
84 | ░␉adipiscing ░
85 | ░elit, sed░
86 | """.trimMargin("░").replace("░", "").replace("␉", "\t")
87 | checkRender(
88 | Text(
89 | text,
90 | whitespace = ws,
91 | align = NONE,
92 | tabWidth = 4
93 | ), expected, width = width
94 | )
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/ThemeTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.rendering
2 |
3 | import com.github.ajalt.mordant.internal.DEFAULT_STYLE
4 | import io.kotest.matchers.shouldBe
5 | import kotlin.test.Test
6 |
7 | class ThemeTest {
8 | private val theme = Theme(Theme.PlainAscii) {
9 | styles["s"] = TextColors.blue
10 | }
11 |
12 | @Test
13 | fun style() {
14 | theme.style("s") shouldBe TextColors.blue
15 | theme.style("foo") shouldBe DEFAULT_STYLE
16 | theme.style("foo", TextColors.blue) shouldBe TextColors.blue
17 | }
18 |
19 | @Test
20 | fun styleOrNull() {
21 | theme.styleOrNull("s") shouldBe TextColors.blue
22 | theme.styleOrNull("foo") shouldBe null
23 | }
24 |
25 | @Test
26 | fun flag() {
27 | theme.flag("markdown.code.block.border") shouldBe true
28 | theme.flag("foo") shouldBe false
29 | theme.flag("foo", true) shouldBe true
30 | }
31 |
32 | @Test
33 | fun flagOrNull() {
34 | theme.flagOrNull("markdown.code.block.border") shouldBe true
35 | theme.flagOrNull("foo") shouldBe null
36 | }
37 |
38 | @Test
39 | fun string() {
40 | theme.string("list.number.separator") shouldBe "."
41 | theme.string("foo") shouldBe ""
42 | theme.string("foo", "bar") shouldBe "bar"
43 | }
44 |
45 | @Test
46 | fun stringOrNull() {
47 | theme.stringOrNull("list.number.separator") shouldBe "."
48 | theme.stringOrNull("foo") shouldBe null
49 | }
50 |
51 | @Test
52 | fun dimension() {
53 | theme.dimension("hr.title.padding") shouldBe 1
54 | theme.dimension("foo") shouldBe 0
55 | theme.dimension("foo", -1) shouldBe -1
56 | }
57 |
58 | @Test
59 | fun dimensionOrNull() {
60 | theme.dimensionOrNull("hr.title.padding") shouldBe 1
61 | theme.dimensionOrNull("foo") shouldBe null
62 | }
63 |
64 | @Test
65 | fun plus() {
66 | val l = Theme {
67 | dimensions["foo"] = 11
68 | dimensions["bar"] = 22
69 | }
70 | val r = Theme {
71 | dimensions["bar"] = 23
72 | dimensions["baz"] = 33
73 | }
74 | val t = l + r
75 | t.dimension("foo") shouldBe 11
76 | t.dimension("bar") shouldBe 23
77 | t.dimension("baz") shouldBe 33
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/internal/CellWidthTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.rendering.internal
2 |
3 | import com.github.ajalt.mordant.internal.cellWidth
4 | import com.github.ajalt.mordant.internal.codepointSequence
5 | import com.github.ajalt.mordant.internal.stringCellWidth
6 | import io.kotest.data.blocking.forAll
7 | import io.kotest.data.row
8 | import io.kotest.matchers.shouldBe
9 | import kotlin.test.Test
10 |
11 |
12 | internal class CellWidthTest {
13 |
14 | @Test
15 | fun cellWidth() = forAll(
16 | row(" ", 1),
17 | row("a", 1),
18 | row("\n", 0),
19 | row("\u0000", 0), // NUL
20 | row("\u001b", 0), // ESC
21 | row("\u1885", 0), // MONGOLIAN LETTER ALI GALI BALUDA (combining)
22 | row("\u007f", -1), // DEL
23 | row("\u0008", -1), // BS
24 | row("가", 2), // HANGUL SYLLABLE GA
25 | row("ぁ", 2), // HIRAGANA LETTER SMALL A
26 | row("💯", 2), // HUNDRED POINTS SYMBOL
27 | ) { char, width ->
28 | cellWidth(codepointSequence(char).single()) shouldBe width
29 | }
30 |
31 | @Test
32 | fun stringCellWidth() = forAll(
33 | row("", 0),
34 | row("a", 1),
35 | row("\n", 0),
36 | row("1\u007F1", 1),
37 | row("모ㄹ단ㅌ", 8),
38 | row("媒人", 4),
39 | row("🙊🙉🙈", 6),
40 | row("en\u0303e", 3),
41 | row("👍🏿", 2),
42 | row("🇩🇪", 2),
43 | row(
44 | "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDB1",
45 | 2
46 | ), // MAN, FITZPATRICK TYPE-5, ZWJ, CURLY HAIR
47 | row(
48 | "\uD83D\uDC69\u200D\uD83D\uDC67",
49 | 2
50 | ), // Emoji_ZWJ_Sequence ; family: woman, girl (👩👧)
51 | row(
52 | "\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66",
53 | 2
54 | ), //Emoji_ZWJ_Sequence ; family: woman, girl, boy (👩👧👦)
55 | row(
56 | "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66",
57 | 2
58 | ), // Emoji_ZWJ_Sequence ; family: woman, woman, boy, boy (👩👩👦👦)
59 |
60 | ) { str, width ->
61 | stringCellWidth(str) shouldBe width
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableBorderTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.table
2 |
3 | import com.github.ajalt.mordant.table.Borders.*
4 | import com.github.ajalt.mordant.test.RenderingTest
5 | import kotlin.test.Test
6 |
7 | @Suppress("TestFunctionName")
8 | class TableBorderTest : RenderingTest() {
9 | @Test
10 | fun NONE() = doTest(
11 | NONE,
12 | """
13 | ░ ×
14 | """
15 | )
16 |
17 | @Test
18 | fun BOTTOM() = doTest(
19 | BOTTOM,
20 | """
21 | ░ ×
22 | ░───
23 | """
24 | )
25 |
26 | @Test
27 | fun RIGHT() = doTest(
28 | RIGHT,
29 | """
30 | ░ × │
31 | """
32 | )
33 |
34 | @Test
35 | fun BOTTOM_RIGHT() = doTest(
36 | BOTTOM_RIGHT,
37 | """
38 | ░ × │
39 | ░───┘
40 | """
41 | )
42 |
43 | @Test
44 | fun TOP() = doTest(
45 | TOP,
46 | """
47 | ░───
48 | ░ ×
49 | """
50 | )
51 |
52 | @Test
53 | fun TOP_BOTTOM() = doTest(
54 | TOP_BOTTOM,
55 | """
56 | ░───
57 | ░ ×
58 | ░───
59 | """
60 | )
61 |
62 | @Test
63 | fun TOP_RIGHT() = doTest(
64 | TOP_RIGHT,
65 | """
66 | ░───┐
67 | ░ × │
68 | """
69 | )
70 |
71 | @Test
72 | fun TOP_RIGHT_BOTTOM() = doTest(
73 | TOP_RIGHT_BOTTOM,
74 | """
75 | ░───┐
76 | ░ × │
77 | ░───┘
78 | """
79 | )
80 |
81 | @Test
82 | fun LEFT() = doTest(
83 | LEFT,
84 | """
85 | ░│ ×
86 | """
87 | )
88 |
89 | @Test
90 | fun LEFT_BOTTOM() = doTest(
91 | LEFT_BOTTOM,
92 | """
93 | ░│ ×
94 | ░└───
95 | """
96 | )
97 |
98 | @Test
99 | fun LEFT_RIGHT() = doTest(
100 | LEFT_RIGHT,
101 | """
102 | ░│ × │
103 | """
104 | )
105 |
106 | @Test
107 | fun LEFT_RIGHT_BOTTOM() = doTest(
108 | LEFT_RIGHT_BOTTOM,
109 | """
110 | ░│ × │
111 | ░└───┘
112 | """
113 | )
114 |
115 | @Test
116 | fun LEFT_TOP() = doTest(
117 | LEFT_TOP,
118 | """
119 | ░┌───
120 | ░│ ×
121 | """
122 | )
123 |
124 | @Test
125 | fun LEFT_TOP_BOTTOM() = doTest(
126 | LEFT_TOP_BOTTOM,
127 | """
128 | ░┌───
129 | ░│ ×
130 | ░└───
131 | """
132 | )
133 |
134 | @Test
135 | fun LEFT_TOP_RIGHT() = doTest(
136 | LEFT_TOP_RIGHT,
137 | """
138 | ░┌───┐
139 | ░│ × │
140 | """
141 | )
142 |
143 | @Test
144 | fun ALL() = doTest(
145 | ALL,
146 | """
147 | ░┌───┐
148 | ░│ × │
149 | ░└───┘
150 | """
151 | )
152 |
153 | private fun doTest(borders: Borders, expected: String) = checkRender(table {
154 | this.cellBorders = borders
155 | body {
156 | row("×")
157 | }
158 | }, expected)
159 | }
160 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/terminal/HtmlRendererTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal
2 |
3 | import com.github.ajalt.mordant.rendering.TextColors.blue
4 | import com.github.ajalt.mordant.rendering.TextColors.red
5 | import io.kotest.matchers.shouldBe
6 | import kotlin.js.JsName
7 | import kotlin.test.Test
8 |
9 | class HtmlRendererTest {
10 | private val vt = TerminalRecorder()
11 | private val t = Terminal(terminalInterface = vt)
12 |
13 | init {
14 | t.print(red("red red"))
15 | t.println("plain")
16 | t.print(blue("blue blue"))
17 | }
18 |
19 | @[Test JsName("no_frame")]
20 | fun `no frame`() {
21 | vt.outputAsHtml(backgroundColor = null) shouldBe """
22 | |
23 | |
24 | |red red plain
25 | |blue blue
26 | |
27 | |
28 | """.trimMargin()
29 | }
30 |
31 | @[Test JsName("frame_no_body_tag")]
32 | fun `frame no body tag`() {
33 | vt.outputAsHtml(includeCodeTag = false, includeBodyTag = false) shouldBe """
34 | |⏺ ⏺ ⏺
35 | |
36 | |red red plain
37 | |blue blue
38 | |
39 | |
40 | """.trimMargin()
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/terminal/TerminalTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal
2 |
3 | import com.github.ajalt.mordant.rendering.TextAlign
4 | import com.github.ajalt.mordant.rendering.TextColors.cyan
5 | import com.github.ajalt.mordant.rendering.Whitespace
6 | import io.kotest.data.blocking.forAll
7 | import io.kotest.data.row
8 | import io.kotest.matchers.shouldBe
9 | import kotlin.js.JsName
10 | import kotlin.test.Test
11 |
12 | class TerminalTest {
13 | private val vt = TerminalRecorder(width = 8)
14 | private val t = Terminal(terminalInterface = vt)
15 |
16 | @Test
17 | fun success() {
18 | t.success("success")
19 | vt.output() shouldBe t.theme.success("success") + "\n"
20 | }
21 |
22 | @Test
23 | fun danger() {
24 | t.danger("danger")
25 | vt.output() shouldBe t.theme.danger("danger") + "\n"
26 | }
27 |
28 | @Test
29 | fun warning() {
30 | t.warning("warning")
31 | vt.output() shouldBe t.theme.warning("warning") + "\n"
32 | }
33 |
34 | @Test
35 | fun info() {
36 | t.info("info")
37 | vt.output() shouldBe t.theme.info("info") + "\n"
38 | }
39 |
40 | @Test
41 | fun muted() {
42 | t.muted("muted")
43 | vt.output() shouldBe t.theme.muted("muted") + "\n"
44 | }
45 |
46 | @Test
47 | fun print() {
48 | t.print("1")
49 | t.print("2", stderr = true)
50 | t.print("3")
51 | vt.stdout() shouldBe "13"
52 | vt.stderr() shouldBe "2"
53 | vt.output() shouldBe "123"
54 | }
55 |
56 | @Test
57 | fun println() {
58 | t.println("1")
59 | t.println("2", stderr = true)
60 | t.println("3")
61 | vt.stdout() shouldBe "1\n3\n"
62 | vt.stderr() shouldBe "2\n"
63 | vt.output() shouldBe "1\n2\n3\n"
64 | }
65 |
66 | @Test
67 | fun rawPrint() {
68 | t.rawPrint(t.cursor.getMoves { left(1) })
69 | t.rawPrint(t.cursor.getMoves { up(1) }, stderr = true)
70 | t.rawPrint(t.cursor.getMoves { right(1) })
71 | vt.stdout() shouldBe t.cursor.getMoves { left(1); right(1) }
72 | vt.stderr() shouldBe t.cursor.getMoves { up(1) }
73 | vt.output() shouldBe t.cursor.getMoves { left(1); up(1); right(1) }
74 |
75 | vt.clearOutput()
76 | t.rawPrint("\t")
77 | vt.output() shouldBe "\t"
78 | }
79 |
80 | @[Test JsName("print_customized")]
81 | fun `print customized`() {
82 | t.print(cyan("print with a wrap"), whitespace = Whitespace.NORMAL, align = TextAlign.RIGHT)
83 | vt.output() shouldBe """
84 | |${cyan(" print")}
85 | |${cyan(" with a")}
86 | |${cyan(" wrap")}
87 | """.trimMargin()
88 | }
89 |
90 | @[Test JsName("width_override")]
91 | fun `width override`() = forAll(
92 | row(1, 2, true, 1),
93 | row(1, 2, false, 2),
94 | row(null, 2, true, 3),
95 | row(null, 2, false, 2),
96 | row(1, null, true, 1),
97 | row(1, null, false, 1),
98 | row(null, null, true, 3),
99 | ) { width, niWidth, interactive, expected ->
100 | val vt = TerminalRecorder(width = 3, outputInteractive = interactive)
101 | val t = Terminal(
102 | terminalInterface = vt,
103 | width = width,
104 | nonInteractiveWidth = niWidth
105 | )
106 | t.size.width shouldBe expected
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/RenderingTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.test
2 |
3 | import com.github.ajalt.mordant.rendering.AnsiLevel
4 | import com.github.ajalt.mordant.rendering.Theme
5 | import com.github.ajalt.mordant.rendering.Widget
6 | import com.github.ajalt.mordant.terminal.Terminal
7 |
8 | abstract class RenderingTest(
9 | private val width: Int = 79,
10 | ) {
11 | protected fun checkRender(
12 | widget: Widget,
13 | expected: String,
14 | trimMargin: Boolean = true,
15 | width: Int = this.width,
16 | height: Int = 24,
17 | tabWidth: Int = 8,
18 | hyperlinks: Boolean = true,
19 | theme: Theme = Theme.Default,
20 | transformActual: (String) -> String = { it },
21 | ) {
22 | val t = Terminal(
23 | ansiLevel = AnsiLevel.TRUECOLOR,
24 | theme = theme,
25 | width = width,
26 | height = height,
27 | hyperlinks = hyperlinks,
28 | tabWidth = tabWidth
29 | )
30 | val actual = transformActual(t.render(widget))
31 | actual.shouldMatchRender(expected, trimMargin)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/TestUtils.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.test
2 |
3 | import com.github.ajalt.mordant.internal.CR_IMPLIES_LF
4 | import com.github.ajalt.mordant.internal.CSI
5 | import com.github.ajalt.mordant.terminal.TerminalRecorder
6 | import io.kotest.matchers.shouldBe
7 |
8 | fun String.normalizeHyperlinks(): String {
9 | var i = 1
10 | val regex = Regex(";id=([^;]+);")
11 | val map = mutableMapOf()
12 | regex.findAll(this).forEach { map.getOrPut(it.value) { i++ } }
13 | return regex.replace(this) { ";id=${map[it.value]};" }
14 | }
15 |
16 | fun String.visibleCrLf(keepBreaks: Boolean = false): String {
17 | return replace("\r", "␍").replace("\n", if (keepBreaks) "\n" else "␊").replace(CSI, "␛")
18 | }
19 |
20 | private val upMove = Regex("${Regex.escape(CSI)}\\d+A")
21 |
22 | // This handles the difference in wasm movements and the other targets
23 | fun TerminalRecorder.normalizedOutput(): String {
24 | return if (CR_IMPLIES_LF) output().replace("\r${CSI}1A", "\r") else output()
25 | }
26 |
27 | fun TerminalRecorder.latestOutput(): String {
28 | return normalizedOutput()
29 | // remove everything before the last cursor movement
30 | .let { it.split(upMove).lastOrNull() ?: it }.substringAfter("\r")
31 | .replace("${CSI}0J", "") // remove clear screen command
32 | }
33 |
34 | infix fun String.shouldMatchRender(expected: String) = shouldMatchRender(expected, true)
35 |
36 | fun String.shouldMatchRender(expected: String, trimMargin: Boolean) {
37 | try {
38 | val trimmed = if (trimMargin) expected.trimMargin("░") else expected
39 | this shouldBe trimmed.replace("░", "")
40 | } catch (e: Throwable) {
41 | println()
42 | println(this)
43 | throw e
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/HorizontalRuleTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.widgets
2 |
3 | import com.github.ajalt.mordant.rendering.TextAlign
4 | import com.github.ajalt.mordant.rendering.TextColors.blue
5 | import com.github.ajalt.mordant.rendering.TextColors.red
6 | import com.github.ajalt.mordant.rendering.Theme
7 | import com.github.ajalt.mordant.rendering.Whitespace
8 | import com.github.ajalt.mordant.test.RenderingTest
9 | import kotlin.js.JsName
10 | import kotlin.test.Test
11 |
12 | class HorizontalRuleTest : RenderingTest() {
13 | @[Test JsName("no_title")]
14 | fun `no title`() {
15 | checkRender(HorizontalRule(), "──────────", width = 10)
16 | }
17 |
18 | @[Test JsName("multiple_character_rules")]
19 | fun `multiple character rules`() {
20 | checkRender(
21 | HorizontalRule(title = "title", ruleCharacter = "1234"),
22 | "123412 title 1234123",
23 | width = 20
24 | )
25 | }
26 |
27 | @[Test JsName("rule_with_whitespace")]
28 | fun `rule with whitespace`() {
29 | checkRender(HorizontalRule(ruleCharacter = "- -"), "- -- -", width = 6)
30 | }
31 |
32 | @[Test JsName("title_align_left")]
33 | fun `title align left`() {
34 | checkRender(
35 | HorizontalRule("title", titleAlign = TextAlign.LEFT),
36 | "─ title ────",
37 | width = 12
38 | )
39 | }
40 |
41 | @[Test JsName("title_align_right")]
42 | fun `title align right`() {
43 | checkRender(
44 | HorizontalRule("title", titleAlign = TextAlign.RIGHT),
45 | "──── title ─",
46 | width = 12
47 | )
48 | }
49 |
50 | @[Test JsName("multiline_title")]
51 | fun `multiline title`() {
52 | checkRender(
53 | HorizontalRule(
54 | title = Text(
55 | "Multiline\nHeader Text",
56 | whitespace = Whitespace.PRE_WRAP
57 | )
58 | ),
59 | """
60 | ░ Multiline ░
61 | ░─── Header Text ───░
62 | """,
63 | width = 19
64 | )
65 | }
66 |
67 | @[Test JsName("styled_title_and_rule")]
68 | fun `styled title and rule`() {
69 | checkRender(
70 | HorizontalRule(title = blue("title"), ruleStyle = blue),
71 | blue("─── title ───"), width = 13
72 | )
73 | }
74 |
75 | @[Test JsName("themed_title_and_rule")]
76 | fun `themed title and rule`() {
77 | checkRender(
78 | HorizontalRule(title = blue("title")),
79 | red("── ${blue("title")} ──"),
80 | width = 13,
81 | theme = Theme {
82 | styles["hr.rule"] = red
83 | dimensions["hr.title.padding"] = 2
84 | }
85 | )
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/OrderedListTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.widgets
2 |
3 | import com.github.ajalt.mordant.test.RenderingTest
4 | import kotlin.js.JsName
5 | import kotlin.test.Test
6 |
7 | class OrderedListTest : RenderingTest() {
8 | @[Test JsName("vararg_string_constructor")]
9 | fun `vararg string constructor`() = checkRender(
10 | OrderedList("one", "two", "three"),
11 | """
12 | ░ 1. one
13 | ░ 2. two
14 | ░ 3. three
15 | """
16 | )
17 |
18 | @[Test JsName("vararg_widget_constructor")]
19 | fun `vararg widget constructor`() = checkRender(
20 | OrderedList(Text("one"), Text("two"), Text("three")),
21 | """
22 | ░ 1. one
23 | ░ 2. two
24 | ░ 3. three
25 | """
26 | )
27 |
28 | @[Test JsName("empty_list")]
29 | fun `empty list`() = checkRender(
30 | OrderedList(emptyList()),
31 | ""
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/PaddingTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.widgets
2 |
3 | import com.github.ajalt.mordant.test.RenderingTest
4 | import kotlin.test.Test
5 |
6 | class PaddingTest : RenderingTest() {
7 | private val w = Text("x")
8 |
9 | @Test
10 | fun all() = checkRender(
11 | w.withPadding(1), """
12 | ░░
13 | ░ x ░
14 | ░░
15 | """
16 | )
17 |
18 | @Test
19 | fun fourValues() = checkRender(
20 | w.withPadding(1, 2, 3, 4), """
21 | ░░
22 | ░ x ░
23 | ░░
24 | ░░
25 | ░░
26 | """
27 | )
28 |
29 | @Test
30 | fun vertical() = checkRender(
31 | w.withPadding { vertical = 1 }, """
32 | ░░
33 | ░x░
34 | ░░
35 | """
36 | )
37 |
38 | @Test
39 | fun horizontal() = checkRender(
40 | w.withPadding { horizontal = 1 }, """
41 | ░ x ░
42 | """
43 | )
44 |
45 | @Test
46 | fun padEmpty() = checkRender(
47 | Text("x\n\ny").withPadding(1, padEmptyLines = true), """
48 | ░░
49 | ░ x ░
50 | ░ ░
51 | ░ y ░
52 | ░░
53 | """
54 | )
55 |
56 | @Test
57 | fun noPadEmpty() = checkRender(
58 | Text("x\n\ny").withPadding(1, padEmptyLines = false), """
59 | ░░
60 | ░ x ░
61 | ░░
62 | ░ y ░
63 | ░░
64 | """
65 | )
66 |
67 | @Test
68 | fun withTopPadding() = checkRender(
69 | w.withPadding { top = 1 }, """
70 | ░░
71 | ░x░
72 | """
73 | )
74 |
75 | @Test
76 | fun withRightPadding() = checkRender(
77 | w.withPadding { right = 1 }, """
78 | ░x ░
79 | """
80 | )
81 |
82 | @Test
83 | fun withBottomPadding() = checkRender(
84 | w.withPadding { bottom = 1 }, """
85 | ░x░
86 | ░░
87 | """
88 | )
89 |
90 | @Test
91 | fun withLeftPadding() = checkRender(
92 | w.withPadding { left = 1 }, """
93 | ░ x░
94 | """
95 | )
96 | }
97 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/ProgressBarTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.widgets
2 |
3 | import com.github.ajalt.mordant.internal.CSI
4 | import com.github.ajalt.mordant.rendering.Theme
5 | import com.github.ajalt.mordant.test.RenderingTest
6 | import kotlin.js.JsName
7 | import kotlin.test.Test
8 |
9 | class ProgressBarTest : RenderingTest() {
10 | @[Test JsName("_0_percent_complete")]
11 | fun `0 percent complete`() = doPercentTest(0, " ")
12 |
13 | @[Test JsName("_10_percent_complete")]
14 | fun `10 percent complete`() = doPercentTest(10, " ")
15 |
16 | @[Test JsName("_20_percent_complete")]
17 | fun `20 percent complete`() = doPercentTest(20, "#> ")
18 |
19 | @[Test JsName("_30_percent_complete")]
20 | fun `30 percent complete`() = doPercentTest(30, "#> ")
21 |
22 | @[Test JsName("_40_percent_complete")]
23 | fun `40 percent complete`() = doPercentTest(40, "##> ")
24 |
25 | @[Test JsName("_60_percent_complete")]
26 | fun `60 percent complete`() = doPercentTest(60, "###> ")
27 |
28 | @[Test JsName("_80_percent_complete")]
29 | fun `80 percent complete`() = doPercentTest(80, "####>")
30 |
31 | @[Test JsName("_99_percent_complete")]
32 | fun `99 percent complete`() = doPercentTest(99, "####>")
33 |
34 | @[Test JsName("_100_percent_complete")]
35 | fun `100 percent complete`() = doPercentTest(100, "#####")
36 |
37 | @[Test JsName("default_theme")]
38 | fun `default theme`() = doPercentTest(
39 | 40,
40 | "${CSI}38;2;97;175;239m━━${CSI}39m ${CSI}38;2;92;99;112m━━${CSI}39m",
41 | theme = Theme.Default
42 | )
43 |
44 | @[Test JsName("pulse_initial")]
45 | fun `pulse initial`() = doPulseTest(
46 | pulsePosition = 0f,
47 | "${CSI}38;2;97;175;239m━━━━━━━━━━${CSI}39m"
48 | )
49 |
50 | @[Test JsName("pulse_25")]
51 | fun `pulse 25`() = doPulseTest(
52 | pulsePosition = .25f,
53 | "${CSI}38;2;97;175;239m━${CSI}38;2;251;253;255m━${CSI}38;2;207;230;251m━${CSI}38;2;107;180;240m━${CSI}38;2;97;175;239m━━━━━━${CSI}39m"
54 | )
55 |
56 | @[Test JsName("pulse_50")]
57 | fun `pulse 50`() = doPulseTest(
58 | pulsePosition = .50f,
59 | "${CSI}38;2;97;175;239m━━━${CSI}38;2;136;194;244m━${CSI}38;2;239;247;254m━━${CSI}38;2;136;194;244m━${CSI}38;2;97;175;239m━━━${CSI}39m"
60 | )
61 |
62 | @[Test JsName("pulse_75")]
63 | fun `pulse 75`() = doPulseTest(
64 | pulsePosition = .75f,
65 | "${CSI}38;2;97;175;239m━━━━━━${CSI}38;2;107;180;240m━${CSI}38;2;207;230;251m━${CSI}38;2;254;255;255m━${CSI}38;2;178;216;249m━${CSI}39m"
66 | )
67 |
68 | @[Test JsName("pulse_100")]
69 | fun `pulse 100`() = doPulseTest(
70 | pulsePosition = 1f,
71 | "${CSI}38;2;97;175;239m━━━━━━━━━━${CSI}39m"
72 | )
73 |
74 | @[Test JsName("narrow_width")]
75 | fun `narrow width`() = doPulseTest(
76 | pulsePosition = 1f,
77 | "${CSI}38;2;97;175;239m━${CSI}39m",
78 | width = 1
79 | )
80 |
81 | private fun doPulseTest(pulsePosition: Float, expected: String, width: Int = 10) {
82 | checkRender(
83 | ProgressBar(indeterminate = true, pulsePosition = pulsePosition),
84 | expected,
85 | width = width
86 | )
87 | }
88 |
89 | private fun doPercentTest(completed: Long, expected: String, theme: Theme = Theme.PlainAscii) {
90 | checkRender(
91 | ProgressBar(completed = completed),
92 | expected,
93 | theme = theme,
94 | width = 5,
95 | trimMargin = false,
96 | )
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/SpinnerTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.widgets
2 |
3 | import com.github.ajalt.mordant.rendering.TextColors.red
4 | import com.github.ajalt.mordant.test.RenderingTest
5 | import io.kotest.matchers.shouldBe
6 | import kotlin.test.Test
7 |
8 | class SpinnerTest : RenderingTest() {
9 | @Test
10 | fun dots() = doTest(Spinner.Dots(), "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
11 |
12 | @Test
13 | fun line() = doTest(Spinner.Lines(), "|", "/", "-", "\\")
14 |
15 | @Test
16 | fun custom() = doTest(Spinner("ab"), "a", "b", "a", "b")
17 |
18 | @Test
19 | fun duration() = doTest(Spinner("12", duration = 2), "1", "1", "2", "2", "1", "1", "2", "2")
20 |
21 | @Test
22 | fun lineStyle() = doTest(Spinner.Lines(red), red("|"), red("/"), red("-"), red("\\"))
23 |
24 | @Test
25 | fun initialTick() = doTest(Spinner.Lines(initial = 2), "-", "\\", "|", "/")
26 |
27 | private fun doTest(
28 | spinner: Spinner,
29 | vararg expected: String,
30 | ) {
31 | for (ex in expected) {
32 | checkRender(spinner, ex, trimMargin = false)
33 | val t = spinner.tick
34 | spinner.advanceTick() shouldBe (t + 1)
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/UnorderedListTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.widgets
2 |
3 | import com.github.ajalt.mordant.test.RenderingTest
4 | import kotlin.js.JsName
5 | import kotlin.test.Test
6 |
7 | class UnorderedListTest : RenderingTest() {
8 | @[Test JsName("vararg_string_constructor")]
9 | fun `vararg string constructor`() = checkRender(
10 | UnorderedList("one", "two", "three"),
11 | """
12 | ░ • one
13 | ░ • two
14 | ░ • three
15 | """
16 | )
17 |
18 | @[Test JsName("vararg_widget_constructor")]
19 | fun `vararg widget constructor`() = checkRender(
20 | UnorderedList(Text("one"), Text("two"), Text("three")),
21 | """
22 | ░ • one
23 | ░ • two
24 | ░ • three
25 | """
26 | )
27 |
28 | @[Test JsName("empty_list")]
29 | fun `empty list`() = checkRender(
30 | UnorderedList(emptyList()),
31 | ""
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/ViewportTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.widgets
2 |
3 | import com.github.ajalt.mordant.rendering.TextColors.red
4 | import com.github.ajalt.mordant.test.RenderingTest
5 | import io.kotest.data.blocking.forAll
6 | import io.kotest.data.row
7 | import kotlin.js.JsName
8 | import kotlin.test.Test
9 |
10 | class ViewportTest : RenderingTest(width = 20) {
11 | @Test
12 | fun crop() = forAll(
13 | row(null, null, "a ␊b c"),
14 | row(null, 1, "a "),
15 | row(1, 1, "a"),
16 | row(2, 1, "a "),
17 | row(1, null, "a␊b"),
18 | row(2, null, "a ␊b "),
19 | row(3, null, "a ␊b c"),
20 | row(2, 2, "a ␊b "),
21 | row(4, 3, "a ␊b c ␊ "),
22 | row(0, 0, ""),
23 | ) { w, h, ex ->
24 | doTest(w, h, 0, 0, ex, "a\nb c")
25 | }
26 |
27 | @Test
28 | fun scroll() = forAll(
29 | row(0, 0, "a ␊b c"),
30 | row(1, 0, " ␊ c "),
31 | row(2, 0, " ␊c "),
32 | row(3, 0, " ␊ "),
33 | row(4, 0, " ␊ "),
34 | row(0, 1, "b c␊ "),
35 | row(0, 2, " ␊ "),
36 | row(1, 1, " c ␊ "),
37 | row(9, 9, " ␊ "),
38 | row(-1, 0, " a ␊ b "),
39 | row(-2, 0, " a␊ b"),
40 | row(-3, 0, " ␊ "),
41 | row(0, -1, " ␊a "),
42 | row(0, -2, " ␊ "),
43 | row(-9, -9, " ␊ "),
44 | ) { x, y, ex ->
45 | doTest(null, null, x, y, ex, "a\nb c")
46 | }
47 |
48 | @[Test JsName("scrolling_splits_span")]
49 | fun `scrolling splits span`() {
50 | doTest(1, 1, 2, 0, red("Z"), "X${red("YZ")}")
51 | doTest(2, 1, 1, 0, "56", "4567")
52 | }
53 |
54 | private fun doTest(w: Int?, h: Int?, x: Int, y: Int, ex: String, txt: String) {
55 | checkRender(Viewport(Text(txt), w, h, x, y), ex, trimMargin = false) {
56 | it.replace('\n', '␊')
57 | }
58 | }
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/mordant/src/commonTest/resources/multiplatform_system_test.txt:
--------------------------------------------------------------------------------
1 | pass
2 |
--------------------------------------------------------------------------------
/mordant/src/iosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.ios.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | import com.github.ajalt.mordant.terminal.TerminalInterface
4 | import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceNativeCopyPasted
5 |
6 | internal actual fun getStandardTerminalInterface(): TerminalInterface {
7 | return TerminalInterfaceNativeCopyPasted()
8 | }
9 |
--------------------------------------------------------------------------------
/mordant/src/iosMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.ios.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal.terminalinterface
2 |
3 | import com.github.ajalt.mordant.rendering.Size
4 | import kotlinx.cinterop.*
5 | import platform.posix.*
6 |
7 | // XXX: The source code for this file is identical between linux and the various apple targets, but
8 | // they have different bit widths for some fields, so the compileMetadata task fails if we don't use
9 | // separate files. Hopefully some day there will be solution that doesn't require copy-pasting.
10 |
11 | internal class TerminalInterfaceNativeCopyPasted : TerminalInterfaceNativePosix() {
12 | override val termiosConstants: TermiosConstants = TermiosConstants(
13 | VTIME = VTIME,
14 | VMIN = VMIN,
15 | INPCK = INPCK.convert(),
16 | ISTRIP = ISTRIP.convert(),
17 | INLCR = INLCR.convert(),
18 | IGNCR = IGNCR.convert(),
19 | ICRNL = ICRNL.convert(),
20 | IXON = IXON.convert(),
21 | OPOST = OPOST.convert(),
22 | CS8 = CS8.convert(),
23 | ISIG = ISIG.convert(),
24 | ICANON = ICANON.convert(),
25 | ECHO = ECHO.convert(),
26 | IEXTEN = IEXTEN.convert(),
27 | )
28 |
29 | override fun readIntoBuffer(c: ByteVar): Long {
30 | return read(platform.posix.STDIN_FILENO, c.ptr, 1u).convert()
31 | }
32 |
33 | override fun getTerminalSize(): Size? = memScoped {
34 | val size = alloc()
35 | if (ioctl(STDIN_FILENO, TIOCGWINSZ.convert(), size) < 0) {
36 | null
37 | } else {
38 | Size(width = size.ws_col.toInt(), height = size.ws_row.toInt())
39 | }
40 | }
41 |
42 | override fun getStdinTermios(): Termios = memScoped {
43 | val termios = alloc()
44 | if (tcgetattr(platform.posix.STDIN_FILENO, termios.ptr) != 0) {
45 | throw RuntimeException("Error reading terminal attributes")
46 | }
47 | return Termios(
48 | iflag = termios.c_iflag.convert(),
49 | oflag = termios.c_oflag.convert(),
50 | cflag = termios.c_cflag.convert(),
51 | lflag = termios.c_lflag.convert(),
52 | cc = ByteArray(NCCS) { termios.c_cc[it].convert() },
53 | )
54 | }
55 |
56 | override fun setStdinTermios(termios: Termios) = memScoped {
57 | val nativeTermios = alloc()
58 | // different platforms have different fields in termios, so we need to read the current
59 | // struct before we set the fields we care about.
60 | if (tcgetattr(platform.posix.STDIN_FILENO, nativeTermios.ptr) != 0) {
61 | throw RuntimeException("Error reading terminal attributes")
62 | }
63 | nativeTermios.c_iflag = termios.iflag.convert()
64 | nativeTermios.c_oflag = termios.oflag.convert()
65 | nativeTermios.c_cflag = termios.cflag.convert()
66 | nativeTermios.c_lflag = termios.lflag.convert()
67 | repeat(NCCS) { nativeTermios.c_cc[it] = termios.cc[it].convert() }
68 | if (tcsetattr(platform.posix.STDIN_FILENO, TCSADRAIN, nativeTermios.ptr) != 0) {
69 | throw RuntimeException("Error setting terminal attributes")
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/mordant/src/jsCommonMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.jsCommon.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | import com.github.ajalt.mordant.terminal.*
4 | import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceBrowser
5 | import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceJsCommon
6 |
7 |
8 | // Since `js()` and `external` work differently in wasm and js, we need to define the functions that
9 | // use them twice
10 | internal expect fun makeNodeTerminalInterface(): TerminalInterfaceJsCommon?
11 | internal expect fun browserPrintln(message: String)
12 |
13 | private class JsAtomicRef(override var value: T) : MppAtomicRef {
14 | override fun compareAndSet(expected: T, newValue: T): Boolean {
15 | if (value != expected) return false
16 | value = newValue
17 | return true
18 | }
19 |
20 | override fun getAndSet(newValue: T): T {
21 | val old = value
22 | value = newValue
23 | return old
24 | }
25 | }
26 |
27 | private class JsAtomicInt(initial: Int) : MppAtomicInt {
28 | private var backing = initial
29 | override fun getAndIncrement(): Int {
30 | return backing++
31 | }
32 |
33 | override fun get(): Int {
34 | return backing
35 | }
36 |
37 | override fun set(value: Int) {
38 | backing = value
39 | }
40 | }
41 |
42 | internal actual fun MppAtomicInt(initial: Int): MppAtomicInt = JsAtomicInt(initial)
43 | internal actual fun MppAtomicRef(value: T): MppAtomicRef = JsAtomicRef(value)
44 |
45 |
46 | internal actual fun getStandardTerminalInterface(): TerminalInterface {
47 | return makeNodeTerminalInterface() ?: TerminalInterfaceBrowser
48 | }
49 |
50 | private val impls get() = STANDARD_TERM_INTERFACE as TerminalInterfaceJsCommon
51 |
52 | internal actual fun runningInIdeaJavaAgent(): Boolean = false
53 |
54 | internal actual fun getEnv(key: String): String? = impls.readEnvvar(key)
55 | internal actual fun printStderr(message: String, newline: Boolean) {
56 | impls.printStderr(message, newline)
57 | }
58 |
59 | internal actual fun exitProcessMpp(status: Int): Unit = impls.exitProcess(status)
60 |
61 | // hideInput is not currently implemented
62 | internal actual fun readLineOrNullMpp(hideInput: Boolean): String? = impls.readLineOrNull()
63 |
64 |
65 | internal actual fun makePrintingTerminalCursor(terminal: Terminal): TerminalCursor {
66 | return impls.makeTerminalCursor(terminal)
67 | }
68 |
69 | internal actual fun readFileIfExists(filename: String): String? = impls.readFileIfExists(filename)
70 |
71 |
72 | internal actual fun sendInterceptedPrintRequest(
73 | request: PrintRequest,
74 | terminalInterface: TerminalInterface,
75 | interceptors: List,
76 | ) {
77 | terminalInterface.completePrintRequest(
78 | interceptors.fold(request) { acc, it -> it.intercept(acc) }
79 | )
80 | }
81 |
82 | internal actual fun testsHaveFileSystem(): Boolean = impls !is TerminalInterfaceBrowser
83 |
--------------------------------------------------------------------------------
/mordant/src/jsCommonMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.jsCommon.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal.terminalinterface
2 |
3 | import com.github.ajalt.mordant.input.InputEvent
4 | import com.github.ajalt.mordant.input.MouseTracking
5 | import com.github.ajalt.mordant.internal.browserPrintln
6 | import com.github.ajalt.mordant.rendering.Size
7 | import com.github.ajalt.mordant.terminal.PrintTerminalCursor
8 | import com.github.ajalt.mordant.terminal.StandardTerminalInterface
9 | import com.github.ajalt.mordant.terminal.Terminal
10 | import com.github.ajalt.mordant.terminal.TerminalCursor
11 | import kotlin.time.TimeMark
12 |
13 | internal abstract class TerminalInterfaceJsCommon : StandardTerminalInterface() {
14 | abstract fun readEnvvar(key: String): String?
15 | abstract fun printStderr(message: String, newline: Boolean)
16 | abstract fun readLineOrNull(): String?
17 | abstract fun makeTerminalCursor(terminal: Terminal): TerminalCursor
18 | abstract fun exitProcess(status: Int)
19 | abstract fun readFileIfExists(filename: String): String?
20 |
21 | }
22 |
23 | internal abstract class TerminalInterfaceNode : TerminalInterfaceJsCommon() {
24 | final override fun readLineOrNull(): String? {
25 | return try {
26 | buildString {
27 | val buf = allocBuffer(1)
28 | do {
29 | val char = readByteWithBuf(buf) ?: break
30 | append(char)
31 | } while (char != "\n" && char != "${0.toChar()}")
32 | }
33 | } catch (e: Exception) {
34 | null
35 | }
36 | }
37 |
38 | abstract fun allocBuffer(size: Int): BufferT
39 | abstract fun bufferToString(buffer: BufferT): String
40 | abstract fun readSync(fd: Int, buffer: BufferT, offset: Int, len: Int): Int
41 |
42 | override fun readInputEvent(timeout: TimeMark, mouseTracking: MouseTracking): InputEvent? {
43 | return PosixEventParser {
44 | readByteWithBuf(allocBuffer(1))?.let { it[0].code }
45 | }.readInputEvent(timeout)
46 | }
47 |
48 | private fun readByteWithBuf(buf: BufferT): String? {
49 | val len = readSync(fd = 0, buffer = buf, offset = 0, len = 1)
50 | if (len == 0) return null
51 | // don't call kotlin's toString here due to KT-55817
52 | return bufferToString(buf)
53 | }
54 | }
55 |
56 | internal object TerminalInterfaceBrowser : TerminalInterfaceJsCommon() {
57 | override fun readEnvvar(key: String): String? = null
58 | override fun stdoutInteractive(): Boolean = false
59 | override fun stdinInteractive(): Boolean = false
60 | override fun getTerminalSize(): Size? = null
61 | override fun printStderr(message: String, newline: Boolean) = browserPrintln(message)
62 | override fun exitProcess(status: Int) {}
63 |
64 | // readlnOrNull will just throw an exception on browsers
65 | override fun readLineOrNull(): String? = readlnOrNull()
66 | override fun makeTerminalCursor(terminal: Terminal): TerminalCursor {
67 | return BrowserTerminalCursor(terminal)
68 | }
69 |
70 | override fun readFileIfExists(filename: String): String? = null
71 |
72 | }
73 |
74 | // There are no shutdown hooks on browsers, so we don't need to do anything here
75 | private class BrowserTerminalCursor(terminal: Terminal) : PrintTerminalCursor(terminal)
76 |
--------------------------------------------------------------------------------
/mordant/src/jsMain/kotlin/com/github/ajalt/mordant/internal/JsCompat.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 |
4 | // https://github.com/iliakan/detect-node
5 | internal val isNode: Boolean = js(
6 | "Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]'"
7 | ) as Boolean
8 |
9 | /** Load module [mod], or throw an exception if not running on NodeJS */
10 | internal fun nodeRequire(mod: String): dynamic {
11 | require(isNode) { "Module not available: $mod" }
12 |
13 | // This hack exists to silence webpack warnings when running on the browser. `require` is a
14 | // built-in function on Node, and doesn't exist on browsers. Webpack will statically look for
15 | // calls to `require` and rewrite them into its own module loading system. This means that when
16 | // we have `require("fs")` in our code, webpack will complain during compilation with two types
17 | // of warnings:
18 | //
19 | // 1. It will warn about the module not existing (since it's node-only), even if we never
20 | // execute that statement at runtime on the browser.
21 | // 2. It will complain with a different warning if the argument to `require` isn't static
22 | // (e.g. `fun myRequire(m:String) { require(m) }`).
23 | //
24 | // If we do run `require("fs")` on the browser, webpack will normally cause it to throw a
25 | // `METHOD_NOT_FOUND` error. If the user marks `fs` as "external" in their webpack
26 | // configuration, it will silence the first type of warning above, and the `require` call
27 | // will now return `undefined` instead of throwing an exception.
28 | //
29 | // So since we never call `require` at runtime on browsers anyway, we hide our `require`
30 | // calls from webpack by loading the method dynamically. This prevents any warnings, and
31 | // doesn't require users to add anything to their webpack config.
32 |
33 | val imported = try {
34 | js("module['' + 'require']")(mod)
35 | } catch (e: dynamic) {
36 | throw IllegalArgumentException("Module not available: $mod", e as? Throwable)
37 | }
38 | require(
39 | imported != null && js("typeof imported !== 'undefined'").unsafeCast()
40 | ) { "Module not available: $mod" }
41 | return imported
42 | }
43 |
--------------------------------------------------------------------------------
/mordant/src/jsMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.js.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceJsCommon
4 | import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceJsNode
5 |
6 | private external val console: dynamic
7 | private external val Symbol: dynamic
8 |
9 | internal actual fun browserPrintln(message: String) {
10 | // No way to avoid the newline on browsers
11 | console.error(message)
12 | }
13 |
14 | internal actual fun makeNodeTerminalInterface(): TerminalInterfaceJsCommon? {
15 | return try {
16 | TerminalInterfaceJsNode(nodeRequire("fs"))
17 | } catch (e: Exception) {
18 | null
19 | }
20 | }
21 |
22 | internal actual fun codepointSequence(string: String): Sequence {
23 | val it = string.asDynamic()[Symbol.iterator]()
24 | return generateSequence {
25 | it.next()["value"]?.codePointAt(0) as? Int
26 | }
27 | }
28 |
29 | internal actual val CR_IMPLIES_LF: Boolean = false
30 |
--------------------------------------------------------------------------------
/mordant/src/jsMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.js.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal.terminalinterface
2 |
3 | import com.github.ajalt.mordant.input.MouseTracking
4 | import com.github.ajalt.mordant.rendering.Size
5 | import com.github.ajalt.mordant.terminal.PrintTerminalCursor
6 | import com.github.ajalt.mordant.terminal.Terminal
7 | import com.github.ajalt.mordant.terminal.TerminalCursor
8 |
9 |
10 | private external val process: dynamic
11 | private external val Buffer: dynamic
12 |
13 | internal class TerminalInterfaceJsNode(private val fs: dynamic) : TerminalInterfaceNode() {
14 | override fun readEnvvar(key: String): String? = process.env[key] as? String
15 | override fun stdoutInteractive(): Boolean = js("Boolean(process.stdout.isTTY)") as Boolean
16 | override fun stdinInteractive(): Boolean = js("Boolean(process.stdin.isTTY)") as Boolean
17 | override fun exitProcess(status: Int) {
18 | process.exit(status)
19 | }
20 |
21 | override fun getTerminalSize(): Size? {
22 | // For some undocumented reason, getWindowSize is undefined sometimes, presumably when isTTY
23 | // is false
24 | if (process.stdout.getWindowSize == undefined) return null
25 | val s = process.stdout.getWindowSize()
26 | return Size(width = s[0] as Int, height = s[1] as Int)
27 | }
28 |
29 | override fun printStderr(message: String, newline: Boolean) {
30 | process.stderr.write(if (newline) message + "\n" else message)
31 | }
32 |
33 | override fun allocBuffer(size: Int): dynamic {
34 | return Buffer.alloc(size)
35 | }
36 |
37 | override fun bufferToString(buffer: dynamic): String {
38 | return js("buffer.toString()") as String
39 | }
40 |
41 | override fun readSync(fd: Int, buffer: dynamic, offset: Int, len: Int): Int {
42 | try {
43 | return fs.readSync(fd, buffer, offset, len, null) as Int
44 | } catch (e: dynamic) {
45 | if (e.code == "EAGAIN") return 0
46 | throw e
47 | }
48 | }
49 |
50 | override fun makeTerminalCursor(terminal: Terminal): TerminalCursor {
51 | return NodeTerminalCursor(terminal)
52 | }
53 |
54 | override fun readFileIfExists(filename: String): String? {
55 | return fs.readFileSync(filename, "utf-8") as? String
56 | }
57 |
58 | override fun enterRawMode(mouseTracking: MouseTracking): AutoCloseable {
59 | if (!stdinInteractive()) {
60 | throw RuntimeException("Cannot enter raw mode on a non-interactive terminal")
61 | }
62 | process.stdin.setRawMode(true)
63 | return AutoCloseable {
64 | process.stdin.setRawMode(false)
65 | Unit
66 | }
67 | }
68 | }
69 |
70 | private class NodeTerminalCursor(terminal: Terminal) : PrintTerminalCursor(terminal) {
71 | private var shutdownHook: (() -> Unit)? = null
72 |
73 | override fun show() {
74 | shutdownHook?.let { process.removeListener("exit", it) }
75 | super.show()
76 | }
77 |
78 | override fun hide(showOnExit: Boolean) {
79 | if (showOnExit && shutdownHook == null) {
80 | shutdownHook = { show() }
81 | process.on("exit", shutdownHook)
82 | }
83 | super.hide(showOnExit)
84 | }
85 | }
86 |
87 |
--------------------------------------------------------------------------------
/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.jvm.posix.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal.terminalinterface
2 |
3 | /**
4 | * A base class for terminal interfaces for JVM POSIX systems that uses `System.in` for input.
5 | */
6 | abstract class TerminalInterfaceJvmPosix : TerminalInterfacePosix() {
7 | override fun readRawByte(): Int? {
8 | return System.`in`.read().takeUnless { it <= 0 }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/mordant/src/jvmTest/kotlin/com/github/ajalt/mordant/animation/progress/ThreadAnimatorTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.animation.progress
2 |
3 | import com.github.ajalt.mordant.animation.textAnimation
4 | import com.github.ajalt.mordant.internal.CSI
5 | import com.github.ajalt.mordant.terminal.Terminal
6 | import com.github.ajalt.mordant.terminal.TerminalRecorder
7 | import com.github.ajalt.mordant.widgets.progress.completed
8 | import com.github.ajalt.mordant.widgets.progress.progressBarLayout
9 | import io.kotest.matchers.shouldBe
10 | import io.kotest.matchers.string.shouldContain
11 | import kotlin.test.Test
12 |
13 | private const val HIDE_CURSOR = "$CSI?25l"
14 | private const val SHOW_CURSOR = "$CSI?25h"
15 |
16 | class ThreadAnimatorTest {
17 | private val vt = TerminalRecorder(width = 56)
18 | private val t = Terminal(terminalInterface = vt)
19 |
20 | @Test
21 | fun `unit animator`() {
22 | var i = 1
23 | val a = t.textAnimation { "${i++}" }.animateOnThread(fps = 10000) { i > 2 }
24 | a.runBlocking()
25 | vt.output() shouldBe "${HIDE_CURSOR}1\r2\r3"
26 | vt.clearOutput()
27 | a.stop()
28 | vt.output() shouldBe "\n$SHOW_CURSOR"
29 | vt.clearOutput()
30 | }
31 |
32 | @Test
33 | fun `multi progress animator`() {
34 | val layout = progressBarLayout { completed(fps = 100) }
35 | val animation = MultiProgressBarAnimation(t).animateOnThread()
36 | val task1 = animation.addTask(layout, total = 10)
37 | val task2 = animation.addTask(layout, total = 10)
38 | task1.advance(10)
39 | task2.advance(10)
40 | animation.runBlocking()
41 | vt.output().shouldContain(" 10/10\n 10/10")
42 | }
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/mordant/src/jvmTest/kotlin/com/github/ajalt/mordant/terminal/StderrTerminalTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal
2 |
3 | import com.github.ajalt.mordant.test.RenderingTest
4 | import io.kotest.matchers.shouldBe
5 | import org.junit.Rule
6 | import org.junit.contrib.java.lang.system.SystemErrRule
7 | import org.junit.contrib.java.lang.system.SystemOutRule
8 | import kotlin.test.Test
9 |
10 | class StderrTerminalTest : RenderingTest() {
11 | @get:Rule
12 | val stdout: SystemOutRule = SystemOutRule().enableLog().muteForSuccessfulTests()
13 |
14 | @get:Rule
15 | val stderr: SystemErrRule = SystemErrRule().enableLog().muteForSuccessfulTests()
16 |
17 | @Test
18 | fun stderrTerminal() {
19 | val terminal = Terminal()
20 |
21 | terminal.print("foo", stderr = true)
22 | terminal.print("bar")
23 |
24 | stderr.logWithNormalizedLineSeparator shouldBe "foo"
25 | stdout.logWithNormalizedLineSeparator shouldBe "bar"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/mordant/src/linuxMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.linux.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | internal actual fun testsHaveFileSystem(): Boolean = true
4 |
--------------------------------------------------------------------------------
/mordant/src/macosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.macos.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | internal actual fun testsHaveFileSystem(): Boolean = true
4 |
--------------------------------------------------------------------------------
/mordant/src/mingwMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.mingw.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | import com.github.ajalt.mordant.terminal.TerminalInterface
4 | import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceNativeWindows
5 |
6 | internal actual fun ttySetEcho(echo: Boolean) = TerminalInterfaceNativeWindows.ttySetEcho(echo)
7 | internal actual fun testsHaveFileSystem(): Boolean = true
8 | internal actual fun getStandardTerminalInterface(): TerminalInterface =
9 | TerminalInterfaceNativeWindows
10 |
--------------------------------------------------------------------------------
/mordant/src/posixMain/kotlin/com/github/ajalt/mordant/internal/tty.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfacePosix
4 | import platform.posix.ECHO
5 |
6 | // https://www.gnu.org/software/libc/manual/html_node/getpass.html
7 | internal actual fun ttySetEcho(echo: Boolean) {
8 | val handlerPosix = STANDARD_TERM_INTERFACE as TerminalInterfacePosix
9 | val termios = handlerPosix.getStdinTermios()
10 | handlerPosix.setStdinTermios(
11 | termios.copy(
12 | lflag = when {
13 | echo -> termios.lflag or ECHO.toUInt()
14 | else -> termios.lflag and ECHO.inv().toUInt()
15 | }
16 | )
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/mordant/src/posixMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.posix.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal.terminalinterface
2 |
3 | import kotlinx.cinterop.ByteVar
4 | import kotlinx.cinterop.alloc
5 | import kotlinx.cinterop.memScoped
6 | import kotlinx.cinterop.value
7 |
8 | internal abstract class TerminalInterfaceNativePosix : TerminalInterfacePosix() {
9 | override fun isatty(fd: Int): Boolean {
10 | return platform.posix.isatty(fd) != 0
11 | }
12 |
13 | override fun readRawByte(): Int? = memScoped {
14 | val c = alloc()
15 | val read = readIntoBuffer(c)
16 | if (read < 0) throw RuntimeException("Error reading from stdin")
17 | if (read > 0) return c.value.toInt()
18 | return null
19 | }
20 |
21 | // `read` has different byte widths on linux and apple
22 | protected abstract fun readIntoBuffer(c: ByteVar): Long
23 | }
24 |
--------------------------------------------------------------------------------
/mordant/src/posixSharedMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.native.posixshared.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | import com.github.ajalt.mordant.terminal.TerminalInterface
4 | import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceNativeShared
5 |
6 | internal actual fun getStandardTerminalInterface(): TerminalInterface {
7 | return TerminalInterfaceNativeShared()
8 | }
9 |
--------------------------------------------------------------------------------
/mordant/src/posixSharedMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.shared.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.terminal.terminalinterface
2 |
3 | import com.github.ajalt.mordant.rendering.Size
4 | import kotlinx.cinterop.*
5 | import platform.posix.*
6 |
7 | // XXX: The source code for this file is identical between linux and the various apple targets, but
8 | // they have different bit widths for some fields, so the compileMetadata task fails if we don't use
9 | // separate files. Hopefully some day there will be solution that doesn't require copy-pasting.
10 |
11 | internal class TerminalInterfaceNativeShared : TerminalInterfaceNativePosix() {
12 | override val termiosConstants: TermiosConstants = TermiosConstants(
13 | VTIME = VTIME,
14 | VMIN = VMIN,
15 | INPCK = INPCK.convert(),
16 | ISTRIP = ISTRIP.convert(),
17 | INLCR = INLCR.convert(),
18 | IGNCR = IGNCR.convert(),
19 | ICRNL = ICRNL.convert(),
20 | IXON = IXON.convert(),
21 | OPOST = OPOST.convert(),
22 | CS8 = CS8.convert(),
23 | ISIG = ISIG.convert(),
24 | ICANON = ICANON.convert(),
25 | ECHO = ECHO.convert(),
26 | IEXTEN = IEXTEN.convert(),
27 | )
28 |
29 | override fun readIntoBuffer(c: ByteVar): Long {
30 | return read(platform.posix.STDIN_FILENO, c.ptr, 1u).convert()
31 | }
32 |
33 | override fun getTerminalSize(): Size? = memScoped {
34 | val size = alloc()
35 | if (ioctl(STDIN_FILENO, TIOCGWINSZ.convert(), size) < 0) {
36 | null
37 | } else {
38 | Size(width = size.ws_col.toInt(), height = size.ws_row.toInt())
39 | }
40 | }
41 |
42 | override fun getStdinTermios(): Termios = memScoped {
43 | val termios = alloc()
44 | if (tcgetattr(platform.posix.STDIN_FILENO, termios.ptr) != 0) {
45 | throw RuntimeException("Error reading terminal attributes")
46 | }
47 | return Termios(
48 | iflag = termios.c_iflag.convert(),
49 | oflag = termios.c_oflag.convert(),
50 | cflag = termios.c_cflag.convert(),
51 | lflag = termios.c_lflag.convert(),
52 | cc = ByteArray(NCCS) { termios.c_cc[it].convert() },
53 | )
54 | }
55 |
56 | override fun setStdinTermios(termios: Termios) = memScoped {
57 | val nativeTermios = alloc()
58 | // different platforms have different fields in termios, so we need to read the current
59 | // struct before we set the fields we care about.
60 | if (tcgetattr(platform.posix.STDIN_FILENO, nativeTermios.ptr) != 0) {
61 | throw RuntimeException("Error reading terminal attributes")
62 | }
63 | nativeTermios.c_iflag = termios.iflag.convert()
64 | nativeTermios.c_oflag = termios.oflag.convert()
65 | nativeTermios.c_cflag = termios.cflag.convert()
66 | nativeTermios.c_lflag = termios.lflag.convert()
67 | repeat(NCCS) { nativeTermios.c_cc[it] = termios.cc[it].convert() }
68 | if (tcsetattr(platform.posix.STDIN_FILENO, TCSADRAIN, nativeTermios.ptr) != 0) {
69 | throw RuntimeException("Error setting terminal attributes")
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/mordant/src/wasmJsMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.wasmJs.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.internal
2 |
3 | import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceJsCommon
4 | import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceWasm
5 |
6 | internal actual fun browserPrintln(message: String): Unit = js("console.error(message)")
7 |
8 | internal actual fun makeNodeTerminalInterface(): TerminalInterfaceJsCommon? {
9 | return if (runningOnNode()) TerminalInterfaceWasm() else null
10 | }
11 |
12 | private external interface CodePointString {
13 | fun codePointAt(index: Int): Int
14 | }
15 |
16 | private external interface StringIteration {
17 | val value: CodePointString?
18 | }
19 |
20 | private external interface StringIterator {
21 | fun next(): StringIteration
22 | }
23 |
24 | private fun stringIterator(@Suppress("UNUSED_PARAMETER") s: String): StringIterator =
25 | js("s[Symbol.iterator]()")
26 |
27 | internal actual fun codepointSequence(string: String): Sequence {
28 | val it = stringIterator(string)
29 | return generateSequence { it.next().value?.codePointAt(0) }
30 | }
31 |
32 | // See jsMain for the details of node detection
33 | private fun runningOnNode(): Boolean =
34 | js("Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]'")
35 |
36 |
37 | // For some reason, \r seems to be treated as \r\n on wasm
38 | internal actual val CR_IMPLIES_LF: Boolean = true
39 |
--------------------------------------------------------------------------------
/prepare_docs.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # The website is built using MkDocs with the Material theme.
4 | # https://squidfunk.github.io/mkdocs-material/
5 | # Mkdocs requires Python to run.
6 | # Install the packages: `pip install mkdocs-material`
7 | # Build the api docs: `./gradlew dokkaHtmlMultiModule`
8 | # Then run this script to prepare the docs for the website.
9 | # Finally, run `mkdocs serve` to preview the site locally or `mkdocs build` to build the site.
10 |
11 | set -ex
12 |
13 | # Copy the changelog into the site, omitting the unreleased section
14 | cat CHANGELOG.md \
15 | | grep -v '^## Unreleased' \
16 | | sed '/^## /,$!d' \
17 | > docs/changelog.md
18 |
19 | # Add the jinja frontmatter to the index
20 | cat > docs/index.md <<- EOM
21 | ---
22 | hide:
23 | - toc # Hide table of contents
24 | ---
25 |
26 | EOM
27 |
28 | # Copy the README into the index, omitting the docs link, license and fixing hrefs
29 | cat README.md \
30 | | sed '/## License/Q' \
31 | | sed -z 's/## Documentation[a-zA-z .\n()/:]*//g' \
32 | | sed 's!docs/img!img!g' \
33 | >> docs/index.md
34 |
--------------------------------------------------------------------------------
/runsample:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Run one of the samples.
3 | # The first argument must be the name of the sample task (e.g. echo).
4 | # Any remaining arguments are forwarded to the sample's argv.
5 |
6 | task=$1
7 | shift 1
8 |
9 | if [ -z "${task}" ] || [ ! -d "samples/${task}" ]
10 | then
11 | echo "Unknown sample: '${task}'"
12 | exit 1
13 | fi
14 |
15 | ./gradlew --quiet ":samples:${task}:installDist" && "./samples/${task}/build/install/${task}/bin/${task}" "$@"
16 |
--------------------------------------------------------------------------------
/runsample.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%"=="" @echo off
2 | :: Run one of the samples.
3 | :: The first argument must be the name of the sample task (e.g. echo).
4 | :: Any remaining arguments are forwarded to the sample's argv.
5 |
6 | if "%OS%"=="Windows_NT" setlocal EnableDelayedExpansion
7 |
8 | set TASK=%~1
9 |
10 | set SAMPLE=false
11 | if defined TASK if not "!TASK: =!"=="" if exist "samples\%TASK%\*" set SAMPLE=true
12 |
13 | if "%SAMPLE%"=="false" (
14 | echo Unknown sample: '%TASK%'
15 | exit /b 1
16 | )
17 |
18 | set ARGS=%*
19 | set ARGS=!ARGS:*%1=!
20 | if "!ARGS:~0,1!"==" " set ARGS=!ARGS:~1!
21 |
22 | call gradlew --quiet ":samples:%TASK%:installDist" && call "samples\%TASK%\build\install\%TASK%\bin\%TASK%" %ARGS%
23 |
24 | if "%OS%"=="Windows_NT" endlocal
25 |
--------------------------------------------------------------------------------
/samples/detection/README.md:
--------------------------------------------------------------------------------
1 | # Detection Sample
2 |
3 | This sample shows the detected terminal, e.g. to collect information for issue reports.
4 |
--------------------------------------------------------------------------------
/samples/detection/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-mpp-sample-conventions")
3 | }
4 |
--------------------------------------------------------------------------------
/samples/detection/src/commonMain/kotlin/com/github/ajalt/mordant/samples/main.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.samples
2 |
3 | import com.github.ajalt.mordant.rendering.Whitespace.NORMAL
4 | import com.github.ajalt.mordant.terminal.Terminal
5 | import com.github.ajalt.mordant.widgets.Panel
6 | import com.github.ajalt.mordant.widgets.Text
7 | import com.github.ajalt.mordant.widgets.withPadding
8 |
9 | fun main() {
10 | val terminal = Terminal()
11 | val theme = terminal.theme
12 | terminal.println(
13 | Panel(
14 | Text(terminal.terminalInfo.toString(), whitespace = NORMAL).withPadding(1),
15 | Text(theme.info("Detected Terminal Info"))
16 | )
17 | )
18 | terminal.println(
19 | Panel(
20 | Text(
21 | "${theme.success("success")}, " +
22 | "${theme.danger("danger")}, " +
23 | "${theme.warning("warning")}, " +
24 | "${theme.info("info")}, " +
25 | theme.muted("muted")
26 | ).withPadding(1),
27 | Text(theme.info("Theme colors"))
28 | )
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/samples/drawing/README.md:
--------------------------------------------------------------------------------
1 | # Drawing
2 |
3 | This sample shows how to read mouse input events to draw on the screen.
--------------------------------------------------------------------------------
/samples/drawing/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-mpp-sample-conventions")
3 | }
4 |
5 | kotlin {
6 | sourceSets {
7 | commonMain.dependencies {
8 | implementation(project(":mordant-coroutines"))
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/samples/drawing/src/commonMain/kotlin/com/github/ajalt/mordant/samples/main.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.samples
2 |
3 | import com.github.ajalt.colormath.Color
4 | import com.github.ajalt.colormath.model.HSL
5 | import com.github.ajalt.colormath.model.Oklab
6 | import com.github.ajalt.colormath.model.RGB
7 | import com.github.ajalt.colormath.transform.interpolator
8 | import com.github.ajalt.mordant.animation.coroutines.animateInCoroutine
9 | import com.github.ajalt.mordant.animation.textAnimation
10 | import com.github.ajalt.mordant.input.KeyboardEvent
11 | import com.github.ajalt.mordant.input.MouseEvent
12 | import com.github.ajalt.mordant.input.MouseTracking
13 | import com.github.ajalt.mordant.input.coroutines.receiveEventsFlow
14 | import com.github.ajalt.mordant.input.isCtrlC
15 | import com.github.ajalt.mordant.rendering.AnsiLevel
16 | import com.github.ajalt.mordant.rendering.TextColors
17 | import com.github.ajalt.mordant.terminal.Terminal
18 | import kotlinx.coroutines.coroutineScope
19 | import kotlinx.coroutines.flow.filter
20 | import kotlinx.coroutines.flow.filterIsInstance
21 | import kotlinx.coroutines.flow.takeWhile
22 | import kotlinx.coroutines.launch
23 |
24 | suspend fun main() = coroutineScope {
25 | val terminal = Terminal(ansiLevel = AnsiLevel.TRUECOLOR, interactive = true)
26 | var hue = 0
27 | val canvas = List(terminal.size.height - 1) {
28 | MutableList(terminal.size.width) { RGB("#000") }
29 | }
30 | val animation = terminal.textAnimation {
31 | buildString {
32 | for ((y, row) in canvas.withIndex()) {
33 | for ((x, color) in row.withIndex()) {
34 | append(TextColors.color(color).bg(" "))
35 | canvas[y][x] = Oklab.interpolator {
36 | stop(color)
37 | stop(RGB("#000"))
38 | }.interpolate(0.025)
39 | }
40 | append("\n")
41 | }
42 | }
43 | }.animateInCoroutine()
44 |
45 | launch { animation.execute() }
46 |
47 | terminal.receiveEventsFlow(MouseTracking.Button)
48 | .takeWhile { it !is KeyboardEvent || !it.isCtrlC }
49 | .filterIsInstance()
50 | .filter { it.left }
51 | .collect { event ->
52 | canvas[event.y][event.x] = HSL(hue.toDouble(), 1, .5)
53 | hue += 2
54 | }
55 |
56 | animation.clear()
57 | }
58 |
--------------------------------------------------------------------------------
/samples/hexviewer/README.md:
--------------------------------------------------------------------------------
1 | # Hexviewer Sample
2 |
3 | This sample is a hex viewer like `xxd` which uses mordant for color and layout.
4 |
5 | ```
6 | $ hexviewer picture.png
7 | ```
8 |
9 | 
10 |
--------------------------------------------------------------------------------
/samples/hexviewer/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-mpp-sample-conventions")
3 | }
4 |
5 | kotlin {
6 | sourceSets {
7 | commonMain.dependencies {
8 | implementation("com.squareup.okio:okio:3.9.0")
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/samples/hexviewer/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ajalt/mordant/f98b0f055f45f32e4a6595ea328e3d9cb2a5fd2d/samples/hexviewer/example.png
--------------------------------------------------------------------------------
/samples/hexviewer/src/commonMain/kotlin/com/github/ajalt/mordant/samples/main.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.samples
2 |
3 | import com.github.ajalt.mordant.rendering.BorderType
4 | import com.github.ajalt.mordant.rendering.TextStyle
5 | import com.github.ajalt.mordant.table.Borders
6 | import com.github.ajalt.mordant.table.table
7 | import com.github.ajalt.mordant.terminal.Terminal
8 | import com.github.ajalt.mordant.terminal.danger
9 | import okio.FileSystem
10 | import okio.Path.Companion.toPath
11 | import okio.SYSTEM
12 |
13 |
14 | fun main(args: Array) {
15 | val terminal = Terminal()
16 | if (args.size != 1) {
17 | terminal.danger("Usage: hexviewer ")
18 | return
19 | }
20 | val path = args[0].toPath()
21 | if (!FileSystem.SYSTEM.exists(path)) {
22 | terminal.danger("File not found: $path")
23 | return
24 | }
25 |
26 | // The characters to show for each byte value
27 | val display = "·␁␂␃␄␅␆␇␈␉␊␋␌␍␎␏␐␑␒␓␔␕␖␗␘␙␚␛␜␝␞␟" +
28 | " !\"#$%&'()*+,-./0123456789:;<=>?@" +
29 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
30 | "[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~␡"
31 |
32 | // The text style for each byte value
33 | val styles = List(256) {
34 | when (it) {
35 | 0 -> TextStyle(dim = true)
36 | in 1..31, 127 -> terminal.theme.danger
37 | in 32..126 -> TextStyle()
38 | else -> terminal.theme.warning
39 | }
40 | }
41 |
42 | // 4 chars per octet, -20 for borders + address
43 | val w = (terminal.size.width - 20) / 4
44 | // round down to nearest multiple of 8
45 | val octetsPerRow = (w - w % 8).coerceAtLeast(1)
46 |
47 | val bytes = FileSystem.SYSTEM.read(path) { readByteArray() }
48 |
49 | val table = table {
50 | cellBorders = Borders.LEFT_RIGHT
51 | tableBorders = Borders.ALL
52 | borderType = BorderType.ROUNDED
53 | column(0) { style = terminal.theme.info }
54 |
55 | body {
56 | for (addr in bytes.indices step octetsPerRow) {
57 | val hex = StringBuilder()
58 | val ascii = StringBuilder()
59 | for (i in addr..<(addr + octetsPerRow).coerceAtMost(bytes.size)) {
60 | val byte = bytes[i].toInt() and 0xff
61 | if (i > addr) {
62 | if (i % 8 == 0) hex.append("┆") else hex.append(" ")
63 | }
64 | val s = styles[byte]
65 | hex.append(s(byte.toString(16).padStart(2, '0')))
66 | ascii.append(s(display.getOrElse(byte) { '·' }.toString()))
67 | }
68 | row(
69 | "0x" + addr.toString(16).padStart(8, '0'),
70 | hex.toString(),
71 | ascii.toString()
72 | )
73 | }
74 | }
75 |
76 | }
77 | terminal.println(table)
78 | }
79 |
--------------------------------------------------------------------------------
/samples/markdown/README.md:
--------------------------------------------------------------------------------
1 | # Markdown Sample
2 |
3 | This sample is a command-line application that takes the path to a markdown file as an argument and renders that file to the terminal.
--------------------------------------------------------------------------------
/samples/markdown/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-jvm-sample-conventions")
3 | }
4 |
5 | kotlin {
6 | sourceSets {
7 | jvmMain.dependencies {
8 | implementation(project(":mordant-markdown"))
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/samples/markdown/src/commonMain/kotlin/com/github/ajalt/mordant/samples/main.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.samples
2 |
3 | import com.github.ajalt.mordant.markdown.Markdown
4 | import com.github.ajalt.mordant.terminal.Terminal
5 | import java.io.File
6 |
7 | fun main(args: Array) {
8 | val terminal = Terminal()
9 | val path = args.singleOrNull() ?: error("must specify a markdown file")
10 | val markdown = File(path).readText()
11 | val widget = Markdown(markdown)
12 | terminal.println(widget)
13 | }
14 |
--------------------------------------------------------------------------------
/samples/progress/README.md:
--------------------------------------------------------------------------------
1 | # Progress Sample
2 |
3 | This sample shows a simulated wget-style download progress.
--------------------------------------------------------------------------------
/samples/progress/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-jvm-sample-conventions")
3 | }
4 |
5 | kotlin {
6 | sourceSets {
7 | jvmMain.dependencies {
8 | implementation(project(":mordant-coroutines"))
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/samples/select/README.md:
--------------------------------------------------------------------------------
1 | # Select List Sample
2 |
3 | This sample shows how to use the interactive select list widgets.
--------------------------------------------------------------------------------
/samples/select/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-mpp-sample-conventions")
3 | }
4 |
--------------------------------------------------------------------------------
/samples/select/src/commonMain/kotlin/com/github/ajalt/mordant/samples/main.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.samples
2 |
3 | import com.github.ajalt.mordant.input.interactiveMultiSelectList
4 | import com.github.ajalt.mordant.input.interactiveSelectList
5 | import com.github.ajalt.mordant.terminal.Terminal
6 | import com.github.ajalt.mordant.terminal.danger
7 | import com.github.ajalt.mordant.terminal.success
8 |
9 |
10 | fun main() {
11 | val terminal = Terminal()
12 | val theme = terminal.theme
13 | val size = terminal.interactiveSelectList(
14 | listOf("Small", "Medium", "Large", "X-Large"),
15 | title = "Select a Pizza Size",
16 | )
17 | if (size == null) {
18 | terminal.danger("Aborted pizza order")
19 | return
20 | }
21 | val toppings = terminal.interactiveMultiSelectList {
22 | addEntry("Pepperoni", selected = true)
23 | addEntry("Sausage", selected = true)
24 | addEntry("Mushrooms")
25 | addEntry("Olives")
26 | addEntry("Pineapple")
27 | addEntry("Anchovies")
28 | title("Select Toppings")
29 | limit(4)
30 | filterable(true)
31 | }
32 |
33 | if (toppings == null) {
34 | terminal.danger("Aborted pizza order")
35 | return
36 | }
37 | val toppingString = if (toppings.isEmpty()) "no toppings" else toppings.joinToString()
38 | terminal.success("You ordered a ${theme.info(size)} pizza with ${theme.info(toppingString)}")
39 | }
40 |
--------------------------------------------------------------------------------
/samples/table/README.md:
--------------------------------------------------------------------------------
1 | # Table Sample
2 |
3 | This sample shows many of the features of the table DSL. It supports multiplatform.
4 |
--------------------------------------------------------------------------------
/samples/table/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-mpp-sample-conventions")
3 | }
4 |
--------------------------------------------------------------------------------
/samples/table/src/commonMain/kotlin/com/github/ajalt/mordant/samples/main.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.samples
2 |
3 | import com.github.ajalt.mordant.rendering.BorderType.Companion.SQUARE_DOUBLE_SECTION_SEPARATOR
4 | import com.github.ajalt.mordant.rendering.TextAlign.LEFT
5 | import com.github.ajalt.mordant.rendering.TextAlign.RIGHT
6 | import com.github.ajalt.mordant.rendering.TextColors.*
7 | import com.github.ajalt.mordant.rendering.TextStyle
8 | import com.github.ajalt.mordant.rendering.TextStyles.dim
9 | import com.github.ajalt.mordant.table.Borders.*
10 | import com.github.ajalt.mordant.table.table
11 | import com.github.ajalt.mordant.terminal.Terminal
12 |
13 |
14 | fun main() {
15 | val terminal = Terminal()
16 |
17 | val table = table {
18 | tableBorders = NONE
19 | borderType = SQUARE_DOUBLE_SECTION_SEPARATOR
20 | align = RIGHT
21 | column(0) {
22 | align = LEFT
23 | style = magenta
24 | }
25 | column(3) {
26 | style = magenta
27 | }
28 | header {
29 | style = magenta
30 | row("", "Projected Cost", "Actual Cost", "Difference")
31 | }
32 | body {
33 | cellBorders = TOP_BOTTOM
34 | column(0) {
35 | style = TextStyle(bold = true)
36 | cellBorders = ALL
37 | }
38 | column(3) {
39 | style = TextStyle(bold = true)
40 | cellBorders = ALL
41 | }
42 | rowStyles(blue, brightBlue)
43 |
44 | row("Food", "$400", "$200", "$200")
45 | row("Data", "$100", "$150", "$-50")
46 | row("Rent", "$800", "$800", "$0")
47 | row("Candles", "$0", "$3,600", "$-3,600")
48 | row("Utility", "$154", "$150", "$-5")
49 | }
50 | footer {
51 | row {
52 | cell("Subtotal")
53 | cell("$-3,455") {
54 | columnSpan = 3
55 | }
56 | }
57 | }
58 | captionBottom(dim("Budget courtesy @dril"))
59 | }
60 |
61 | terminal.println(table)
62 | }
63 |
--------------------------------------------------------------------------------
/samples/tour/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("mordant-mpp-sample-conventions")
3 | }
4 |
5 | kotlin {
6 | sourceSets {
7 | commonMain.dependencies {
8 | implementation(project(":mordant-markdown"))
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "mordant"
2 |
3 | include(
4 | "mordant",
5 | "mordant-omnibus",
6 | "mordant-jvm-jna",
7 | "mordant-jvm-ffm",
8 | "mordant-jvm-graal-ffi",
9 | "mordant-coroutines",
10 | "mordant-markdown",
11 | "samples:detection",
12 | "samples:drawing",
13 | "samples:hexviewer",
14 | "samples:markdown",
15 | "samples:progress",
16 | "samples:select",
17 | "samples:table",
18 | "samples:tour",
19 | "test:graalvm",
20 | "test:proguard",
21 | )
22 |
23 | @Suppress("UnstableApiUsage")
24 | dependencyResolutionManagement {
25 | repositories {
26 | mavenCentral()
27 | }
28 | }
29 |
30 | pluginManagement {
31 | repositories {
32 | gradlePluginPortal()
33 | mavenCentral()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/test/graalvm/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm")
3 | alias(libs.plugins.graalvm.nativeimage)
4 | }
5 |
6 | tasks.test {
7 | useJUnitPlatform()
8 | }
9 |
10 | dependencies {
11 | implementation(project(":mordant-omnibus"))
12 | implementation(project(":mordant-markdown"))
13 | testImplementation(kotlin("test"))
14 | }
15 |
16 | graalvmNative {
17 | binaries {
18 | named("test") {
19 | quickBuild.set(true)
20 | buildArgs(
21 | // https://github.com/oracle/graal/issues/6957
22 | "--initialize-at-build-time=kotlin.annotation.AnnotationTarget,kotlin.annotation.AnnotationRetention",
23 | )
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/test/graalvm/src/test/kotlin/GraalSmokeTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.graalvm
2 |
3 | import com.github.ajalt.mordant.animation.progress.animateOnThread
4 | import com.github.ajalt.mordant.animation.progress.execute
5 | import com.github.ajalt.mordant.markdown.Markdown
6 | import com.github.ajalt.mordant.rendering.AnsiLevel
7 | import com.github.ajalt.mordant.rendering.TextStyles.bold
8 | import com.github.ajalt.mordant.terminal.Terminal
9 | import com.github.ajalt.mordant.terminal.TerminalRecorder
10 | import com.github.ajalt.mordant.widgets.progress.progressBar
11 | import com.github.ajalt.mordant.widgets.progress.progressBarLayout
12 | import org.junit.jupiter.api.Assertions.assertEquals
13 | import org.junit.jupiter.api.Test
14 | import org.junit.jupiter.api.condition.EnabledInNativeImage
15 | import java.util.concurrent.TimeUnit
16 | import kotlin.test.fail
17 |
18 | /**
19 | * Smoke tests for the GraalVM platform.
20 | *
21 | * They just make sure nothing crashes; the actual output is verified in the normal test suite.
22 | */
23 | @EnabledInNativeImage
24 | class GraalSmokeTest {
25 | @Test
26 | fun `terminal detection test`() {
27 | val name = Terminal().terminalInterface::class.simpleName
28 | Terminal()
29 | val assertion = name!!.startsWith("TerminalInterfaceNativeImage")
30 | if (!assertion) {
31 | fail("Incorrect terminal interface: $name")
32 | }
33 | }
34 |
35 | @Test
36 | fun `progress animation test`() {
37 | val t = Terminal(interactive = true, ansiLevel = AnsiLevel.TRUECOLOR)
38 | val animation = progressBarLayout { progressBar() }.animateOnThread(t, total = 1)
39 | val future = animation.execute()
40 | Thread.sleep(100)
41 | animation.update { completed = 1 }
42 | future.get(1000, TimeUnit.MILLISECONDS)
43 | }
44 |
45 | @Test
46 | fun `markdown test`() {
47 | val vt = TerminalRecorder()
48 | val t = Terminal(terminalInterface = vt)
49 | t.print(Markdown("- Some **bold** text"))
50 | assertEquals(" • Some ${bold("bold")} text", vt.output())
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/test/proguard/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm")
3 | }
4 |
5 | repositories {
6 | google()
7 | mavenCentral()
8 | }
9 |
10 | val r8: Configuration by configurations.creating
11 |
12 | dependencies {
13 | implementation(project(":mordant-omnibus"))
14 | implementation(project(":mordant-markdown"))
15 | implementation(project(":mordant-coroutines"))
16 | r8(libs.r8)
17 | }
18 |
19 |
20 | val fatJar by tasks.register("fatJar") {
21 | archiveClassifier = "fat"
22 | duplicatesStrategy = DuplicatesStrategy.INCLUDE
23 |
24 | from(sourceSets.main.get().output)
25 |
26 | dependsOn(configurations.runtimeClasspath)
27 | from({
28 | configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
29 | })
30 |
31 | manifest {
32 | attributes["Main-Class"] = "com.github.ajalt.mordant.main.R8SmokeTestKt"
33 | attributes["Implementation-Version"] = archiveVersion
34 | }
35 |
36 | exclude("**/*.kotlin_metadata")
37 | exclude("**/*.kotlin_module")
38 | exclude("**/*.kotlin_builtins")
39 | exclude("**/module-info.class")
40 | }
41 |
42 |
43 | val r8JarProvider by tasks.register("r8Jar") {
44 | dependsOn(fatJar)
45 |
46 | val r8File = layout.buildDirectory.file("libs/main-r8.jar")
47 | val rulesFile = project.file("src/main/rules.pro")
48 |
49 | val fatJarFile = fatJar.archiveFile
50 |
51 | inputs.files(fatJarFile, rulesFile)
52 | outputs.file(r8File)
53 |
54 | classpath(r8)
55 | mainClass.set("com.android.tools.r8.R8")
56 | args = listOf(
57 | "--release",
58 | "--classfile",
59 | "--output", r8File.get().asFile.toString(),
60 | "--pg-conf", rulesFile.path,
61 | "--lib", System.getProperty("java.home").toString(),
62 | fatJarFile.get().toString(),
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/test/proguard/src/main/kotlin/R8SmokeTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.ajalt.mordant.main
2 |
3 | import com.github.ajalt.mordant.animation.coroutines.animateInCoroutine
4 | import com.github.ajalt.mordant.markdown.Markdown
5 | import com.github.ajalt.mordant.rendering.AnsiLevel
6 | import com.github.ajalt.mordant.terminal.Terminal
7 | import com.github.ajalt.mordant.widgets.progress.progressBar
8 | import com.github.ajalt.mordant.widgets.progress.progressBarLayout
9 | import kotlinx.coroutines.coroutineScope
10 | import kotlinx.coroutines.delay
11 | import kotlinx.coroutines.launch
12 |
13 | suspend fun main(args: Array) = coroutineScope {
14 | // make sure that the terminal detection doesn't crash.
15 | Terminal()
16 |
17 | // make sure animations and markdown don't crash.
18 | val t = Terminal(interactive = true, ansiLevel = AnsiLevel.TRUECOLOR)
19 | val animation = progressBarLayout { progressBar() }.animateInCoroutine(t, total = 1)
20 | launch { animation.execute() }
21 | t.print(Markdown("- Your args: **${args.asList()}**"))
22 | delay(100)
23 | animation.update { completed = 1 }
24 | }
25 |
--------------------------------------------------------------------------------
/test/proguard/src/main/rules.pro:
--------------------------------------------------------------------------------
1 | -keep class com.github.ajalt.mordant.main.R8SmokeTestKt {
2 | public static void main(java.lang.String[]);
3 | }
4 |
--------------------------------------------------------------------------------