diff --git a/core/js/src/main/scala/terminus/Terminal.scala b/core/js/src/main/scala/terminus/Terminal.scala index ea9cb71..cf31ce1 100644 --- a/core/js/src/main/scala/terminus/Terminal.scala +++ b/core/js/src/main/scala/terminus/Terminal.scala @@ -24,9 +24,9 @@ import scala.concurrent.Future import scala.concurrent.Promise class Terminal(root: HTMLElement, options: XtermJsOptions) - extends effect.Color[Terminal], + extends effect.Color, effect.Cursor, - effect.Format[Terminal], + effect.Format, effect.Erase, effect.Dimensions, effect.Writer { diff --git a/core/jvm/src/main/scala/terminus/JLineTerminal.scala b/core/jvm/src/main/scala/terminus/JLineTerminal.scala index ced35a3..75cc77f 100644 --- a/core/jvm/src/main/scala/terminus/JLineTerminal.scala +++ b/core/jvm/src/main/scala/terminus/JLineTerminal.scala @@ -55,10 +55,10 @@ class JLineTerminal(terminal: JTerminal) extends Terminal, TerminalKeyReader { def write(string: String): Unit = writer.write(string) - def raw[A](f: Terminal ?=> A): A = { + def raw[A](f: () => A): A = { val attrs = terminal.enterRawMode() try { - val result = f(using this) + val result = f() result } finally { terminal.setAttributes(attrs) @@ -73,20 +73,20 @@ class JLineTerminal(terminal: JTerminal) extends Terminal, TerminalKeyReader { def setDimensions(dimensions: TerminalDimensions): Unit = terminal.setSize(Size(dimensions.columns, dimensions.rows)) - def application[A](f: Terminal ?=> A): A = { + def application[A](f: () => A): A = { try { terminal.puts(Capability.keypad_xmit) - val result = f(using this) + val result = f() result } finally { val _ = terminal.puts(Capability.keypad_local) } } - def alternateScreen[A](f: Terminal ?=> A): A = { + def alternateScreen[A](f: () => A): A = { try { terminal.puts(Capability.enter_ca_mode) - val result = f(using this) + val result = f() result } finally { val _ = terminal.puts(Capability.exit_ca_mode) diff --git a/core/jvm/src/main/scala/terminus/Terminal.scala b/core/jvm/src/main/scala/terminus/Terminal.scala index e107875..d61ed53 100644 --- a/core/jvm/src/main/scala/terminus/Terminal.scala +++ b/core/jvm/src/main/scala/terminus/Terminal.scala @@ -17,17 +17,17 @@ package terminus trait Terminal - extends effect.AlternateScreenMode[Terminal], - effect.ApplicationMode[Terminal], - effect.Color[Terminal], + extends effect.AlternateScreenMode, + effect.ApplicationMode, + effect.Color, effect.Cursor, - effect.Format[Terminal], + effect.Format, effect.Dimensions, effect.Erase, effect.KeyReader, effect.NonBlockingReader, effect.Peeker, - effect.RawMode[Terminal], + effect.RawMode, effect.Reader, effect.Writer type Program[A] = Terminal ?=> A diff --git a/core/native/src/main/scala/terminus/NativeTerminal.scala b/core/native/src/main/scala/terminus/NativeTerminal.scala index d2914bf..cd423aa 100644 --- a/core/native/src/main/scala/terminus/NativeTerminal.scala +++ b/core/native/src/main/scala/terminus/NativeTerminal.scala @@ -24,15 +24,12 @@ import scala.concurrent.duration.Duration import scala.scalanative.libc import scala.scalanative.meta.LinktimeInfo import scala.scalanative.posix -import scala.scalanative.unsigned.UInt +import scala.scalanative.unsigned.* import scalanative.unsafe.* /** A Terminal implementation for Scala Native. */ -object NativeTerminal - extends Terminal, - WithEffect[Terminal], - TerminalKeyReader { +object NativeTerminal extends Terminal, WithEffect, TerminalKeyReader { private given termiosAccess: TermiosAccess[?] = if LinktimeInfo.isMac then clongTermiosAccess @@ -70,10 +67,10 @@ object NativeTerminal val origAttrs = termios.getAttributes() val attrs = termios.getAttributes() try { - attrs.setSpecialCharacter(posix.termios.VMIN, 0) + attrs.setSpecialCharacter(posix.termios.VMIN, 0.toUByte) attrs.setSpecialCharacter( posix.termios.VTIME, - (duration.toMillis / 100).toByte + (duration.toMillis / 100).toUByte ) termios.setAttributes(attrs) @@ -111,23 +108,23 @@ object NativeTerminal } } - def application[A](f: (terminus.Terminal) ?=> A): A = { + def application[A](f: () => A): A = { withEffect(AnsiCodes.mode.application.on, AnsiCodes.mode.application.off)(f) } - def alternateScreen[A](f: (terminus.Terminal) ?=> A): A = { + def alternateScreen[A](f: () => A): A = { withEffect( AnsiCodes.mode.alternateScreen.on, AnsiCodes.mode.alternateScreen.off )(f) } - def raw[A](f: Terminal ?=> A): A = { + def raw[A](f: () => A): A = { Zone { val origAttrs = termios.getAttributes() try { termios.setRawMode() - f(using this) + f() } finally { termios.setAttributes(origAttrs) } diff --git a/core/native/src/main/scala/terminus/Terminal.scala b/core/native/src/main/scala/terminus/Terminal.scala index 083200e..99f2ca6 100644 --- a/core/native/src/main/scala/terminus/Terminal.scala +++ b/core/native/src/main/scala/terminus/Terminal.scala @@ -17,15 +17,15 @@ package terminus trait Terminal - extends effect.AlternateScreenMode[Terminal], - effect.ApplicationMode[Terminal], - effect.Color[Terminal], + extends effect.AlternateScreenMode, + effect.ApplicationMode, + effect.Color, effect.Cursor, - effect.Format[Terminal], + effect.Format, effect.Erase, effect.KeyReader, effect.NonBlockingReader, - effect.RawMode[Terminal], + effect.RawMode, effect.Reader, effect.Writer type Program[A] = Terminal ?=> A diff --git a/core/native/src/main/scala/terminus/TermiosAccess.scala b/core/native/src/main/scala/terminus/TermiosAccess.scala index 4bd5b51..0bb3aa0 100644 --- a/core/native/src/main/scala/terminus/TermiosAccess.scala +++ b/core/native/src/main/scala/terminus/TermiosAccess.scala @@ -20,6 +20,7 @@ import scala.annotation.implicitNotFound import scala.scalanative.posix import scala.scalanative.posix.unistd.STDIN_FILENO import scala.scalanative.unsafe.* +import scala.scalanative.unsigned.* /** Typeclass for handling the messiness of reading, updating, and writing * termios structs in a platform specific way. @@ -89,11 +90,17 @@ trait TermiosAccess[T] { /** Remove flags from termios c_lflag struct member */ def removeLocalFlags(attrs: Ptr[T], flags: CInt): Unit + /** Get the value at the given index of the termios c_cc struct member. Valid + * indices are defined as constants such as `VMIN` and `VTIME` in + * [[scala.scalanative.posix.termios]] + */ + def getSpecialCharacter(attrs: Ptr[T], idx: CInt): UByte + /** Set the value at the given index of the termios c_cc struct member. Valid * indices are defined as constants such as `VMIN` and `VTIME` in * [[scala.scalanative.posix.termios]] */ - def setSpecialCharacter(attrs: Ptr[T], idx: CInt, value: CChar): Unit + def setSpecialCharacter(attrs: Ptr[T], idx: CInt, value: UByte): Unit } /** Extension methods to simplify modifying a termios struct */ @@ -106,12 +113,16 @@ extension [T](ptr: Ptr[T])(using au: TermiosAccess[T]) { def removeControlFlags(flags: CInt): Unit = au.removeControlFlags(ptr, flags) def addLocalFlags(flags: CInt): Unit = au.addLocalFlags(ptr, flags) def removeLocalFlags(flags: CInt): Unit = au.removeLocalFlags(ptr, flags) - def setSpecialCharacter(idx: CInt, value: CChar) = + def getSpecialCharacter(idx: CInt): UByte = + au.getSpecialCharacter(ptr, idx) + def setSpecialCharacter(idx: CInt, value: UByte) = au.setSpecialCharacter(ptr, idx, value) } /** [[TermiosAccess]] instance for structs with CLong bitflags */ given clongTermiosAccess: TermiosAccess[TermiosStruct.clong_flags] = + import posix.termiosOps.* + new TermiosAccess[TermiosStruct.clong_flags] { override type FlagType = CLong @@ -124,7 +135,7 @@ given clongTermiosAccess: TermiosAccess[TermiosStruct.clong_flags] = override def set(ptr: Ptr[TermiosStruct.clong_flags]): Unit = { val _ = - posix.termios.tcsetattr(STDIN_FILENO, posix.termios.TCSAFLUSH, ptr) + posix.termios.tcsetattr(STDIN_FILENO, posix.termios.TCSANOW, ptr) () } @@ -132,55 +143,60 @@ given clongTermiosAccess: TermiosAccess[TermiosStruct.clong_flags] = attrs: Ptr[TermiosStruct.clong_flags], flags: CInt ): Unit = - attrs._1 = attrs._1 | flags + attrs.c_iflag = attrs.c_iflag | flags.toUInt override def removeInputFlags( attrs: Ptr[TermiosStruct.clong_flags], flags: CInt ): Unit = - attrs._1 = attrs._1 & ~flags + attrs.c_iflag = attrs.c_iflag & ~(flags.toUInt) override def addOutputFlags( attrs: Ptr[TermiosStruct.clong_flags], flags: CInt ): Unit = - attrs._2 = attrs._2 | flags + attrs.c_oflag = attrs.c_oflag | flags.toUInt override def removeOutputFlags( attrs: Ptr[TermiosStruct.clong_flags], flags: CInt ): Unit = - attrs._2 = attrs._2 & ~flags + attrs.c_oflag = attrs.c_oflag & ~(flags.toUInt) override def addControlFlags( attrs: Ptr[TermiosStruct.clong_flags], flags: CInt ): Unit = - attrs._3 = attrs._3 | flags + attrs.c_cflag = attrs.c_cflag | flags.toUInt override def removeControlFlags( attrs: Ptr[TermiosStruct.clong_flags], flags: CInt ): Unit = - attrs._3 = attrs._3 & ~flags + attrs.c_cflag = attrs.c_cflag & ~(flags.toUInt) override def addLocalFlags( attrs: Ptr[TermiosStruct.clong_flags], flags: CInt ): Unit = - attrs._4 = attrs._4 | flags + attrs.c_lflag = attrs.c_lflag | flags.toUInt override def removeLocalFlags( attrs: Ptr[TermiosStruct.clong_flags], flags: CInt ): Unit = - attrs._4 = attrs._4 & ~flags + attrs.c_lflag = attrs.c_lflag & ~(flags.toUInt) + + override def getSpecialCharacter( + attrs: Ptr[TermiosStruct.clong_flags], + idx: CInt + ): UByte = attrs.c_cc(idx) override def setSpecialCharacter( attrs: Ptr[TermiosStruct.clong_flags], idx: CInt, - value: CChar - ): Unit = attrs._5(idx) = value + value: UByte + ): Unit = attrs.c_cc(idx) = value } /** [[TermiosAccess]] instance for structs with CInt bitflags */ @@ -196,7 +212,7 @@ given cintTermiosAccess: TermiosAccess[TermiosStruct.cint_flags] = } override def set(ptr: Ptr[TermiosStruct.cint_flags]): Unit = { - val _ = tcsetattr(STDIN_FILENO, posix.termios.TCSAFLUSH, ptr) + val _ = tcsetattr(STDIN_FILENO, posix.termios.TCSANOW, ptr) () } @@ -246,11 +262,19 @@ given cintTermiosAccess: TermiosAccess[TermiosStruct.cint_flags] = flags: CInt ): Unit = attrs._4 = attrs._4 & ~flags + override def getSpecialCharacter( + attrs: Ptr[TermiosStruct.cint_flags], + idx: CInt + ): UByte = { + val c_cc = attrs._5 + c_cc(idx) + } + override def setSpecialCharacter( attrs: Ptr[TermiosStruct.cint_flags], idx: CInt, - value: CChar - ): Unit = attrs._5(idx) = value + value: UByte + ): Unit = attrs._5(idx) = value.toUByte // Custom `tcgetattr` and `tcsetattr` definitions, since we can't use the ones defined in scala native since // they use CLong for bitflags. These should point to the functions defined in the systems termios library diff --git a/core/native/src/main/scala/terminus/TermiosStruct.scala b/core/native/src/main/scala/terminus/TermiosStruct.scala index 911509b..a917959 100644 --- a/core/native/src/main/scala/terminus/TermiosStruct.scala +++ b/core/native/src/main/scala/terminus/TermiosStruct.scala @@ -19,6 +19,7 @@ package terminus import scala.scalanative.posix import scala.scalanative.unsafe.CInt import scala.scalanative.unsafe.CStruct7 +import scala.scalanative.unsafe.CUnsignedLong /** Type aliases for the two possible termios structures, one with CInt bitflags * and one with CLong bitflags. CLong types are the default in scala-native, @@ -44,7 +45,7 @@ object TermiosStruct { linux_tcflag_t, /* c_oflag - output flags */ linux_tcflag_t, /* c_cflag - control flags */ linux_tcflag_t, /* c_lflag - local flags */ - posix.termios.c_cc, /* cc_t c_cc[NCCS] - control chars */ + posix.termios.cc_t_arr, /* cc_t c_cc[NCCS] - control chars */ linux_speed_t, /* c_ispeed - input speed */ linux_speed_t /* c_ospeed - output speed */ ] diff --git a/core/native/src/test/scala/terminus/TermiosAccessSpec.scala b/core/native/src/test/scala/terminus/TermiosAccessSpec.scala new file mode 100644 index 0000000..3c21f4f --- /dev/null +++ b/core/native/src/test/scala/terminus/TermiosAccessSpec.scala @@ -0,0 +1,16 @@ +/* + * Copyright 2024 Creative Scala + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/core/native/src/test/scala/terminus/TermiosAccessSuite.scala b/core/native/src/test/scala/terminus/TermiosAccessSuite.scala new file mode 100644 index 0000000..ae06086 --- /dev/null +++ b/core/native/src/test/scala/terminus/TermiosAccessSuite.scala @@ -0,0 +1,72 @@ +/* + * Copyright 2024 Creative Scala + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package terminus + +import munit.FunSuite + +import scala.scalanative.posix +import scala.scalanative.unsafe.CInt +import scala.scalanative.unsafe.Ptr +import scala.scalanative.unsafe.Zone +import scala.scalanative.unsigned.* + +class CLongTermiosAccessSuite extends FunSuite { + val termios = clongTermiosAccess + + def testSettingSpecialCharacter( + attrs: Ptr[TermiosStruct.clong_flags], + idx: CInt, + value: UByte, + name: String + )(using Zone): Unit = { + attrs.setSpecialCharacter(idx, value) + termios.set(attrs) + val updated = termios.get + val read = updated.getSpecialCharacter(idx) + assertEquals(read, value, name) + } + + test("Test reading special characters returns set value") { + Zone { + val orig = termios.get + try { + val attrs = termios.get + + List( + (posix.termios.VEOF, "VEOF"), + (posix.termios.VEOL, "VEOL"), + (posix.termios.VERASE, "VERASE"), + (posix.termios.VINTR, "VINTR"), + (posix.termios.VKILL, "VKILL"), + (posix.termios.VMIN, "VMIN"), + (posix.termios.VQUIT, "VQUIT"), + (posix.termios.VSTART, "VSTART"), + (posix.termios.VSTOP, "VSTOP"), + (posix.termios.VSUSP, "VSUSP"), + (posix.termios.VTIME, "VTIME") + ).foreach { (idx, name) => + testSettingSpecialCharacter(attrs, idx, 1.toUByte, name) + testSettingSpecialCharacter(attrs, idx, 2.toUByte, name) + testSettingSpecialCharacter(attrs, idx, 3.toUByte, name) + testSettingSpecialCharacter(attrs, idx, 4.toUByte, name) + } + } finally { + termios.set(orig) + } + } + } +} diff --git a/core/shared/src/main/scala/terminus/AlternateScreenMode.scala b/core/shared/src/main/scala/terminus/AlternateScreenMode.scala index 1ed5e1f..be33c83 100644 --- a/core/shared/src/main/scala/terminus/AlternateScreenMode.scala +++ b/core/shared/src/main/scala/terminus/AlternateScreenMode.scala @@ -24,6 +24,6 @@ trait AlternateScreenMode { */ def alternateScreen[F <: effect.Effect, A]( f: F ?=> A - ): (F & effect.AlternateScreenMode[F]) ?=> A = - effect ?=> effect.alternateScreen(f) + ): (F & effect.AlternateScreenMode) ?=> A = + effect ?=> effect.alternateScreen(() => f(using effect)) } diff --git a/core/shared/src/main/scala/terminus/ApplicationMode.scala b/core/shared/src/main/scala/terminus/ApplicationMode.scala index 05cd353..8a15ced 100644 --- a/core/shared/src/main/scala/terminus/ApplicationMode.scala +++ b/core/shared/src/main/scala/terminus/ApplicationMode.scala @@ -24,6 +24,6 @@ trait ApplicationMode { */ def application[F <: effect.Effect, A]( f: F ?=> A - ): (F & effect.ApplicationMode[F]) ?=> A = - effect ?=> effect.application(f) + ): (F & effect.ApplicationMode) ?=> A = + effect ?=> effect.application(() => f(using effect)) } diff --git a/core/shared/src/main/scala/terminus/Color.scala b/core/shared/src/main/scala/terminus/Color.scala index 2862ca5..1130ef7 100644 --- a/core/shared/src/main/scala/terminus/Color.scala +++ b/core/shared/src/main/scala/terminus/Color.scala @@ -18,176 +18,176 @@ package terminus trait Color { object foreground { - def default[F <: effect.Writer, A]( + def default[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.default(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.default(() => f(using effect)) - def black[F <: effect.Writer, A]( + def black[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.black(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.black(() => f(using effect)) - def red[F <: effect.Writer, A]( + def red[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.red(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.red(() => f(using effect)) - def green[F <: effect.Writer, A]( + def green[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.green(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.green(() => f(using effect)) - def yellow[F <: effect.Writer, A]( + def yellow[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.yellow(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.yellow(() => f(using effect)) - def blue[F <: effect.Writer, A]( + def blue[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.blue(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.blue(() => f(using effect)) - def magenta[F <: effect.Writer, A]( + def magenta[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.magenta(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.magenta(() => f(using effect)) - def cyan[F <: effect.Writer, A]( + def cyan[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.cyan(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.cyan(() => f(using effect)) - def white[F <: effect.Writer, A]( + def white[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.white(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.white(() => f(using effect)) - def brightBlack[F <: effect.Writer, A]( + def brightBlack[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.brightBlack(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.brightBlack(() => f(using effect)) - def brightRed[F <: effect.Writer, A]( + def brightRed[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.brightRed(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.brightRed(() => f(using effect)) - def brightGreen[F <: effect.Writer, A]( + def brightGreen[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.brightGreen(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.brightGreen(() => f(using effect)) - def brightYellow[F <: effect.Writer, A]( + def brightYellow[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.brightYellow(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.brightYellow(() => f(using effect)) - def brightBlue[F <: effect.Writer, A]( + def brightBlue[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.brightBlue(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.brightBlue(() => f(using effect)) - def brightMagenta[F <: effect.Writer, A]( + def brightMagenta[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.brightMagenta(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.brightMagenta(() => f(using effect)) - def brightCyan[F <: effect.Writer, A]( + def brightCyan[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.brightCyan(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.brightCyan(() => f(using effect)) - def brightWhite[F <: effect.Writer, A]( + def brightWhite[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.foreground.brightWhite(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.foreground.brightWhite(() => f(using effect)) } object background { - def default[F <: effect.Writer, A]( + def default[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.default(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.default(() => f(using effect)) - def black[F <: effect.Writer, A]( + def black[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.black(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.black(() => f(using effect)) - def red[F <: effect.Writer, A]( + def red[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.red(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.red(() => f(using effect)) - def green[F <: effect.Writer, A]( + def green[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.green(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.green(() => f(using effect)) - def yellow[F <: effect.Writer, A]( + def yellow[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.yellow(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.yellow(() => f(using effect)) - def blue[F <: effect.Writer, A]( + def blue[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.blue(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.blue(() => f(using effect)) - def magenta[F <: effect.Writer, A]( + def magenta[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.magenta(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.magenta(() => f(using effect)) - def cyan[F <: effect.Writer, A]( + def cyan[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.cyan(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.cyan(() => f(using effect)) - def white[F <: effect.Writer, A]( + def white[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.white(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.white(() => f(using effect)) - def brightBlack[F <: effect.Writer, A]( + def brightBlack[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.brightBlack(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.brightBlack(() => f(using effect)) - def brightRed[F <: effect.Writer, A]( + def brightRed[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.brightRed(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.brightRed(() => f(using effect)) - def brightGreen[F <: effect.Writer, A]( + def brightGreen[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.brightGreen(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.brightGreen(() => f(using effect)) - def brightYellow[F <: effect.Writer, A]( + def brightYellow[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.brightYellow(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.brightYellow(() => f(using effect)) - def brightBlue[F <: effect.Writer, A]( + def brightBlue[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.brightBlue(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.brightBlue(() => f(using effect)) - def brightMagenta[F <: effect.Writer, A]( + def brightMagenta[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.brightMagenta(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.brightMagenta(() => f(using effect)) - def brightCyan[F <: effect.Writer, A]( + def brightCyan[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.brightCyan(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.brightCyan(() => f(using effect)) - def brightWhite[F <: effect.Writer, A]( + def brightWhite[F, A]( f: F ?=> A - ): (F & effect.Color[F]) ?=> A = - effect ?=> effect.background.brightWhite(f) + ): (F & effect.Color) ?=> A = + effect ?=> effect.background.brightWhite(() => f(using effect)) } } diff --git a/core/shared/src/main/scala/terminus/Format.scala b/core/shared/src/main/scala/terminus/Format.scala index 61fbe41..1dc2a5c 100644 --- a/core/shared/src/main/scala/terminus/Format.scala +++ b/core/shared/src/main/scala/terminus/Format.scala @@ -18,114 +18,114 @@ package terminus trait Format { object format { - def bold[F <: effect.Writer, A](f: F ?=> A): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.bold(f) + def bold[F, A](f: F ?=> A): (F & effect.Format) ?=> A = + effect ?=> effect.format.bold(() => f(using effect)) - def light[F <: effect.Writer, A]( + def light[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.light(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.light(() => f(using effect)) - def normal[F <: effect.Writer, A]( + def normal[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.normal(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.normal(() => f(using effect)) object underline { - def none[F <: effect.Writer, A]( + def none[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.none(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.none(() => f(using effect)) - def straight[F <: effect.Writer, A]( + def straight[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.straight(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.straight(() => f(using effect)) - def double[F <: effect.Writer, A]( + def double[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.double(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.double(() => f(using effect)) - def curly[F <: effect.Writer, A]( + def curly[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.curly(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.curly(() => f(using effect)) - def dotted[F <: effect.Writer, A]( + def dotted[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.dotted(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.dotted(() => f(using effect)) - def dashed[F <: effect.Writer, A]( + def dashed[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.dashed(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.dashed(() => f(using effect)) - def default[F <: effect.Writer, A]( + def default[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.default(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.default(() => f(using effect)) - def black[F <: effect.Writer, A]( + def black[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.black(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.black(() => f(using effect)) - def red[F <: effect.Writer, A]( + def red[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.red(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.red(() => f(using effect)) - def green[F <: effect.Writer, A]( + def green[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.green(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.green(() => f(using effect)) - def yellow[F <: effect.Writer, A]( + def yellow[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.yellow(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.yellow(() => f(using effect)) - def blue[F <: effect.Writer, A]( + def blue[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.blue(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.blue(() => f(using effect)) - def magenta[F <: effect.Writer, A]( + def magenta[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.magenta(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.magenta(() => f(using effect)) - def cyan[F <: effect.Writer, A]( + def cyan[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.cyan(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.cyan(() => f(using effect)) - def white[F <: effect.Writer, A]( + def white[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.underline.white(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.underline.white(() => f(using effect)) } - def blink[F <: effect.Writer, A]( + def blink[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.blink(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.blink(() => f(using effect)) - def invert[F <: effect.Writer, A]( + def invert[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.invert(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.invert(() => f(using effect)) - def invisible[F <: effect.Writer, A]( + def invisible[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.invisible(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.invisible(() => f(using effect)) - def strikethrough[F <: effect.Writer, A]( + def strikethrough[F, A]( f: F ?=> A - ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.strikethrough(f) + ): (F & effect.Format) ?=> A = + effect ?=> effect.format.strikethrough(() => f(using effect)) } } diff --git a/core/shared/src/main/scala/terminus/RawMode.scala b/core/shared/src/main/scala/terminus/RawMode.scala index ae58e52..c0e8173 100644 --- a/core/shared/src/main/scala/terminus/RawMode.scala +++ b/core/shared/src/main/scala/terminus/RawMode.scala @@ -22,6 +22,6 @@ trait RawMode { * program can read user input a character at a time. In canonical mode, * which is the default, user input is only available a line at a time. */ - def raw[F <: effect.Effect, A](f: F ?=> A): (F & effect.RawMode[F]) ?=> A = - effect ?=> effect.raw(f) + def raw[F <: effect.Effect, A](f: F ?=> A): (F & effect.RawMode) ?=> A = + effect ?=> effect.raw(() => f(using effect)) } diff --git a/core/shared/src/main/scala/terminus/StringBuilderTerminal.scala b/core/shared/src/main/scala/terminus/StringBuilderTerminal.scala index 058f741..ac5c3b8 100644 --- a/core/shared/src/main/scala/terminus/StringBuilderTerminal.scala +++ b/core/shared/src/main/scala/terminus/StringBuilderTerminal.scala @@ -19,9 +19,9 @@ package terminus import scala.collection.mutable final class StringBuilderTerminal() - extends effect.Color[StringBuilderTerminal], + extends effect.Color, effect.Cursor, - effect.Format[StringBuilderTerminal], + effect.Format, effect.Erase, effect.Writer { diff --git a/core/shared/src/main/scala/terminus/effect/AlternateScreenMode.scala b/core/shared/src/main/scala/terminus/effect/AlternateScreenMode.scala index cdcf798..717b6d6 100644 --- a/core/shared/src/main/scala/terminus/effect/AlternateScreenMode.scala +++ b/core/shared/src/main/scala/terminus/effect/AlternateScreenMode.scala @@ -16,11 +16,11 @@ package terminus.effect -trait AlternateScreenMode[+F <: Effect] { self: F => +trait AlternateScreenMode { /** Run the given terminal program `f` in alternate screen mode, which means * that whatever is displayed by `f` will not been shown when the program * exits, and similarly key presses will not be saved in the history buffer. */ - def alternateScreen[A](f: F ?=> A): A + def alternateScreen[A](f: () => A): A } diff --git a/core/shared/src/main/scala/terminus/effect/ApplicationMode.scala b/core/shared/src/main/scala/terminus/effect/ApplicationMode.scala index a717d89..d6de5eb 100644 --- a/core/shared/src/main/scala/terminus/effect/ApplicationMode.scala +++ b/core/shared/src/main/scala/terminus/effect/ApplicationMode.scala @@ -16,12 +16,12 @@ package terminus.effect -trait ApplicationMode[+F <: Effect] { self: F => +trait ApplicationMode { /** Run the given terminal program `f` in application mode, which changes the * input sent to the program when arrow keys are pressed. See * https://invisible-island.net/xterm/xterm.faq.html#xterm_arrows */ - def application[A](f: F ?=> A): A + def application[A](f: () => A): A } diff --git a/core/shared/src/main/scala/terminus/effect/Color.scala b/core/shared/src/main/scala/terminus/effect/Color.scala index 3244d2a..ad4ad5c 100644 --- a/core/shared/src/main/scala/terminus/effect/Color.scala +++ b/core/shared/src/main/scala/terminus/effect/Color.scala @@ -16,113 +16,113 @@ package terminus.effect -trait Color[+F <: Writer] extends WithStack[F] { self: F => +trait Color extends WithStack { self: Writer => object foreground { private val foregroundStack: Stack = Stack(AnsiCodes.foreground.default) - def default[A](f: F ?=> A): A = + def default[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.default)(f) - def black[A](f: F ?=> A): A = + def black[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.black)(f) - def red[A](f: F ?=> A): A = + def red[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.red)(f) - def green[A](f: F ?=> A): A = + def green[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.green)(f) - def yellow[A](f: F ?=> A): A = + def yellow[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.yellow)(f) - def blue[A](f: F ?=> A): A = + def blue[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.blue)(f) - def magenta[A](f: F ?=> A): A = + def magenta[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.magenta)(f) - def cyan[A](f: F ?=> A): A = + def cyan[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.cyan)(f) - def white[A](f: F ?=> A): A = + def white[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.white)(f) - def brightBlack[A](f: F ?=> A): A = + def brightBlack[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.brightBlack)(f) - def brightRed[A](f: F ?=> A): A = + def brightRed[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.brightRed)(f) - def brightGreen[A](f: F ?=> A): A = + def brightGreen[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.brightGreen)(f) - def brightYellow[A](f: F ?=> A): A = + def brightYellow[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.brightYellow)(f) - def brightBlue[A](f: F ?=> A): A = + def brightBlue[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.brightBlue)(f) - def brightMagenta[A](f: F ?=> A): A = + def brightMagenta[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.brightMagenta)(f) - def brightCyan[A](f: F ?=> A): A = + def brightCyan[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.brightCyan)(f) - def brightWhite[A](f: F ?=> A): A = + def brightWhite[A](f: () => A): A = withStack(foregroundStack, AnsiCodes.foreground.brightWhite)(f) } object background { private val backgroundStack: Stack = Stack(AnsiCodes.background.default) - def default[A](f: F ?=> A): A = + def default[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.default)(f) - def black[A](f: F ?=> A): A = + def black[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.black)(f) - def red[A](f: F ?=> A): A = + def red[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.red)(f) - def green[A](f: F ?=> A): A = + def green[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.green)(f) - def yellow[A](f: F ?=> A): A = + def yellow[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.yellow)(f) - def blue[A](f: F ?=> A): A = + def blue[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.blue)(f) - def magenta[A](f: F ?=> A): A = + def magenta[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.magenta)(f) - def cyan[A](f: F ?=> A): A = + def cyan[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.cyan)(f) - def white[A](f: F ?=> A): A = + def white[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.white)(f) - def brightBlack[A](f: F ?=> A): A = + def brightBlack[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.brightBlack)(f) - def brightRed[A](f: F ?=> A): A = + def brightRed[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.brightRed)(f) - def brightGreen[A](f: F ?=> A): A = + def brightGreen[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.brightGreen)(f) - def brightYellow[A](f: F ?=> A): A = + def brightYellow[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.brightYellow)(f) - def brightBlue[A](f: F ?=> A): A = + def brightBlue[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.brightBlue)(f) - def brightMagenta[A](f: F ?=> A): A = + def brightMagenta[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.brightMagenta)(f) - def brightCyan[A](f: F ?=> A): A = + def brightCyan[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.brightCyan)(f) - def brightWhite[A](f: F ?=> A): A = + def brightWhite[A](f: () => A): A = withStack(backgroundStack, AnsiCodes.background.brightWhite)(f) } } diff --git a/core/shared/src/main/scala/terminus/effect/Format.scala b/core/shared/src/main/scala/terminus/effect/Format.scala index b9b2351..981d491 100644 --- a/core/shared/src/main/scala/terminus/effect/Format.scala +++ b/core/shared/src/main/scala/terminus/effect/Format.scala @@ -17,91 +17,91 @@ package terminus.effect /** Terminal effects that can change character formatting properties. */ -trait Format[+F <: Writer] extends WithStack[F], WithToggle[F] { self: F => +trait Format extends WithStack, WithToggle { self: Writer => object format { // Bold and light share this stack private val fontWeightStack = Stack(AnsiCodes.format.bold.off) - def bold[A](f: F ?=> A): A = + def bold[A](f: () => A): A = withStack(fontWeightStack, AnsiCodes.format.bold.on)(f) - def light[A](f: F ?=> A): A = + def light[A](f: () => A): A = withStack(fontWeightStack, AnsiCodes.format.light.on)(f) /** Normal weight text, neither bold nor light. */ - def normal[A](f: F ?=> A): A = + def normal[A](f: () => A): A = withStack(fontWeightStack, AnsiCodes.format.bold.off)(f) object underline { private val underlineStyleStack = Stack(AnsiCodes.format.underline.off) - def none[A](f: F ?=> A): A = + def none[A](f: () => A): A = withStack(underlineStyleStack, AnsiCodes.format.underline.off)(f) - def straight[A](f: F ?=> A): A = + def straight[A](f: () => A): A = withStack(underlineStyleStack, AnsiCodes.format.underline.straight)(f) - def double[A](f: F ?=> A): A = + def double[A](f: () => A): A = withStack(underlineStyleStack, AnsiCodes.format.underline.double)(f) - def curly[A](f: F ?=> A): A = + def curly[A](f: () => A): A = withStack(underlineStyleStack, AnsiCodes.format.underline.curly)(f) - def dotted[A](f: F ?=> A): A = + def dotted[A](f: () => A): A = withStack(underlineStyleStack, AnsiCodes.format.underline.dotted)(f) - def dashed[A](f: F ?=> A): A = + def dashed[A](f: () => A): A = withStack(underlineStyleStack, AnsiCodes.format.underline.dashed)(f) private val underlineColorStack = Stack( AnsiCodes.format.underline.default ) - def default[A](f: F ?=> A): A = + def default[A](f: () => A): A = withStack(underlineColorStack, AnsiCodes.format.underline.default)(f) - def black[A](f: F ?=> A): A = + def black[A](f: () => A): A = withStack(underlineColorStack, AnsiCodes.format.underline.black)(f) - def red[A](f: F ?=> A): A = + def red[A](f: () => A): A = withStack(underlineColorStack, AnsiCodes.format.underline.red)(f) - def green[A](f: F ?=> A): A = + def green[A](f: () => A): A = withStack(underlineColorStack, AnsiCodes.format.underline.green)(f) - def yellow[A](f: F ?=> A): A = + def yellow[A](f: () => A): A = withStack(underlineColorStack, AnsiCodes.format.underline.yellow)(f) - def blue[A](f: F ?=> A): A = + def blue[A](f: () => A): A = withStack(underlineColorStack, AnsiCodes.format.underline.blue)(f) - def magenta[A](f: F ?=> A): A = + def magenta[A](f: () => A): A = withStack(underlineColorStack, AnsiCodes.format.underline.magenta)(f) - def cyan[A](f: F ?=> A): A = + def cyan[A](f: () => A): A = withStack(underlineColorStack, AnsiCodes.format.underline.cyan)(f) - def white[A](f: F ?=> A): A = + def white[A](f: () => A): A = withStack(underlineColorStack, AnsiCodes.format.underline.white)(f) } private val blinkToggle = Toggle(AnsiCodes.format.blink.on, AnsiCodes.format.blink.off) - def blink[A](f: F ?=> A): A = + def blink[A](f: () => A): A = withToggle(blinkToggle)(f) private val invertToggle = Toggle(AnsiCodes.format.invert.on, AnsiCodes.format.invert.off) - def invert[A](f: F ?=> A): A = + def invert[A](f: () => A): A = withToggle(invertToggle)(f) private val invisibleToggle = Toggle(AnsiCodes.format.invisible.on, AnsiCodes.format.invisible.off) - def invisible[A](f: F ?=> A): A = + def invisible[A](f: () => A): A = withToggle(invisibleToggle)(f) private val strikethroughToggle = Toggle( @@ -109,7 +109,7 @@ trait Format[+F <: Writer] extends WithStack[F], WithToggle[F] { self: F => AnsiCodes.format.strikethrough.off ) - def strikethrough[A](f: F ?=> A): A = + def strikethrough[A](f: () => A): A = withToggle(strikethroughToggle)(f) } } diff --git a/core/shared/src/main/scala/terminus/effect/RawMode.scala b/core/shared/src/main/scala/terminus/effect/RawMode.scala index b51cbb6..1cbd612 100644 --- a/core/shared/src/main/scala/terminus/effect/RawMode.scala +++ b/core/shared/src/main/scala/terminus/effect/RawMode.scala @@ -16,11 +16,11 @@ package terminus.effect -trait RawMode[+F <: Effect] { self: F => +trait RawMode { /** Run the given terminal program `f` in raw mode, which means that the * program can read user input a character at a time. In canonical mode, * which is the default, user input is only available a line at a time. */ - def raw[A](f: F ?=> A): A + def raw[A](f: () => A): A } diff --git a/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala b/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala index 97d7017..b467c17 100644 --- a/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala +++ b/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala @@ -16,8 +16,13 @@ package terminus.effect +import terminus.Eof +import terminus.Key +import terminus.KeyMappings +import terminus.KeySequence +import terminus.Timeout + import scala.annotation.tailrec -import terminus.{Eof, Key, KeyMappings, KeySequence, Timeout} import scala.concurrent.duration.* /** An implementation of KeyReader that interprets the standard terminal escape diff --git a/core/shared/src/main/scala/terminus/effect/WithEffect.scala b/core/shared/src/main/scala/terminus/effect/WithEffect.scala index 39dd666..7ac0938 100644 --- a/core/shared/src/main/scala/terminus/effect/WithEffect.scala +++ b/core/shared/src/main/scala/terminus/effect/WithEffect.scala @@ -16,18 +16,18 @@ package terminus.effect -trait WithEffect[+F <: Writer] { self: F => - protected def withEffect[A](on: String, off: String)(f: F ?=> A): A = { +trait WithEffect { self: Writer => + protected def withEffect[A](on: String, off: String)(f: () => A): A = { write(on) try { - f(using this) + f() } finally { write(off) } } - protected def withEffect[A](on: String)(f: F ?=> A): A = { + protected def withEffect[A](on: String)(f: () => A): A = { write(on) - f(using this) + f() } } diff --git a/core/shared/src/main/scala/terminus/effect/WithStack.scala b/core/shared/src/main/scala/terminus/effect/WithStack.scala index 199fac7..e39b5bd 100644 --- a/core/shared/src/main/scala/terminus/effect/WithStack.scala +++ b/core/shared/src/main/scala/terminus/effect/WithStack.scala @@ -17,15 +17,15 @@ package terminus.effect /** Utility trait for working with `Stack`. */ -trait WithStack[+F <: Writer] { self: F => +trait WithStack { self: Writer => /** Use `withStack` to ensure a stack is pushed on before `f` is evaluated, * and popped when `f` finishes. */ - protected def withStack[A](stack: Stack, code: String)(f: F ?=> A): A = { + protected def withStack[A](stack: Stack, code: String)(f: () => A): A = { stack.push(code, self) try { - f(using this) + f() } finally { stack.pop(self) } diff --git a/core/shared/src/main/scala/terminus/effect/WithToggle.scala b/core/shared/src/main/scala/terminus/effect/WithToggle.scala index e0aeda1..3e1a7fc 100644 --- a/core/shared/src/main/scala/terminus/effect/WithToggle.scala +++ b/core/shared/src/main/scala/terminus/effect/WithToggle.scala @@ -17,15 +17,15 @@ package terminus.effect /** Utility trait for working with `Toggle`. */ -trait WithToggle[+F <: Writer] { self: F => +trait WithToggle { self: Writer => /** Use `withToggle` to ensure a toggle is turned on before `f` is evaluated, * and turned off when `f` finishes. */ - protected def withToggle[A](toggle: Toggle)(f: F ?=> A): A = { + protected def withToggle[A](toggle: Toggle)(f: () => A): A = { toggle.on(self) try { - f(using this) + f() } finally { toggle.off(self) } diff --git a/core/shared/src/main/scala/terminus/example/Prompt.scala b/core/shared/src/main/scala/terminus/example/Prompt.scala index 5492704..0442421 100644 --- a/core/shared/src/main/scala/terminus/example/Prompt.scala +++ b/core/shared/src/main/scala/terminus/example/Prompt.scala @@ -22,8 +22,8 @@ import terminus.effect import scala.annotation.tailrec class Prompt[ - Terminal <: effect.Color[Terminal] & effect.Cursor & - effect.Format[Terminal] & effect.Erase & effect.KeyReader & effect.Writer + Terminal <: effect.Color & effect.Cursor & effect.Format & effect.Erase & + effect.KeyReader & effect.Writer ](terminal: Color & Cursor & Format & Erase & KeyReader & Writer) { type Program[A] = Terminal ?=> A @@ -59,10 +59,11 @@ class Prompt[ throw new Exception("Received an EOF") case key: Key => key match { - case Key(_, KeyCode.Enter) => KeyCode.Enter - case Key(_, KeyCode.Up) => KeyCode.Up - case Key(_, KeyCode.Down) => KeyCode.Down - case other => read() + case Key(_, KeyCode.Enter) => KeyCode.Enter + case Key(_, KeyCode.Character('\n')) => KeyCode.Enter + case Key(_, KeyCode.Up) => KeyCode.Up + case Key(_, KeyCode.Down) => KeyCode.Down + case other => read() } } } diff --git a/core/shared/src/test/scala/terminus/KeyShowSuite.scala b/core/shared/src/test/scala/terminus/KeyShowSuite.scala index c8bae60..48fa0a6 100644 --- a/core/shared/src/test/scala/terminus/KeyShowSuite.scala +++ b/core/shared/src/test/scala/terminus/KeyShowSuite.scala @@ -16,8 +16,8 @@ package terminus -import munit.FunSuite import cats.syntax.show.* +import munit.FunSuite class KeyShowSuite extends FunSuite { test("Show[Key] produces the expected string representation") { diff --git a/core/shared/src/test/scala/terminus/effect/ColorSuite.scala b/core/shared/src/test/scala/terminus/effect/ColorSuite.scala index 547f584..e28452f 100644 --- a/core/shared/src/test/scala/terminus/effect/ColorSuite.scala +++ b/core/shared/src/test/scala/terminus/effect/ColorSuite.scala @@ -25,9 +25,9 @@ class ColorSuite extends FunSuite { ) { val result = StringBuilderTerminal.run { t ?=> - t.foreground.blue { + t.foreground.blue { () => t.write("Blue ") - t.foreground.red { t.write("Red ") } + t.foreground.red { () => t.write("Red ") } t.write("Blue ") } } diff --git a/core/shared/src/test/scala/terminus/effect/FormatSuite.scala b/core/shared/src/test/scala/terminus/effect/FormatSuite.scala index 98ee47c..3477cc7 100644 --- a/core/shared/src/test/scala/terminus/effect/FormatSuite.scala +++ b/core/shared/src/test/scala/terminus/effect/FormatSuite.scala @@ -23,9 +23,9 @@ class FormatSuite extends FunSuite { test("Bold and light stack in nested scopes") { val result = StringBuilderTerminal.run { t ?=> - t.format.bold { + t.format.bold { () => t.write("Bold ") - t.format.light { t.write("Light ") } + t.format.light { () => t.write("Light ") } t.write("Bold ") } } @@ -39,9 +39,9 @@ class FormatSuite extends FunSuite { test("Blink toggles off only when exiting outermost block") { val result = StringBuilderTerminal.run { t ?=> - t.format.blink { + t.format.blink { () => t.write("Blink ") - t.format.blink { t.write("Blink ") } + t.format.blink { () => t.write("Blink ") } t.write("Blink ") } } diff --git a/docs/src/pages/design.md b/docs/src/pages/design.md index 1c9003a..7ba5f9c 100644 --- a/docs/src/pages/design.md +++ b/docs/src/pages/design.md @@ -16,14 +16,18 @@ We want to support differences across terminals, but where common functionality A backend specific `Terminal` type is the union of the interfaces, or effects, that describe the functionality it supports. For example, here is the JVM `Terminal` type at the time of writing: ```scala -trait Terminal - extends effect.Color[Terminal], +type Terminal = + effect.AlternateScreenMode, + effect.ApplicationMode, + effect.Color, effect.Cursor, - effect.Format[Terminal], + effect.Format, + effect.Dimensions, effect.Erase, - effect.AlternateScreenMode[Terminal], - effect.ApplicationMode[Terminal], - effect.RawMode[Terminal], + effect.KeyReader, + effect.NonBlockingReader, + effect.Peeker, + effect.RawMode, effect.Reader, effect.Writer ``` @@ -78,17 +82,25 @@ There are a few things to note: 1. `Erase` extends `Writer`, and uses the `write` method from `Writer`. 2. The methods are name-spaced by putting them inside an object named `erase`. This is aesthetic choice, leading to method calls like `erase.up()` and `cursor.up()` instead of `eraseUp()` and `cursorUp()`. -There is one final kind of effect that is more involved, which is an effect that only applies to some scope. @:api(terminus.effect.Format) is an example, as is @:api(terminues.effect.Color). These all change the way output is formatted, but only for the `Program` they take as a parameter. Here's part of the implementation of `Format`, which is a bit simpler than `Color`. +There is one final kind of effect that is more involved, which is an effect that only applies to some scope. @:api(terminus.effect.Format) is an example, as is @:api(terminus.effect.Color). These all change the way output is formatted, but only for the `Program` they take as a parameter. For example, when we create a program like ```scala -trait Format[+F <: Writer] extends WithStack[F], WithToggle[F] { self: F => +Terminal.invert(Terminal.write("Inverted")) +``` + +we want only the `Program` inside the call to `Terminal.invert` (that `Program` is `Terminal.write("Inverted")`) to be formatted with inverted text. + +Here's part of the implementation of `Format`, which is a bit simpler than `Color`. + +```scala +trait Format extends WithStack, WithToggle { self: Writer => object format { // ... private val invertToggle = Toggle(AnsiCodes.format.invert.on, AnsiCodes.format.invert.off) - def invert[A](f: F ?=> A): A = + def invert[A](f: () => A): A = withToggle(invertToggle)(f) // ... @@ -96,26 +108,27 @@ trait Format[+F <: Writer] extends WithStack[F], WithToggle[F] { self: F => } ``` -When we create a program like +The types look a bit odd. The `invert` method doesn't receive a `Program`, with type `F ?=> A`, but a thunk with type `() => A`. The reason for this is that it's a bit tricky to write down the type of `F` at this point. It's whatever terminal effects the program `f` needs, which must also be the same as the type that the particular instance of `Format` is mixed into. We can define this, using a type parameter on `Format`, like so: ```scala -Terminal.invert(Terminal.write("Inverted")) +trait Format[F <: Writer] extends WithStack[F], WithToggle[F] { self: F => + def invert[A](f: F ?=> A): A = + withToggle(invertToggle)(f(self)) +} ``` -we want only the `Program` inside the call to `Terminal.invert` (that `Program` is `Terminal.write("Inverted")`) to be formatted with inverted text. Inversion is relatively simply, as it is either on or off. This is what the call to `withToggle` does: it sends the code to turn on inverted text when we enter the block and turns it off when we exit. It also handles nested calls correctly. For more complicated cases, like color, there is also a stack abstraction which reverts to the previous setting when a block exits. - -The types deserve some explanation. We call `invert` with some `Program` type. Remember that `Program` is `Terminal ?=> A` and `Terminal` is backend specific. In the `Format` effect we need to actually carry out the effects within the `Program`, so we need to pass an instance of `Terminal` to the `Program`. Where do we get this instance from? It is `this`. - -How do we make sure that `Terminal` is actually of the same type as `this`? Firstly, in `invert` the type of a program is `F ?=> A`, where `F` is a type parameter of `Format`. The meaning of `F` is all the effect types that make up a particular `Terminal` type. If you look at the definition of the JVM terminal above you will see +A previous version of Terminus did this. However this ends up causing problems in other parts of the code. The issue is that `F` ends up a recursive type, which we define like ```scala - effect.Format[Terminal], +Terminal extends Format[Terminal] ``` -so `F` *is* `Terminal`. The [self type](https://docs.scala-lang.org/tour/self-types.html) ensures that instances of `Format` are mixed into the correct type, and hence `this` is `F`. +We can only define recursive types in concrete types like a `trait` as done above. This in turn means that we cannot write down the types of programs that work across multiple backends, unless we can arrange in advance for all backends to implement some common type. This goes against the idea of different backends implementing only the features they support. + +Our solution is to push the `F` type, and the application to the `Program`, into what we call program syntax. What `invert` receives is a thunk that wraps up the application of `Program` to `F`. Let's look at this "syntax" now. -## Implementing Programs +## Program Syntax Now let's look at how the `Program` types are implemented. We'll start with @:api(terminus.Writer), as it is very simple. @@ -139,6 +152,8 @@ trait Writer { Firstly, notice this is the `Writer` *program* **not** the `Writer` effect. They are separate concepts. A `Program` is just `Terminal ?=> A`. In the specific case of `Writer` these programs simply require a `Writer` effect and then call the appropriate method on that effect. +These definitions are very simple, and the user could write them themselves. However, it makes life easier if we provide these definitions for them. This is why we call this "program syntax"; we're providing some syntax to make writing programs easier. + `Format` is a bit more complex. Here's the definition, with most of the methods removed. ```scala @@ -147,17 +162,25 @@ trait Format { // ... - def invert[F <: effect.Writer, A]( + def invert[F, A]( f: F ?=> A ): (F & effect.Format[F]) ?=> A = - effect ?=> effect.format.invert(f) + effect ?=> effect.format.invert(() => f(using effect)) // ... } } ``` -`invert` wraps a `Program` with another `Program`. What is the type of the inner program, the method argument `f`? It is whatever effects it requires to run, with the constraint that these effects much include the `Writer` effect. This is represented by the type parameter `F`. The result of calling `invert` is another program that requires all the effects of the argument `f` *and* the `Format` effect. In this way programs are constructed to require only the effects they need to run. So long as we apply them to a concrete `Terminal` type that is a super-type of these effects they can be run. +`invert` wraps a `Program` with another `Program`. What is the type of the inner program, the method argument `f`? It is whatever effects it requires to run, represented by the type parameter `F`. The result of calling `invert` is another program that requires all the effects of the argument `f` *and* the `Format` effect. In this way programs are constructed to require only the effects they need to run. So long as we apply them to a concrete `Terminal` type that is a super-type of these effects they can be run. At the point of use all these types are known and type inference is easy. + +Now the bit that ties into the definition of the `Format` effect above. Notice in the body of `invert` we construct the thunk + +```scala +() => f(using effect) +``` + +This applies `f` to the instance of the effect type `F`, but by wrapping it in a thunk it is delayed until the `invert` effect chooses to run it—which is after the inversion escape code has been sent to the terminal. This gives the effect full control over order-of-evaluation, but still keeps type inference simple. Hence the effect types and the program syntax work together to implement a system that is correct, usable, and has simple types. ## Low-level Code diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 4863ded..f27322d 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -10,13 +10,13 @@ object Dependencies { val fs2Version = "3.11.0" val scalatagsVersion = "0.13.1" - val scalajsDomVersion = "2.8.0" + val scalajsDomVersion = "2.8.1" - val jlineVersion = "3.29.0" + val jlineVersion = "4.0.12" val scalaCheckVersion = "1.15.4" - val munitVersion = "1.1.0" - val munitScalacheckVersion = "1.1.0" + val munitVersion = "1.3.0" + val munitScalacheckVersion = "1.3.0" val munitCatsEffectVersion = "2.0.0" // Libraries diff --git a/project/build.properties b/project/build.properties index e88a0d8..08a6fc0 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.6 +sbt.version=1.12.8 diff --git a/project/plugins.sbt b/project/plugins.sbt index c237a3b..31e80e0 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,9 +1,9 @@ -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.21.0") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.2") addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.7") addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.7.7") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.7") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.11") addSbtPlugin("org.creativescala" % "creative-scala-theme" % "0.5.2")