├── .editorconfig ├── .git-blame-ignore-revs ├── .github └── workflows │ └── scala.yml ├── .gitignore ├── .gitmodules ├── .jvmopts ├── .scala-steward.conf ├── .scalafmt.conf ├── LICENSE ├── README.md ├── application.conf ├── assets └── recording.gif ├── bin └── cc-tools ├── build.sbt ├── cli └── src │ └── main │ └── scala │ └── commandcenter │ └── cli │ ├── CliArgs.scala │ ├── CliCommand.scala │ ├── Main.scala │ └── message │ └── CCRequest.scala ├── core-ui └── src │ └── main │ └── scala │ └── commandcenter │ ├── ui │ ├── CCTheme.scala │ ├── CliTerminal.scala │ ├── Cursor.scala │ ├── EventResult.scala │ └── RenderTrigger.scala │ └── util │ └── TextUtils.scala ├── core └── src │ ├── main │ ├── resources │ │ ├── applescript │ │ │ ├── finder │ │ │ │ ├── pwd.applescript │ │ │ │ └── selected-files.applescript │ │ │ ├── itunes │ │ │ │ ├── adjust-volume.applescript │ │ │ │ ├── delete-track.applescript │ │ │ │ ├── next-track.applescript │ │ │ │ ├── pause.applescript │ │ │ │ ├── play.applescript │ │ │ │ ├── previous-track.applescript │ │ │ │ ├── rate.applescript │ │ │ │ ├── seek.applescript │ │ │ │ ├── stop.applescript │ │ │ │ └── track-details.applescript │ │ │ └── system │ │ │ │ └── notify.applescript │ │ ├── audio │ │ │ ├── button-switch-off.wav │ │ │ └── button-switch-on.wav │ │ ├── lipsum │ │ └── powershell │ │ │ └── system │ │ │ ├── notify.ps1 │ │ │ └── speak.ps1 │ └── scala │ │ └── commandcenter │ │ ├── CCConfig.scala │ │ ├── CCLogging.scala │ │ ├── CCRuntime.scala │ │ ├── CCTerminal.scala │ │ ├── CommandContext.scala │ │ ├── Conf.scala │ │ ├── HeadlessTerminal.scala │ │ ├── Sttp.scala │ │ ├── TerminalType.scala │ │ ├── cache │ │ └── ZCache.scala │ │ ├── codec │ │ └── Codecs.scala │ │ ├── command │ │ ├── CalculatorCommand.scala │ │ ├── Command.scala │ │ ├── CommandError.scala │ │ ├── CommandInput.scala │ │ ├── CommandPlugin.scala │ │ ├── CommandType.scala │ │ ├── CommonArgs.scala │ │ ├── CommonOpts.scala │ │ ├── ConfigCommand.scala │ │ ├── DecodeBase64Command.scala │ │ ├── DecodeUrlCommand.scala │ │ ├── EncodeBase64Command.scala │ │ ├── EncodeUrlCommand.scala │ │ ├── EpochMillisCommand.scala │ │ ├── EpochUnixCommand.scala │ │ ├── ExitCommand.scala │ │ ├── ExternalIPCommand.scala │ │ ├── FileNavigationCommand.scala │ │ ├── FindFileCommand.scala │ │ ├── FindInFileCommand.scala │ │ ├── Foobar2000Command.scala │ │ ├── HashCommand.scala │ │ ├── HelpMessage.scala │ │ ├── HoogleCommand.scala │ │ ├── HttpCommand.scala │ │ ├── ITunesCommand.scala │ │ ├── LocalIPCommand.scala │ │ ├── LockCommand.scala │ │ ├── LoremIpsumCommand.scala │ │ ├── MoreResults.scala │ │ ├── MuteCommand.scala │ │ ├── OpacityCommand.scala │ │ ├── OpenBrowserCommand.scala │ │ ├── PreviewResult.scala │ │ ├── PreviewResults.scala │ │ ├── ProcessIdCommand.scala │ │ ├── RadixCommand.scala │ │ ├── RebootCommand.scala │ │ ├── ReloadCommand.scala │ │ ├── ResizeCommand.scala │ │ ├── RunError.scala │ │ ├── RunOption.scala │ │ ├── Scores.scala │ │ ├── SearchCratesCommand.scala │ │ ├── SearchInput.scala │ │ ├── SearchMavenCommand.scala │ │ ├── SearchResults.scala │ │ ├── SearchUrlCommand.scala │ │ ├── SnippetsCommand.scala │ │ ├── SpeakCommand.scala │ │ ├── SuspendProcessCommand.scala │ │ ├── SwitchWindowCommand.scala │ │ ├── SystemCommand.scala │ │ ├── TemperatureCommand.scala │ │ ├── TerminalCommand.scala │ │ ├── TimerCommand.scala │ │ ├── ToggleDesktopIconsCommand.scala │ │ ├── ToggleHiddenFilesCommand.scala │ │ ├── UUIDCommand.scala │ │ ├── WorldTimesCommand.scala │ │ ├── native │ │ │ └── win │ │ │ │ └── PowrProf.scala │ │ └── util │ │ │ ├── CalculatorUtil.scala │ │ │ ├── HashUtil.scala │ │ │ └── PathUtil.scala │ │ ├── config │ │ ├── ConfigParserExtensions.scala │ │ └── Decoders.scala │ │ ├── event │ │ ├── KeyCode.scala │ │ ├── KeyModifier.scala │ │ └── KeyboardShortcut.scala │ │ ├── locale │ │ ├── JapaneseText.scala │ │ ├── KoreanText.scala │ │ └── Language.scala │ │ ├── scorers │ │ └── LengthScorer.scala │ │ ├── shortcuts │ │ └── Shortcuts.scala │ │ ├── tools │ │ ├── Tools.scala │ │ └── ToolsLive.scala │ │ ├── util │ │ ├── AppleScript.scala │ │ ├── CollectionExtensions.scala │ │ ├── Debouncer.scala │ │ ├── JavaVM.scala │ │ ├── NtApi.java │ │ ├── OS.scala │ │ ├── Orderings.scala │ │ ├── PowerShellScript.scala │ │ ├── ProcessUtil.scala │ │ ├── StringExtensions.scala │ │ ├── TTS.scala │ │ ├── TimeZones.scala │ │ └── WindowManager.scala │ │ └── view │ │ ├── Rendered.scala │ │ ├── Renderer.scala │ │ └── Style.scala │ └── test │ └── scala │ └── commandcenter │ ├── CommandBaseSpec.scala │ ├── ConfigFake.scala │ ├── SearchSpec.scala │ ├── TestTerminal.scala │ ├── cache │ └── CacheSpec.scala │ ├── command │ └── EpochMillisCommandSpec.scala │ ├── locale │ └── LanguageSpec.scala │ └── util │ └── DebouncerSpec.scala ├── emulator-core └── src │ └── main │ └── scala │ ├── com │ └── tulskiy │ │ └── keymaster │ │ └── windows │ │ └── WindowsProvider.java │ └── commandcenter │ ├── BaseGuiTerminal.scala │ ├── GlobalActions.scala │ ├── TerminalBuffer.scala │ └── emulator │ └── util │ └── Lists.scala ├── emulator-swing ├── logs │ └── application.log ├── plugins │ └── PLUGINS.md └── src │ └── main │ └── scala │ └── commandcenter │ └── emulator │ └── swing │ ├── Main.scala │ ├── event │ └── KeyboardShortcutUtil.scala │ ├── shortcuts │ └── ShortcutsLive.scala │ └── ui │ ├── SwingTerminal.scala │ ├── ZKeyListener.scala │ └── ZTextField.scala ├── emulator-swt └── src │ └── main │ └── scala │ └── commandcenter │ └── emulator │ └── swt │ ├── Main.scala │ ├── event │ ├── KeyEventExtensions.scala │ └── KeyboardShortcutUtil.scala │ ├── shortcuts │ └── ShortcutsLive.scala │ └── ui │ ├── RawSwtTerminal.scala │ └── SwtTerminal.scala ├── extras ├── ject │ └── src │ │ └── main │ │ └── scala │ │ └── commandcenter │ │ └── ject │ │ ├── JectJaCommand.scala │ │ ├── JectKoCommand.scala │ │ └── KanjiCommand.scala └── stroke-order │ └── src │ └── main │ └── scala │ └── commandcenter │ └── strokeorder │ └── StrokeOrderCommand.scala ├── project ├── Build.scala ├── OS.scala ├── Plugins.scala ├── V.scala ├── build.properties └── plugins.sbt └── tools ├── README.md └── macos └── main.swift /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.5.9 2 | 2741126a3555581cc7ca3067d5c40b4dc44e5bcf 3 | 4 | # Scala Steward: Reformat with scalafmt 3.7.17 5 | 6ccadb1db4a791675fd1bfd004a7731fe8a37dcc 6 | 7 | # Scala Steward: Reformat with scalafmt 3.8.0 8 | d389cdf8e339e8e21189a683e6103d75d0d94e58 9 | 10 | # Scala Steward: Reformat with scalafmt 3.8.3 11 | b9235b8d43679d849bf30f0c46f7b4b8efe72213 12 | 13 | # Scala Steward: Reformat with scalafmt 3.8.5 14 | d49db9de01db82ff4bd963665d6c1ec109b6d32f 15 | 16 | # Scala Steward: Reformat with scalafmt 3.9.7 17 | dec4ae0aaa56b5b2b8d7dfc270d9f74d97c15137 18 | -------------------------------------------------------------------------------- /.github/workflows/scala.yml: -------------------------------------------------------------------------------- 1 | name: Scala CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up JDK 21 17 | uses: actions/setup-java@v4 18 | with: 19 | distribution: temurin 20 | java-version: 21 21 | cache: sbt 22 | 23 | - uses: sbt/setup-sbt@v1 24 | 25 | - name: Run tests 26 | run: sbt -Dscalac.lenientDev.enabled=false fmtCheck test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea 3 | .idea_modules 4 | .bloop 5 | .bsp 6 | .metals 7 | /.classpath 8 | /.project 9 | /.settings 10 | /RUNNING_PID 11 | /out/ 12 | *.iws 13 | *.iml 14 | /db 15 | .eclipse 16 | /lib/ 17 | /logs/ 18 | /modules 19 | tmp/ 20 | test-result 21 | server.pid 22 | *.eml 23 | /dist/ 24 | .cache 25 | /reference 26 | local.conf 27 | /logs 28 | **/plugins/*.jar 29 | node_modules 30 | Scratch.scala 31 | Playground.scala 32 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "extras/experimental"] 2 | path = extras/experimental 3 | url = https://github.com/reibitto/command-center-extras.git 4 | -------------------------------------------------------------------------------- /.jvmopts: -------------------------------------------------------------------------------- 1 | -Dfile.encoding=UTF-8 2 | -Xmx4g 3 | -XX:MaxMetaspaceSize=1g 4 | -XX:MaxInlineLevel=20 5 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | updates.includeScala = true 2 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.9.7 2 | 3 | runner.dialect = scala213source3 4 | 5 | align = none 6 | maxColumn = 120 # For wide displays. 7 | assumeStandardLibraryStripMargin = true 8 | align.tokens = [{code = "=>", owner = "Case"}, {code = "<-"}] 9 | align.allowOverflow = true 10 | align.arrowEnumeratorGenerator = true 11 | align.openParenCallSite = false 12 | align.openParenDefnSite = false 13 | newlines.topLevelStatementBlankLines = [ 14 | {blanks {before = 1}} 15 | {regex = "^Import|^Term.ApplyInfix"} 16 | ] 17 | newlines.alwaysBeforeElseAfterCurlyIf = false 18 | indentOperator.topLevelOnly = true 19 | docstrings.style = SpaceAsterisk 20 | docstrings.wrapMaxColumn = 80 21 | 22 | includeCurlyBraceInSelectChains = false 23 | includeNoParensInSelectChains = false 24 | 25 | rewrite.rules = [SortModifiers, PreferCurlyFors, SortImports, RedundantBraces] 26 | rewrite.scala3.convertToNewSyntax = true 27 | rewrite.imports.groups = [ 28 | [".*"], 29 | ["java\\..*", "javax\\..*", "scala\\..*"] 30 | ] 31 | 32 | project.excludeFilters = ["/target/"] 33 | 34 | lineEndings = preserve 35 | 36 | fileOverride { 37 | "glob:**/*.sbt" { 38 | runner.dialect = sbt1 39 | rewrite.scala3.convertToNewSyntax = false 40 | } 41 | "glob:**/project/*.scala" { 42 | rewrite.scala3.convertToNewSyntax = false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /assets/recording.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reibitto/command-center/3f4615c8872a862344a80a31701802dfa05c066d/assets/recording.gif -------------------------------------------------------------------------------- /bin/cc-tools: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reibitto/command-center/3f4615c8872a862344a80a31701802dfa05c066d/bin/cc-tools -------------------------------------------------------------------------------- /cli/src/main/scala/commandcenter/cli/CliArgs.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.cli 2 | 3 | import com.monovore.decline 4 | import com.monovore.decline.{Command, Opts} 5 | 6 | object CliArgs { 7 | val versionCommand = decline.Command("version", "Print the current version")(Opts(CliCommand.Version)) 8 | val helpCommand = decline.Command("help", "Display usage help")(Opts(CliCommand.Help)) 9 | 10 | val opts: Opts[CliCommand] = 11 | Opts.subcommand(versionCommand) orElse 12 | Opts.subcommand(helpCommand) withDefault CliCommand.Standalone 13 | 14 | val rootCommand: Command[CliCommand] = decline.Command("commandcenter", "Command Center commands")(opts) 15 | } 16 | -------------------------------------------------------------------------------- /cli/src/main/scala/commandcenter/cli/CliCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.cli 2 | 3 | sealed trait CliCommand 4 | 5 | object CliCommand { 6 | case object Standalone extends CliCommand 7 | case object Help extends CliCommand 8 | case object Version extends CliCommand 9 | } 10 | -------------------------------------------------------------------------------- /cli/src/main/scala/commandcenter/cli/Main.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.cli 2 | 3 | import commandcenter.* 4 | import commandcenter.command.* 5 | import commandcenter.shortcuts.Shortcuts 6 | import commandcenter.tools.ToolsLive 7 | import commandcenter.ui.{CliTerminal, EventResult} 8 | import commandcenter.CCRuntime.Env 9 | import zio.* 10 | import zio.stream.ZStream 11 | import zio.Console.printLine 12 | 13 | object Main extends ZIOApp { 14 | 15 | override type Environment = Env 16 | 17 | val environmentTag: EnvironmentTag[Environment] = EnvironmentTag[Environment] 18 | 19 | override def bootstrap: ZLayer[Any, Any, Environment] = ZLayer.make[Environment]( 20 | ConfigLive.layer, 21 | Shortcuts.unsupported, 22 | ToolsLive.make, 23 | SttpLive.make, 24 | Runtime.removeDefaultLoggers >>> CCLogging.addLoggerFor(TerminalType.Cli), 25 | Runtime.setUnhandledErrorLogLevel(LogLevel.Warning) 26 | ) 27 | 28 | def uiLoop(config: CCConfig): RIO[Scope & Env, Unit] = 29 | for { 30 | terminal <- CliTerminal.createNative 31 | _ <- terminal.keyHandlersRef.set(terminal.defaultKeyHandlers) 32 | _ <- ZIO.attempt(terminal.screen.startScreen()) 33 | _ <- terminal.render(SearchResults.empty) 34 | _ <- ZStream 35 | .fromQueue(terminal.renderQueue) 36 | .foreach(terminal.render) 37 | .forkDaemon 38 | _ <- terminal 39 | .processEvent(config.commands, config.aliases) 40 | .repeatWhile { 41 | case EventResult.Exit => 42 | false 43 | 44 | case EventResult.UnexpectedError(t) => 45 | // TODO: Log error. Either to file or in an error PreviewResult as to not ruin the CLI GUI layout. 46 | true 47 | 48 | case EventResult.Success | EventResult.RemainOpen => 49 | true 50 | } 51 | } yield () 52 | 53 | def run: ZIO[ZIOAppArgs & Scope & Environment, Any, ExitCode] = 54 | (for { 55 | args <- ZIOAppArgs.getArgs 56 | config <- Conf.load 57 | _ <- CliArgs.rootCommand 58 | .parse(args) 59 | .fold( 60 | help => printLine(help.toString), 61 | { 62 | case CliCommand.Standalone => 63 | uiLoop(config) 64 | 65 | case CliCommand.Help => 66 | printLine(CliArgs.rootCommand.showHelp) 67 | 68 | case CliCommand.Version => 69 | printLine(s"Command Center CLI v${commandcenter.BuildInfo.version}") 70 | } 71 | ) 72 | } yield ExitCode.success).tapErrorCause { t => 73 | ZIO.succeed(t.squash.printStackTrace()).exitCode 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /cli/src/main/scala/commandcenter/cli/message/CCRequest.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.cli.message 2 | 3 | import commandcenter.codec.Codecs 4 | import io.circe.* 5 | 6 | sealed trait CCRequest { 7 | def correlationId: Option[String] 8 | } 9 | 10 | object CCRequest { 11 | final case class Search(term: String, correlationId: Option[String]) extends CCRequest 12 | 13 | object Search { 14 | implicit val decoder: Decoder[Search] = Decoder.forProduct2("term", "correlationId")(Search.apply) 15 | implicit val encoder: Encoder[Search] = Encoder.forProduct2("term", "correlationId")(s => (s.term, s.correlationId)) 16 | } 17 | 18 | final case class Run(index: Int, correlationId: Option[String]) extends CCRequest 19 | 20 | object Run { 21 | implicit val decoder: Decoder[Run] = Decoder.forProduct2("index", "correlationId")(Run.apply) 22 | implicit val encoder: Encoder[Run] = Encoder.forProduct2("index", "correlationId")(s => (s.index, s.correlationId)) 23 | } 24 | 25 | final case class Exit(correlationId: Option[String]) extends CCRequest 26 | 27 | object Exit { 28 | implicit val decoder: Decoder[Exit] = Decoder.forProduct1("correlationId")(Exit.apply) 29 | implicit val encoder: Encoder[Exit] = Encoder.forProduct1("correlationId")(s => s.correlationId) 30 | } 31 | 32 | implicit val decoder: Decoder[CCRequest] = Codecs.decodeSumBySoleKey { 33 | case ("search", c) => c.as[Search] 34 | case ("run", c) => c.as[Run] 35 | case ("exit", c) => c.as[Exit] 36 | } 37 | 38 | implicit val encoder: Encoder[CCRequest] = Encoder.instance { 39 | case a: Search => Search.encoder(a) 40 | case a: Run => Run.encoder(a) 41 | case a: Exit => Exit.encoder(a) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /core-ui/src/main/scala/commandcenter/ui/CCTheme.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.ui 2 | 3 | import java.awt.Color 4 | 5 | // TODO: Add scrollbar colors 6 | final case class CCTheme( 7 | background: Color, 8 | foreground: Color, 9 | black: Color, 10 | red: Color, 11 | green: Color, 12 | yellow: Color, 13 | blue: Color, 14 | magenta: Color, 15 | cyan: Color, 16 | lightGray: Color, 17 | darkGray: Color, 18 | lightRed: Color, 19 | lightGreen: Color, 20 | lightYellow: Color, 21 | lightBlue: Color, 22 | lightMagenta: Color, 23 | lightCyan: Color, 24 | white: Color 25 | ) { 26 | 27 | def fromFansiColorCode(colorCode: Int): Option[Color] = 28 | colorCode match { 29 | case 1 => Some(black) 30 | case 2 => Some(red) 31 | case 3 => Some(green) 32 | case 4 => Some(yellow) 33 | case 5 => Some(blue) 34 | case 6 => Some(magenta) 35 | case 7 => Some(cyan) 36 | case 8 => Some(lightGray) 37 | case 9 => Some(darkGray) 38 | case 10 => Some(lightRed) 39 | case 11 => Some(lightGreen) 40 | case 12 => Some(lightYellow) 41 | case 13 => Some(lightBlue) 42 | case 14 => Some(lightMagenta) 43 | case 15 => Some(lightCyan) 44 | case 16 => Some(white) 45 | case _ => None 46 | } 47 | } 48 | 49 | object CCTheme { 50 | 51 | val default: CCTheme = CCTheme( 52 | new Color(0x0f111a), 53 | new Color(198, 198, 198), 54 | new Color(0x0f111a), 55 | new Color(236, 91, 57), 56 | new Color(122, 202, 107), 57 | new Color(245, 218, 55), 58 | new Color(66, 142, 255), 59 | new Color(135, 129, 211), 60 | new Color(22, 180, 236), 61 | new Color(100, 100, 100), 62 | new Color(50, 50, 50), 63 | new Color(255, 135, 119), 64 | new Color(165, 222, 153), 65 | new Color(255, 236, 131), 66 | new Color(75, 149, 255), 67 | new Color(135, 129, 211), 68 | new Color(111, 214, 255), 69 | new Color(209, 209, 209) 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /core-ui/src/main/scala/commandcenter/ui/Cursor.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.ui 2 | 3 | final case class Cursor(column: Int, row: Int) { 4 | def +(other: Cursor): Cursor = Cursor(column + other.column, row + other.row) 5 | 6 | def -(other: Cursor): Cursor = Cursor(column - other.column, row - other.row) 7 | } 8 | 9 | object Cursor { 10 | val unit: Cursor = Cursor(0, 0) 11 | } 12 | 13 | /** Represents a text cursor position. There is the concept of a "logical" 14 | * position and an "actual" one. A CJK char can be "wide" meaning it takes up 2 15 | * width units rather than 1. For example, after typing あ the cursor will 16 | * advance 1 logical unit and 2 actual units. 17 | */ 18 | final case class TextCursor(logical: Cursor, actual: Cursor) { 19 | 20 | def offsetColumnBy(logicalAmount: Int, actualAmount: Int): TextCursor = 21 | copy( 22 | logical.copy(column = logical.column + logicalAmount), 23 | actual.copy(column = actual.column + actualAmount) 24 | ) 25 | } 26 | 27 | object TextCursor { 28 | val unit: TextCursor = TextCursor(Cursor.unit, Cursor.unit) 29 | } 30 | -------------------------------------------------------------------------------- /core-ui/src/main/scala/commandcenter/ui/EventResult.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.ui 2 | 3 | sealed trait EventResult 4 | 5 | object EventResult { 6 | case object Success extends EventResult 7 | case object RemainOpen extends EventResult 8 | case object Exit extends EventResult 9 | final case class UnexpectedError(throwable: Throwable) extends EventResult 10 | } 11 | -------------------------------------------------------------------------------- /core-ui/src/main/scala/commandcenter/ui/RenderTrigger.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.ui 2 | 3 | sealed trait RenderTrigger 4 | 5 | object RenderTrigger { 6 | case object InputChange extends RenderTrigger 7 | case object WindowResize extends RenderTrigger 8 | } 9 | -------------------------------------------------------------------------------- /core-ui/src/main/scala/commandcenter/util/TextUtils.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.util 2 | 3 | import com.googlecode.lanterna.TerminalTextUtils 4 | 5 | object TextUtils { 6 | 7 | def charWidth(c: Char): Int = 8 | if (TerminalTextUtils.isCharDoubleWidth(c)) 2 else 1 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/resources/applescript/finder/pwd.applescript: -------------------------------------------------------------------------------- 1 | tell application "Finder" 2 | set current_window to window 1 3 | return (POSIX path of (target of current_window as alias)) 4 | end tell -------------------------------------------------------------------------------- /core/src/main/resources/applescript/finder/selected-files.applescript: -------------------------------------------------------------------------------- 1 | tell application "Finder" 2 | set selectedItems to selection 3 | set paths to "" 4 | repeat with selectedItem in selectedItems 5 | set paths to (paths & (POSIX path of (selectedItem as text)) & "\n") 6 | end repeat 7 | 8 | return paths 9 | end tell -------------------------------------------------------------------------------- /core/src/main/resources/applescript/itunes/adjust-volume.applescript: -------------------------------------------------------------------------------- 1 | tell application "iTunes" 2 | set sound volume to (sound volume + {0}) 3 | end tell -------------------------------------------------------------------------------- /core/src/main/resources/applescript/itunes/delete-track.applescript: -------------------------------------------------------------------------------- 1 | global addenda 2 | 3 | tell application "iTunes" 4 | set is_playing to player state is not stopped 5 | 6 | #if player state is not stopped then 7 | #display dialog "Are you SURE you want to delete every copy of the currently playing track and move its file to the Trash?" default button 1 with icon 1 8 | set ofi to fixed indexing 9 | set fixed indexing to true 10 | 11 | try 12 | set dbid to database ID of current track 13 | set cla to class of current track 14 | try 15 | set floc to (get location of current track) 16 | end try 17 | try 18 | delete (some track of library playlist 1 whose database ID is dbid) 19 | end try 20 | set addenda to "Done. The track has been deleted." 21 | if cla is file track then 22 | #set my addenda to "Done. The track has been deleted and its file has been moved to the Trash." 23 | set addenda to null 24 | my delete_the_file(floc) 25 | end if 26 | on error 27 | set addenda to "The track could not be deleted." 28 | end try 29 | 30 | set fixed indexing to ofi 31 | 32 | if addenda is not null then 33 | display dialog addenda buttons {"Thanks"} default button 1 with icon 1 34 | end if 35 | #end if 36 | 37 | if is_playing then 38 | tell application "iTunes" to play 39 | end if 40 | end tell 41 | 42 | to delete_the_file(floc) 43 | try 44 | -- tell application "Finder" to delete floc 45 | do shell script "mv " & quoted form of POSIX path of (floc as string) & " " & quoted form of POSIX path of (path to trash as string) 46 | on error 47 | set addenda to "Done. However, the file could not be moved to the Trash." 48 | end try 49 | end delete_the_file -------------------------------------------------------------------------------- /core/src/main/resources/applescript/itunes/next-track.applescript: -------------------------------------------------------------------------------- 1 | tell application "iTunes" to next track -------------------------------------------------------------------------------- /core/src/main/resources/applescript/itunes/pause.applescript: -------------------------------------------------------------------------------- 1 | tell application "iTunes" to pause -------------------------------------------------------------------------------- /core/src/main/resources/applescript/itunes/play.applescript: -------------------------------------------------------------------------------- 1 | tell application "iTunes" to play -------------------------------------------------------------------------------- /core/src/main/resources/applescript/itunes/previous-track.applescript: -------------------------------------------------------------------------------- 1 | tell application "iTunes" to previous track -------------------------------------------------------------------------------- /core/src/main/resources/applescript/itunes/rate.applescript: -------------------------------------------------------------------------------- 1 | tell application "System Events" 2 | if (name of processes) contains "iTunes" then 3 | set iTunesRunning to true 4 | else 5 | set iTunesRunning to false 6 | end if 7 | end tell 8 | 9 | if iTunesRunning then 10 | tell application "iTunes" 11 | set rating of current track to {0} * 20 12 | end tell 13 | end if -------------------------------------------------------------------------------- /core/src/main/resources/applescript/itunes/seek.applescript: -------------------------------------------------------------------------------- 1 | tell application "iTunes" 2 | try 3 | set new_position to (get player position) + {0} 4 | 5 | if new_position < 0 then 6 | set new_position to 0 7 | end if 8 | 9 | set player position to new_position 10 | end try 11 | end tell 12 | -------------------------------------------------------------------------------- /core/src/main/resources/applescript/itunes/stop.applescript: -------------------------------------------------------------------------------- 1 | tell application "iTunes" to stop -------------------------------------------------------------------------------- /core/src/main/resources/applescript/itunes/track-details.applescript: -------------------------------------------------------------------------------- 1 | tell application "iTunes" 2 | set currentTrack to the current track 3 | 4 | if exists name of currentTrack then 5 | -- TODO: Escape tabs 6 | return (name of currentTrack) & "\t" & (artist of currentTrack) & "\t" & (album of currentTrack) & "\t" & (rating of currentTrack) 7 | end if 8 | end tell -------------------------------------------------------------------------------- /core/src/main/resources/applescript/system/notify.applescript: -------------------------------------------------------------------------------- 1 | display notification "{0}" with title "{1}" -------------------------------------------------------------------------------- /core/src/main/resources/audio/button-switch-off.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reibitto/command-center/3f4615c8872a862344a80a31701802dfa05c066d/core/src/main/resources/audio/button-switch-off.wav -------------------------------------------------------------------------------- /core/src/main/resources/audio/button-switch-on.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reibitto/command-center/3f4615c8872a862344a80a31701802dfa05c066d/core/src/main/resources/audio/button-switch-on.wav -------------------------------------------------------------------------------- /core/src/main/resources/lipsum: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus iaculis consequat accumsan. Duis ipsum odio, tincidunt in vulputate ac, blandit vitae est. Morbi malesuada imperdiet massa, vitae luctus velit. Donec vulputate et lectus sed porta. Vestibulum eget tellus elementum, pharetra libero malesuada, luctus lectus. Quisque vitae diam sapien. Nulla imperdiet nisi sed nunc gravida, non mattis nisi imperdiet. Maecenas ac vulputate orci. Aliquam et velit id odio fringilla dictum. Duis ut neque vitae quam semper ultrices lobortis a justo. Vivamus sit amet molestie odio, sit amet faucibus justo. Etiam in quam vestibulum, dapibus quam vel, egestas magna. Aliquam vestibulum purus in congue vestibulum. Suspendisse a augue augue. -------------------------------------------------------------------------------- /core/src/main/resources/powershell/system/notify.ps1: -------------------------------------------------------------------------------- 1 | Add-Type -AssemblyName System.Windows.Forms 2 | $notification = New-Object System.Windows.Forms.NotifyIcon 3 | $notification.Icon = [System.Drawing.SystemIcons]::Information 4 | $notification.BalloonTipIcon = 'Info' 5 | $notification.BalloonTipText = '{0}' 6 | $notification.BalloonTipTitle = '{1}' 7 | $notification.Visible = $True 8 | $notification.ShowBalloonTip(1000) -------------------------------------------------------------------------------- /core/src/main/resources/powershell/system/speak.ps1: -------------------------------------------------------------------------------- 1 | Add-Type -AssemblyName System.Speech; 2 | $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; 3 | $speak.SelectVoice('{0}'); 4 | $speak.Speak('{1}'); 5 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/CCLogging.scala: -------------------------------------------------------------------------------- 1 | package commandcenter 2 | 3 | import zio.* 4 | import zio.logging.* 5 | import zio.logging.LogFilter.LogLevelByNameConfig 6 | import zio.logging.LogFormat.* 7 | 8 | import java.time.format.DateTimeFormatter 9 | 10 | object CCLogging { 11 | 12 | val coloredFormat: LogFormat = 13 | timestamp(DateTimeFormatter.ofPattern("H:mm:ss.SSS")).color(LogColor.BLUE) |-| 14 | level.highlight |-| 15 | fiberId.color(LogColor(fansi.Color.DarkGray.escape)) |-| 16 | (LogFormat.enclosingClass + LogFormat.text(":") + LogFormat.traceLine).color(LogColor.MAGENTA) |-| 17 | line.highlight |-| 18 | cause.highlight.filter(LogFilter.causeNonEmpty) 19 | 20 | def addLoggerFor(terminalType: TerminalType): ZLayer[Any, Nothing, Unit] = 21 | terminalType match { 22 | case TerminalType.Cli => ZLayer.empty.unit // TODO: File logging 23 | 24 | case TerminalType.Swing | TerminalType.Swt => 25 | val defaultLogLevel = LogLevel.Info 26 | 27 | ZLayer.scopedEnvironment( 28 | for { 29 | logLevelString <- System 30 | .envOrElse("COMMAND_CENTER_LOG_LEVEL", defaultLogLevel.label) 31 | .catchAllCause(t => ZIO.debug(t.prettyPrint).as(defaultLogLevel.label)) 32 | logLevel <- ZIO.fromOption(LogLevel.levels.find(_.label.equalsIgnoreCase(logLevelString))).catchAll { _ => 33 | ZIO 34 | .debug(s"Could not find log level: `$logLevelString`. Defaulting to $defaultLogLevel") 35 | .as(defaultLogLevel) 36 | } 37 | env <- consoleLogger(ConsoleLoggerConfig(coloredFormat, LogLevelByNameConfig(logLevel))).build 38 | } yield env 39 | ) 40 | 41 | case TerminalType.Test => ZLayer.empty.unit 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/CCRuntime.scala: -------------------------------------------------------------------------------- 1 | package commandcenter 2 | 3 | import commandcenter.shortcuts.Shortcuts 4 | import commandcenter.tools.Tools 5 | 6 | object CCRuntime { 7 | type Env = Conf & Tools & Shortcuts & Sttp 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/CCTerminal.scala: -------------------------------------------------------------------------------- 1 | package commandcenter 2 | 3 | import commandcenter.command.PreviewResult 4 | import commandcenter.CCRuntime.Env 5 | import zio.* 6 | 7 | import java.awt.Dimension 8 | 9 | trait CCTerminal { 10 | def terminalType: TerminalType 11 | 12 | def opacity: RIO[Env, Float] 13 | def setOpacity(opacity: Float): RIO[Env, Unit] 14 | def isOpacitySupported: URIO[Env, Boolean] 15 | 16 | def size: RIO[Env, Dimension] 17 | def setSize(width: Int, height: Int): RIO[Env, Unit] 18 | 19 | def reset: URIO[Env, Unit] 20 | def reload: RIO[Env, Unit] 21 | 22 | def showMore[A]( 23 | moreResults: Chunk[PreviewResult[A]], 24 | previewSource: PreviewResult[A], 25 | pageSize: Int 26 | ): RIO[Env, Unit] 27 | } 28 | 29 | trait GuiTerminal extends CCTerminal { 30 | def open: RIO[Env, Unit] 31 | def hide: URIO[Env, Unit] 32 | def activate: RIO[Env, Unit] 33 | def deactivate: RIO[Env, Unit] 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/CommandContext.scala: -------------------------------------------------------------------------------- 1 | package commandcenter 2 | 3 | import java.util.Locale 4 | 5 | final case class CommandContext(locale: Locale, terminal: CCTerminal, matchScore: Double) { 6 | def matchScore(score: Double): CommandContext = copy(matchScore = score) 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/Conf.scala: -------------------------------------------------------------------------------- 1 | package commandcenter 2 | 3 | import commandcenter.CCRuntime.Env 4 | import zio.* 5 | 6 | trait Conf { 7 | def config: UIO[CCConfig] 8 | def load: RIO[Scope & Env, CCConfig] 9 | def reload: RIO[Env, CCConfig] 10 | } 11 | 12 | object Conf { 13 | def get[A](f: CCConfig => A): URIO[Conf, A] = config.map(f) 14 | 15 | def config: URIO[Conf, CCConfig] = ZIO.serviceWithZIO[Conf](_.config) 16 | 17 | def load: RIO[Scope & Env, CCConfig] = ZIO.serviceWithZIO[Conf](_.load) 18 | 19 | def reload: RIO[Env, CCConfig] = ZIO.serviceWithZIO[Conf](_.reload) 20 | } 21 | 22 | final case class ConfigLive(configRef: Ref[Option[ReloadableConfig]]) extends Conf { 23 | 24 | def config: UIO[CCConfig] = configRef.get.some 25 | .map(_.config) 26 | .orDieWith(_ => new Exception("A config hasn't been loaded. You likely forgot to call `Conf.load` on startup.")) 27 | 28 | def load: RIO[Scope & Env, CCConfig] = 29 | (for { 30 | (release, config) <- Scope.global.use(CCConfig.load.withEarlyRelease) 31 | _ <- configRef.set(Some(ReloadableConfig(config, release))) 32 | } yield config).withFinalizer { _ => 33 | ZIO.whenCaseZIO(configRef.get) { case Some(reloadableConfig) => 34 | reloadableConfig.release 35 | } 36 | } 37 | 38 | def reload: RIO[Env, CCConfig] = 39 | for { 40 | _ <- ZIO.whenCaseZIO(configRef.get) { case Some(reloadableConfig) => 41 | reloadableConfig.release 42 | } 43 | (release, config) <- Scope.global.use(CCConfig.load.withEarlyRelease) 44 | _ <- configRef.set(Some(ReloadableConfig(config, release))) 45 | } yield config 46 | } 47 | 48 | object ConfigLive { 49 | 50 | def layer: ZLayer[Any, Nothing, ConfigLive] = 51 | ZLayer { 52 | for { 53 | configRef <- Ref.make(Option.empty[ReloadableConfig]) 54 | } yield ConfigLive(configRef) 55 | } 56 | 57 | } 58 | 59 | final case class ReloadableConfig(config: CCConfig, release: UIO[Unit]) 60 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/HeadlessTerminal.scala: -------------------------------------------------------------------------------- 1 | package commandcenter 2 | 3 | import commandcenter.command.{Command, PreviewResult, SearchResults} 4 | import commandcenter.locale.Language 5 | import commandcenter.CCRuntime.Env 6 | import zio.* 7 | 8 | import java.awt.Dimension 9 | 10 | final case class HeadlessTerminal(searchResultsRef: Ref[SearchResults[Any]]) extends CCTerminal { 11 | def terminalType: TerminalType = TerminalType.Test 12 | 13 | def opacity: RIO[Env, Float] = ZIO.succeed(1.0f) 14 | 15 | def setOpacity(opacity: Float): RIO[Env, Unit] = ZIO.unit 16 | 17 | def isOpacitySupported: URIO[Env, Boolean] = ZIO.succeed(false) 18 | 19 | def size: RIO[Env, Dimension] = ZIO.succeed(new Dimension(80, 40)) 20 | 21 | def setSize(width: Int, height: Int): RIO[Env, Unit] = ZIO.unit 22 | 23 | def reload: RIO[Env, Unit] = ZIO.unit 24 | 25 | def reset: URIO[Env, Unit] = searchResultsRef.set(SearchResults.empty) 26 | 27 | def search(commands: Vector[Command[Any]], aliases: Map[String, List[String]])( 28 | searchTerm: String 29 | ): URIO[Env, SearchResults[Any]] = { 30 | val context = CommandContext(Language.detect(searchTerm), this, 1.0) 31 | 32 | Command 33 | .search(commands, aliases, searchTerm, context) 34 | .tap { r => 35 | searchResultsRef.set(r) 36 | } 37 | } 38 | 39 | def run(cursorIndex: Int): URIO[Env, Option[PreviewResult[Any]]] = 40 | for { 41 | results <- searchResultsRef.get 42 | previewResult = results.previews.lift(cursorIndex) 43 | _ <- ZIO.foreachDiscard(previewResult) { preview => 44 | preview.onRunSandboxedLogged.forkDaemon 45 | } 46 | } yield previewResult 47 | 48 | def showMore[A]( 49 | moreResults: Chunk[PreviewResult[A]], 50 | previewSource: PreviewResult[A], 51 | pageSize: Int 52 | ): RIO[Env, Unit] = 53 | ZIO.unit 54 | } 55 | 56 | object HeadlessTerminal { 57 | 58 | def create: UIO[HeadlessTerminal] = 59 | for { 60 | searchResultsRef <- Ref.make(SearchResults.empty[Any]) 61 | } yield HeadlessTerminal(searchResultsRef) 62 | } 63 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/Sttp.scala: -------------------------------------------------------------------------------- 1 | package commandcenter 2 | 3 | import sttp.capabilities.zio.ZioStreams 4 | import sttp.client3.{Identity, RequestT, Response, SttpBackend} 5 | import sttp.client3.httpclient.zio.HttpClientZioBackend 6 | import zio.* 7 | 8 | trait Sttp { 9 | def send[T](request: RequestT[Identity, T, ZioStreams]): Task[Response[T]] 10 | } 11 | 12 | object Sttp { 13 | 14 | def send[T](request: RequestT[Identity, T, ZioStreams]): ZIO[Sttp, Throwable, Response[T]] = 15 | ZIO.serviceWithZIO[Sttp](_.send(request)) 16 | } 17 | 18 | final case class SttpLive(backend: SttpBackend[Task, ZioStreams]) extends Sttp { 19 | 20 | def send[T](request: RequestT[Identity, T, ZioStreams]): Task[Response[T]] = 21 | request.send(backend) 22 | } 23 | 24 | object SttpLive { 25 | 26 | def make: ZLayer[Any, Throwable, Sttp] = 27 | ZLayer { 28 | for { 29 | backend <- HttpClientZioBackend() 30 | } yield SttpLive(backend) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/TerminalType.scala: -------------------------------------------------------------------------------- 1 | package commandcenter 2 | 3 | import enumeratum.{Enum, EnumEntry} 4 | 5 | sealed trait TerminalType extends EnumEntry 6 | 7 | object TerminalType extends Enum[TerminalType] { 8 | case object Cli extends TerminalType 9 | case object Swing extends TerminalType 10 | case object Swt extends TerminalType 11 | case object Test extends TerminalType 12 | 13 | lazy val values: IndexedSeq[TerminalType] = findValues 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/codec/Codecs.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.codec 2 | 3 | import cats.syntax.either.* 4 | import io.circe.* 5 | 6 | import java.time.ZoneId 7 | import java.util.Locale 8 | 9 | object Codecs { 10 | 11 | implicit val localeDecoder: Decoder[Locale] = Decoder.decodeString.emap { s => 12 | Either.catchNonFatal(Locale.forLanguageTag(s)).leftMap(_.getMessage) 13 | } 14 | 15 | implicit val localeEncoder: Encoder[Locale] = (locale: Locale) => Json.fromString(locale.toLanguageTag) 16 | 17 | implicit val zoneIdDecoder: Decoder[ZoneId] = Decoder.decodeString.emap { s => 18 | Either.catchNonFatal(ZoneId.of(s)).leftMap(_.getMessage) 19 | } 20 | 21 | implicit val zoneIdEncoder: Encoder[ZoneId] = (locale: ZoneId) => Json.fromString(locale.getId) 22 | 23 | def decodeSumBySoleKey[A](f: PartialFunction[(String, ACursor), Decoder.Result[A]]): Decoder[A] = { 24 | def keyErr = "Expected a single key indicating the subtype" 25 | Decoder.instance { c => 26 | c.keys match { 27 | case Some(it) => 28 | it.toList match { 29 | case singleKey :: Nil => 30 | val arg = (singleKey, c.downField(singleKey)) 31 | def fail = Left(DecodingFailure("Unknown subtype: " + singleKey, c.history)) 32 | f.applyOrElse(arg, (_: (String, ACursor)) => fail) 33 | case Nil => Left(DecodingFailure(keyErr, c.history)) 34 | case keys => Left(DecodingFailure(s"$keyErr, found multiple: $keys", c.history)) 35 | } 36 | case None => Left(DecodingFailure(keyErr, c.history)) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/CommandError.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.monovore.decline.Help 4 | import commandcenter.view.Rendered 5 | 6 | sealed trait CommandError { 7 | def toThrowable: Throwable 8 | } 9 | 10 | object CommandError { 11 | 12 | final case class ShowMessage(rendered: Rendered, score: Double) extends CommandError { 13 | def previewResult: PreviewResult[Nothing] = PreviewResult.nothing(rendered).score(score) 14 | 15 | override def toThrowable: Throwable = new Exception(s"Show message: $rendered") 16 | } 17 | 18 | final case class UnexpectedError[A](throwable: Throwable, source: Command[A]) extends CommandError { 19 | def toThrowable: Throwable = throwable 20 | } 21 | 22 | object UnexpectedError { 23 | 24 | def apply[A](source: Command[A])(throwable: Throwable): UnexpectedError[A] = 25 | UnexpectedError(throwable, source) 26 | 27 | def fromMessage[A](source: Command[A])(message: String): UnexpectedError[A] = 28 | UnexpectedError(new Exception(message), source) 29 | } 30 | 31 | final case class CliError(help: Help) extends CommandError { 32 | def toThrowable: Throwable = new Exception(help.toString()) 33 | } 34 | 35 | case object NotApplicable extends CommandError { 36 | override def toThrowable: Throwable = new Exception("Not applicable") 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/CommandInput.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import commandcenter.CommandContext 4 | 5 | trait CommandInput 6 | 7 | object CommandInput { 8 | final case class Args(commandName: String, args: List[String], context: CommandContext) extends CommandInput 9 | final case class Prefixed(prefix: String, rest: String, context: CommandContext) extends CommandInput 10 | final case class Keyword(keyword: String, context: CommandContext) extends CommandInput 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/CommandPlugin.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.config.ConfigParserExtensions 5 | import commandcenter.util.OS 6 | import commandcenter.CCRuntime.Env 7 | import zio.* 8 | 9 | trait CommandPlugin[A <: Command[?]] extends ConfigParserExtensions { 10 | def make(config: Config): RIO[Scope & Env, A] 11 | } 12 | 13 | object CommandPlugin { 14 | 15 | def loadAll( 16 | config: Config, 17 | path: String 18 | ): ZIO[Scope & Env, CommandPluginError.UnexpectedException, List[Command[?]]] = { 19 | import scala.jdk.CollectionConverters.* 20 | 21 | for { 22 | commandConfigs <- 23 | ZIO 24 | .attempt(config.getConfigList(path).asScala.toList) 25 | .mapError(CommandPluginError.UnexpectedException.apply) 26 | commands <- ZIO.foreach(commandConfigs) { c => 27 | Command 28 | .parse(c) 29 | .foldZIO( 30 | { 31 | case CommandPluginError.PluginNotApplicable(commandType, reason) => 32 | ZIO 33 | .logDebug( 34 | s"Skipping loading `$commandType` plugin because it's not applicable: $reason" 35 | ) 36 | .as(None) 37 | 38 | case CommandPluginError.PluginNotFound(typeName, _) => 39 | ZIO.logWarning(s"Plugin '$typeName' not found").as(None) 40 | 41 | case CommandPluginError.PluginsNotSupported(typeName) => 42 | ZIO 43 | .logWarning( 44 | s"Cannot load `$typeName` because external plugins not yet supported for Substrate VM." 45 | ) 46 | .as(None) 47 | 48 | case error: CommandPluginError.UnexpectedException => 49 | ZIO.fail(error) 50 | 51 | }, 52 | c => ZIO.succeed(Some(c)) 53 | ) 54 | } 55 | } yield commands.flatten.filter(c => c.supportedOS.isEmpty || c.supportedOS.contains(OS.os)) 56 | } 57 | 58 | def loadDynamically(c: Config, typeName: String): ZIO[Scope & Env, CommandPluginError, Command[Any]] = { 59 | val mirror = scala.reflect.runtime.universe.runtimeMirror(CommandPlugin.getClass.getClassLoader) 60 | 61 | for { 62 | pluginClass <- ZIO 63 | .attempt(Class.forName(typeName)) 64 | .mapError { 65 | case e: ClassNotFoundException => CommandPluginError.PluginNotFound(typeName, e) 66 | case other => CommandPluginError.UnexpectedException(other) 67 | } 68 | plugin <- ZIO 69 | .attempt( 70 | mirror 71 | .reflectModule(mirror.moduleSymbol(pluginClass)) 72 | .instance 73 | .asInstanceOf[CommandPlugin[Command[?]]] 74 | ) 75 | .mapError(CommandPluginError.UnexpectedException.apply) 76 | command <- plugin.make(c).mapError(CommandPluginError.UnexpectedException.apply) 77 | } yield command 78 | } 79 | } 80 | 81 | sealed abstract class CommandPluginError(cause: Throwable) extends Exception(cause) with Product with Serializable 82 | 83 | object CommandPluginError { 84 | final case class PluginNotApplicable(commandType: CommandType, reason: String) extends CommandPluginError(null) 85 | final case class PluginNotFound(typeName: String, cause: Throwable) extends CommandPluginError(cause) 86 | final case class PluginsNotSupported(typeName: String) extends CommandPluginError(null) 87 | final case class UnexpectedException(cause: Throwable) extends CommandPluginError(cause) 88 | } 89 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/CommandType.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import enumeratum.* 4 | 5 | sealed trait CommandType extends EnumEntry 6 | 7 | object CommandType extends Enum[CommandType] { 8 | case object CalculatorCommand extends CommandType 9 | case object ConfigCommand extends CommandType 10 | case object DecodeBase64Command extends CommandType 11 | case object DecodeUrlCommand extends CommandType 12 | case object EncodeBase64Command extends CommandType 13 | case object EncodeUrlCommand extends CommandType 14 | case object EpochMillisCommand extends CommandType 15 | case object EpochUnixCommand extends CommandType 16 | case object ExitCommand extends CommandType 17 | case object ExternalIPCommand extends CommandType 18 | case object FileNavigationCommand extends CommandType 19 | case object FindFileCommand extends CommandType 20 | case object FindInFileCommand extends CommandType 21 | case object Foobar2000Command extends CommandType 22 | case object HashCommand extends CommandType 23 | case object HoogleCommand extends CommandType 24 | case object HttpCommand extends CommandType 25 | case object ITunesCommand extends CommandType 26 | case object LocalIPCommand extends CommandType 27 | case object LockCommand extends CommandType 28 | case object LoremIpsumCommand extends CommandType 29 | case object MuteCommand extends CommandType 30 | case object OpacityCommand extends CommandType 31 | case object OpenBrowserCommand extends CommandType 32 | case object ProcessIdCommand extends CommandType 33 | case object RadixCommand extends CommandType 34 | case object RebootCommand extends CommandType 35 | case object ReloadCommand extends CommandType 36 | case object ResizeCommand extends CommandType 37 | case object SearchCratesCommand extends CommandType 38 | case object SearchMavenCommand extends CommandType 39 | case object SearchUrlCommand extends CommandType 40 | case object SnippetsCommand extends CommandType 41 | case object SpeakCommand extends CommandType 42 | case object SuspendProcessCommand extends CommandType 43 | case object SwitchWindowCommand extends CommandType 44 | case object SystemCommand extends CommandType 45 | case object TemperatureCommand extends CommandType 46 | case object TerminalCommand extends CommandType 47 | case object TimerCommand extends CommandType 48 | case object ToggleDesktopIconsCommand extends CommandType 49 | case object ToggleHiddenFilesCommand extends CommandType 50 | case object UUIDCommand extends CommandType 51 | case object WorldTimesCommand extends CommandType 52 | 53 | final case class External(typeName: String) extends CommandType 54 | 55 | object External { 56 | 57 | def of[T](commandClass: Class[T]): CommandType.External = 58 | External(commandClass.getCanonicalName) 59 | } 60 | 61 | lazy val values: IndexedSeq[CommandType] = findValues 62 | } 63 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/CommonArgs.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import cats.data.{Validated, ValidatedNel} 4 | import com.monovore.decline.Argument 5 | import zio.Duration 6 | 7 | object CommonArgs { 8 | 9 | implicit val durationArgument: Argument[Duration] = new Argument[Duration] { 10 | 11 | override def read(string: String): ValidatedNel[String, Duration] = 12 | try Validated.valid(Duration.fromScala(scala.concurrent.duration.Duration(string))) 13 | catch { case _: IllegalArgumentException => Validated.invalidNel(s"Invalid Duration: $string") } 14 | 15 | override def defaultMetavar: String = "duration" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/CommonOpts.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import cats.data.Validated 4 | import com.monovore.decline.Opts 5 | 6 | import java.nio.charset.Charset 7 | import scala.util.Try 8 | 9 | object CommonOpts { 10 | // TODO: Remove this or make it a def with metavar for better usage details 11 | val stringArg: Opts[String] = Opts.argument[String]() 12 | 13 | val encodingOpt: Opts[Charset] = Opts 14 | .option[String]("charset", "charset (e.g. utf8)", "c") 15 | .withDefault("UTF-8") 16 | .mapValidated { charset => 17 | Try(Charset.forName(charset)).fold(t => Validated.invalidNel(s"${t.getMessage}"), Validated.Valid(_)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/ConfigCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.view.Renderer 5 | import commandcenter.CCConfig 6 | import commandcenter.CCRuntime.Env 7 | import zio.* 8 | 9 | import java.awt.Desktop 10 | 11 | final case class ConfigCommand(commandNames: List[String]) extends Command[Unit] { 12 | val commandType: CommandType = CommandType.ConfigCommand 13 | val title: String = "Command Center Config" 14 | 15 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 16 | for { 17 | input <- ZIO.fromOption(searchInput.asKeyword).orElseFail(CommandError.NotApplicable) 18 | file <- CCConfig.defaultConfigFile 19 | } yield { 20 | val run = ZIO.attempt(Desktop.getDesktop.open(file)) 21 | 22 | PreviewResults.one( 23 | Preview.unit 24 | .rendered(Renderer.renderDefault(title, "")) 25 | .score(Scores.veryHigh(input.context)) 26 | .onRun(run) 27 | ) 28 | } 29 | } 30 | 31 | object ConfigCommand extends CommandPlugin[ConfigCommand] { 32 | 33 | def make(config: Config): IO[CommandPluginError, ConfigCommand] = 34 | for { 35 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 36 | } yield ConfigCommand(commandNames.getOrElse(List("config"))) 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/DecodeBase64Command.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import cats.syntax.apply.* 4 | import com.monovore.decline 5 | import com.typesafe.config.Config 6 | import commandcenter.command.CommonOpts.* 7 | import commandcenter.tools.Tools 8 | import commandcenter.CCRuntime.Env 9 | import zio.* 10 | 11 | import java.util.Base64 12 | 13 | final case class DecodeBase64Command(commandNames: List[String]) extends Command[String] { 14 | val commandType: CommandType = CommandType.DecodeBase64Command 15 | val title: String = "Decode (Base64)" 16 | 17 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[String]] = 18 | for { 19 | input <- ZIO.fromOption(searchInput.asArgs).orElseFail(CommandError.NotApplicable) 20 | all = (stringArg, encodingOpt).tupled 21 | parsedCommand = decline.Command("", s"Base64 decodes the given string")(all).parse(input.args) 22 | (valueToDecode, charset) <- ZIO.fromEither(parsedCommand).mapError(CommandError.CliError.apply) 23 | decoded = new String(Base64.getDecoder.decode(valueToDecode.getBytes(charset)), charset) 24 | } yield PreviewResults.one( 25 | Preview(decoded).onRun(Tools.setClipboard(decoded)).score(Scores.veryHigh(input.context)) 26 | ) 27 | } 28 | 29 | object DecodeBase64Command extends CommandPlugin[DecodeBase64Command] { 30 | 31 | def make(config: Config): IO[CommandPluginError, DecodeBase64Command] = 32 | for { 33 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 34 | } yield DecodeBase64Command(commandNames.getOrElse(List("decodebase64"))) 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/DecodeUrlCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import cats.syntax.apply.* 4 | import com.monovore.decline 5 | import com.typesafe.config.Config 6 | import commandcenter.command.CommonOpts.* 7 | import commandcenter.tools.Tools 8 | import commandcenter.CCRuntime.Env 9 | import zio.* 10 | 11 | import java.net.URLDecoder 12 | 13 | final case class DecodeUrlCommand(commandNames: List[String]) extends Command[String] { 14 | val commandType: CommandType = CommandType.DecodeUrlCommand 15 | val title: String = "Decode (URL)" 16 | 17 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[String]] = 18 | for { 19 | input <- ZIO.fromOption(searchInput.asArgs).orElseFail(CommandError.NotApplicable) 20 | all = (stringArg, encodingOpt).tupled 21 | parsedCommand = decline.Command("", s"URL decodes the given string")(all).parse(input.args) 22 | (valueToDecode, charset) <- ZIO.fromEither(parsedCommand).mapError(CommandError.CliError.apply) 23 | decoded <- ZIO.attempt(URLDecoder.decode(valueToDecode, charset)).mapError(CommandError.UnexpectedError(this)) 24 | } yield PreviewResults.one( 25 | Preview(decoded).onRun(Tools.setClipboard(decoded)).score(Scores.veryHigh(input.context)) 26 | ) 27 | } 28 | 29 | object DecodeUrlCommand extends CommandPlugin[DecodeUrlCommand] { 30 | 31 | def make(config: Config): IO[CommandPluginError, DecodeUrlCommand] = 32 | for { 33 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 34 | } yield DecodeUrlCommand(commandNames.getOrElse(List("decodeurl"))) 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/EncodeBase64Command.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import cats.syntax.apply.* 4 | import com.monovore.decline 5 | import com.typesafe.config.Config 6 | import commandcenter.command.CommonOpts.* 7 | import commandcenter.tools.Tools 8 | import commandcenter.CCRuntime.Env 9 | import zio.* 10 | 11 | import java.util.Base64 12 | 13 | final case class EncodeBase64Command(commandNames: List[String]) extends Command[String] { 14 | val commandType: CommandType = CommandType.EncodeBase64Command 15 | val title: String = "Encode (Base64)" 16 | 17 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[String]] = 18 | for { 19 | input <- ZIO.fromOption(searchInput.asArgs).orElseFail(CommandError.NotApplicable) 20 | all = (stringArg, encodingOpt).tupled 21 | parsedCommand = decline.Command("", s"Base64 encodes the given string")(all).parse(input.args) 22 | (valueToEncode, charset) <- ZIO.fromEither(parsedCommand).mapError(CommandError.CliError.apply) 23 | encoded = Base64.getEncoder.encodeToString(valueToEncode.getBytes(charset)) 24 | } yield PreviewResults.one( 25 | Preview(encoded).onRun(Tools.setClipboard(encoded)).score(Scores.veryHigh(input.context)) 26 | ) 27 | } 28 | 29 | object EncodeBase64Command extends CommandPlugin[EncodeBase64Command] { 30 | 31 | def make(config: Config): IO[CommandPluginError, EncodeBase64Command] = 32 | for { 33 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 34 | } yield EncodeBase64Command(commandNames.getOrElse(List("encodebase64"))) 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/EncodeUrlCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import cats.syntax.apply.* 4 | import com.monovore.decline 5 | import com.typesafe.config.Config 6 | import commandcenter.command.CommonOpts.* 7 | import commandcenter.tools.Tools 8 | import commandcenter.CCRuntime.Env 9 | import zio.* 10 | 11 | import java.net.URLEncoder 12 | 13 | final case class EncodeUrlCommand(commandNames: List[String]) extends Command[String] { 14 | val commandType: CommandType = CommandType.EncodeUrlCommand 15 | 16 | val title: String = "Encode (URL)" 17 | 18 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[String]] = 19 | for { 20 | input <- ZIO.fromOption(searchInput.asArgs).orElseFail(CommandError.NotApplicable) 21 | all = (stringArg, encodingOpt).tupled 22 | parsedCommand = decline.Command("", s"URL encodes the given string")(all).parse(input.args) 23 | (valueToEncode, charset) <- ZIO.fromEither(parsedCommand).mapError(CommandError.CliError.apply) 24 | encoded = URLEncoder.encode(valueToEncode, charset) 25 | } yield PreviewResults.one( 26 | Preview(encoded).onRun(Tools.setClipboard(encoded)).score(Scores.veryHigh(input.context)) 27 | ) 28 | } 29 | 30 | object EncodeUrlCommand extends CommandPlugin[EncodeUrlCommand] { 31 | 32 | def make(config: Config): IO[CommandPluginError, EncodeUrlCommand] = 33 | for { 34 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 35 | } yield EncodeUrlCommand(commandNames.getOrElse(List("encodeurl"))) 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/EpochMillisCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.tools.Tools 5 | import commandcenter.CCRuntime.Env 6 | import zio.* 7 | 8 | import java.time.{Instant, ZoneId} 9 | import java.time.format.{DateTimeFormatter, FormatStyle} 10 | import java.util.concurrent.TimeUnit 11 | 12 | final case class EpochMillisCommand(commandNames: List[String]) extends Command[String] { 13 | val commandType: CommandType = CommandType.EpochMillisCommand 14 | val title: String = "Epoch (milliseconds)" 15 | 16 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[String]] = 17 | for { 18 | input <- ZIO.fromOption(searchInput.asPrefixed).orElseFail(CommandError.NotApplicable) 19 | (output, score) <- if (input.rest.trim.isEmpty) { 20 | Clock.currentTime(TimeUnit.MILLISECONDS).map(time => (time.toString, Scores.veryHigh)) 21 | } else { 22 | input.rest.toLongOption match { 23 | case Some(millis) => 24 | ZIO.attempt { 25 | val formatted = Instant 26 | .ofEpochMilli(millis) 27 | .atZone(ZoneId.systemDefault()) 28 | .format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)) 29 | 30 | val score = if (millis < 100000000000L) { 31 | Scores.veryHigh(input.context) * 0.9 32 | } else { 33 | Scores.veryHigh(input.context) 34 | } 35 | 36 | (formatted, score) 37 | }.mapError(CommandError.UnexpectedError(this)) 38 | 39 | case None => ZIO.fail(CommandError.NotApplicable) 40 | } 41 | } 42 | } yield PreviewResults.one( 43 | Preview(output) 44 | .score(score) 45 | .onRun(Tools.setClipboard(output)) 46 | ) 47 | } 48 | 49 | object EpochMillisCommand extends CommandPlugin[EpochMillisCommand] { 50 | 51 | def make(config: Config): IO[CommandPluginError, EpochMillisCommand] = 52 | for { 53 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 54 | } yield EpochMillisCommand(commandNames.getOrElse(List("epochmillis"))) 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/EpochUnixCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.tools.Tools 5 | import commandcenter.CCRuntime.Env 6 | import zio.* 7 | 8 | import java.time.{Instant, ZoneId} 9 | import java.time.format.{DateTimeFormatter, FormatStyle} 10 | import java.util.concurrent.TimeUnit 11 | 12 | final case class EpochUnixCommand(commandNames: List[String]) extends Command[String] { 13 | val commandType: CommandType = CommandType.EpochUnixCommand 14 | val title: String = "Epoch (Unix time)" 15 | 16 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[String]] = 17 | for { 18 | input <- ZIO.fromOption(searchInput.asPrefixed).orElseFail(CommandError.NotApplicable) 19 | (output, score) <- if (input.rest.trim.isEmpty) { 20 | Clock.currentTime(TimeUnit.SECONDS).map(time => (time.toString, Scores.veryHigh)) 21 | } else { 22 | input.rest.toLongOption match { 23 | case Some(seconds) => 24 | ZIO.attempt { 25 | val formatted = Instant 26 | .ofEpochSecond(seconds) 27 | .atZone(ZoneId.systemDefault()) 28 | .format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)) 29 | 30 | val score = if (seconds < 100000000000L) { 31 | Scores.veryHigh(input.context) 32 | } else { 33 | Scores.veryHigh(input.context) * 0.9 34 | } 35 | 36 | (formatted, score) 37 | }.mapError(CommandError.UnexpectedError(this)) 38 | 39 | case None => ZIO.fail(CommandError.NotApplicable) 40 | } 41 | } 42 | } yield PreviewResults.one( 43 | Preview(output) 44 | .score(score) 45 | .onRun(Tools.setClipboard(output)) 46 | ) 47 | } 48 | 49 | object EpochUnixCommand extends CommandPlugin[EpochUnixCommand] { 50 | 51 | def make(config: Config): IO[CommandPluginError, EpochUnixCommand] = 52 | for { 53 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 54 | } yield EpochUnixCommand(commandNames.getOrElse(List("epochunix"))) 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/ExitCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.view.Renderer 5 | import commandcenter.CCRuntime.Env 6 | import zio.* 7 | 8 | final case class ExitCommand(commandNames: List[String]) extends Command[Unit] { 9 | val commandType: CommandType = CommandType.ExitCommand 10 | val title: String = "Exit Command Center" 11 | 12 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 13 | for { 14 | input <- ZIO.fromOption(searchInput.asKeyword).orElseFail(CommandError.NotApplicable) 15 | } yield PreviewResults.one( 16 | Preview.unit 17 | .rendered(Renderer.renderDefault(title, "")) 18 | .score(Scores.veryHigh(input.context)) 19 | .runOption(RunOption.Exit) 20 | ) 21 | } 22 | 23 | object ExitCommand extends CommandPlugin[ExitCommand] { 24 | 25 | def make(config: Config): IO[CommandPluginError, ExitCommand] = 26 | for { 27 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 28 | } yield ExitCommand(commandNames.getOrElse(List("exit", "quit"))) 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/ExternalIPCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.tools.Tools 5 | import commandcenter.util.OS 6 | import commandcenter.view.Renderer 7 | import commandcenter.CCRuntime.Env 8 | import fansi.Color 9 | import zio.* 10 | import zio.process.Command as PCommand 11 | 12 | final case class ExternalIPCommand(commandNames: List[String]) extends Command[String] { 13 | val commandType: CommandType = CommandType.ExternalIPCommand 14 | val title: String = "External IP" 15 | 16 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[String]] = 17 | for { 18 | input <- ZIO.fromOption(searchInput.asKeyword).orElseFail(CommandError.NotApplicable) 19 | externalIP <- getExternalIP 20 | } yield PreviewResults.one( 21 | Preview(externalIP) 22 | .score(Scores.veryHigh(input.context)) 23 | .onRun(Tools.setClipboard(externalIP)) 24 | .rendered( 25 | Renderer.renderDefault(title, Color.Magenta(externalIP)) 26 | ) 27 | ) 28 | 29 | private def getExternalIP: ZIO[Any, CommandError, String] = 30 | OS.os match { 31 | case OS.Windows => 32 | val prefix = "Address:" 33 | (for { 34 | lines <- PCommand("nslookup", "myip.opendns.com", "resolver1.opendns.com").lines 35 | ipOpt = lines.filter(_.startsWith("Address:")).drop(1).headOption.map { a => 36 | a.drop(prefix.length).trim 37 | } 38 | } yield ipOpt) 39 | .mapError(CommandError.UnexpectedError(this)) 40 | .someOrFail(CommandError.UnexpectedError.fromMessage(this)("Could not parse nslookup results")) 41 | 42 | case _ => 43 | PCommand("dig", "+short", "myip.opendns.com", "@resolver1.opendns.com").string 44 | .mapError(CommandError.UnexpectedError(this)) 45 | .map(_.trim) 46 | } 47 | } 48 | 49 | object ExternalIPCommand extends CommandPlugin[ExternalIPCommand] { 50 | 51 | def make(config: Config): IO[CommandPluginError, ExternalIPCommand] = 52 | for { 53 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 54 | } yield ExternalIPCommand(commandNames.getOrElse(List("externalip"))) 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/FileNavigationCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.command.CommandError.* 5 | import commandcenter.util.ProcessUtil 6 | import commandcenter.CCRuntime.Env 7 | import fansi.{Color, Str} 8 | import zio.* 9 | import zio.stream.ZStream 10 | 11 | import java.io.File 12 | import java.nio.file.Files 13 | import scala.util.matching.Regex 14 | 15 | final case class FileNavigationCommand(homeDirectory: Option[String]) extends Command[File] { 16 | val commandType: CommandType = CommandType.FileNavigationCommand 17 | 18 | val commandNames: List[String] = List.empty 19 | 20 | val title: String = "File navigation" 21 | 22 | // For Windows-style paths like `C:/folder` 23 | val drivePathRegex: Regex = "[A-Za-z]:".r 24 | 25 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[File]] = { 26 | val input = searchInput.input 27 | if (!input.startsWith("/") && !input.startsWith("~/") && drivePathRegex.findPrefixOf(input).isEmpty) 28 | ZIO.fail(NotApplicable) 29 | else { 30 | val file = homeDirectory match { 31 | case Some(home) if input == "~/" => new File(home) 32 | case Some(home) if input.startsWith("~/") => new File(home, input.tail) 33 | case _ => new File(input) 34 | } 35 | 36 | val exists = file.exists() 37 | val score = if (exists) 101 else 100 38 | val titleColor = if (exists) Color.Blue else Color.Red 39 | 40 | val sameLevel = Option(file.getParentFile) 41 | .map(f => ZStream.fromJavaStream(Files.list(f.toPath)).catchAll(_ => ZStream.empty)) 42 | .getOrElse(ZStream.empty) 43 | .map(_.toFile) 44 | .filter { f => 45 | val listedPath = f.getAbsolutePath.toLowerCase 46 | val inputFile = file.getAbsolutePath.toLowerCase 47 | listedPath.startsWith(inputFile) && listedPath.length != inputFile.length 48 | } 49 | 50 | val children = Option 51 | .when(file.isDirectory)(file) 52 | .filter(_.isDirectory) 53 | .map(f => ZStream.fromJavaStream(Files.list(f.toPath)).catchAll(_ => ZStream.empty)) 54 | .getOrElse(ZStream.empty) 55 | .map(_.toFile) 56 | .filter { f => 57 | val listedPath = f.getAbsolutePath.toLowerCase 58 | val inputFile = file.getAbsolutePath.toLowerCase 59 | listedPath.startsWith(inputFile) && listedPath.length != inputFile.length 60 | } 61 | 62 | ZIO.succeed { 63 | PreviewResults.paginated( 64 | ZStream.succeed( 65 | Preview(file) 66 | .score(score) 67 | .renderedAnsi(titleColor(file.getAbsolutePath) ++ Str(" Open file location")) 68 | .onRun(ProcessUtil.browseToFile(file)) 69 | ) ++ (sameLevel ++ children).map { f => 70 | Preview(f) 71 | .score(score) 72 | .renderedAnsi(Color.Blue(f.getAbsolutePath) ++ Str(" Open file location")) 73 | .onRun(ProcessUtil.browseToFile(f)) 74 | }, 75 | initialPageSize = 10, 76 | morePageSize = 20 77 | ) 78 | } 79 | } 80 | } 81 | } 82 | 83 | object FileNavigationCommand extends CommandPlugin[FileNavigationCommand] { 84 | 85 | def make(config: Config): IO[CommandPluginError, FileNavigationCommand] = 86 | for { 87 | homeDirectory <- System.property("user.home").catchAll { t => 88 | ZIO.logWarningCause("Could not obtain location of home directory", Cause.die(t)).as(None) 89 | } 90 | } yield FileNavigationCommand(homeDirectory) 91 | } 92 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/FindFileCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.command.util.PathUtil 5 | import commandcenter.command.CommandError.* 6 | import commandcenter.util.OS 7 | import commandcenter.view.Renderer 8 | import commandcenter.CCRuntime.Env 9 | import zio.* 10 | import zio.process.Command as PCommand 11 | 12 | import java.io.File 13 | 14 | final case class FindFileCommand(commandNames: List[String]) extends Command[File] { 15 | val commandType: CommandType = CommandType.FindFileCommand 16 | val title: String = "Find files" 17 | 18 | override val supportedOS: Set[OS] = Set(OS.MacOS) 19 | 20 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[File]] = 21 | for { 22 | input <- ZIO.fromOption(searchInput.asPrefixed).orElseFail(CommandError.NotApplicable) 23 | result <- // TODO: mdfind/Spotlight can be really slow, especially when there are a lot of matches. Streaming the top N results 24 | // only seems to help a little bit (there appears to be no "top N" command line option for mdfind). This needs some 25 | // investigation. Or look into alternative approaches. 26 | if (input.rest.length < 3) ZIO.fail(NotApplicable) // TODO: Show feedback to user in this case? 27 | else 28 | (for { 29 | lines <- PCommand("mdfind", "-name", input.rest).linesStream.take(8).runCollect 30 | files = lines.map(new File(_)) 31 | // TODO: The default sorting is awful with mdfind. Should we intervene somehow? That would conflict a bit with 32 | // `take(N)` though. 33 | results = files.map { file => 34 | val shortenedPath = PathUtil.shorten(file.getAbsolutePath) 35 | 36 | Preview(file) 37 | .rendered(Renderer.renderDefault(shortenedPath, "Open file")) 38 | .onRun(PCommand("open", file.getAbsolutePath).exitCode.unit) 39 | .score(Scores.veryHigh(input.context)) 40 | } 41 | } yield PreviewResults.fromIterable(results)).mapError(CommandError.UnexpectedError(this)) 42 | } yield result 43 | } 44 | 45 | object FindFileCommand extends CommandPlugin[FindFileCommand] { 46 | 47 | def make(config: Config): IO[CommandPluginError, FindFileCommand] = 48 | for { 49 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 50 | } yield FindFileCommand(commandNames.getOrElse(List("find"))) 51 | } 52 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/FindInFileCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.command.CommandError.* 5 | import commandcenter.util.OS 6 | import commandcenter.view.Renderer 7 | import commandcenter.CCRuntime.Env 8 | import zio.* 9 | import zio.process.Command as PCommand 10 | 11 | import java.io.File 12 | 13 | final case class FindInFileCommand(commandNames: List[String]) extends Command[File] { 14 | val commandType: CommandType = CommandType.FindInFileCommand 15 | val title: String = "Find in files" 16 | 17 | override val supportedOS: Set[OS] = Set(OS.MacOS) 18 | 19 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[File]] = 20 | for { 21 | input <- ZIO.fromOption(searchInput.asPrefixed).orElseFail(CommandError.NotApplicable) 22 | // TODO: mdfind/Spotlight can be really slow, especially when there are a lot of matches. Streaming the top N results 23 | // only seems to help a little bit (there appears to be no "top N" command line option for mdfind). This needs some 24 | // investigation. Or look into alternative approaches. 25 | result <- if (input.rest.length < 3) ZIO.fail(NotApplicable) // TODO: Show feedback to user in this case? 26 | else 27 | (for { 28 | lines <- PCommand("mdfind", input.rest).linesStream.take(8).runCollect.map(_.toList) 29 | files = lines.map(new File(_)) 30 | // TODO: The default sorting is awful with mdfind. Should we intervene somehow? That would conflict a bit with 31 | // `take(N)` though. 32 | results = files.map { file => 33 | Preview(file) 34 | .rendered(Renderer.renderDefault(file.getAbsolutePath, "Open file")) 35 | .onRun(PCommand("open", file.getAbsolutePath).exitCode.unit) 36 | .score(Scores.veryHigh(input.context)) 37 | } 38 | } yield PreviewResults.fromIterable(results)).mapError(CommandError.UnexpectedError(this)) 39 | } yield result 40 | } 41 | 42 | object FindInFileCommand extends CommandPlugin[FindInFileCommand] { 43 | 44 | def make(config: Config): IO[CommandPluginError, FindInFileCommand] = 45 | for { 46 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 47 | } yield FindInFileCommand(commandNames.getOrElse(List("in"))) 48 | } 49 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/HashCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import cats.syntax.apply.* 4 | import com.monovore.decline 5 | import com.typesafe.config.Config 6 | import commandcenter.command.util.HashUtil 7 | import commandcenter.command.CommonOpts.* 8 | import commandcenter.tools.Tools 9 | import commandcenter.view.Renderer 10 | import commandcenter.CCRuntime.Env 11 | import io.circe.Decoder 12 | import zio.* 13 | 14 | final case class HashCommand(algorithm: String) extends Command[String] { 15 | val commandType: CommandType = CommandType.HashCommand 16 | val commandNames: List[String] = List(algorithm, algorithm.replace("-", "")).distinct 17 | val title: String = algorithm 18 | 19 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[String]] = 20 | for { 21 | input <- ZIO.fromOption(searchInput.asArgs).orElseFail(CommandError.NotApplicable) 22 | all = (stringArg, encodingOpt).tupled 23 | parsedCommand = decline.Command(algorithm, s"Hashes the argument with $algorithm")(all).parse(input.args) 24 | (valueToHash, charset) <- ZIO.fromEither(parsedCommand).mapError(CommandError.CliError.apply) 25 | hashResult <- ZIO 26 | .fromEither(HashUtil.hash(algorithm)(valueToHash, charset)) 27 | .mapError(CommandError.UnexpectedError(this)) 28 | } yield PreviewResults.one( 29 | Preview(hashResult) 30 | .score(Scores.veryHigh(input.context)) 31 | .onRun(Tools.setClipboard(hashResult)) 32 | .rendered(Renderer.renderDefault(algorithm, hashResult)) 33 | ) 34 | } 35 | 36 | object HashCommand extends CommandPlugin[HashCommand] { 37 | implicit val decoder: Decoder[HashCommand] = Decoder.forProduct1("algorithm")(HashCommand.apply) 38 | 39 | def make(config: Config): IO[CommandPluginError, HashCommand] = 40 | ZIO.fromEither(config.as[HashCommand]).mapError(CommandPluginError.UnexpectedException.apply) 41 | } 42 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/HelpMessage.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.monovore.decline.Help 4 | import fansi.{Color, Str} 5 | 6 | object HelpMessage { 7 | 8 | def formatted(help: Help): Str = { 9 | val errors = Color.Red(help.errors.mkString("\n")) 10 | 11 | if (help.usage.exists(_.nonEmpty)) 12 | errors ++ "\nUsage: " ++ Color.Yellow(help.usage.mkString("\n")) 13 | else 14 | errors 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/HoogleCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.command.HoogleCommand.HoogleResult 5 | import commandcenter.util.ProcessUtil 6 | import commandcenter.CCRuntime.Env 7 | import commandcenter.Sttp 8 | import fansi.Color 9 | import io.circe.{Decoder, Json} 10 | import sttp.client3.* 11 | import sttp.client3.circe.* 12 | import zio.* 13 | 14 | final case class HoogleCommand(commandNames: List[String]) extends Command[Unit] { 15 | val commandType: CommandType = CommandType.HoogleCommand 16 | val title: String = "Hoogle" 17 | 18 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 19 | for { 20 | input <- ZIO.fromOption(searchInput.asPrefixed.filter(_.rest.nonEmpty)).orElseFail(CommandError.NotApplicable) 21 | request = basicRequest 22 | .get(uri"https://hoogle.haskell.org?mode=json&format=text&hoogle=${input.rest}&start=1&count=5") 23 | .response(asJson[Json]) 24 | response <- Sttp 25 | .send(request) 26 | .map(_.body) 27 | .absolve 28 | .mapError(CommandError.UnexpectedError(this)) 29 | results <- ZIO 30 | .fromEither( 31 | response.as[List[HoogleResult]] 32 | ) 33 | .mapError(CommandError.UnexpectedError(this)) 34 | } yield PreviewResults.fromIterable(results.map { result => 35 | Preview.unit 36 | .onRun(ProcessUtil.openBrowser(result.url)) 37 | .score(Scores.veryHigh(input.context)) 38 | .renderedAnsi( 39 | Color.Magenta(result.item) ++ " " ++ Color.Yellow(result.module.name) ++ " " ++ 40 | Color.Cyan(result.`package`.name) ++ "\n" ++ result.docs 41 | ) 42 | }) 43 | } 44 | 45 | object HoogleCommand extends CommandPlugin[HoogleCommand] { 46 | 47 | final case class HoogleResult( 48 | url: String, 49 | item: String, 50 | docs: String, 51 | module: HoogleReference, 52 | `package`: HoogleReference 53 | ) 54 | 55 | object HoogleResult { 56 | 57 | implicit val decoder: Decoder[HoogleResult] = Decoder.instance { c => 58 | for { 59 | url <- c.get[String]("url") 60 | item <- c.get[String]("item") 61 | docs <- c.get[String]("docs").map(sanitizeDocs) 62 | module <- c.get[HoogleReference]("module") 63 | `package` <- c.get[HoogleReference]("package") 64 | } yield HoogleResult(url, item, docs, module, `package`) 65 | } 66 | 67 | private def sanitizeDocs(docs: String): String = 68 | docs.trim.replaceAll("[\n]{3,}", "\n\n") 69 | } 70 | 71 | final case class HoogleReference(name: String, url: String) 72 | 73 | object HoogleReference { 74 | implicit val decoder: Decoder[HoogleReference] = Decoder.forProduct2("name", "url")(HoogleReference.apply) 75 | } 76 | 77 | def make(config: Config): IO[CommandPluginError, HoogleCommand] = 78 | for { 79 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 80 | } yield HoogleCommand(commandNames.getOrElse(List("hoogle", "h"))) 81 | } 82 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/LocalIPCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.tools.Tools 5 | import commandcenter.view.Renderer 6 | import commandcenter.CCRuntime.Env 7 | import fansi.{Color, Str} 8 | import zio.* 9 | 10 | import java.net.{Inet4Address, NetworkInterface} 11 | import scala.jdk.CollectionConverters.* 12 | 13 | final case class LocalIPCommand(commandNames: List[String]) extends Command[String] { 14 | val commandType: CommandType = CommandType.LocalIPCommand 15 | val title: String = "Local IP" 16 | 17 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[String]] = 18 | for { 19 | input <- ZIO.fromOption(searchInput.asKeyword).orElseFail(CommandError.NotApplicable) 20 | localIps <- ZIO.attemptBlocking { 21 | val interfaces = NetworkInterface.getNetworkInterfaces.asScala.toList 22 | interfaces 23 | .filter(interface => !interface.isLoopback && !interface.isVirtual && interface.isUp) 24 | .flatMap { interface => 25 | interface.getInetAddresses.asScala.collect { case address: Inet4Address => 26 | interface.getDisplayName -> address.getHostAddress 27 | } 28 | } 29 | }.mapError(CommandError.UnexpectedError(this)) 30 | } yield PreviewResults.fromIterable(localIps.map { case (interfaceName, localIp) => 31 | Preview(localIp) 32 | .onRun(Tools.setClipboard(localIp)) 33 | .score(Scores.veryHigh(input.context)) 34 | .rendered( 35 | Renderer.renderDefault(title, Str(interfaceName) ++ Str(": ") ++ Color.Magenta(localIp)) 36 | ) 37 | }) 38 | } 39 | 40 | object LocalIPCommand extends CommandPlugin[LocalIPCommand] { 41 | 42 | def make(config: Config): IO[CommandPluginError, LocalIPCommand] = 43 | for { 44 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 45 | } yield LocalIPCommand(commandNames.getOrElse(List("localip"))) 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/LockCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.util.OS 5 | import commandcenter.CCRuntime.Env 6 | import zio.* 7 | import zio.process.Command as PCommand 8 | 9 | // TODO: Sleep vs lock distinction? 10 | // /System/Library/CoreServices/Menu\ Extras/User.menu/Contents/Resources/CGSession -suspend 11 | final case class LockCommand(commandNames: List[String]) extends Command[Unit] { 12 | val commandType: CommandType = CommandType.LockCommand 13 | val title: String = "Lock Computer" 14 | 15 | override val supportedOS: Set[OS] = Set(OS.MacOS, OS.Windows) 16 | 17 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 18 | for { 19 | input <- ZIO.fromOption(searchInput.asKeyword).orElseFail(CommandError.NotApplicable) 20 | } yield PreviewResults.one( 21 | Preview.unit.onRun(pCommand.exitCode.unit).score(Scores.veryHigh(input.context)) 22 | ) 23 | 24 | private def pCommand: PCommand = 25 | OS.os match { 26 | case OS.MacOS => PCommand("pmset", "displaysleepnow") 27 | case _ => PCommand("rundll32", "user32.dll,LockWorkStation") 28 | } 29 | } 30 | 31 | object LockCommand extends CommandPlugin[LockCommand] { 32 | 33 | def make(config: Config): IO[CommandPluginError, LockCommand] = 34 | for { 35 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 36 | } yield LockCommand(commandNames.getOrElse(List("lock"))) 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/LoremIpsumCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import cats.data.Validated 4 | import cats.syntax.apply.* 5 | import com.monovore.decline 6 | import com.monovore.decline.Opts 7 | import com.typesafe.config.Config 8 | import commandcenter.command.LoremIpsumCommand.ChunkType 9 | import commandcenter.tools.Tools 10 | import commandcenter.view.Renderer 11 | import commandcenter.CCRuntime.Env 12 | import fansi.Str 13 | import zio.* 14 | 15 | import scala.io.Source 16 | 17 | final case class LoremIpsumCommand(commandNames: List[String], lipsum: String) extends Command[Unit] { 18 | val commandType: CommandType = CommandType.LoremIpsumCommand 19 | val title: String = "Lorem Ipsum" 20 | 21 | val numOpt = Opts.argument[Int]("number").withDefault(1) 22 | 23 | val typeOpt = Opts 24 | .argument[String]("words, sentences, or paragraphs") 25 | .mapValidated { 26 | case arg if arg.matches("words?") => Validated.valid(ChunkType.Word) 27 | case arg if arg.matches("sentences?") => Validated.valid(ChunkType.Sentence) 28 | case arg if arg.matches("paragraphs?") => Validated.valid(ChunkType.Paragraph) 29 | case s => Validated.invalidNel(s"$s is not valid: should be 'words', 'sentences', or 'paragraphs'.") 30 | } 31 | .withDefault(ChunkType.Paragraph) 32 | 33 | val lipsumCommand = decline.Command(title, title)((numOpt, typeOpt).tupled) 34 | 35 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 36 | for { 37 | input <- ZIO.fromOption(searchInput.asArgs).orElseFail(CommandError.NotApplicable) 38 | parsed = lipsumCommand.parse(input.args) 39 | message <- ZIO 40 | .fromEither(parsed) 41 | .fold( 42 | HelpMessage.formatted, 43 | { case (i, chunkType) => 44 | Str(s"Will generate $i ${chunkType.toString}s to the clipboard") 45 | } 46 | ) 47 | } yield { 48 | val run = for { 49 | (i, chunkType) <- ZIO.fromEither(parsed).orElseFail(RunError.Ignore) 50 | text = chunkType match { 51 | case ChunkType.Word => Iterator.continually(lipsum.split("\\s")).flatten.take(i).mkString(" ") 52 | case ChunkType.Sentence => 53 | Iterator.continually(lipsum.split("\\.")).flatten.take(i).mkString(". ") ++ "." 54 | case ChunkType.Paragraph => Iterator.continually(lipsum).take(i).mkString("\n") 55 | } 56 | _ <- Tools.setClipboard(text) 57 | } yield () 58 | PreviewResults.one( 59 | Preview.unit 60 | .onRun(run) 61 | .score(Scores.veryHigh(input.context)) 62 | .rendered(Renderer.renderDefault("Lorem Ipsum", message)) 63 | ) 64 | } 65 | } 66 | 67 | object LoremIpsumCommand extends CommandPlugin[LoremIpsumCommand] { 68 | sealed trait ChunkType 69 | 70 | object ChunkType { 71 | case object Word extends ChunkType 72 | case object Sentence extends ChunkType 73 | case object Paragraph extends ChunkType 74 | } 75 | 76 | def make(config: Config): IO[CommandPluginError, LoremIpsumCommand] = 77 | for { 78 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 79 | lipsum <- ZIO.scoped { 80 | ZIO 81 | .fromAutoCloseable(ZIO.attempt(Source.fromResource("lipsum"))) 82 | .mapAttempt(_.getLines().mkString("\n")) 83 | .mapError(CommandPluginError.UnexpectedException.apply) 84 | } 85 | } yield LoremIpsumCommand(commandNames.getOrElse(List("lipsum", "lorem", "ipsum")), lipsum) 86 | 87 | } 88 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/MoreResults.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | sealed trait MoreResults 4 | 5 | object MoreResults { 6 | case object Exhausted extends MoreResults 7 | final case class Remaining[A](paginated: PreviewResults.Paginated[A]) extends MoreResults 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/MuteCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.sun.jna.platform.win32.User32 4 | import com.sun.jna.platform.win32.WinDef.{LPARAM, WPARAM} 5 | import com.typesafe.config.Config 6 | import commandcenter.util.OS 7 | import commandcenter.CCRuntime.Env 8 | import zio.* 9 | 10 | final case class MuteCommand(commandNames: List[String]) extends Command[Unit] { 11 | val commandType: CommandType = CommandType.MuteCommand 12 | val title: String = "Toggle Mute" 13 | 14 | override val supportedOS: Set[OS] = Set(OS.MacOS, OS.Windows) 15 | 16 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 17 | for { 18 | input <- ZIO.fromOption(searchInput.asKeyword).orElseFail(CommandError.NotApplicable) 19 | } yield PreviewResults.one( 20 | Preview.unit.onRun(muteTask).score(Scores.veryHigh(input.context)) 21 | ) 22 | 23 | private def muteTask: Task[Unit] = 24 | OS.os match { 25 | case OS.Windows => 26 | ZIO.attemptBlocking { 27 | val APPCOMMAND_VOLUME_MUTE = 0x80000 28 | // val APPCOMMAND_VOLUME_UP = 0xA0000 29 | // val APPCOMMAND_VOLUME_DOWN = 0x90000 30 | val WM_APPCOMMAND = 0x319 31 | 32 | User32.INSTANCE.SendMessage( 33 | User32.INSTANCE.GetForegroundWindow(), 34 | WM_APPCOMMAND, 35 | new WPARAM(0), 36 | new LPARAM(APPCOMMAND_VOLUME_MUTE) 37 | ) 38 | } 39 | case _ => ZIO.unit 40 | } 41 | } 42 | 43 | object MuteCommand extends CommandPlugin[MuteCommand] { 44 | 45 | def make(config: Config): IO[CommandPluginError, MuteCommand] = 46 | for { 47 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 48 | } yield MuteCommand(commandNames.getOrElse(List("mute"))) 49 | } 50 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/OpacityCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.monovore.decline 4 | import com.monovore.decline.Opts 5 | import com.typesafe.config.Config 6 | import commandcenter.view.Renderer 7 | import commandcenter.CCRuntime.Env 8 | import fansi.Str 9 | import zio.* 10 | 11 | final case class OpacityCommand(commandNames: List[String]) extends Command[Unit] { 12 | val commandType: CommandType = CommandType.OpacityCommand 13 | val title: String = "Set Opacity" 14 | 15 | val opacity = Opts.argument[Float]("opacity").validate("Opacity must be between 0.0-1.0")(o => o >= 0.0f && o <= 1.0f) 16 | 17 | val opacityCommand = decline.Command("opacity", title)(opacity) 18 | 19 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 20 | for { 21 | _ <- ZIO.fail(CommandError.NotApplicable).unlessZIO(searchInput.context.terminal.isOpacitySupported) 22 | input <- ZIO.fromOption(searchInput.asArgs).orElseFail(CommandError.NotApplicable) 23 | parsed = opacityCommand.parse(input.args) 24 | message <- ZIO 25 | .fromEither(parsed) 26 | .fold(HelpMessage.formatted, o => Str(s"Set opacity to $o")) 27 | currentOpacity <- input.context.terminal.opacity.mapError(CommandError.UnexpectedError(this)) 28 | } yield { 29 | val run = for { 30 | opacity <- ZIO.fromEither(parsed).orElseFail(RunError.Ignore) 31 | _ <- input.context.terminal.setOpacity(opacity) 32 | } yield () 33 | 34 | PreviewResults.one( 35 | Preview.unit 36 | .onRun(run) 37 | .score(Scores.veryHigh(input.context)) 38 | .rendered(Renderer.renderDefault(s"$title (current: $currentOpacity)", message)) 39 | ) 40 | } 41 | } 42 | 43 | object OpacityCommand extends CommandPlugin[OpacityCommand] { 44 | 45 | def make(config: Config): IO[CommandPluginError, OpacityCommand] = 46 | for { 47 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 48 | } yield OpacityCommand(commandNames.getOrElse(List("opacity"))) 49 | } 50 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/OpenBrowserCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.command.CommandError.* 5 | import commandcenter.util.ProcessUtil 6 | import commandcenter.CCRuntime.Env 7 | import zio.* 8 | 9 | final case class OpenBrowserCommand() extends Command[Unit] { 10 | val commandType: CommandType = CommandType.OpenBrowserCommand 11 | 12 | val commandNames: List[String] = List.empty 13 | 14 | val title: String = "Open in Browser" 15 | 16 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = { 17 | val input = searchInput.input 18 | val startsWith = input.startsWith("http://") || input.startsWith("https://") 19 | 20 | // TODO: also check endsWith TLD + URL.isValid 21 | 22 | if (startsWith) 23 | ZIO.succeed(PreviewResults.one(Preview.unit.onRun(ProcessUtil.openBrowser(input)))) 24 | else 25 | ZIO.fail(NotApplicable) 26 | } 27 | } 28 | 29 | object OpenBrowserCommand extends CommandPlugin[OpenBrowserCommand] { 30 | def make(config: Config): UIO[OpenBrowserCommand] = ZIO.succeed(OpenBrowserCommand()) 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/PreviewResult.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import commandcenter.event.KeyboardShortcut 4 | import commandcenter.view.Rendered 5 | import commandcenter.CCRuntime.Env 6 | import zio.* 7 | 8 | sealed trait PreviewResult[+A] { 9 | def onRun: RIO[Env, Unit] 10 | def runOption: RunOption 11 | def moreResults: MoreResults 12 | def score: Double 13 | def renderFn: () => Rendered 14 | def shortcuts: Set[KeyboardShortcut] 15 | 16 | def runOption(runOption: RunOption): PreviewResult[A] 17 | 18 | def moreResults(moreResults: MoreResults): PreviewResult[A] 19 | 20 | def score(score: Double): PreviewResult[A] 21 | 22 | def onRun(onRun: RIO[Env, Unit]): PreviewResult[A] 23 | 24 | def rendered(rendered: => Rendered): PreviewResult[A] 25 | 26 | def renderedAnsi(renderedAnsi: => fansi.Str): PreviewResult[A] = rendered(Rendered.Ansi(renderedAnsi)) 27 | 28 | def onRunSandboxedLogged: RIO[Env, Unit] = 29 | onRun.tapErrorCause { c => 30 | val commandName = this match { 31 | case _: PreviewResult.None => "" 32 | case p: PreviewResult.Some[A] => p.source.getClass.getCanonicalName 33 | } 34 | 35 | c.squash match { 36 | case RunError.Ignore => ZIO.unit 37 | case _ => ZIO.logWarningCause(s"Failed to run $commandName", c) 38 | } 39 | }.absorb 40 | } 41 | 42 | object PreviewResult { 43 | 44 | final case class None( 45 | onRun: RIO[Env, Unit], 46 | runOption: RunOption, 47 | moreResults: MoreResults, 48 | score: Double, 49 | renderFn: () => Rendered 50 | ) extends PreviewResult[Nothing] { 51 | def runOption(runOption: RunOption): PreviewResult[Nothing] = copy(runOption = runOption) 52 | 53 | def moreResults(moreResults: MoreResults): PreviewResult[Nothing] = copy(moreResults = moreResults) 54 | 55 | def score(score: Double): PreviewResult[Nothing] = copy(score = score) 56 | 57 | def onRun(onRun: RIO[Env, Unit]): PreviewResult[Nothing] = copy(onRun = onRun) 58 | 59 | def rendered(rendered: => Rendered): PreviewResult[Nothing] = copy(renderFn = () => rendered) 60 | 61 | def shortcuts: Set[KeyboardShortcut] = Set.empty 62 | } 63 | 64 | final case class Some[+A]( 65 | source: Command[A], 66 | result: A, 67 | onRun: RIO[Env, Unit], 68 | runOption: RunOption, 69 | moreResults: MoreResults, 70 | score: Double, 71 | renderFn: () => Rendered 72 | ) extends PreviewResult[A] { 73 | def runOption(runOption: RunOption): PreviewResult[A] = copy(runOption = runOption) 74 | 75 | def moreResults(moreResults: MoreResults): PreviewResult[A] = copy(moreResults = moreResults) 76 | 77 | def score(score: Double): PreviewResult[A] = copy(score = score) 78 | 79 | def onRun(onRun: RIO[Env, Unit]): PreviewResult[A] = copy(onRun = onRun) 80 | 81 | def rendered(rendered: => Rendered): PreviewResult[A] = copy(renderFn = () => rendered) 82 | def renderFn(renderFn: A => Rendered): PreviewResult[A] = copy(renderFn = () => renderFn(result)) 83 | 84 | def shortcuts: Set[KeyboardShortcut] = source.shortcuts 85 | } 86 | 87 | def nothing(rendered: Rendered): PreviewResult[Nothing] = 88 | PreviewResult.None(ZIO.unit, RunOption.Hide, MoreResults.Exhausted, Scores.default, () => rendered) 89 | 90 | def unit(source: Command[Unit], rendered: Rendered): PreviewResult[Unit] = 91 | PreviewResult.Some(source, (), ZIO.unit, RunOption.Hide, MoreResults.Exhausted, Scores.default, () => rendered) 92 | } 93 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/PreviewResults.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import commandcenter.CCRuntime.Env 4 | import zio.* 5 | import zio.stream.ZStream 6 | 7 | sealed trait PreviewResults[+A] 8 | 9 | object PreviewResults { 10 | 11 | def one[A](result: PreviewResult[A]): PreviewResults[A] = 12 | PreviewResults.Single(result) 13 | 14 | def multiple[A](result: PreviewResult[A], resultsRest: PreviewResult[A]*): PreviewResults[A] = 15 | PreviewResults.Multiple(NonEmptyChunk(result, resultsRest*)) 16 | 17 | def fromIterable[A](results: Iterable[PreviewResult[A]]): PreviewResults[A] = 18 | PreviewResults.Multiple(Chunk.fromIterable(results)) 19 | 20 | def paginated[A]( 21 | stream: ZStream[Env, CommandError, PreviewResult[A]], 22 | initialPageSize: Int, 23 | morePageSize: Int, 24 | totalRemaining: Option[Long] = None 25 | ): PreviewResults.Paginated[A] = 26 | PreviewResults.Paginated(stream, initialPageSize, morePageSize, totalRemaining) 27 | 28 | final case class Single[A](result: PreviewResult[A]) extends PreviewResults[A] 29 | 30 | final case class Multiple[A](results: Chunk[PreviewResult[A]]) extends PreviewResults[A] 31 | 32 | final case class Paginated[A]( 33 | results: ZStream[Env, CommandError, PreviewResult[A]], 34 | initialPageSize: Int, 35 | morePageSize: Int, 36 | totalRemaining: Option[Long] 37 | ) extends PreviewResults[A] { 38 | 39 | def moreMessage: String = 40 | totalRemaining match { 41 | case Some(remaining) => s"Load $morePageSize of $remaining more..." 42 | case None => "More..." 43 | } 44 | 45 | } 46 | 47 | object Paginated { 48 | 49 | def fromIterable[A]( 50 | results: Iterable[PreviewResult[A]], 51 | initialPageSize: Int, 52 | morePageSize: Int, 53 | totalRemaining: Option[Long] = None 54 | ): Paginated[A] = 55 | PreviewResults.Paginated(ZStream.fromIterable(results), initialPageSize, morePageSize, totalRemaining) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/ProcessIdCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.command.ProcessIdCommand.ProcessInfo 5 | import commandcenter.tools.Tools 6 | import commandcenter.CCRuntime.Env 7 | import fansi.Color 8 | import zio.* 9 | 10 | import java.time.Instant 11 | import scala.jdk.CollectionConverters.* 12 | import scala.jdk.OptionConverters.* 13 | 14 | final case class ProcessIdCommand(commandNames: List[String]) extends Command[Unit] { 15 | val commandType: CommandType = CommandType.ProcessIdCommand 16 | val title: String = "Process ID" 17 | 18 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 19 | for { 20 | input <- ZIO.fromOption(searchInput.asPrefixed).orElseFail(CommandError.NotApplicable) 21 | processes = ProcessHandle 22 | .allProcesses() 23 | .iterator() 24 | .asScala 25 | .toSeq 26 | .flatMap { p => 27 | (p.info.command.toScala, p.info.startInstant.toScala) match { 28 | case (Some(command), Some(startTime)) => 29 | Some(ProcessInfo(p.pid(), command, startTime)) 30 | 31 | case _ => 32 | None 33 | } 34 | } 35 | .sortBy(_.startTime)(Ordering[Instant].reverse) 36 | } yield PreviewResults.Paginated.fromIterable( 37 | processes.map { process => 38 | val run = Tools.setClipboard(process.pid.toString) 39 | 40 | Preview.unit 41 | .onRun(run) 42 | .score(Scores.veryHigh(input.context)) 43 | .renderedAnsi( 44 | Color.Cyan(process.command) ++ "\n" ++ "Copy PID to clipboard: " ++ Color.Magenta(process.pid.toString) 45 | ) 46 | }, 47 | initialPageSize = 15, 48 | morePageSize = 30 49 | ) 50 | } 51 | 52 | object ProcessIdCommand extends CommandPlugin[ProcessIdCommand] { 53 | 54 | def make(config: Config): IO[CommandPluginError, ProcessIdCommand] = 55 | for { 56 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 57 | } yield ProcessIdCommand(commandNames.getOrElse(List("pid", "process"))) 58 | 59 | final case class ProcessInfo(pid: Long, command: String, startTime: Instant) 60 | } 61 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/RadixCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import cats.syntax.apply.* 4 | import com.monovore.decline 5 | import com.monovore.decline.{Help, Opts} 6 | import com.typesafe.config.Config 7 | import commandcenter.tools.Tools 8 | import commandcenter.view.Renderer 9 | import commandcenter.CCRuntime.Env 10 | import commandcenter.CommandContext 11 | import fansi.Str 12 | import zio.* 13 | 14 | import scala.util.matching.Regex 15 | import scala.util.Try 16 | 17 | final case class RadixCommand(commandNames: List[String]) extends Command[Unit] { 18 | val commandType: CommandType = CommandType.RadixCommand 19 | val title: String = "Convert base" 20 | 21 | val fromRadixOpt = Opts.option[Int]("from", "Radix to convert from", "f").orNone 22 | val toRadixOpt = Opts.option[Int]("to", "Radix to convert to", "t").orNone 23 | val numberArg = Opts.argument[String]("number") 24 | 25 | val radixCommand = decline.Command("radix", title)((fromRadixOpt, toRadixOpt, numberArg).tupled) 26 | 27 | val hexRegex: Regex = "0[xX]([0-9a-fA-F]+)".r 28 | 29 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 30 | ZIO 31 | .succeed(detectAndPreviewHex(searchInput)) 32 | .someOrElseZIO(previewRadixCommand(searchInput)) 33 | 34 | private def previewRadixCommand(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 35 | for { 36 | input <- ZIO.fromOption(searchInput.asArgs).orElseFail(CommandError.NotApplicable) 37 | parsed = radixCommand.parse(input.args) 38 | preview <- ZIO 39 | .fromEither(parsed) 40 | .fold( 41 | h => Preview.help(h).score(Scores.veryHigh(input.context)), 42 | { case (fromRadixOpt, toRadixOpt, numberAsString) => 43 | val fromRadix = fromRadixOpt.getOrElse(10) 44 | val toRadix = toRadixOpt.getOrElse(10) 45 | 46 | Try { 47 | convertNumber(numberAsString, fromRadix, toRadix, input.context) 48 | }.getOrElse { 49 | Preview.help(Help.fromCommand(radixCommand)).score(Scores.veryHigh(input.context)) 50 | } 51 | } 52 | ) 53 | } yield PreviewResults.one(preview) 54 | 55 | private def detectAndPreviewHex(searchInput: SearchInput): Option[PreviewResults[Unit]] = 56 | searchInput.input match { 57 | case hexRegex(n) => 58 | Some( 59 | PreviewResults.multiple( 60 | convertNumber(n, 16, 10, searchInput.context), 61 | convertNumber(n, 16, 2, searchInput.context) 62 | ) 63 | ) 64 | 65 | case _ => None 66 | } 67 | 68 | private def convertNumber( 69 | numberAsString: String, 70 | fromRadix: Int, 71 | toRadix: Int, 72 | context: CommandContext 73 | ): PreviewResult[Unit] = { 74 | val n = java.lang.Long.valueOf(numberAsString, fromRadix) 75 | val formatted = java.lang.Long.toString(n, toRadix) 76 | val message = Str(s"$formatted") 77 | 78 | Preview.unit 79 | .score(Scores.veryHigh(context)) 80 | .onRun(Tools.setClipboard(message.plainText)) 81 | .rendered(Renderer.renderDefault(s"Convert base $fromRadix to $toRadix", message)) 82 | } 83 | 84 | } 85 | 86 | object RadixCommand extends CommandPlugin[RadixCommand] { 87 | 88 | def make(config: Config): IO[CommandPluginError, RadixCommand] = 89 | for { 90 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 91 | } yield RadixCommand(commandNames.getOrElse(List("radix", "base"))) 92 | } 93 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/RebootCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.util.{AppleScript, OS} 5 | import commandcenter.view.Renderer 6 | import commandcenter.CCRuntime.Env 7 | import zio.* 8 | import zio.process.{Command as PCommand, CommandError as PCommandError} 9 | 10 | final case class RebootCommand(commandNames: List[String]) extends Command[Unit] { 11 | val commandType: CommandType = CommandType.RebootCommand 12 | val title: String = "Reboot" 13 | 14 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 15 | for { 16 | input <- ZIO.fromOption(searchInput.asKeyword).orElseFail(CommandError.NotApplicable) 17 | } yield PreviewResults.fromIterable( 18 | Vector( 19 | Preview.unit 20 | .onRun(RebootCommand.reboot) 21 | .score(Scores.veryHigh(input.context)) 22 | .rendered(Renderer.renderDefault(title, "Restart your computer")) 23 | ) ++ Vector( 24 | Preview.unit 25 | .onRun(RebootCommand.rebootIntoBios) 26 | .score(Scores.veryHigh(input.context)) 27 | .rendered( 28 | Renderer.renderDefault(s"$title (into BIOS setup)", "Restart your computer and enter BIOS upon startup") 29 | ) 30 | ).filter(_ => OS.os == OS.Windows || OS.os == OS.Linux) 31 | ) 32 | } 33 | 34 | object RebootCommand extends CommandPlugin[RebootCommand] { 35 | 36 | def make(config: Config): IO[CommandPluginError, RebootCommand] = 37 | for { 38 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 39 | } yield RebootCommand(commandNames.getOrElse(List("reboot", "restart"))) 40 | 41 | def reboot: ZIO[Any, Throwable, Unit] = 42 | ZIO 43 | .whenCase(OS.os) { 44 | case OS.Windows => 45 | PCommand("shutdown", "/r", "/t", "0").successfulExitCode.unit 46 | 47 | case OS.Linux => 48 | // TODO: Probably need to check for different flavors of Linux. Not sure if this works everywhere. 49 | PCommand("systemctl", "reboot").successfulExitCode.unit 50 | 51 | case OS.MacOS => 52 | AppleScript.runScript("tell application \"System Events\" to restart").unit 53 | } 54 | .unit 55 | 56 | def rebootIntoBios: ZIO[Any, PCommandError, Unit] = 57 | ZIO 58 | .whenCase(OS.os) { 59 | case OS.Windows => 60 | PCommand("shutdown", "/r", "/fw", "/t", "0").successfulExitCode.unit 61 | 62 | case OS.Linux => 63 | // TODO: Probably need to check for different flavors of Linux. Not sure if this works everywhere. 64 | PCommand("systemctl", "reboot", "--firmware-setup").successfulExitCode.unit 65 | } 66 | .unit 67 | } 68 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/ReloadCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.view.Renderer 5 | import commandcenter.CCRuntime.Env 6 | import zio.* 7 | 8 | final case class ReloadCommand(commandNames: List[String]) extends Command[Unit] { 9 | val commandType: CommandType = CommandType.ResizeCommand 10 | val title: String = "Reload Config" 11 | 12 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 13 | for { 14 | input <- ZIO.fromOption(searchInput.asKeyword).orElseFail(CommandError.NotApplicable) 15 | } yield PreviewResults.one( 16 | Preview.unit 17 | .onRun(input.context.terminal.reload) 18 | .score(Scores.veryHigh(input.context)) 19 | .rendered(Renderer.renderDefault(title, "")) 20 | ) 21 | } 22 | 23 | object ReloadCommand extends CommandPlugin[ReloadCommand] { 24 | 25 | def make(config: Config): IO[CommandPluginError, ReloadCommand] = 26 | for { 27 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 28 | } yield ReloadCommand(commandNames.getOrElse(List("reload"))) 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/ResizeCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import cats.implicits.* 4 | import com.monovore.decline 5 | import com.monovore.decline.Opts 6 | import com.typesafe.config.Config 7 | import commandcenter.view.Renderer 8 | import commandcenter.CCRuntime.Env 9 | import fansi.Str 10 | import zio.* 11 | 12 | final case class ResizeCommand(commandNames: List[String]) extends Command[Unit] { 13 | val commandType: CommandType = CommandType.ResizeCommand 14 | val title: String = "Resize Window" 15 | 16 | val width = Opts.argument[Int]("width").validate("Width must be greater than 0")(_ > 0) 17 | val height = Opts.argument[Int]("height").validate("Height must be greater than 0")(_ > 0) 18 | 19 | val resizeCommand = decline.Command("resize", title)((width, height).tupled) 20 | 21 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 22 | for { 23 | input <- ZIO.fromOption(searchInput.asArgs).orElseFail(CommandError.NotApplicable) 24 | parsed = resizeCommand.parse(input.args) 25 | message <- ZIO 26 | .fromEither(parsed) 27 | .fold( 28 | HelpMessage.formatted, 29 | { case (w, h) => 30 | Str(s"Set window size to width: $w, maxHeight: $h)") 31 | } 32 | ) 33 | } yield { 34 | val run = for { 35 | (w, h) <- ZIO.fromEither(parsed).orElseFail(RunError.Ignore) 36 | _ <- input.context.terminal.setSize(w, h) 37 | } yield () 38 | 39 | PreviewResults.one( 40 | Preview.unit 41 | .onRun(run) 42 | .score(Scores.veryHigh(input.context)) 43 | .rendered(Renderer.renderDefault(title, message)) 44 | ) 45 | } 46 | } 47 | 48 | object ResizeCommand extends CommandPlugin[ResizeCommand] { 49 | 50 | def make(config: Config): IO[CommandPluginError, ResizeCommand] = 51 | for { 52 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 53 | } yield ResizeCommand(commandNames.getOrElse(List("resize", "size"))) 54 | } 55 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/RunError.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.monovore.decline.Help 4 | 5 | sealed abstract class RunError(cause: Throwable) extends Exception(cause) with Product with Serializable 6 | 7 | object RunError { 8 | final case class UnexpectedException(cause: Throwable) extends RunError(cause) 9 | final case class InternalError(message: String) extends RunError(null) 10 | final case class CliError(help: Help) extends RunError(null) 11 | 12 | case object Ignore extends RunError(null) 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/RunOption.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | sealed trait RunOption 4 | 5 | object RunOption { 6 | case object Exit extends RunOption 7 | case object Hide extends RunOption 8 | case object RemainOpen extends RunOption 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/Scores.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import commandcenter.CommandContext 4 | 5 | object Scores { 6 | val hide: Double = 0 7 | val low: Double = 1 8 | val default: Double = 10 9 | val high: Double = 100 10 | val veryHigh: Double = 1000 11 | 12 | def low(context: CommandContext): Double = low * context.matchScore 13 | def default(context: CommandContext): Double = default * context.matchScore 14 | def high(context: CommandContext): Double = high * context.matchScore 15 | def veryHigh(context: CommandContext): Double = veryHigh * context.matchScore 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/SearchCratesCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.command.SearchCratesCommand.CrateResults 5 | import commandcenter.tools.Tools 6 | import commandcenter.CCRuntime.Env 7 | import commandcenter.Sttp 8 | import fansi.{Color, Str} 9 | import io.circe.Decoder 10 | import sttp.client3.* 11 | import sttp.client3.circe.* 12 | import zio.* 13 | import zio.stream.ZStream 14 | 15 | import java.time.OffsetDateTime 16 | 17 | final case class SearchCratesCommand(commandNames: List[String]) extends Command[Unit] { 18 | val commandType: CommandType = CommandType.SearchCratesCommand 19 | val title: String = "Crates" 20 | val pageSize: Int = 10 21 | 22 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 23 | for { 24 | input <- ZIO.fromOption(searchInput.asPrefixed.filter(_.rest.nonEmpty)).orElseFail(CommandError.NotApplicable) 25 | cratesStream = ZStream.paginateChunkZIO(1) { page => 26 | val request = basicRequest 27 | .get(uri"https://crates.io/api/v1/crates?page=$page&per_page=$pageSize&q=${input.rest}") 28 | .response(asJson[CrateResults]) 29 | 30 | Sttp 31 | .send(request) 32 | .map(_.body) 33 | .absolve 34 | .mapBoth( 35 | CommandError.UnexpectedError(this), 36 | r => (r.crates, r.meta.nextPage.map(_ => page + 1)) 37 | ) 38 | } 39 | } yield PreviewResults.paginated( 40 | cratesStream.map { result => 41 | Preview.unit 42 | .onRun(Tools.setClipboard(result.render)) 43 | .score(Scores.veryHigh(input.context)) 44 | .renderedAnsi(result.renderColored) 45 | }, 46 | pageSize, 47 | pageSize 48 | ) 49 | } 50 | 51 | object SearchCratesCommand extends CommandPlugin[SearchCratesCommand] { 52 | final case class CrateResults(crates: Chunk[CrateResult], meta: MetaResult) 53 | 54 | object CrateResults { 55 | 56 | implicit val decoder: Decoder[CrateResults] = 57 | Decoder.forProduct2("crates", "meta")(CrateResults.apply) 58 | } 59 | 60 | final case class MetaResult(total: Int, nextPage: Option[String]) 61 | 62 | object MetaResult { 63 | 64 | implicit val decoder: Decoder[MetaResult] = 65 | Decoder.forProduct2("total", "next_page")(MetaResult.apply) 66 | } 67 | 68 | final case class CrateResult( 69 | id: String, 70 | name: String, 71 | description: String, 72 | createdAt: OffsetDateTime, 73 | updatedAt: OffsetDateTime, 74 | downloads: Int, 75 | recentDownloads: Int, 76 | maxVersion: String 77 | ) { 78 | def descriptionSanitized: String = description.replace("\n", "").trim 79 | 80 | def render: String = 81 | s"""$name = "$maxVersion"""" 82 | 83 | def renderColored: Str = 84 | Str( 85 | Color.Cyan(name), 86 | Color.LightGray(" = "), 87 | Color.Green(s""""$maxVersion""""), 88 | Color.LightGray(s" # $descriptionSanitized") 89 | ) 90 | } 91 | 92 | object CrateResult { 93 | 94 | implicit val decoder: Decoder[CrateResult] = 95 | Decoder.forProduct8( 96 | "id", 97 | "name", 98 | "description", 99 | "created_at", 100 | "updated_at", 101 | "downloads", 102 | "recent_downloads", 103 | "max_version" 104 | )(CrateResult.apply) 105 | } 106 | 107 | def make(config: Config): IO[CommandPluginError, SearchCratesCommand] = 108 | for { 109 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 110 | } yield SearchCratesCommand(commandNames.getOrElse(List("crate", "crates"))) 111 | } 112 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/SearchResults.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import commandcenter.view.Rendered 4 | import zio.* 5 | 6 | final case class SearchResults[A]( 7 | searchTerm: String, 8 | previews: Chunk[PreviewResult[A]], 9 | errors: Chunk[CommandError] = Chunk.empty 10 | ) { 11 | lazy val rendered: Chunk[Rendered] = previews.map(_.renderFn()) 12 | 13 | def hasChange(otherSearchTerm: String): Boolean = 14 | searchTerm.trim != otherSearchTerm.trim 15 | } 16 | 17 | object SearchResults { 18 | def empty[A]: SearchResults[A] = SearchResults("", Chunk.empty) 19 | } 20 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/SearchUrlCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.codec.Codecs.localeDecoder 5 | import commandcenter.command.CommandError.* 6 | import commandcenter.config.Decoders.* 7 | import commandcenter.event.KeyboardShortcut 8 | import commandcenter.util.ProcessUtil 9 | import commandcenter.view.Renderer 10 | import commandcenter.CCRuntime.Env 11 | import fansi.{Color, Str} 12 | import sttp.model.internal.Rfc3986 13 | import zio.* 14 | 15 | import java.nio.file.Path 16 | import java.util.Locale 17 | 18 | final case class SearchUrlCommand( 19 | title: String, 20 | urlTemplate: String, 21 | override val commandNames: List[String], 22 | override val locales: Set[Locale], 23 | override val shortcuts: Set[KeyboardShortcut], 24 | firefoxPath: Option[Path] 25 | ) extends Command[Unit] { 26 | val commandType: CommandType = CommandType.SearchUrlCommand 27 | 28 | val encodeQuery: String => String = 29 | Rfc3986.encode(Rfc3986.Query -- Set('&', '='), spaceAsPlus = false, encodePlus = true) 30 | 31 | def openBrowser(query: String): Task[Unit] = { 32 | val url = urlTemplate.replace("{query}", encodeQuery(query)) 33 | ProcessUtil.openBrowser(url, firefoxPath) 34 | } 35 | 36 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = { 37 | def prefixPreview: ZIO[Env, CommandError, PreviewResults[Unit]] = 38 | for { 39 | input <- ZIO.fromOption(searchInput.asPrefixed.filter(_.rest.nonEmpty)).orElseFail(CommandError.NotApplicable) 40 | localeBoost = if (locales.contains(input.context.locale)) 2 else 1 41 | } yield PreviewResults.one( 42 | Preview.unit 43 | .score(Scores.veryHigh * localeBoost) 44 | .onRun(openBrowser(input.rest)) 45 | .rendered(Renderer.renderDefault(title, Str("Search for ") ++ Color.Magenta(input.rest))) 46 | ) 47 | 48 | def rawInputPreview: ZIO[Env, CommandError, PreviewResults[Unit]] = 49 | if (searchInput.input.isEmpty || !(locales.isEmpty || locales.contains(searchInput.context.locale))) 50 | ZIO.fail(NotApplicable) 51 | else { 52 | ZIO.succeed( 53 | PreviewResults.one( 54 | Preview.unit 55 | .score(Scores.high * 0.35) 56 | .onRun(openBrowser(searchInput.input)) 57 | .rendered( 58 | Renderer.renderDefault(title, Str("Search for ") ++ Color.Magenta(searchInput.input)) 59 | ) 60 | ) 61 | ) 62 | } 63 | 64 | prefixPreview.orElse(rawInputPreview) 65 | } 66 | } 67 | 68 | object SearchUrlCommand extends CommandPlugin[SearchUrlCommand] { 69 | 70 | def make(config: Config): IO[CommandPluginError, SearchUrlCommand] = 71 | for { 72 | title <- config.getZIO[String]("title") 73 | urlTemplate <- config.getZIO[String]("urlTemplate") 74 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 75 | locales <- config.getZIO[Option[Set[Locale]]]("locales") 76 | shortcuts <- config.getZIO[Option[Set[KeyboardShortcut]]]("shortcuts") 77 | firefoxPath <- config.getZIO[Option[Path]]("firefoxPath") 78 | } yield SearchUrlCommand( 79 | title, 80 | urlTemplate, 81 | commandNames.getOrElse(Nil), 82 | locales.getOrElse(Set.empty), 83 | shortcuts.getOrElse(Set.empty), 84 | firefoxPath 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/SnippetsCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.command.SnippetsCommand.Snippet 5 | import commandcenter.scorers.LengthScorer 6 | import commandcenter.tools.Tools 7 | import commandcenter.view.Renderer 8 | import commandcenter.CCRuntime.Env 9 | import fansi.Color 10 | import io.circe.Decoder 11 | import zio.* 12 | 13 | final case class SnippetsCommand(commandNames: List[String], snippets: List[Snippet]) extends Command[String] { 14 | val commandType: CommandType = CommandType.SnippetsCommand 15 | val title: String = "Snippets" 16 | 17 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[String]] = 18 | for { 19 | (isPrefixed, snippetSearch) <- 20 | ZIO.fromOption(searchInput.asPrefixed).mapBoth(_ => (false, searchInput.input), s => (true, s.rest)).merge 21 | 22 | } yield PreviewResults.fromIterable(snippets.map { snip => 23 | val score = 24 | if (isPrefixed) 25 | Scores.veryHigh(searchInput.context) 26 | else { 27 | // TODO: Implement better scoring algorithm (like Sublime Text fuzzy search) 28 | val matchScore = LengthScorer.scoreDefault(snip.keyword, snippetSearch) 29 | Scores.veryHigh(searchInput.context) * matchScore 30 | } 31 | 32 | (snip, score) 33 | }.collect { 34 | case (snippet, score) if score > 0 => 35 | Preview(snippet.value) 36 | .onRun(Tools.setClipboard(snippet.value)) 37 | .score(score) 38 | .rendered(Renderer.renderDefault(title, Color.Magenta(snippet.keyword) ++ " " ++ snippet.value)) 39 | }) 40 | } 41 | 42 | object SnippetsCommand extends CommandPlugin[SnippetsCommand] { 43 | final case class Snippet(keyword: String, value: String) 44 | 45 | object Snippet { 46 | implicit val decoder: Decoder[Snippet] = Decoder.forProduct2("keyword", "value")(Snippet.apply) 47 | } 48 | 49 | def make(config: Config): IO[CommandPluginError, SnippetsCommand] = 50 | for { 51 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 52 | snippets <- config.getZIO[Option[List[Snippet]]]("snippets") 53 | } yield SnippetsCommand( 54 | commandNames.getOrElse(List("snippets", "snippet", "snip")), 55 | snippets.getOrElse(Nil) 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/SpeakCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.cache.ZCache 5 | import commandcenter.event.KeyboardShortcut 6 | import commandcenter.locale.Language 7 | import commandcenter.util.{OS, PowerShellScript} 8 | import commandcenter.view.Renderer 9 | import commandcenter.CCRuntime.Env 10 | import zio.* 11 | import zio.process.Command as PCommand 12 | 13 | import java.util.Locale 14 | import scala.io.Source 15 | 16 | final case class SpeakCommand( 17 | commandNames: List[String], 18 | override val shortcuts: Set[KeyboardShortcut] = Set.empty, 19 | voice: String, 20 | cache: ZCache[String, String] 21 | ) extends Command[Unit] { 22 | val commandType: CommandType = CommandType.SpeakCommand 23 | val title: String = "Speak" 24 | 25 | def speak(voice: String, speakText: String): ZIO[Any, Throwable, Unit] = 26 | OS.os match { 27 | case OS.Windows => 28 | speakFn(voice, speakText).unit 29 | 30 | case OS.MacOS => 31 | val voice = Language.detect(speakText) match { 32 | case Locale.ENGLISH => "Samantha" 33 | case Locale.KOREAN => "Yuna" 34 | case Locale.JAPANESE => "Kyoko" 35 | } 36 | 37 | PCommand("say", "-v", voice, speakText).exitCode.unit 38 | 39 | case _ => ZIO.unit 40 | 41 | } 42 | 43 | val speakFn = 44 | if (OS.os == OS.Windows) 45 | PowerShellScript.loadFunction2[String, String](cache)("system/speak.ps1") 46 | else 47 | (_: String, _: String) => ZIO.succeed("") 48 | 49 | // TODO: Support Linux 50 | override val supportedOS: Set[OS] = Set(OS.Windows, OS.MacOS) 51 | 52 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = { 53 | val inputOpt = searchInput.asPrefixed 54 | 55 | for { 56 | speakText <- ZIO.fromOption(inputOpt).fold(_ => searchInput.input, _.rest) 57 | _ <- ZIO.fail(CommandError.NotApplicable).when(speakText.isEmpty) 58 | } yield { 59 | val run = for { 60 | _ <- speak(voice, speakText) 61 | } yield () 62 | 63 | PreviewResults.one( 64 | Preview.unit 65 | .onRun(run) 66 | .runOption(RunOption.RemainOpen) 67 | .score(inputOpt.fold(Scores.hide)(input => Scores.veryHigh(input.context))) 68 | .rendered(Renderer.renderDefault(title, "")) 69 | ) 70 | } 71 | } 72 | 73 | } 74 | 75 | object SpeakCommand extends CommandPlugin[SpeakCommand] { 76 | 77 | def make(config: Config): IO[CommandPluginError, SpeakCommand] = 78 | for { 79 | runtime <- ZIO.runtime[Any] 80 | cache = ZCache 81 | .memoizeZIO(1024, None)((resource: String) => 82 | ZIO.succeed(Some(Source.fromResource(resource)).map(_.mkString)) 83 | )(runtime) 84 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 85 | shortcuts <- config.getZIO[Option[Set[KeyboardShortcut]]]("shortcuts") 86 | voice <- config.getZIO[Option[String]]("voice") 87 | } yield SpeakCommand( 88 | commandNames.getOrElse(List("speak", "say")), 89 | shortcuts.getOrElse(Set.empty), 90 | voice.getOrElse(""), 91 | cache 92 | ) 93 | 94 | } 95 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/SwitchWindowCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.util.{OS, WindowManager} 5 | import commandcenter.CCRuntime.Env 6 | import fansi.Color 7 | import zio.* 8 | 9 | final case class SwitchWindowCommand(commandNames: List[String]) extends Command[Unit] { 10 | val commandType: CommandType = CommandType.SwitchWindowCommand 11 | val title: String = "Switch Window" 12 | 13 | // TODO: Support macOS and Linux too 14 | override val supportedOS: Set[OS] = Set(OS.Windows) 15 | 16 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 17 | for { 18 | input <- ZIO.fromOption(searchInput.asPrefixed).orElseFail(CommandError.NotApplicable) 19 | // TODO: Consider adding more info than just the title. Like "File Explorer" and so on. 20 | windows <- WindowManager.topLevelWindows.mapBoth( 21 | CommandError.UnexpectedError(this), 22 | _.tail.filter(_.title.toLowerCase.contains(input.rest)) 23 | ) 24 | } yield PreviewResults.fromIterable(windows.map { w => 25 | Preview.unit 26 | .onRun(WindowManager.giveWindowFocus(w.windowHandle)) 27 | .score(Scores.veryHigh(input.context)) 28 | .renderedAnsi(Color.Cyan(w.title)) 29 | }) 30 | } 31 | 32 | object SwitchWindowCommand extends CommandPlugin[SwitchWindowCommand] { 33 | 34 | def make(config: Config): IO[CommandPluginError, SwitchWindowCommand] = 35 | for { 36 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 37 | } yield SwitchWindowCommand(commandNames.getOrElse(List("window", "w"))) 38 | } 39 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/TemperatureCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.command.CommandError.* 5 | import commandcenter.tools.Tools 6 | import commandcenter.view.Renderer 7 | import commandcenter.CCRuntime.Env 8 | import zio.* 9 | 10 | import scala.util.matching.Regex 11 | 12 | final case class TemperatureCommand() extends Command[Double] { 13 | val commandType: CommandType = CommandType.TemperatureCommand 14 | 15 | val commandNames: List[String] = List.empty 16 | 17 | val title: String = "Temperature" 18 | 19 | val temperatureRegex: Regex = """(-?\d+\.?\d*)\s*([cCCfFF度どド])""".r 20 | 21 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Double]] = 22 | for { 23 | (temperature, unit) <- ZIO.fromOption { 24 | temperatureRegex.unapplySeq(searchInput.input.trim).flatMap { 25 | case List(value, sourceUnit) => 26 | val v = value.toDouble 27 | 28 | val targetUnit = if (sourceUnit.equalsIgnoreCase("f")) "C" else "F" 29 | 30 | val temp = 31 | if (sourceUnit.equalsIgnoreCase("f")) 32 | (v - 32) * (5 / 9.0) 33 | else 34 | v * (9.0 / 5) + 32 35 | 36 | Some((temp, targetUnit)) 37 | 38 | case _ => None 39 | } 40 | }.orElseFail(NotApplicable) 41 | temperatureFormatted = f"$temperature%.1f $unit" 42 | } yield PreviewResults.one( 43 | Preview(temperature) 44 | .score(Scores.veryHigh(searchInput.context)) 45 | .rendered(Renderer.renderDefault(title, temperatureFormatted)) 46 | .onRun(Tools.setClipboard(temperatureFormatted)) 47 | ) 48 | } 49 | 50 | object TemperatureCommand extends CommandPlugin[TemperatureCommand] { 51 | def make(config: Config): UIO[TemperatureCommand] = ZIO.succeed(TemperatureCommand()) 52 | } 53 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/TerminalCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.view.Renderer 5 | import commandcenter.CCRuntime.Env 6 | import zio.* 7 | 8 | // TODO: Work in progress 9 | final case class TerminalCommand(commandNames: List[String]) extends Command[Unit] { 10 | val commandType: CommandType = CommandType.TerminalCommand 11 | val title: String = "Terminal" 12 | 13 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 14 | for { 15 | input <- ZIO.fromOption(searchInput.asPrefixed).orElseFail(CommandError.NotApplicable) 16 | } yield PreviewResults.one( 17 | Preview.unit.rendered(Renderer.renderDefault("Terminal", input.rest)).score(Scores.veryHigh(input.context)) 18 | ) 19 | } 20 | 21 | object TerminalCommand extends CommandPlugin[TerminalCommand] { 22 | 23 | def make(config: Config): IO[CommandPluginError, TerminalCommand] = 24 | for { 25 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 26 | } yield TerminalCommand(commandNames.getOrElse(List("$", ">"))) 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/TimerCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import cats.syntax.apply.* 4 | import com.monovore.decline 5 | import com.monovore.decline.Opts 6 | import com.typesafe.config.Config 7 | import commandcenter.cache.ZCache 8 | import commandcenter.command.CommonArgs.* 9 | import commandcenter.command.TimerCommand.ActiveTimer 10 | import commandcenter.util.AppleScript 11 | import commandcenter.util.OS 12 | import commandcenter.util.PowerShellScript 13 | import commandcenter.view.Renderer 14 | import commandcenter.CCRuntime.Env 15 | import fansi.Color 16 | import fansi.Str 17 | import zio.* 18 | 19 | import java.time.Instant 20 | import scala.io.Source 21 | 22 | final case class TimerCommand( 23 | commandNames: List[String], 24 | activeTimersRef: Ref[Set[ActiveTimer]], 25 | cache: ZCache[String, String] 26 | ) extends Command[Unit] { 27 | val commandType: CommandType = CommandType.TimerCommand 28 | val title: String = "Timer" 29 | 30 | val messageOpt = Opts.option[String]("message", "Message to display when timer is done", "m").orNone 31 | val durationArg = Opts.argument[Duration]("duration") 32 | 33 | val timerCommand = decline.Command("timer", title)((durationArg, messageOpt).tupled) 34 | 35 | val notifyFn = 36 | if (OS.os == OS.MacOS) 37 | AppleScript.loadFunction2[String, String](cache)("system/notify.applescript") 38 | else 39 | PowerShellScript.loadFunction2[String, String](cache)("system/notify.ps1") 40 | 41 | // TODO: Support all OSes 42 | override val supportedOS: Set[OS] = Set(OS.MacOS, OS.Windows) 43 | 44 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 45 | for { 46 | input <- ZIO.fromOption(searchInput.asArgs).orElseFail(CommandError.NotApplicable) 47 | parsed = timerCommand.parse(input.args) 48 | message <- ZIO 49 | .fromEither(parsed) 50 | .foldZIO( 51 | h => 52 | for { 53 | activeTimers <- activeTimersRef.get 54 | now <- Clock.instant 55 | lines = 56 | activeTimers.map { timer => 57 | val relativeTime = java.time.Duration.between(now, timer.runsAt) 58 | s"\n${Color.Green(Str(timer.message.getOrElse("untitled")))} in ${relativeTime.render}" 59 | } 60 | } yield HelpMessage.formatted(h) ++ lines.mkString, 61 | { case (duration, messageOpt) => 62 | val messagePart = messageOpt.fold("")(m => s" for ${Color.Magenta(m)}") 63 | 64 | ZIO.succeed(Str(s"Reminder after ${duration.render}$messagePart")) 65 | } 66 | ) 67 | } yield { 68 | val run = for { 69 | (delay, timerMessageOpt) <- ZIO.fromEither(parsed).orElseFail(RunError.Ignore) 70 | runsAt <- Clock.instant.map(_.plus(delay)) 71 | activeTimer = ActiveTimer(timerMessageOpt, runsAt) 72 | _ <- activeTimersRef.update(_ + activeTimer) 73 | timerDoneMessage = timerMessageOpt.getOrElse(s"Timer completed after ${delay.render}") 74 | // Note: Can't use JOptionPane here for message dialogs until Graal native-image supports Swing 75 | _ <- notifyFn(timerDoneMessage, "Command Center Timer Event").delay(delay) 76 | _ <- activeTimersRef.update(_ - activeTimer) 77 | } yield () 78 | 79 | PreviewResults.one( 80 | Preview.unit 81 | .onRun(run) 82 | .score(Scores.veryHigh(input.context)) 83 | .rendered(Renderer.renderDefault(title, message)) 84 | ) 85 | } 86 | 87 | } 88 | 89 | object TimerCommand extends CommandPlugin[TimerCommand] { 90 | 91 | final case class ActiveTimer(message: Option[String], runsAt: Instant) 92 | 93 | def make(config: Config): IO[CommandPluginError, TimerCommand] = 94 | for { 95 | runtime <- ZIO.runtime[Any] 96 | activeTimersRef <- Ref.make(Set.empty[ActiveTimer]) 97 | cache = ZCache 98 | .memoizeZIO(1024, None)((resource: String) => 99 | ZIO.succeed(Some(Source.fromResource(resource)).map(_.mkString)) 100 | )(runtime) 101 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 102 | } yield TimerCommand(commandNames.getOrElse(List("timer", "remind")), activeTimersRef, cache) 103 | 104 | } 105 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/ToggleDesktopIconsCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.util.OS 5 | import commandcenter.CCRuntime.Env 6 | import zio.* 7 | import zio.process.Command as PCommand 8 | 9 | final case class ToggleDesktopIconsCommand(commandNames: List[String]) extends Command[Unit] { 10 | val commandType: CommandType = CommandType.ToggleDesktopIconsCommand 11 | val title: String = "Toggle Desktop Icons" 12 | 13 | override val supportedOS: Set[OS] = Set(OS.MacOS) 14 | 15 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 16 | for { 17 | input <- ZIO.fromOption(searchInput.asKeyword).orElseFail(CommandError.NotApplicable) 18 | } yield { 19 | val run = for { 20 | showingIcons <- PCommand("defaults", "read", "com.apple.finder", "CreateDesktop").string.map(_.trim == "1") 21 | _ <- 22 | PCommand("defaults", "write", "com.apple.finder", "CreateDesktop", "-bool", (!showingIcons).toString).exitCode 23 | _ <- PCommand("killall", "Finder").exitCode 24 | } yield () 25 | 26 | PreviewResults.one(Preview.unit.onRun(run).score(Scores.veryHigh(input.context))) 27 | } 28 | } 29 | 30 | object ToggleDesktopIconsCommand extends CommandPlugin[ToggleDesktopIconsCommand] { 31 | 32 | def make(config: Config): IO[CommandPluginError, ToggleDesktopIconsCommand] = 33 | for { 34 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 35 | } yield ToggleDesktopIconsCommand(commandNames.getOrElse(List("icons"))) 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/ToggleHiddenFilesCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.util.OS 5 | import commandcenter.CCRuntime.Env 6 | import zio.* 7 | import zio.process.{Command as PCommand, CommandError as PCommandError} 8 | 9 | final case class ToggleHiddenFilesCommand(commandNames: List[String]) extends Command[Unit] { 10 | val commandType: CommandType = CommandType.ToggleHiddenFilesCommand 11 | val title: String = "Toggle Hidden Files" 12 | 13 | override val supportedOS: Set[OS] = Set(OS.MacOS, OS.Windows) 14 | 15 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 16 | for { 17 | input <- ZIO.fromOption(searchInput.asKeyword).orElseFail(CommandError.NotApplicable) 18 | } yield { 19 | val run = OS.os match { 20 | case OS.MacOS => runMacOS 21 | case _ => runWindows 22 | } 23 | 24 | PreviewResults.one(Preview.unit.onRun(run).score(Scores.veryHigh(input.context))) 25 | } 26 | 27 | private def runMacOS: ZIO[Any, PCommandError, Unit] = 28 | for { 29 | showingAll <- PCommand("defaults", "read", "com.apple.finder", "AppleShowAllFiles").string.map(_.trim == "1") 30 | _ <- PCommand( 31 | "defaults", 32 | "write", 33 | "com.apple.finder", 34 | "AppleShowAllFiles", 35 | "-bool", 36 | (!showingAll).toString 37 | ).exitCode 38 | _ <- PCommand("killall", "Finder").exitCode 39 | } yield () 40 | 41 | private def runWindows: ZIO[Any, PCommandError, Unit] = 42 | for { 43 | showingAllFlag <- 44 | PCommand( 45 | "powershell", 46 | "(Get-ItemProperty Registry::HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced -Name Hidden).Hidden" 47 | ).string.map(_.trim.toInt) 48 | _ <- PCommand( 49 | "reg", 50 | "add", 51 | "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced", 52 | "/v", 53 | "Hidden", 54 | "/t", 55 | "REG_DWORD", 56 | "/d", 57 | (1 - showingAllFlag).toString, 58 | "/f" 59 | ).exitCode 60 | } yield () 61 | } 62 | 63 | object ToggleHiddenFilesCommand extends CommandPlugin[ToggleHiddenFilesCommand] { 64 | 65 | def make(config: Config): IO[CommandPluginError, ToggleHiddenFilesCommand] = 66 | for { 67 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 68 | } yield ToggleHiddenFilesCommand(commandNames.getOrElse(List("hidden"))) 69 | } 70 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/UUIDCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.tools.Tools 5 | import commandcenter.CCRuntime.Env 6 | import zio.* 7 | 8 | import java.util.UUID 9 | 10 | final case class UUIDCommand(commandNames: List[String]) extends Command[UUID] { 11 | val commandType: CommandType = CommandType.UUIDCommand 12 | val title: String = "UUID" 13 | 14 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[UUID]] = 15 | for { 16 | input <- ZIO.fromOption(searchInput.asKeyword).orElseFail(CommandError.NotApplicable) 17 | uuid = UUID.randomUUID() 18 | } yield PreviewResults.one( 19 | Preview(uuid).onRun(Tools.setClipboard(uuid.toString)).score(Scores.veryHigh(input.context)) 20 | ) 21 | } 22 | 23 | object UUIDCommand extends CommandPlugin[UUIDCommand] { 24 | 25 | def make(config: Config): IO[CommandPluginError, UUIDCommand] = 26 | for { 27 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 28 | } yield UUIDCommand(commandNames.getOrElse(List("uuid", "guid"))) 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/native/win/PowrProf.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command.native.win 2 | 3 | import com.sun.jna.Native 4 | 5 | object PowrProf { 6 | Native.register("powrprof") 7 | 8 | @native def SetSuspendState(bHibernate: Boolean, bForce: Boolean, bWakeupEventsDisabled: Boolean): Boolean 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/util/HashUtil.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command.util 2 | 3 | import cats.syntax.either.* 4 | 5 | import java.nio.charset.Charset 6 | import java.nio.charset.StandardCharsets.UTF_8 7 | import java.security.MessageDigest 8 | 9 | object HashUtil { 10 | 11 | def hash(algorithm: String)(text: String, charset: Charset = UTF_8): Either[Throwable, String] = 12 | Either.catchNonFatal { 13 | // Note: MessageDigest is not thread-safe. It requires creating a new instance for each hash. 14 | val hashFunction = MessageDigest.getInstance(algorithm) 15 | 16 | hashFunction.digest(text.getBytes(charset)).map("%02x".format(_)).mkString 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/command/util/PathUtil.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command.util 2 | 3 | import scala.util.Try 4 | 5 | object PathUtil { 6 | val userHomeOpt: Option[String] = Try(System.getProperty("user.home")).toOption 7 | 8 | // TODO: Also have a version that has maxLength and will shorten names that are too long with `..` 9 | def shorten(path: String): String = 10 | userHomeOpt match { 11 | case Some(userHome) if path.startsWith(userHome) => 12 | s"~${path.substring(userHome.length)}" 13 | 14 | case _ => path 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/config/ConfigParserExtensions.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.config 2 | 3 | import com.typesafe.config.* 4 | import commandcenter.command.CommandPluginError 5 | import io.circe.{Decoder, Json} 6 | import io.circe.config.parser 7 | import zio.* 8 | 9 | import scala.jdk.CollectionConverters.* 10 | 11 | trait ConfigParserExtensions { 12 | 13 | implicit class ConfigExtension(val config: com.typesafe.config.Config) { 14 | 15 | def convertValueUnsafe(value: ConfigValue): Json = 16 | value match { 17 | case obj: ConfigObject => 18 | Json.fromFields(obj.asScala.view.mapValues(convertValueUnsafe).toMap) 19 | 20 | case list: ConfigList => 21 | Json.fromValues(list.asScala.map(convertValueUnsafe)) 22 | 23 | case _ => 24 | (value.valueType, value.unwrapped) match { 25 | case (ConfigValueType.NULL, _) => Json.Null 26 | case (ConfigValueType.NUMBER, int: java.lang.Integer) => Json.fromInt(int) 27 | case (ConfigValueType.NUMBER, long: java.lang.Long) => Json.fromLong(long) 28 | case (ConfigValueType.BOOLEAN, boolean: java.lang.Boolean) => Json.fromBoolean(boolean) 29 | case (ConfigValueType.STRING, str: String) => Json.fromString(str) 30 | 31 | case (ConfigValueType.NUMBER, double: java.lang.Double) => 32 | Json.fromDouble(double).getOrElse { 33 | throw new NumberFormatException(s"Invalid numeric string ${value.render}") 34 | } 35 | 36 | case (valueType, _) => 37 | throw new RuntimeException(s"No conversion for $valueType with value $value") 38 | } 39 | } 40 | 41 | /** Read config settings into the specified type. 42 | */ 43 | def as[A: Decoder]: Either[io.circe.Error, A] = parser.decode[A](config) 44 | 45 | /** Read config settings at given path into the specified type. 46 | */ 47 | def as[A: Decoder](path: String): Either[io.circe.Error, A] = parser.decodePath[A](config, path) 48 | 49 | /** Get the value at given path into the specified type. 50 | */ 51 | def get[A: Decoder](path: String): Either[io.circe.Error, A] = { 52 | val json = 53 | if (config.hasPath(path)) 54 | convertValueUnsafe(config.getValue(path)) 55 | else 56 | Json.Null 57 | 58 | implicitly[Decoder[A]].decodeJson(json) 59 | } 60 | 61 | def getZIO[A: Decoder](path: String): IO[CommandPluginError, A] = 62 | ZIO.fromEither(get(path)).mapError(CommandPluginError.UnexpectedException.apply) 63 | 64 | } 65 | } 66 | 67 | object ConfigParserExtensions extends ConfigParserExtensions 68 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/config/Decoders.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.config 2 | 3 | import io.circe.Decoder 4 | 5 | import java.awt.Font 6 | import java.io.File 7 | import java.nio.file.{Path, Paths} 8 | import java.time.format.{DateTimeFormatter, FormatStyle} 9 | import scala.util.Try 10 | 11 | object Decoders { 12 | 13 | implicit val fontDecoder: Decoder[Font] = 14 | Decoder.instance { c => 15 | for { 16 | name <- c.get[String]("name") 17 | size <- c.get[Int]("size") 18 | } yield new Font(name, Font.PLAIN, size) // TODO: Also support style 19 | } 20 | 21 | implicit val pathDecoder: Decoder[Path] = Decoder.decodeString.emap { s => 22 | Try(Paths.get(s)).toEither.left.map(_.getMessage) 23 | } 24 | 25 | implicit val fileDecoder: Decoder[File] = Decoder.decodeString.emap { s => 26 | Try(new File(s)).toEither.left.map(_.getMessage) 27 | } 28 | 29 | implicit val scalaDurationDecoder: Decoder[scala.concurrent.duration.Duration] = Decoder.decodeString.emap { s => 30 | Try { 31 | scala.concurrent.duration.Duration(s) 32 | }.toEither.left.map(_.getMessage) 33 | } 34 | 35 | implicit val dateTimeFormatterDecoder: Decoder[DateTimeFormatter] = Decoder.decodeString.emap { s => 36 | if (s.equalsIgnoreCase("short")) Right(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)) 37 | else if (s.equalsIgnoreCase("medium")) Right(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)) 38 | else if (s.equalsIgnoreCase("long")) Right(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG)) 39 | else if (s.equalsIgnoreCase("full")) Right(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL)) 40 | else Try(DateTimeFormatter.ofPattern(s)).toEither.left.map(_.getMessage) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/event/KeyModifier.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.event 2 | 3 | import enumeratum.{Enum, EnumEntry} 4 | 5 | sealed trait KeyModifier extends EnumEntry with Product with Serializable 6 | 7 | object KeyModifier extends Enum[KeyModifier] { 8 | case object Shift extends KeyModifier 9 | case object Control extends KeyModifier 10 | case object Alt extends KeyModifier 11 | case object AltGraph extends KeyModifier 12 | case object Meta extends KeyModifier 13 | 14 | lazy val values: IndexedSeq[KeyModifier] = findValues 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/event/KeyboardShortcut.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.event 2 | 3 | import cats.data.{NonEmptyList, ValidatedNel} 4 | import cats.data.Validated.{Invalid, Valid} 5 | import io.circe.Decoder 6 | 7 | final case class KeyboardShortcut(key: KeyCode, modifiers: Set[KeyModifier]) { 8 | def isEmpty: Boolean = this == KeyboardShortcut.empty 9 | 10 | def nonEmpty: Boolean = !isEmpty 11 | 12 | override def toString: String = { 13 | def modifierPart(modifier: KeyModifier): String = 14 | if (modifiers.contains(modifier)) s"${modifier.entryName} " else "" 15 | 16 | // Done this way to maintain order of modifiers 17 | s"${KeyModifier.values.map(modifierPart).mkString}$key" 18 | } 19 | } 20 | 21 | object KeyboardShortcut { 22 | val empty: KeyboardShortcut = KeyboardShortcut(KeyCode.CharUndefined, Set.empty) 23 | 24 | def fromString(shortcut: String): ValidatedNel[String, KeyboardShortcut] = 25 | shortcut.split("[ +]+") match { 26 | case Array() => Invalid(NonEmptyList.one("Keyboard shortcut cannot be empty")) 27 | case Array(keyName) => 28 | KeyCode.withNameInsensitiveOption(keyName) match { 29 | case Some(keyCode) => Valid(KeyboardShortcut(keyCode, Set.empty)) 30 | case None => Invalid(NonEmptyList.one(s"$keyName is not a valid key code")) 31 | } 32 | 33 | case parts => 34 | val (errors, keyModifiers) = parts.init 35 | .map(s => KeyModifier.withNameInsensitiveOption(s).toRight(s"$s not a valid key modifier")) 36 | .partitionMap(identity) 37 | 38 | if (errors.isEmpty) { 39 | val keyName = parts.last 40 | KeyCode.withNameInsensitiveOption(keyName) match { 41 | case Some(keyCode) => Valid(KeyboardShortcut(keyCode, keyModifiers.toSet)) 42 | case None => Invalid(NonEmptyList.one(s"$keyName is not a valid key code")) 43 | } 44 | } else 45 | Invalid(NonEmptyList.fromListUnsafe(errors.toList)) 46 | } 47 | 48 | implicit val keyboardShortcutDecoder: Decoder[KeyboardShortcut] = Decoder.decodeString.emap { s => 49 | if (s.isEmpty) 50 | Right(KeyboardShortcut.empty) 51 | else 52 | KeyboardShortcut.fromString(s).toEither.left.map(_.toList.mkString("; ")) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/locale/JapaneseText.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.locale 2 | 3 | object JapaneseText { 4 | 5 | def isHiragana(c: Char): Boolean = 6 | c match { 7 | case code if code >= 0x3041 && code <= 0x3094 => true 8 | case 0x309b | 0x309c | 0x30fc | 0x30fd | 0x30fe => true 9 | case _ => false 10 | } 11 | 12 | def isHalfWidthKatakana(c: Char): Boolean = c >= 0xff65 && c <= 0xff9f 13 | 14 | def isFullWidthKatakana(c: Char): Boolean = 15 | c match { 16 | case code if code >= 0x30a1 && code <= 0x30fb => true // Katakana 17 | case code if code >= 0x31f0 && code <= 0x31ff => true // Phonetic Extensions for Ainu 18 | case 0x309b | 0x309c | 0x30fc | 0x30fd | 0x30fe => true 19 | case _ => false 20 | } 21 | 22 | def isKatakana(c: Char): Boolean = isHalfWidthKatakana(c) || isFullWidthKatakana(c) 23 | 24 | def isKana(c: Char): Boolean = 25 | (c >= 0x3041 && c <= 0x3094) || // Hiragana (without punctuation/symbols because it's included in the `isKatakana` check) 26 | isKatakana(c) 27 | 28 | def isKanji(c: Char): Boolean = c >= 0x4e00 && c <= 0x9fcc 29 | 30 | def isJapanese(c: Char): Boolean = isKana(c) || isKanji(c) 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/locale/KoreanText.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.locale 2 | 3 | object KoreanText { 4 | 5 | def isKorean(c: Char): Boolean = 6 | c match { 7 | case code if code >= 0xac00 && code <= 0xd7a3 => true 8 | case code if code >= 0x1100 && code <= 0x11ff => true 9 | case code if code >= 0x3130 && code <= 0x318f => true 10 | case code if code >= 0xa960 && code <= 0xa97f => true 11 | case code if code >= 0xd7b0 && code <= 0xd7ff => true 12 | case _ => false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/locale/Language.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.locale 2 | 3 | import java.util.Locale 4 | 5 | object Language { 6 | 7 | // TODO: Improve auto-detection and support other languages. This is very naive right now. Things will get more 8 | // complicated when we have multiple languages that share the same script. Look for good 3rd party libraries 9 | def detect(text: String): Locale = 10 | if (text.exists(JapaneseText.isJapanese)) 11 | Locale.JAPANESE 12 | else if (text.exists(KoreanText.isKorean)) 13 | Locale.KOREAN 14 | else 15 | Locale.ENGLISH 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/scorers/LengthScorer.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.scorers 2 | 3 | import commandcenter.util.StringExtensions.* 4 | 5 | object LengthScorer { 6 | 7 | def scoreDefault(target: String, input: String): Double = 8 | if (target.startsWithIgnoreCase(input)) 9 | 1.0 - (target.length - input.length) / target.length.toDouble 10 | else if (target.contains(input)) 11 | (1.0 - (target.length - input.length) / target.length.toDouble) * 0.5 12 | else 13 | 0.0 14 | 15 | def scorePrefix(target: String, input: String): Double = 16 | if (target.startsWithIgnoreCase(input)) 17 | 1.0 - (target.length - input.length) / target.length.toDouble 18 | else 19 | 0.0 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/shortcuts/Shortcuts.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.shortcuts 2 | 3 | import commandcenter.event.KeyboardShortcut 4 | import commandcenter.CCRuntime.Env 5 | import zio.* 6 | 7 | trait Shortcuts { 8 | def addGlobalShortcut(shortcut: KeyboardShortcut)(handler: KeyboardShortcut => URIO[Env, Unit]): RIO[Env, Unit] 9 | } 10 | 11 | object Shortcuts { 12 | 13 | def addGlobalShortcut( 14 | shortcut: KeyboardShortcut 15 | )(handler: KeyboardShortcut => URIO[Env, Unit]): RIO[Env, Unit] = 16 | ZIO.serviceWithZIO[Shortcuts](_.addGlobalShortcut(shortcut)(handler)) 17 | 18 | def unsupported: ULayer[Shortcuts] = 19 | ZLayer.succeed( 20 | new Shortcuts { 21 | 22 | def addGlobalShortcut(shortcut: KeyboardShortcut)(handler: KeyboardShortcut => URIO[Env, Unit]): Task[Unit] = 23 | ZIO.unit 24 | } 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/tools/Tools.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.tools 2 | 3 | import zio.* 4 | 5 | import java.io.InputStream 6 | 7 | trait Tools { 8 | def processId: Long 9 | def activate: Task[Unit] 10 | def hide: Task[Unit] 11 | def setClipboard(text: String): Task[Unit] 12 | def beep: Task[Unit] 13 | def playSound(inputStream: InputStream): Task[Unit] 14 | } 15 | 16 | object Tools { 17 | 18 | def processId: URIO[Tools, Long] = 19 | ZIO.serviceWith[Tools](_.processId) 20 | 21 | def activate: RIO[Tools, Unit] = 22 | ZIO.serviceWithZIO[Tools](_.activate) 23 | 24 | def hide: RIO[Tools, Unit] = 25 | ZIO.serviceWithZIO[Tools](_.hide) 26 | 27 | def setClipboard(text: String): RIO[Tools, Unit] = 28 | ZIO.serviceWithZIO[Tools](_.setClipboard(text)) 29 | 30 | def beep: RIO[Tools, Unit] = 31 | ZIO.serviceWithZIO[Tools](_.beep) 32 | 33 | def playSound(inputStream: InputStream): RIO[Tools, Unit] = 34 | ZIO.serviceWithZIO[Tools](_.playSound(inputStream)) 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/tools/ToolsLive.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.tools 2 | 3 | import commandcenter.util.AppleScript 4 | import zio.* 5 | import zio.process.Command as PCommand 6 | 7 | import java.awt.datatransfer.StringSelection 8 | import java.awt.Toolkit 9 | import java.io.BufferedInputStream 10 | import java.io.File 11 | import java.io.InputStream 12 | import javax.sound.sampled.AudioSystem 13 | import javax.sound.sampled.FloatControl 14 | import javax.sound.sampled.LineEvent 15 | import scala.util.Try 16 | 17 | // TODO: Handle Windows and Linux cases. Perhaps fallback to doing nothing since this is only needed for macOS for now. 18 | final case class ToolsLive(pid: Long, toolsPath: Option[File]) extends Tools { 19 | def processId: Long = pid 20 | 21 | def activate: Task[Unit] = 22 | toolsPath match { 23 | case Some(ccTools) => PCommand(ccTools.getAbsolutePath, "activate", pid.toString).exitCode.unit 24 | case None => 25 | AppleScript 26 | .runScript(s""" 27 | |tell application "System Events" 28 | | set frontmost of the first process whose unix id is $processId to true 29 | |end tell 30 | |""".stripMargin) 31 | .unit 32 | } 33 | 34 | def hide: Task[Unit] = 35 | toolsPath match { 36 | case Some(ccTools) => PCommand(ccTools.getAbsolutePath, "hide", pid.toString).exitCode.unit 37 | // TODO: Fallback to AppleScript if macOS 38 | case None => ZIO.unit 39 | } 40 | 41 | def setClipboard(text: String): Task[Unit] = 42 | toolsPath match { 43 | case Some(ccTools) => 44 | PCommand(ccTools.getAbsolutePath, "set-clipboard", text).exitCode.unit 45 | case None => 46 | ZIO.attemptBlocking { 47 | Toolkit.getDefaultToolkit.getSystemClipboard.setContents(new StringSelection(text), null) 48 | } 49 | } 50 | 51 | def beep: Task[Unit] = ZIO.attempt(Toolkit.getDefaultToolkit.beep()) 52 | 53 | def playSound(inputStream: InputStream): Task[Unit] = 54 | ZIO.scoped { 55 | for { 56 | audioInputStream <- ZIO.attemptBlocking(AudioSystem.getAudioInputStream(new BufferedInputStream(inputStream))) 57 | _ <- ZIO.addFinalizer(ZIO.succeed(audioInputStream.close())) 58 | clip <- ZIO.attemptBlocking(AudioSystem.getClip) 59 | _ <- ZIO.addFinalizer(ZIO.succeed(clip.close())) 60 | donePromise <- Promise.make[Nothing, Unit] 61 | _ <- ZIO 62 | .async[Any, Throwable, Boolean] { cb => 63 | clip.addLineListener { e => 64 | if (e.getType == LineEvent.Type.STOP) { 65 | cb(donePromise.succeed(())) 66 | } 67 | } 68 | } 69 | .fork 70 | _ <- ZIO.attemptBlocking { 71 | clip.open(audioInputStream) 72 | 73 | val control = clip.getControl(FloatControl.Type.MASTER_GAIN).asInstanceOf[FloatControl] 74 | control.setValue(-15) // unit is decibels 75 | 76 | clip.start() 77 | } 78 | _ <- donePromise.await 79 | } yield () 80 | } 81 | } 82 | 83 | object ToolsLive { 84 | 85 | def make: TaskLayer[Tools] = 86 | ZLayer { 87 | for { 88 | pid <- ZIO.attempt(ProcessHandle.current.pid) 89 | toolsPath = sys.env.get("COMMAND_CENTER_TOOLS_PATH").map(new File(_)).orElse { 90 | Try(java.lang.System.getProperty("user.home")).toOption 91 | .map(home => new File(home, ".command-center/cc-tools")) 92 | .filter(_.exists()) 93 | } 94 | } yield new ToolsLive(pid, toolsPath) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/util/AppleScript.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.util 2 | 3 | import commandcenter.cache.ZCache 4 | import zio.* 5 | import zio.process.Command 6 | 7 | object AppleScript { 8 | 9 | def runScript(script: String): Task[String] = 10 | Command("osascript", "-e", script).string 11 | 12 | def loadFunction0(cache: ZCache[String, String])(resource: String): Task[String] = 13 | for { 14 | script <- cache(s"applescript/$resource") 15 | result <- Command("osascript", "-e", script).string 16 | } yield result 17 | 18 | def loadFunction1[A](cache: ZCache[String, String])(resource: String): A => Task[String] = 19 | p => 20 | for { 21 | script <- cache(s"applescript/$resource") 22 | // TODO: Escape properly 23 | result <- Command("osascript", "-e", script.replace("{0}", p.toString)).string 24 | } yield result 25 | 26 | def loadFunction2[A, A2]( 27 | cache: ZCache[String, String] 28 | )(resource: String): (A, A2) => Task[String] = 29 | (a, a2) => 30 | for { 31 | script <- cache(s"applescript/$resource") 32 | // TODO: Escape properly 33 | result <- Command("osascript", "-e", script.replace("{0}", a.toString).replace("{1}", a2.toString)).string 34 | } yield result 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/util/CollectionExtensions.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.util 2 | 3 | object CollectionExtensions { 4 | 5 | implicit class ListExtension[A](val self: List[A]) extends AnyVal { 6 | 7 | def intersperse(separator: A): List[A] = 8 | (separator, self) match { 9 | case (_, Nil) => Nil 10 | case (_, list @ _ :: Nil) => list 11 | case (sep, y :: ys) => y :: sep :: ys.intersperse(sep) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/util/Debouncer.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.util 2 | 3 | import zio.* 4 | 5 | final case class DebounceState[E, A]( 6 | running: Fiber[E, A], 7 | delay: Promise[Nothing, Unit], 8 | completion: Promise[Nothing, Unit], 9 | triggered: Boolean 10 | ) 11 | 12 | final case class Debouncer[R, E, A]( 13 | debounceFn: ZIO[R, E, A] => URIO[R, Fiber[E, A]], 14 | stateRef: Ref.Synchronized[Option[DebounceState[E, A]]] 15 | ) { 16 | def apply(zio: ZIO[R, E, A]): URIO[R, Fiber[E, A]] = debounceFn(zio) 17 | 18 | /** Interrupt the in-progress search immediately (if it exists) */ 19 | def interruptSearch: UIO[Unit] = 20 | stateRef.getAndUpdateZIO { stateOpt => 21 | ZIO 22 | .foreachDiscard(stateOpt) { state => 23 | state.running.interrupt 24 | } 25 | .as(stateOpt) 26 | }.unit 27 | 28 | def triggerNow: IO[E, Option[Promise[Nothing, Unit]]] = 29 | stateRef.modifyZIO { 30 | case Some(state) => 31 | state.delay.succeed(()).as { 32 | (Some(state.completion), Some(state.copy(triggered = true))) 33 | } 34 | 35 | case None => ZIO.succeed((None, None)) 36 | } 37 | 38 | def triggerNowAwait: IO[E, Unit] = 39 | for { 40 | promiseOpt <- triggerNow 41 | _ <- ZIO.foreachDiscard(promiseOpt) { promise => 42 | promise.await 43 | } 44 | } yield () 45 | 46 | } 47 | 48 | object Debouncer { 49 | 50 | def make[R, E, A](waitTime: Duration, opTimeout: Option[Duration]): UIO[Debouncer[R, E, A]] = 51 | for { 52 | ref <- Ref.Synchronized.make(Option.empty[DebounceState[E, A]]) 53 | opFn = (op: ZIO[R, E, A]) => 54 | ref.modifyZIO { state => 55 | for { 56 | delayPromise <- Promise.make[Nothing, Unit] 57 | completionPromise <- Promise.make[Nothing, Unit] 58 | _ <- ZIO.foreachDiscard(state) { s => 59 | s.running.interruptFork.unless(s.triggered) 60 | } 61 | opFiber <- (for { 62 | _ <- delayPromise.await.timeout(waitTime) 63 | result <- opTimeout match { 64 | case Some(timeout) => 65 | op.timeout(timeout).flatMap { 66 | case Some(a) => ZIO.succeed(a) 67 | case None => 68 | val errorMessage = "Operation in debouncer timed out" 69 | ZIO.logWarning(errorMessage) *> ZIO.dieMessage(errorMessage) 70 | } 71 | case None => op 72 | } 73 | _ <- completionPromise.succeed(()) 74 | } yield result).forkDaemon 75 | updatedDebounceState = DebounceState(opFiber, delayPromise, completionPromise, triggered = false) 76 | } yield (opFiber, Some(updatedDebounceState)) 77 | } 78 | } yield Debouncer(opFn, ref) 79 | } 80 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/util/JavaVM.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.util 2 | 3 | object JavaVM { 4 | // Note: It's important for this to *not* be a val. We want to prevent Graal native-image from making this a build-time constant. 5 | lazy val isSubstrateVM: Boolean = System.getProperty("java.vm.name") == "Substrate VM" 6 | } 7 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/util/NtApi.java: -------------------------------------------------------------------------------- 1 | package commandcenter.util; 2 | 3 | import com.sun.jna.Library; 4 | import com.sun.jna.Native; 5 | import com.sun.jna.platform.win32.WinNT; 6 | import com.sun.jna.win32.W32APIOptions; 7 | 8 | public class NtApi { 9 | public interface Lib extends Library { 10 | Lib INSTANCE = Native.load("NtDll", Lib.class, W32APIOptions.DEFAULT_OPTIONS); 11 | 12 | public int NtResumeProcess(WinNT.HANDLE handle); 13 | 14 | public int NtSuspendProcess(WinNT.HANDLE handle); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/util/OS.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.util 2 | 3 | import enumeratum.* 4 | 5 | import java.util.Locale 6 | 7 | sealed trait OS extends EnumEntry 8 | 9 | object OS extends Enum[OS] { 10 | case object MacOS extends OS 11 | case object Windows extends OS 12 | case object Linux extends OS 13 | final case class Other(name: String) extends OS 14 | 15 | lazy val os: OS = { 16 | val osName = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH) 17 | 18 | if (osName.contains("mac") || osName.contains("darwin")) 19 | OS.MacOS 20 | else if (osName.contains("win")) 21 | OS.Windows 22 | else if (osName.contains("nux")) 23 | OS.Linux 24 | else 25 | OS.Other(osName) 26 | } 27 | 28 | lazy val values: IndexedSeq[OS] = findValues 29 | 30 | } 31 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/util/Orderings.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.util 2 | 3 | import java.time.LocalDate 4 | import scala.math.Ordering 5 | 6 | object Orderings { 7 | 8 | object LocalDateOrdering extends Ordering[LocalDate] { 9 | def compare(x: LocalDate, y: LocalDate): Int = x.compareTo(y) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/util/PowerShellScript.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.util 2 | 3 | import commandcenter.cache.ZCache 4 | import zio.* 5 | import zio.process.* 6 | 7 | object PowerShellScript { 8 | 9 | def loadFunction2[A, A2]( 10 | cache: ZCache[String, String] 11 | )(resource: String): (A, A2) => Task[String] = 12 | (a, a2) => 13 | for { 14 | script <- cache(s"powershell/$resource") 15 | result <- Command("powershell", script.replace("{0}", a.toString).replace("{1}", a2.toString)).string 16 | } yield result 17 | 18 | def executeCommand(command: String): IO[CommandError, Process] = 19 | Command("powershell", "-command", command).run 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/util/ProcessUtil.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.util 2 | 3 | import zio.* 4 | import zio.process.Command as PCommand 5 | 6 | import java.awt.Desktop 7 | import java.io.File 8 | import java.net.URI 9 | import java.nio.file.Path 10 | import scala.util.Try 11 | 12 | object ProcessUtil { 13 | 14 | def openBrowser(url: String, firefoxPath: Option[Path] = None): Task[Unit] = { 15 | val scheme = Try(new URI(url)).toOption.map(_.getScheme) 16 | 17 | scheme match { 18 | case Some("moz-extension") => 19 | ZIO.foreachDiscard(firefoxPath) { path => 20 | PCommand(path.toString, url).exitCode.unit 21 | } 22 | 23 | case _ => 24 | OS.os match { 25 | case OS.MacOS => 26 | PCommand("open", url).exitCode.unit 27 | 28 | case OS.Linux => 29 | PCommand("xdg-open", url).exitCode.unit 30 | 31 | case OS.Windows | OS.Other(_) => 32 | ZIO.attemptBlocking( 33 | Desktop.getDesktop.browse(new URI(url)) 34 | ) 35 | } 36 | } 37 | } 38 | 39 | def frontProcessId: Task[Option[Long]] = 40 | OS.os match { 41 | case OS.MacOS => 42 | for { 43 | asn <- PCommand("lsappinfo", "front").string 44 | pidString <- PCommand("lsappinfo", "info", "-only", "pid", asn.trim).string 45 | pid <- pidString.split('=') match { 46 | case Array(_, pid) => ZIO.succeed(pid.trim.toLong) 47 | case _ => ZIO.fail(new Exception(s"pid could not be extracted from: $pidString")) 48 | } 49 | } yield Some(pid) 50 | 51 | case OS.Windows => 52 | WindowManager.frontWindow.map(_.map(_.pid)) 53 | 54 | case _ => 55 | ZIO.fail( 56 | new UnsupportedOperationException(s"Getting the frontmost process's PID not supported yet for ${OS.os}") 57 | ) 58 | } 59 | 60 | def browseToFile(file: File): Task[Unit] = 61 | OS.os match { 62 | case OS.MacOS => 63 | val command = 64 | if (file.isDirectory) PCommand("open", file.getAbsolutePath) 65 | else PCommand("open", "-R", file.getAbsolutePath) 66 | 67 | command.successfulExitCode.unit 68 | 69 | case OS.Windows => 70 | val arg = 71 | if (file.isDirectory) file.getCanonicalPath 72 | else s"/select,${file.getAbsolutePath}" 73 | 74 | PCommand("explorer.exe", arg).successfulExitCode.unit 75 | 76 | // TODO: To properly support Linux, we probably need to detect the Linux flavor 77 | case OS.Linux | OS.Other(_) => ZIO.attemptBlocking(Desktop.getDesktop.browseFileDirectory(file)) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/util/StringExtensions.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.util 2 | 3 | object StringExtensions { 4 | 5 | implicit class StringExtension[A](val self: String) extends AnyVal { 6 | 7 | def startsWithIgnoreCase(prefix: String): Boolean = 8 | self.regionMatches(true, 0, prefix, 0, prefix.length) 9 | 10 | def endsWithIgnoreCase(suffix: String): Boolean = { 11 | val suffixLength = suffix.length 12 | self.regionMatches(true, self.length - suffixLength, suffix, 0, suffixLength) 13 | } 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/util/TTS.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.util 2 | 3 | import zio.* 4 | import zio.process.Command 5 | 6 | object TTS { 7 | 8 | // TODO: Support other OSes 9 | def say(text: String): Task[Unit] = 10 | Command("say", text).exitCode.unit 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/view/Rendered.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.view 2 | 3 | import fansi.Str 4 | 5 | sealed trait Rendered 6 | 7 | object Rendered { 8 | final case class Ansi(ansiStr: Str) extends Rendered 9 | 10 | final case class Styled(segments: Vector[StyledText]) extends Rendered { 11 | lazy val plainText: String = segments.map(_.text).mkString 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/view/Renderer.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.view 2 | 3 | import fansi.{Color, Str} 4 | 5 | object Renderer { 6 | 7 | def renderDefault(title: String, content: Str): Rendered.Ansi = 8 | Rendered.Ansi(Color.Blue(title) ++ Str(" ") ++ content) 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/scala/commandcenter/view/Style.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.view 2 | 3 | import java.awt.Color 4 | 5 | sealed trait Style 6 | 7 | object Style { 8 | case object Bold extends Style 9 | case object Underline extends Style 10 | case object Italic extends Style 11 | final case class ForegroundColor(value: Color) extends Style 12 | final case class BackgroundColor(value: Color) extends Style 13 | final case class FontFamily(value: String) extends Style 14 | final case class FontSize(value: Int) extends Style 15 | } 16 | 17 | final case class StyledText(text: String, styles: Set[Style] = Set.empty) 18 | -------------------------------------------------------------------------------- /core/src/test/scala/commandcenter/CommandBaseSpec.scala: -------------------------------------------------------------------------------- 1 | package commandcenter 2 | 3 | import commandcenter.shortcuts.Shortcuts 4 | import commandcenter.tools.ToolsLive 5 | import commandcenter.CCRuntime.Env 6 | import zio.* 7 | import zio.test.{TestEnvironment, ZIOSpec} 8 | 9 | import java.util.Locale 10 | 11 | trait CommandBaseSpec extends ZIOSpec[TestEnvironment & Env] { 12 | 13 | override def bootstrap: ZLayer[Any, Any, TestEnvironment & Env] = 14 | zio.test.testEnvironment ++ CommandBaseSpec.testLayer 15 | 16 | val defaultCommandContext: CommandContext = 17 | CommandContext(Locale.ENGLISH, TestTerminal, 1.0) 18 | 19 | def eventuallySucceed(timeout: Duration): Schedule[Any, Any, Duration] = 20 | Schedule.spaced(10.millis) zipRight Schedule.elapsed.whileOutput(_ < timeout) 21 | } 22 | 23 | object CommandBaseSpec { 24 | 25 | val testLayer: ZLayer[Any, Any, Env] = ZLayer.make[Env]( 26 | ConfigFake.layer, 27 | Shortcuts.unsupported, 28 | ToolsLive.make, 29 | SttpLive.make, 30 | Runtime.removeDefaultLoggers >>> CCLogging.addLoggerFor(TerminalType.Test), 31 | Runtime.setUnhandledErrorLogLevel(LogLevel.Warning) 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /core/src/test/scala/commandcenter/ConfigFake.scala: -------------------------------------------------------------------------------- 1 | package commandcenter 2 | 3 | import commandcenter.event.KeyboardShortcut 4 | import commandcenter.CCRuntime.Env 5 | import zio.* 6 | 7 | final case class ConfigFake() extends Conf { 8 | 9 | def config: UIO[CCConfig] = ZIO.succeed( 10 | CCConfig( 11 | commands = Vector.empty, 12 | aliases = Map.empty, 13 | general = GeneralConfig( 14 | debounceDelay = 150.millis, 15 | opTimeout = None, 16 | reopenDelay = None, 17 | hideOnKeyRelease = false, 18 | keepOpen = false 19 | ), 20 | display = DisplayConfig(width = 0, maxHeight = 0, opacity = 1.0f, fonts = Nil, alternateOpacity = None), 21 | keyboard = KeyboardConfig(KeyboardShortcut.empty, None, KeyboardShortcut.empty), 22 | globalActions = Vector.empty 23 | ) 24 | ) 25 | 26 | def load: Task[CCConfig] = config 27 | 28 | def reload: RIO[Env, CCConfig] = config 29 | } 30 | 31 | object ConfigFake { 32 | def layer: ULayer[Conf] = ZLayer.succeed(ConfigFake()) 33 | } 34 | -------------------------------------------------------------------------------- /core/src/test/scala/commandcenter/SearchSpec.scala: -------------------------------------------------------------------------------- 1 | package commandcenter 2 | 3 | import commandcenter.command.* 4 | import commandcenter.CCRuntime.Env 5 | import zio.* 6 | import zio.test.* 7 | 8 | import java.time.Instant 9 | 10 | object SearchSpec extends CommandBaseSpec { 11 | 12 | val defectCommand: Command[Unit] = new Command[Unit] { 13 | val commandType: CommandType = CommandType.ExitCommand 14 | 15 | def commandNames: List[String] = List("exit") 16 | 17 | def title: String = "Exit" 18 | 19 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 20 | ZIO.dieMessage("This command is intentionally broken!") 21 | } 22 | 23 | def spec: Spec[TestEnvironment & Env, Any] = 24 | suite("SearchSpec")( 25 | test("defect in one command should not fail entire search") { 26 | val commands = Vector(defectCommand, EpochMillisCommand(List("epochmillis"))) 27 | val results = Command.search(commands, Map.empty, "e", defaultCommandContext) 28 | val time = Instant.now() 29 | 30 | for { 31 | _ <- TestClock.setTime(time) 32 | previews <- results.map(_.previews) 33 | } yield assertTrue(previews.head.asInstanceOf[PreviewResult.Some[Any]].result == time.toEpochMilli.toString) 34 | } 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /core/src/test/scala/commandcenter/TestTerminal.scala: -------------------------------------------------------------------------------- 1 | package commandcenter 2 | 3 | import commandcenter.command.PreviewResult 4 | import commandcenter.CCRuntime.Env 5 | import zio.* 6 | 7 | import java.awt.Dimension 8 | 9 | object TestTerminal extends CCTerminal { 10 | def terminalType: TerminalType = TerminalType.Test 11 | 12 | def opacity: RIO[Env, Float] = ZIO.succeed(1.0f) 13 | 14 | def setOpacity(opacity: Float): RIO[Env, Unit] = ZIO.unit 15 | 16 | def isOpacitySupported: URIO[Env, Boolean] = ZIO.succeed(false) 17 | 18 | def size: RIO[Env, Dimension] = ZIO.succeed(new Dimension(80, 40)) 19 | 20 | def setSize(width: Int, height: Int): RIO[Env, Unit] = ZIO.unit 21 | 22 | def reload: RIO[Env, Unit] = ZIO.unit 23 | 24 | def showMore[A]( 25 | moreResults: Chunk[PreviewResult[A]], 26 | previewSource: PreviewResult[A], 27 | pageSize: Int 28 | ): RIO[Env, Unit] = ZIO.unit 29 | 30 | def reset: URIO[Env, Unit] = ZIO.unit 31 | } 32 | -------------------------------------------------------------------------------- /core/src/test/scala/commandcenter/cache/CacheSpec.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.cache 2 | 3 | import zio.* 4 | import zio.test.* 5 | 6 | import java.util.concurrent.atomic.AtomicInteger 7 | 8 | object CacheSpec extends ZIOSpecDefault { 9 | 10 | def spec: Spec[TestEnvironment, Any] = 11 | suite("CacheSpec")( 12 | test("only perform underlying operation once when receiving multiple parallel requests") { 13 | val cache = ZCache.make[String, String]() 14 | val callsToMake = 10 15 | val expensiveGetOperationsCalled = new AtomicInteger(0) 16 | 17 | def simulateExpensiveGetOperation: UIO[String] = 18 | for { 19 | _ <- ZIO.sleep(1.second) 20 | _ = expensiveGetOperationsCalled.incrementAndGet() 21 | } yield scala.util.Random.nextInt().toString 22 | 23 | for { 24 | results <- ZIO.foreachPar((1 to callsToMake).toVector) { _ => 25 | cache.getOrElseUpdate("some-expensive-op-key", 15.minutes)(simulateExpensiveGetOperation) 26 | } 27 | } yield assertTrue( 28 | results.length == callsToMake, 29 | results.distinct.length == 1, 30 | expensiveGetOperationsCalled.get == 1 31 | ) 32 | }, 33 | test("set expiration per key") { 34 | val cache = ZCache.make[String, String]() 35 | 36 | for { 37 | _ <- cache.set("foo1", "bar1", 1.second) 38 | _ <- cache.set("foo2", "bar2", 2.second) 39 | _ <- cache.set("foo3", "bar3", 3.second) 40 | foo1 <- cache.get("foo1") 41 | foo2 <- cache.get("foo2") 42 | foo3 <- cache.get("foo3") 43 | foo4 <- cache.get("foo4") 44 | _ <- assertTrue( 45 | foo1.get == "bar1", 46 | foo2.get == "bar2", 47 | foo3.get == "bar3", 48 | foo4.isEmpty 49 | ) 50 | _ <- ZIO.sleep(1.second) 51 | foo1 <- cache.get("foo1") 52 | foo2 <- cache.get("foo2") 53 | foo3 <- cache.get("foo3") 54 | foo4 <- cache.get("foo4") 55 | // 1st entry should expire after 1 second 56 | _ <- assertTrue( 57 | foo1.isEmpty, 58 | foo2.get == "bar2", 59 | foo3.get == "bar3", 60 | foo4.isEmpty 61 | ) 62 | _ <- ZIO.sleep(1.second) 63 | foo1 <- cache.get("foo1") 64 | foo2 <- cache.get("foo2") 65 | foo3 <- cache.get("foo3") 66 | foo4 <- cache.get("foo4") 67 | // 2nd entry should expire after 1 more second 68 | _ <- assertTrue( 69 | foo1.isEmpty, 70 | foo2.isEmpty, 71 | foo3.get == "bar3", 72 | foo4.isEmpty 73 | ) 74 | _ <- ZIO.sleep(1.second) 75 | foo1 <- cache.get("foo1") 76 | foo2 <- cache.get("foo2") 77 | foo3 <- cache.get("foo3") 78 | foo4 <- cache.get("foo4") 79 | // 3rd entry should expire after 1 more second 80 | _ <- assertTrue( 81 | foo1.isEmpty, 82 | foo2.isEmpty, 83 | foo3.isEmpty, 84 | foo4.isEmpty 85 | ) 86 | } yield assertCompletes 87 | } 88 | ) @@ TestAspect.withLiveClock 89 | } 90 | -------------------------------------------------------------------------------- /core/src/test/scala/commandcenter/command/EpochMillisCommandSpec.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.command 2 | 3 | import commandcenter.CCRuntime.Env 4 | import commandcenter.CommandBaseSpec 5 | import zio.test.* 6 | 7 | import java.time.Instant 8 | 9 | object EpochMillisCommandSpec extends CommandBaseSpec { 10 | val command: EpochMillisCommand = EpochMillisCommand(List("epochmillis")) 11 | 12 | def spec: Spec[TestEnvironment & Env, Any] = 13 | suite("EpochMillisCommandSpec")( 14 | test("get current time") { 15 | val time = Instant.now() 16 | 17 | for { 18 | _ <- TestClock.setTime(time) 19 | results <- Command.search(Vector(command), Map.empty, "epochmillis", defaultCommandContext) 20 | previews = results.previews 21 | } yield assertTrue(previews.head.asInstanceOf[PreviewResult.Some[Any]].result == time.toEpochMilli.toString) 22 | }, 23 | test("return nothing for non-matching search") { 24 | for { 25 | results <- Command.search(Vector(command), Map.empty, "not matching", defaultCommandContext) 26 | } yield assertTrue(results.previews.isEmpty) 27 | } 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /core/src/test/scala/commandcenter/locale/LanguageSpec.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.locale 2 | 3 | import zio.test.* 4 | import zio.test.Assertion.* 5 | 6 | import java.util.Locale 7 | 8 | object LanguageSpec extends ZIOSpecDefault { 9 | 10 | def spec: Spec[TestEnvironment, Any] = 11 | suite("LanguageSpec")( 12 | test("detect plain English text") { 13 | assert(Language.detect("Some english text."))(equalTo(Locale.ENGLISH)) 14 | }, 15 | test("detect plain Japanese text") { 16 | assert(Language.detect("日本語"))(equalTo(Locale.JAPANESE)) 17 | }, 18 | test("detect Japanese even if it's mixed with some English words") { 19 | assert(Language.detect("Do you 日本語?"))(equalTo(Locale.JAPANESE)) 20 | }, 21 | test("detect plain Korean text") { 22 | assert(Language.detect("한국어"))(equalTo(Locale.KOREAN)) 23 | } 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /core/src/test/scala/commandcenter/util/DebouncerSpec.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.util 2 | 3 | import commandcenter.CCRuntime.Env 4 | import commandcenter.CommandBaseSpec 5 | import zio.* 6 | import zio.test.* 7 | 8 | import java.util.concurrent.atomic.AtomicInteger 9 | 10 | object DebouncerSpec extends CommandBaseSpec { 11 | 12 | def spec: Spec[TestEnvironment & Env, Any] = 13 | suite("DebouncerSpec")( 14 | test("only run once when multiple requests happen in parallel") { 15 | val countRef = new AtomicInteger(0) 16 | 17 | for { 18 | debouncer <- Debouncer.make[Env, Nothing, Unit](10.millis, None) 19 | _ <- ZIO.foreachParDiscard(1 to 10) { _ => 20 | debouncer( 21 | ZIO.succeed(countRef.incrementAndGet()) 22 | ) 23 | } 24 | _ <- ZIO.attempt { 25 | scala.Predef.assert(countRef.get() == 1) 26 | }.retry(eventuallySucceed(5.seconds)) 27 | } yield assertTrue(countRef.get() == 1) 28 | }, 29 | test("interrupt operation in progress if newer debounced request comes in") { 30 | val startedRef = new AtomicInteger(0) 31 | val finishedRef = new AtomicInteger(0) 32 | 33 | def doOperation = 34 | for { 35 | _ <- ZIO.succeed(startedRef.incrementAndGet()) 36 | _ <- ZIO.sleep(100.millis) 37 | _ <- ZIO.succeed(finishedRef.incrementAndGet()) 38 | } yield () 39 | 40 | for { 41 | debouncer <- Debouncer.make[Env, Nothing, Unit](10.millis, None) 42 | fiber1 <- debouncer(doOperation).fork 43 | _ <- ZIO.sleep(50.millis) 44 | fiber2 <- debouncer(doOperation).fork 45 | _ <- ZIO.sleep(50.millis) 46 | fiber3 <- debouncer(doOperation).fork 47 | _ <- ZIO.sleep(50.millis) 48 | _ <- fiber1.join 49 | _ <- fiber2.join 50 | _ <- fiber3.join 51 | _ <- ZIO.sleep(110.millis) 52 | } yield assertTrue(startedRef.get() == 3, finishedRef.get() == 1) 53 | } 54 | ) @@ TestAspect.withLiveClock 55 | } 56 | -------------------------------------------------------------------------------- /emulator-core/src/main/scala/commandcenter/GlobalActions.scala: -------------------------------------------------------------------------------- 1 | package commandcenter 2 | 3 | import commandcenter.shortcuts.Shortcuts 4 | import commandcenter.util.{CycleWindowState, WindowBounds, WindowManager} 5 | import commandcenter.CCRuntime.Env 6 | import zio.* 7 | 8 | object GlobalActions { 9 | 10 | def setupCommon(actions: Vector[GlobalAction]): RIO[Env, Unit] = 11 | for { 12 | cycleWindowStateRef <- Ref.make(Option.empty[CycleWindowState]) 13 | _ <- ZIO.foreachDiscard(actions) { action => 14 | val run = action.id match { 15 | case GlobalActionId.MinimizeWindow => WindowManager.minimizeWindow 16 | case GlobalActionId.MaximizeWindow => WindowManager.maximizeWindow 17 | case GlobalActionId.ToggleMaximizeWindow => WindowManager.toggleMaximizeWindow 18 | case GlobalActionId.CenterWindow => WindowManager.centerScreen 19 | case GlobalActionId.MoveToPreviousScreen => WindowManager.moveToPreviousDisplay 20 | case GlobalActionId.MoveToNextScreen => WindowManager.moveToNextDisplay 21 | case GlobalActionId.ResizeToScreenSize => WindowManager.resizeToScreenSize 22 | case GlobalActionId.ResizeFullHeightMaintainAspectRatio => 23 | WindowManager.resizeFullHeightMaintainAspectRatio 24 | 25 | case id @ GlobalActionId.CycleWindowSizeLeft => 26 | WindowManager 27 | .cycleWindowSize(cycleWindowStateRef)(1, id.entryName)( 28 | Vector( 29 | WindowBounds(left = 0, top = 0, right = 0.5, bottom = 1.0), 30 | WindowBounds(left = 0 / 3.0, top = 0, right = 2.0 / 3.0, bottom = 1.0), 31 | WindowBounds(left = 0 / 3.0, top = 0, right = 1.0 / 3.0, bottom = 1.0) 32 | ) 33 | ) 34 | 35 | case id @ GlobalActionId.CycleWindowSizeRight => 36 | WindowManager 37 | .cycleWindowSize(cycleWindowStateRef)(1, id.entryName)( 38 | Vector( 39 | WindowBounds(left = 0.5, top = 0, right = 1.0, bottom = 1.0), 40 | WindowBounds(left = 1.0 / 3.0, top = 0, right = 1.0, bottom = 1.0), 41 | WindowBounds(left = 2.0 / 3.0, top = 0, right = 1.0, bottom = 1.0) 42 | ) 43 | ) 44 | 45 | case id @ GlobalActionId.CycleWindowSizeTop => 46 | WindowManager 47 | .cycleWindowSize(cycleWindowStateRef)(1, id.entryName)( 48 | Vector( 49 | WindowBounds(left = 0, top = 0, right = 1.0, bottom = 0.5), 50 | WindowBounds(left = 0, top = 0, right = 1.0, bottom = 2.0 / 3.0), 51 | WindowBounds(left = 0, top = 0, right = 1.0, bottom = 1.0 / 3.0) 52 | ) 53 | ) 54 | 55 | case id @ GlobalActionId.CycleWindowSizeBottom => 56 | WindowManager 57 | .cycleWindowSize(cycleWindowStateRef)(1, id.entryName)( 58 | Vector( 59 | WindowBounds(left = 0, top = 0.5, right = 1.0, bottom = 1.0), 60 | WindowBounds(left = 0, top = 1.0 / 3.0, right = 1.0, bottom = 1.0), 61 | WindowBounds(left = 0, top = 2.0 / 3.0, right = 1.0, bottom = 1.0) 62 | ) 63 | ) 64 | } 65 | 66 | Shortcuts.addGlobalShortcut(action.shortcut)(_ => run.ignore) 67 | } 68 | } yield () 69 | 70 | } 71 | -------------------------------------------------------------------------------- /emulator-core/src/main/scala/commandcenter/TerminalBuffer.scala: -------------------------------------------------------------------------------- 1 | package commandcenter 2 | 3 | import scala.collection.mutable 4 | 5 | class TerminalBuffer(val buffer: StringBuilder, val lineStartIndices: mutable.ArrayDeque[Int]) { 6 | 7 | def clear(): Unit = { 8 | buffer.clear() 9 | lineStartIndices.clear() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /emulator-core/src/main/scala/commandcenter/emulator/util/Lists.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.emulator.util 2 | 3 | import scala.annotation.tailrec 4 | 5 | object Lists { 6 | 7 | @tailrec 8 | def groupConsecutive[A](list: List[A], acc: List[List[A]] = Nil): List[List[A]] = 9 | list match { 10 | case head :: tail => 11 | val (t1, t2) = tail.span(_ == head) 12 | groupConsecutive(t2, acc :+ (head :: t1)) 13 | case _ => acc 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /emulator-swing/logs/application.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reibitto/command-center/3f4615c8872a862344a80a31701802dfa05c066d/emulator-swing/logs/application.log -------------------------------------------------------------------------------- /emulator-swing/plugins/PLUGINS.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | ## Installation 4 | 5 | You can place external plugins in this directory and they'll be included in the classpath at startup. You can then refer to them by its full type name in `application.conf`. For example: 6 | 7 | ``` 8 | {type: "commandcenter.strokeorder.StrokeOrderCommand$"} 9 | ``` 10 | 11 | Note: The '$' is needed as it's referring to the `StrokeOrderCommand` object, not class. 12 | 13 | ## Developing your own plugin 14 | 15 | Besides the installation and configuration step above, there is nothing different between external Commands and built-in Commands. 16 | 17 | You can refer to the `extras` module for example external plugins, such as `StrokeOrderCommand`. 18 | 19 | ## Limitations 20 | 21 | Currently external plugins don't work with Graal native-image due to limitations regarding reflection. 22 | -------------------------------------------------------------------------------- /emulator-swing/src/main/scala/commandcenter/emulator/swing/Main.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.emulator.swing 2 | 3 | import commandcenter.* 4 | import commandcenter.emulator.swing.shortcuts.ShortcutsLive 5 | import commandcenter.emulator.swing.ui.SwingTerminal 6 | import commandcenter.shortcuts.Shortcuts 7 | import commandcenter.tools.ToolsLive 8 | import commandcenter.CCRuntime.Env 9 | import zio.* 10 | 11 | object Main extends ZIOApp { 12 | 13 | override type Environment = Env 14 | 15 | val environmentTag: EnvironmentTag[Environment] = EnvironmentTag[Environment] 16 | 17 | override def bootstrap: ZLayer[Any, Any, Environment] = ZLayer.make[Environment]( 18 | ConfigLive.layer, 19 | ShortcutsLive.layer, 20 | ToolsLive.make, 21 | SttpLive.make, 22 | Runtime.removeDefaultLoggers >>> CCLogging.addLoggerFor(TerminalType.Swing), 23 | Runtime.setUnhandledErrorLogLevel(LogLevel.Warning), 24 | Scope.default 25 | ) 26 | 27 | def run: ZIO[ZIOAppArgs & Scope & Environment, Any, ExitCode] = 28 | (for { 29 | config <- Conf.load 30 | terminal <- SwingTerminal.create 31 | _ <- Shortcuts.addGlobalShortcut(config.keyboard.openShortcut)(_ => 32 | (for { 33 | _ <- ZIO.logDebug("Opening emulated terminal...") 34 | _ <- terminal.open 35 | _ <- terminal.activate 36 | _ <- ZIO.foreachDiscard(config.general.reopenDelay) { delay => 37 | for { 38 | _ <- ZIO.sleep(delay) 39 | _ <- terminal.open 40 | _ <- terminal.activate 41 | } yield () 42 | } 43 | } yield ()).ignore 44 | ) 45 | _ <- ZIO.logInfo( 46 | s"Ready to accept input. Press `${config.keyboard.openShortcut}` to open the terminal." 47 | ) 48 | _ <- GlobalActions.setupCommon(config.globalActions) 49 | _ <- terminal.closePromise.await 50 | } yield ()).tapErrorCause(c => ZIO.logFatalCause(c)).exitCode 51 | 52 | } 53 | -------------------------------------------------------------------------------- /emulator-swing/src/main/scala/commandcenter/emulator/swing/event/KeyboardShortcutUtil.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.emulator.swing.event 2 | 3 | import commandcenter.event.{KeyCode, KeyModifier, KeyboardShortcut} 4 | 5 | import java.awt.event.{InputEvent, KeyEvent} 6 | import javax.swing.KeyStroke 7 | 8 | object KeyboardShortcutUtil { 9 | 10 | def fromKeyEvent(e: KeyEvent): KeyboardShortcut = { 11 | val modifiers: Set[KeyModifier] = (if (e.isShiftDown) Set(KeyModifier.Shift) else Set.empty) ++ 12 | (if (e.isControlDown) Set(KeyModifier.Control) else Set.empty) ++ 13 | (if (e.isAltDown) Set(KeyModifier.Alt) else Set.empty) ++ 14 | (if (e.isAltGraphDown) Set(KeyModifier.AltGraph) else Set.empty) ++ 15 | (if (e.isMetaDown) Set(KeyModifier.Meta) else Set.empty) 16 | 17 | KeyboardShortcut(KeyCode.fromCode(e.getKeyCode), modifiers) 18 | } 19 | 20 | def toKeyStroke(shortcut: KeyboardShortcut): KeyStroke = { 21 | var modifiers: Int = 0 22 | 23 | shortcut.modifiers.foreach { 24 | case KeyModifier.Shift => modifiers = modifiers | InputEvent.SHIFT_DOWN_MASK 25 | case KeyModifier.Control => modifiers = modifiers | InputEvent.CTRL_DOWN_MASK 26 | case KeyModifier.Alt => modifiers = modifiers | InputEvent.ALT_DOWN_MASK 27 | case KeyModifier.AltGraph => modifiers = modifiers | InputEvent.ALT_GRAPH_DOWN_MASK 28 | case KeyModifier.Meta => modifiers = modifiers | InputEvent.META_DOWN_MASK 29 | } 30 | 31 | KeyStroke.getKeyStroke(shortcut.key.value, modifiers) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /emulator-swing/src/main/scala/commandcenter/emulator/swing/shortcuts/ShortcutsLive.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.emulator.swing.shortcuts 2 | 3 | import com.tulskiy.keymaster.common.{HotKey, HotKeyListener, Provider} 4 | import commandcenter.emulator.swing.event.KeyboardShortcutUtil 5 | import commandcenter.event.KeyboardShortcut 6 | import commandcenter.shortcuts.Shortcuts 7 | import commandcenter.CCRuntime.Env 8 | import zio.* 9 | 10 | import java.awt.Toolkit 11 | import scala.util.Try 12 | 13 | final case class ShortcutsLive(provider: Provider) extends Shortcuts { 14 | 15 | def addGlobalShortcut(shortcut: KeyboardShortcut)(handler: KeyboardShortcut => URIO[Env, Unit]): RIO[Env, Unit] = 16 | for { 17 | runtime <- ZIO.runtime[Env] 18 | _ <- ZIO.succeed { 19 | provider.register( 20 | KeyboardShortcutUtil.toKeyStroke(shortcut), 21 | new HotKeyListener { 22 | 23 | def onHotKey(hotKey: HotKey): Unit = 24 | Unsafe.unsafe { implicit u => 25 | runtime.unsafe.fork(handler(shortcut)) 26 | } 27 | } 28 | ) 29 | } 30 | } yield () 31 | } 32 | 33 | object ShortcutsLive { 34 | 35 | def layer: ZLayer[Scope, Throwable, Shortcuts] = 36 | ZLayer { 37 | for { 38 | shortcuts <- ZIO.acquireRelease(ZIO.attempt { 39 | // This is a hack for macOS to get the separate `java` Application started (it shows up as an icon in the Dock). 40 | // Otherwise the hot key provider won't be fully started up yet. 41 | Try(Toolkit.getDefaultToolkit) 42 | 43 | new ShortcutsLive(Provider.getCurrentProvider(true)) 44 | })(shortcuts => 45 | ZIO.succeed { 46 | shortcuts.provider.reset() 47 | shortcuts.provider.stop() 48 | } 49 | ) 50 | } yield shortcuts 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /emulator-swing/src/main/scala/commandcenter/emulator/swing/ui/ZKeyListener.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.emulator.swing.ui 2 | 3 | import commandcenter.CCRuntime.Env 4 | import zio.* 5 | 6 | import java.awt.event.KeyEvent 7 | 8 | class ZKeyAdapter { 9 | def keyTyped(e: KeyEvent): URIO[Env, Unit] = ZIO.unit 10 | 11 | def keyPressed(e: KeyEvent): URIO[Env, Unit] = ZIO.unit 12 | 13 | def keyReleased(e: KeyEvent): URIO[Env, Unit] = ZIO.unit 14 | } 15 | -------------------------------------------------------------------------------- /emulator-swing/src/main/scala/commandcenter/emulator/swing/ui/ZTextField.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.emulator.swing.ui 2 | 3 | import commandcenter.CCRuntime.Env 4 | import zio.* 5 | 6 | import java.awt.event.{KeyEvent, KeyListener} 7 | import javax.swing.event.{DocumentEvent, DocumentListener} 8 | import javax.swing.JTextField 9 | 10 | class ZTextField(implicit runtime: Runtime[Env]) extends JTextField { 11 | 12 | def addZKeyListener(keyListener: ZKeyAdapter): Unit = 13 | addKeyListener(new KeyListener { 14 | 15 | def keyTyped(e: KeyEvent): Unit = Unsafe.unsafe { implicit u => 16 | runtime.unsafe.fork(keyListener.keyTyped(e)) 17 | } 18 | 19 | def keyPressed(e: KeyEvent): Unit = Unsafe.unsafe { implicit u => 20 | runtime.unsafe.fork(keyListener.keyPressed(e)) 21 | } 22 | 23 | def keyReleased(e: KeyEvent): Unit = Unsafe.unsafe { implicit u => 24 | runtime.unsafe.fork(keyListener.keyReleased(e)) 25 | } 26 | }) 27 | 28 | def addOnChangeListener(handler: DocumentEvent => URIO[Env, Unit]): Unit = 29 | getDocument.addDocumentListener( 30 | new DocumentListener { 31 | 32 | def onChange(e: DocumentEvent): Unit = 33 | Unsafe.unsafe { implicit u => 34 | runtime.unsafe.run(handler(e)) 35 | } 36 | 37 | override def insertUpdate(e: DocumentEvent): Unit = onChange(e) 38 | override def removeUpdate(e: DocumentEvent): Unit = onChange(e) 39 | override def changedUpdate(e: DocumentEvent): Unit = onChange(e) 40 | } 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /emulator-swt/src/main/scala/commandcenter/emulator/swt/Main.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.emulator.swt 2 | 3 | import commandcenter.* 4 | import commandcenter.emulator.swt.shortcuts.ShortcutsLive 5 | import commandcenter.emulator.swt.ui.{RawSwtTerminal, SwtTerminal} 6 | import commandcenter.shortcuts.Shortcuts 7 | import commandcenter.tools.ToolsLive 8 | import commandcenter.CCRuntime.Env 9 | import zio.* 10 | 11 | object Main { 12 | type Environment = Scope & Env 13 | 14 | def main(args: Array[String]): Unit = { 15 | val runtime: Runtime.Scoped[Environment] = Unsafe.unsafe { implicit u => 16 | Runtime.unsafe.fromLayer( 17 | ZLayer.make[Environment]( 18 | ShortcutsLive.layer, 19 | ConfigLive.layer, 20 | ToolsLive.make, 21 | SttpLive.make, 22 | Runtime.removeDefaultLoggers >>> CCLogging.addLoggerFor(TerminalType.Swt), 23 | Runtime.setUnhandledErrorLogLevel(LogLevel.Warning), 24 | Scope.default 25 | ) 26 | ) 27 | } 28 | 29 | Unsafe.unsafe { implicit u => 30 | runtime.unsafe.run { 31 | (for { 32 | runtime <- ZIO.runtime[Env] 33 | config <- Conf.load 34 | rawTerminal = new RawSwtTerminal(config) 35 | terminal <- SwtTerminal.create(runtime, rawTerminal) 36 | _ <- Shortcuts.addGlobalShortcut(config.keyboard.openShortcut)(_ => 37 | (for { 38 | _ <- ZIO.logDebug("Opening emulated terminal...") 39 | _ <- terminal.openActivated 40 | } yield ()).ignore 41 | ) 42 | _ <- ZIO.logInfo( 43 | s"Ready to accept input. Press `${config.keyboard.openShortcut}` to open the terminal." 44 | ) 45 | _ <- GlobalActions.setupCommon(config.globalActions) 46 | // Written this way because SWT's UI loop must run from the main thread 47 | _ = rawTerminal.loop() 48 | } yield ()).tapErrorCause { c => 49 | ZIO.logFatalCause("Fatal error", c) 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /emulator-swt/src/main/scala/commandcenter/emulator/swt/event/KeyEventExtensions.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.emulator.swt.event 2 | 3 | import org.eclipse.swt.events.KeyEvent 4 | import org.eclipse.swt.SWT 5 | 6 | object KeyEventExtensions { 7 | 8 | implicit class KeyEventExtension(val self: KeyEvent) extends AnyVal { 9 | 10 | def isAltDown: Boolean = 11 | (self.stateMask & SWT.ALT) == SWT.ALT 12 | 13 | def isControlDown: Boolean = 14 | (self.stateMask & SWT.CONTROL) == SWT.CONTROL 15 | 16 | def isShiftDown: Boolean = 17 | (self.stateMask & SWT.SHIFT) == SWT.SHIFT 18 | 19 | def isMetaDown: Boolean = 20 | (self.stateMask & SWT.COMMAND) == SWT.COMMAND 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /emulator-swt/src/main/scala/commandcenter/emulator/swt/shortcuts/ShortcutsLive.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.emulator.swt.shortcuts 2 | 3 | import com.tulskiy.keymaster.common.{HotKey, HotKeyListener, Provider} 4 | import commandcenter.emulator.swt.event.KeyboardShortcutUtil 5 | import commandcenter.event.KeyboardShortcut 6 | import commandcenter.shortcuts.Shortcuts 7 | import commandcenter.CCRuntime.Env 8 | import zio.* 9 | 10 | import java.awt.Toolkit 11 | import scala.util.Try 12 | 13 | final case class ShortcutsLive(provider: Provider) extends Shortcuts { 14 | 15 | def addGlobalShortcut(shortcut: KeyboardShortcut)(handler: KeyboardShortcut => URIO[Env, Unit]): RIO[Env, Unit] = 16 | for { 17 | runtime <- ZIO.runtime[Env] 18 | _ <- ZIO.succeed { 19 | provider.register( 20 | KeyboardShortcutUtil.toKeyStroke(shortcut), 21 | new HotKeyListener { 22 | 23 | def onHotKey(hotKey: HotKey): Unit = 24 | Unsafe.unsafe { implicit u => 25 | runtime.unsafe.fork(handler(shortcut)) 26 | } 27 | } 28 | ) 29 | } 30 | _ <- ZIO.logDebug(s"Registered global shortcut: $shortcut") 31 | } yield () 32 | } 33 | 34 | object ShortcutsLive { 35 | 36 | def layer: ZLayer[Scope, Throwable, Shortcuts] = 37 | ZLayer { 38 | for { 39 | shortcuts <- ZIO.acquireRelease(ZIO.attempt { 40 | // This is a hack for macOS to get the separate `java` Application started (it shows up as an icon in the Dock). 41 | // Otherwise the hot key provider won't be fully started up yet. 42 | Try(Toolkit.getDefaultToolkit) 43 | 44 | new ShortcutsLive(Provider.getCurrentProvider(false)) 45 | })(shortcuts => 46 | ZIO.succeed { 47 | shortcuts.provider.reset() 48 | shortcuts.provider.stop() 49 | } 50 | ) 51 | } yield shortcuts 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /extras/ject/src/main/scala/commandcenter/ject/JectJaCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.ject 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.command.* 5 | import commandcenter.config.Decoders.* 6 | import commandcenter.locale.JapaneseText 7 | import commandcenter.tools.Tools 8 | import commandcenter.CCRuntime.Env 9 | import fansi.{Back, Color, Str} 10 | import ject.ja.docs.WordDoc 11 | import ject.ja.lucene.WordReader 12 | import ject.SearchPattern 13 | import zio.* 14 | 15 | import java.nio.file.Path 16 | 17 | final case class JectJaCommand(commandNames: List[String], luceneIndex: WordReader, showScore: Boolean) 18 | extends Command[Unit] { 19 | val commandType: CommandType = CommandType.External.of(getClass) 20 | val title: String = "Ject (ja)" 21 | 22 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 23 | for { 24 | input <- ZIO 25 | .fromOption(searchInput.asPrefixed.filter(_.rest.nonEmpty).map(_.rest)) 26 | .orElseFail(CommandError.NotApplicable) 27 | .orElse { 28 | if (searchInput.input.exists(JapaneseText.isJapanese)) 29 | ZIO.succeed(searchInput.input) 30 | else 31 | ZIO.fail(CommandError.NotApplicable) 32 | } 33 | searchPattern = SearchPattern(input) 34 | wordStream = luceneIndex.search(searchPattern).mapError(CommandError.UnexpectedError(this)) 35 | } yield PreviewResults.paginated( 36 | wordStream.map { word => 37 | val targetWord = word.doc.kanjiTerms.headOption 38 | .orElse(word.doc.readingTerms.headOption) 39 | .getOrElse(input) 40 | 41 | Preview.unit 42 | .score(Scores.high(searchInput.context)) 43 | .onRun(Tools.setClipboard(targetWord)) 44 | .renderedAnsi(renderWord(word.doc, word.score)) 45 | }, 46 | initialPageSize = 10, 47 | morePageSize = 100 48 | ) 49 | 50 | def renderWord(word: WordDoc, score: Double): Str = { 51 | val kanjiTerms = (word.kanjiTerms.headOption.map { k => 52 | Color.Green(k) 53 | }.toList ++ word.kanjiTerms.drop(1).map { k => 54 | Color.LightGreen(k) 55 | }).reduceOption(_ ++ " " ++ _).getOrElse(Str("")) 56 | 57 | val readingTerms = (word.readingTerms.headOption.map { k => 58 | Color.Blue(k) 59 | }.toList ++ word.readingTerms.drop(1).map { k => 60 | Color.LightBlue(k) 61 | }).reduceOption(_ ++ " " ++ _).getOrElse(Str("")) 62 | 63 | val definitions = word.definitions match { 64 | case Seq(d) => Str(d) 65 | 66 | case definitions => 67 | definitions.zipWithIndex.map { case (d, i) => 68 | Color.LightGray((i + 1).toString) ++ " " ++ d 69 | }.reduceOption(_ ++ "\n" ++ _).getOrElse(Str("")) 70 | } 71 | 72 | val partsOfSpeech = word.partsOfSpeech.map { pos => 73 | Back.DarkGray(pos) 74 | }.reduceOption(_ ++ " " ++ _).getOrElse(Str("")) 75 | 76 | // TODO: Consider creating a StrBuilder class to make this nicer 77 | (if (kanjiTerms.length == 0) Str("") else kanjiTerms ++ " ") ++ 78 | (if (readingTerms.length == 0) Str("") else readingTerms ++ " ") ++ 79 | partsOfSpeech ++ (if (showScore) Color.DarkGray(" %1.2f".format(score)) else "") ++ "\n" ++ 80 | definitions 81 | } 82 | 83 | } 84 | 85 | object JectJaCommand extends CommandPlugin[JectJaCommand] { 86 | 87 | def make(config: Config): ZIO[Scope, CommandPluginError, JectJaCommand] = 88 | // TODO: Ensure index exists. If not, create it here (put data in .command-center folder) 89 | for { 90 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 91 | dictionaryPath <- config.getZIO[Path]("dictionaryPath") 92 | luceneIndex <- WordReader.make(dictionaryPath).mapError(CommandPluginError.UnexpectedException.apply) 93 | showScore <- config.getZIO[Option[Boolean]]("showScore") 94 | } yield JectJaCommand(commandNames.getOrElse(List("ject", "j")), luceneIndex, showScore.getOrElse(false)) 95 | } 96 | -------------------------------------------------------------------------------- /extras/ject/src/main/scala/commandcenter/ject/JectKoCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.ject 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.command.* 5 | import commandcenter.config.Decoders.* 6 | import commandcenter.locale.KoreanText 7 | import commandcenter.tools.Tools 8 | import commandcenter.CCRuntime.Env 9 | import fansi.{Color, Str} 10 | import ject.ko.docs.WordDoc 11 | import ject.ko.lucene.WordReader 12 | import ject.SearchPattern 13 | import zio.* 14 | 15 | import java.nio.file.Path 16 | 17 | final case class JectKoCommand(commandNames: List[String], luceneIndex: WordReader, showScore: Boolean) 18 | extends Command[Unit] { 19 | val commandType: CommandType = CommandType.External.of(getClass) 20 | val title: String = "Ject (ko)" 21 | 22 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 23 | for { 24 | input <- ZIO 25 | .fromOption(searchInput.asPrefixed.filter(_.rest.nonEmpty).map(_.rest)) 26 | .orElseFail(CommandError.NotApplicable) 27 | .orElse { 28 | if (searchInput.input.exists(KoreanText.isKorean)) 29 | ZIO.succeed(searchInput.input) 30 | else 31 | ZIO.fail(CommandError.NotApplicable) 32 | } 33 | searchPattern = SearchPattern(input) 34 | wordStream = luceneIndex.search(searchPattern).mapError(CommandError.UnexpectedError(this)) 35 | } yield PreviewResults.paginated( 36 | wordStream.map { word => 37 | val targetWord = word.doc.hangulTerms.headOption 38 | .orElse(word.doc.hanjaTerms.headOption) 39 | .getOrElse(input) 40 | 41 | Preview.unit 42 | .score(Scores.high(searchInput.context)) 43 | .onRun(Tools.setClipboard(targetWord)) 44 | .renderedAnsi(renderWord(word.doc, word.score)) 45 | }, 46 | initialPageSize = 10, 47 | morePageSize = 50 48 | ) 49 | 50 | def renderWord(word: WordDoc, score: Double): Str = { 51 | // Only do this hacky rendering method for krdict* dictionaries 52 | val definitionLines = word.definitions.headOption.getOrElse("").linesIterator.toSeq 53 | 54 | val term = Color.Blue(definitionLines.head) 55 | val pronunciation = Color.Green(word.pronunciation.mkString(" ")) 56 | 57 | val line1 = 58 | if (word.pronunciation.isEmpty) 59 | term.toString 60 | else 61 | s"$term $pronunciation" 62 | 63 | val lineN = definitionLines.tail.mkString("\n") 64 | 65 | s"$line1\n$lineN" 66 | } 67 | 68 | } 69 | 70 | object JectKoCommand extends CommandPlugin[JectKoCommand] { 71 | 72 | def make(config: Config): ZIO[Scope, CommandPluginError, JectKoCommand] = 73 | // TODO: Ensure index exists. If not, create it here (put data in .command-center folder) 74 | for { 75 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 76 | dictionaryPath <- config.getZIO[Path]("dictionaryPath") 77 | luceneIndex <- WordReader.make(dictionaryPath).mapError(CommandPluginError.UnexpectedException.apply) 78 | showScore <- config.getZIO[Option[Boolean]]("showScore") 79 | } yield JectKoCommand(commandNames.getOrElse(List("ject", "j")), luceneIndex, showScore.getOrElse(false)) 80 | } 81 | -------------------------------------------------------------------------------- /extras/ject/src/main/scala/commandcenter/ject/KanjiCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.ject 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.command.* 5 | import commandcenter.config.Decoders.* 6 | import commandcenter.tools.Tools 7 | import commandcenter.CCRuntime.Env 8 | import fansi.{Color, Str} 9 | import ject.ja.docs.KanjiDoc 10 | import ject.ja.lucene.KanjiReader 11 | import zio.* 12 | 13 | import java.nio.file.Path 14 | 15 | final case class KanjiCommand( 16 | commandNames: List[String], 17 | luceneIndex: KanjiReader, 18 | quickPrefixes: List[String], 19 | showScore: Boolean 20 | ) extends Command[Unit] { 21 | val commandType: CommandType = CommandType.External.of(getClass) 22 | val title: String = "Kanji" 23 | 24 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 25 | for { 26 | input <- 27 | ZIO 28 | .fromOption( 29 | searchInput 30 | .asPrefixedQuick(quickPrefixes*) 31 | .orElse(searchInput.asPrefixed) 32 | .filter(_.rest.nonEmpty) 33 | .map(_.rest) 34 | ) 35 | .orElseFail(CommandError.NotApplicable) 36 | kanjiStream = luceneIndex.searchByParts(input).mapError(CommandError.UnexpectedError(this)) 37 | } yield PreviewResults.paginated( 38 | kanjiStream.map { kanji => 39 | Preview.unit 40 | .score(Scores.veryHigh(searchInput.context) * 1.5) 41 | .onRun(Tools.setClipboard(kanji.doc.kanji)) 42 | .renderedAnsi(render(kanji.doc, kanji.score)) 43 | }, 44 | initialPageSize = 30, 45 | morePageSize = 50 46 | ) 47 | 48 | // TODO: Consider creating a StrBuilder class to make this nicer 49 | def render(k: KanjiDoc, score: Double): Str = 50 | (if (k.isJouyouKanji) Str(" ") else Color.Red("×")) ++ 51 | Color.Green(k.kanji) ++ (if (k.kunYomi.isEmpty) "" else " ") ++ k.kunYomi.map { ku => 52 | Color.Magenta(ku) 53 | }.reduceOption(_ ++ " " ++ _).getOrElse(Str("")) ++ " " ++ k.onYomi.map { o => 54 | Color.Cyan(o) 55 | }.reduceOption(_ ++ " " ++ _).getOrElse(Str("")) ++ " " ++ k.meaning.mkString("; ") ++ 56 | (if (showScore) Color.DarkGray(" %1.2f".format(score)) else "") 57 | 58 | } 59 | 60 | object KanjiCommand extends CommandPlugin[KanjiCommand] { 61 | 62 | def make(config: Config): ZIO[Scope, CommandPluginError, KanjiCommand] = 63 | // TODO: Ensure index exists. If not, create it here (put data in .command-center folder) 64 | for { 65 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 66 | dictionaryPath <- config.getZIO[Path]("dictionaryPath") 67 | luceneIndex <- KanjiReader.make(dictionaryPath).mapError(CommandPluginError.UnexpectedException.apply) 68 | showScore <- config.getZIO[Option[Boolean]]("showScore") 69 | quickPrefixes <- config.getZIO[Option[List[String]]]("quickPrefixes") 70 | } yield KanjiCommand( 71 | commandNames.getOrElse(List("kanji", "k")), 72 | luceneIndex, 73 | quickPrefixes.getOrElse(Nil), 74 | showScore.getOrElse(false) 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /extras/stroke-order/src/main/scala/commandcenter/strokeorder/StrokeOrderCommand.scala: -------------------------------------------------------------------------------- 1 | package commandcenter.strokeorder 2 | 3 | import com.typesafe.config.Config 4 | import commandcenter.command.* 5 | import commandcenter.tools.Tools 6 | import commandcenter.view.{Rendered, Style, StyledText} 7 | import commandcenter.CCRuntime.Env 8 | import zio.* 9 | 10 | final case class StrokeOrderCommand(commandNames: List[String]) extends Command[Unit] { 11 | val commandType: CommandType = CommandType.External.of(getClass) 12 | val title: String = "Stroke Order" 13 | 14 | def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] = 15 | for { 16 | input <- ZIO.fromOption(searchInput.asPrefixed.filter(_.rest.nonEmpty)).orElseFail(CommandError.NotApplicable) 17 | } yield PreviewResults.one( 18 | Preview.unit 19 | .score(Scores.veryHigh(searchInput.context)) 20 | .onRun(Tools.setClipboard(input.rest)) 21 | .rendered( 22 | Rendered.Styled( 23 | Vector( 24 | StyledText(input.rest, Set(Style.FontFamily("KanjiStrokeOrders"), Style.FontSize(175))) 25 | ) 26 | ) 27 | ) 28 | ) 29 | } 30 | 31 | object StrokeOrderCommand extends CommandPlugin[StrokeOrderCommand] { 32 | 33 | def make(config: Config): IO[CommandPluginError, StrokeOrderCommand] = 34 | for { 35 | commandNames <- config.getZIO[Option[List[String]]]("commandNames") 36 | } yield StrokeOrderCommand(commandNames.getOrElse(List("stroke", "strokeorder"))) 37 | } 38 | -------------------------------------------------------------------------------- /project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt.* 2 | import Keys.* 3 | 4 | import scala.Console 5 | 6 | object Build { 7 | val ScalaVersion = "2.13.16" 8 | 9 | val CommandCenterVersion = "0.0.1" 10 | 11 | // If you set this to None you can test with your locally installed version of Graal. Otherwise it will run in Docker 12 | // and build a Linux image (e.g. setting it to s"$graal-java11"). 13 | val imageGraal: Option[String] = None 14 | 15 | lazy val ScalacOptions = Seq( 16 | "-encoding", 17 | "UTF-8", 18 | "-unchecked", 19 | "-deprecation", 20 | "-feature", 21 | "-language:postfixOps", 22 | "-language:implicitConversions", 23 | "-language:higherKinds", 24 | "-Xsource:3", 25 | "-Xfatal-warnings", 26 | "-Ymacro-annotations", 27 | "-Xlint:nullary-unit", // Warn when nullary methods return Unit. 28 | "-Xlint:inaccessible", // Warn about inaccessible types in method signatures. 29 | "-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id. 30 | "-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element. 31 | "-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field. 32 | "-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope. 33 | "-Xlint:delayedinit-select", // Selecting member of DelayedInit. 34 | "-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component. 35 | "-Xlint:option-implicit", // Option.apply used implicit view. 36 | "-Xlint:poly-implicit-overload", // Parameterized overloaded implicit methods are not visible as view bounds. 37 | "-Ywarn-extra-implicit" // Warn when more than one implicit parameter section is defined. 38 | ) ++ 39 | Seq( 40 | "-Ywarn-unused:imports", // Warn if an import selector is not referenced. 41 | "-Ywarn-unused:locals", // Warn if a local definition is unused. 42 | "-Ywarn-unused:privates", // Warn if a private member is unused. 43 | "-Ywarn-unused:implicits" // Warn if an implicit parameter is unused. 44 | ).filter(_ => !lenientDevEnabled) 45 | 46 | def defaultSettings(projectName: String) = 47 | Seq( 48 | name := projectName, 49 | version := CommandCenterVersion, 50 | Test / javaOptions += "-Duser.timezone=UTC", 51 | scalacOptions := ScalacOptions, 52 | javaOptions += "-Dfile.encoding=UTF-8", 53 | ThisBuild / scalaVersion := ScalaVersion, 54 | outputStrategy := Some(StdoutOutput), // Remove prefixes like `[info]` 55 | unmanagedBase := baseDirectory.value / "plugins", 56 | libraryDependencies ++= Plugins.BaseCompilerPlugins, 57 | libraryDependencies ++= Seq( 58 | "dev.zio" %% "zio-test" % V.zio % Test, 59 | "dev.zio" %% "zio-test-sbt" % V.zio % Test 60 | ), 61 | incOptions ~= (_.withLogRecompileOnMacro(false)), 62 | autoAPIMappings := true, 63 | resolvers := Resolvers, 64 | testFrameworks := Seq(TestFrameworks.ZIOTest), 65 | fork := true, 66 | run / connectInput := true, 67 | Test / logBuffered := false 68 | ) 69 | 70 | // Order of resolvers affects resolution time. More general purpose repositories should come first. 71 | lazy val Resolvers = 72 | Resolver.sonatypeOssRepos("releases") ++ 73 | Seq(Resolver.typesafeRepo("releases"), Resolver.jcenterRepo) ++ 74 | Resolver.sonatypeOssRepos("snapshots") :+ 75 | Resolver.mavenLocal 76 | 77 | def compilerOption(key: String): Option[String] = 78 | sys.props.get(key).orElse { 79 | val envVarName = key.replace('.', '_').replace('-', '_').toUpperCase 80 | sys.env.get(envVarName) 81 | } 82 | 83 | def compilerFlag(key: String, default: Boolean): Boolean = 84 | compilerOption(key).map(_.toBoolean).getOrElse(default) 85 | 86 | /** Uses more lenient rules for local development so that warnings for unused 87 | * imports and so on doesn't get in your way when code is still a work in 88 | * progress. CI has all the strict rules enabled. 89 | */ 90 | lazy val lenientDevEnabled: Boolean = compilerFlag("scalac.lenientDev.enabled", true) 91 | 92 | } 93 | -------------------------------------------------------------------------------- /project/OS.scala: -------------------------------------------------------------------------------- 1 | import java.util.Locale 2 | 3 | sealed trait OS 4 | 5 | object OS { 6 | case object MacOS extends OS 7 | case object Windows extends OS 8 | case object Linux extends OS 9 | final case class Other(name: String) extends OS 10 | 11 | lazy val os: OS = { 12 | val osName = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH) 13 | 14 | if (osName.contains("mac") || osName.contains("darwin")) 15 | OS.MacOS 16 | else if (osName.contains("win")) 17 | OS.Windows 18 | else if (osName.contains("nux")) 19 | OS.Linux 20 | else 21 | OS.Other(osName) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /project/Plugins.scala: -------------------------------------------------------------------------------- 1 | import sbt.* 2 | 3 | object Plugins { 4 | 5 | lazy val BaseCompilerPlugins = Seq( 6 | compilerPlugin("com.hmemcpy" %% "zio-clippy" % "0.0.5"), 7 | compilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"), 8 | compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.3" cross CrossVersion.full) 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /project/V.scala: -------------------------------------------------------------------------------- 1 | object V { 2 | val circe = "0.14.13" 3 | 4 | val circeConfig = "0.10.1" 5 | 6 | val decline = "2.5.0" 7 | 8 | val enumeratum = "1.9.0" 9 | 10 | val enumeratumCirce = "1.9.0" 11 | 12 | val fansi = "0.5.0" 13 | 14 | val fastparse = "3.1.1" 15 | 16 | val graal = "20.2.0" 17 | 18 | val ject = "0.6.0" 19 | 20 | val jkeymaster = "1.3" 21 | 22 | val jna = "5.17.0" 23 | 24 | val lanterna = "3.2.0-alpha1" 25 | 26 | val pprint = "0.9.0" 27 | 28 | val prettytime = "5.0.9.Final" 29 | 30 | val scalaReflect = "2.13.16" 31 | 32 | val scalaScraper = "3.2.0" 33 | 34 | val slf4j = "1.7.30" 35 | 36 | val spire = "0.18.0" 37 | 38 | val sttp = "3.11.0" 39 | 40 | val swt = "3.129.0" 41 | 42 | val zio = "2.1.19" 43 | 44 | val zioLogging = "2.5.0" 45 | 46 | val zioPrelude = "1.0.0-RC40" 47 | 48 | val zioProcess = "0.7.2" 49 | } 50 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | # `cli/run` doesn't work with sbt 1.4+ for some reason. Maybe due to sbt 1.4's jline3 upgrade? 2 | # I don't know of a workaround currently. 3 | sbt.version=1.11.0 4 | #sbt.version=1.3.13 5 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") 2 | 3 | addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.1") 4 | 5 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.1") 6 | 7 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") 8 | 9 | addSbtPlugin("com.github.reibitto" % "sbt-welcome" % "0.5.0") 10 | 11 | addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.3") 12 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # Command Center Tools 2 | 3 | For certain operations that we can't do due to certain limitations of the JVM (or Graal native-image), Command Center 4 | uses a native executable for calling certain system tasks. 5 | 6 | For example, say on macOS you want to bring the window to the front and activate it. This is not possible to do with 7 | the JVM by itself. Instead, you can call `cc-tools activate pid` to do this for you. 8 | -------------------------------------------------------------------------------- /tools/macos/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AppKit 3 | 4 | func activate() -> Void { 5 | let pid = Int32(CommandLine.arguments[2]).unsafelyUnwrapped 6 | 7 | NSRunningApplication.init(processIdentifier: pid).unsafelyUnwrapped.activate(options: NSApplication.ActivationOptions.activateIgnoringOtherApps) 8 | } 9 | 10 | func hide() -> Void { 11 | let pid = Int32(CommandLine.arguments[2]).unsafelyUnwrapped 12 | 13 | NSRunningApplication.init(processIdentifier: pid).unsafelyUnwrapped.hide() 14 | } 15 | 16 | func getClipboard() -> Void { 17 | if let s = NSPasteboard.general.pasteboardItems?.first?.string(forType: .string) { 18 | print(s) 19 | } 20 | } 21 | 22 | func setClipboard() -> Void { 23 | let text = CommandLine.arguments[2] 24 | 25 | let pasteboard = NSPasteboard.general 26 | pasteboard.declareTypes([NSPasteboard.PasteboardType.string], owner: nil) 27 | pasteboard.setString(text, forType: NSPasteboard.PasteboardType.string) 28 | } 29 | 30 | if(CommandLine.arguments.count <= 1) { 31 | print("Command Center Utility Tool") 32 | print() 33 | print("Usage: [subcommand] ") 34 | print("Subcommands:") 35 | print(" activate [pid]") 36 | print(" hide [pid]") 37 | print(" get-clipboard") 38 | print(" set-clipboard [text]") 39 | } else { 40 | let command = CommandLine.arguments[1] 41 | 42 | if(command == "activate") { 43 | activate() 44 | } else if(command == "hide") { 45 | hide() 46 | } else if(command == "get-clipboard") { 47 | getClipboard() 48 | } else if(command == "set-clipboard") { 49 | setClipboard() 50 | } 51 | } 52 | --------------------------------------------------------------------------------