termonad-4.0.0.1/0000755000000000000000000000000007346545000011654 5ustar0000000000000000termonad-4.0.0.1/.nix-helpers/0000755000000000000000000000000007346545000014170 5ustar0000000000000000termonad-4.0.0.1/.nix-helpers/nixops.nix0000755000000000000000000000655507346545000016246 0ustar0000000000000000# This file can be used with `nixops` to create a virtual machine that has # Termonad installed. # # I use this to test out Termonad in a desktop environment with a menubar. # # On my development machine, I use XMonad as a Window Manager, so there are # no Window decorations for any X application. This file creates a VM # with Termonad installed in Gnome 3. This lets you see what Termonad # looks like when it has Window decorations, a title bar, etc. # # A virtual machine can be created based on this file with the following # commands: # # $ nixops create --deployment termonad-test .nix-helpers/nixops.nix # $ nixops deploy --deployment termonad-test # # This should open up a VirtualBox machine and start installing Gnome 3, # Termonad, etc. # # You should be able to login with the username "myuser" and password "foobar". # # When you are done you can destroy the machine and delete the deployment: # # $ nixops destroy --deployment termonad-test # $ nixops delete --deployment termonad-test # { network.description = "Gnome With Termonad"; termonad-machine = { config, pkgs, ...}: { imports = [ ]; deployment = { targetEnv = "virtualbox"; virtualbox = { disks.disk1.size = 20480; headless = false; memorySize = 2024; vcpu = 1; }; }; environment = { systemPackages = let pkgList = with pkgs; [ acpi aspell aspellDicts.en autojump bash bash-completion bc chromium curl dmenu emacs evince file firefoxWrapper gcc geeqie gimp gitAndTools.gitFull gitAndTools.hub gnumake gnupg hexchat htop imagemagick jq k2pdfopt ltrace manpages ncurses nix-bash-completions nixops p7zip pkgconfig psmisc python3 redshift roxterm screen strace tree unzip usbutils vimHugeX wget wirelesstools xfce.terminal xorg.xbacklight xorg.xmodmap xscreensaver xterm zlib ]; termonad = import ../default.nix { }; in [ termonad ] ++ pkgList; variables.EDITOR = "vim"; }; fonts.fonts = with pkgs; [ dejavu_fonts ipafont source-code-pro ttf_bitstream_vera ]; i18n = { consoleFont = "Lat2-Terminus16"; consoleKeyMap = "us"; defaultLocale = "en_US.UTF-8"; inputMethod = { enabled = "fcitx"; fcitx.engines = with pkgs.fcitx-engines; [ mozc ]; }; }; programs.bash.enableCompletion = true; services = { xserver = { enable = true; layout = "us"; desktopManager.gnome3.enable = true; }; openssh = { enable = true; forwardX11 = true; challengeResponseAuthentication = true; passwordAuthentication = true; permitRootLogin = "yes"; }; }; security.sudo = { enable = true; extraConfig = '' %wheel ALL=(ALL:ALL) NOPASSWD: ${pkgs.systemd}/bin/poweroff %wheel ALL=(ALL:ALL) NOPASSWD: ${pkgs.systemd}/bin/reboot %wheel ALL=(ALL:ALL) NOPASSWD: ${pkgs.systemd}/bin/systemctl suspend ''; }; users.extraUsers.myuser = { extraGroups = [ "audio" "systemd-journal" "video" "wheel" ]; initialPassword = "foobar"; isNormalUser = true; }; }; } termonad-4.0.0.1/.nix-helpers/nixpkgs.nix0000755000000000000000000001054107346545000016377 0ustar0000000000000000# This file pins the version of nixpkgs to a known good version. The nixpkgs is # imported with an overlay adding Termonad. It is imported from various other # files. { # String representing a GHC version to use. Normally something like # "ghc865". If null, then use a known-working GHC version. compiler ? null , # A path to nixpkgs. This will be imported. If null, use a known-working # nixpkgs version. nixpkgs ? null , # Additional overlays to apply when importing nixpkgs. additionalOverlays ? [] , # Build all the examples bundled with termonad. Normally this is only used # in CI for testing that the examples all still compile. buildExamples ? false , # This is only used for `termonadShell`. # # If this is `true`, Hoogle will also index the Termonad libraries, # however this will mean the environment will need to be rebuilt every # time the termonad source changes. indexTermonad ? false }: let nixpkgsSrc = if isNull nixpkgs then builtins.fetchTarball { # Recent version of nixpkgs master as of 2020-08-18 which uses LTS-16.9. url = "https://github.com/NixOS/nixpkgs/archive/c5815280e92112a25d958a2ec8b3704d7d90c506.tar.gz"; sha256 = "09ic4s9s7w3lm0gmcxszm5j20cfv4n5lfvhdvgi7jzdbbbdps1nh"; } else nixpkgs; compilerVersion = if isNull compiler then "ghc884" else compiler; # An overlay that adds termonad to all haskell package sets. haskellPackagesOverlay = self: super: { haskell = super.haskell // { packageOverrides = hself: hsuper: super.haskell.packageOverrides hself hsuper // { termonad = let filesToIgnore = [ ".git" ".nix-helpers" "result" ".stack-work" ".travis.yml" ]; src = builtins.filterSource (path: type: with self.stdenv.lib; ! elem (baseNameOf path) filesToIgnore && ! any (flip hasPrefix (baseNameOf path)) [ "dist" ".ghc" ] ) ./..; extraCabal2nixOptions = self.lib.optionalString buildExamples "-fbuildexamples"; termonadDrv = hself.callCabal2nixWithOptions "termonad" src extraCabal2nixOptions { inherit (self) gtk3; vte_291 = self.vte; }; in termonadDrv; }; }; # A Haskell package set where we know the GHC version works to compile # Termonad. This is basically just a shortcut so that other Nix files # don't need to figure out the correct compiler version to use when it is # not given by the user. termonadKnownWorkingHaskellPkgSet = self.haskell.packages.${compilerVersion}; # This is a shell environment for hacking on Termonad with cabal. See the # top-level shell.nix for an explanation. termonadShell = let # Nix-shell environment for hacking on termonad. termonadEnv = self.termonadKnownWorkingHaskellPkgSet.termonad.env; # Build tools that are nice to have. It is okay to get Haskell build tools # from any Haskell package set, since they do not depend on the GHC version # we are using. We get these from the normal haskellPackages pkg set because # then they don't have to be compiled from scratch. convenientNativeBuildTools = [ self.cabal-install self.gnome3.glade self.haskellPackages.ghcid ]; in if indexTermonad then termonadEnv.overrideAttrs (oldAttrs: { nativeBuildInputs = let ghcEnvWithTermonad = self.termonadKnownWorkingHaskellPkgSet.ghcWithHoogle (hpkgs: [ hpkgs.termonad ]); in oldAttrs.nativeBuildInputs ++ convenientNativeBuildTools ++ [ ghcEnvWithTermonad ]; }) else self.termonadKnownWorkingHaskellPkgSet.shellFor { withHoogle = true; packages = hpkgs: [ hpkgs.termonad ]; nativeBuildInputs = termonadEnv.nativeBuildInputs ++ convenientNativeBuildTools; }; }; in import nixpkgsSrc { overlays = [ haskellPackagesOverlay ] ++ additionalOverlays; } termonad-4.0.0.1/.nix-helpers/stack-shell.nix0000755000000000000000000000054507346545000017131 0ustar0000000000000000# This is the shell file specified in the stack.yaml file. # This runs stack commands in an environment created with nix. with (import ./nixpkgs.nix {}); haskell.lib.buildStackProject { name = "termonad"; buildInputs = [ cairo git gnome3.vte gobjectIntrospection gtk3 zlib ]; ghc = termonadKnownWorkingHaskellPkgSet.ghc; } termonad-4.0.0.1/.nix-helpers/termonad-with-packages.nix0000755000000000000000000001047607346545000021261 0ustar0000000000000000# This file produces a wrapper around Termonad that will know where to find a # GHC with the libraries needed to recompile its config file. # # This is not NixOS only; it should work with nix on any system. # # There are 4 different ways this file can be used. # # 1. build directly with `nix-build` # # This method allows you to build termonad from the command line with # `nix-build`. This is the easiest method. # # You can call `nix-build` like the following: # # $ nix-build .nix-helpers/termonad-with-packages.nix # # (This is the same as just calling `nix-build` on the `../default.nix` file.) # # This produces a `result` directory that contains the `termonad` exectuable as # `result/bin/termonad`. # # By default, you will be able to use the Haskell packages `lens` and `colour` # in your `~/.config/termonad/termonad.hs` configuration file. # # If you want to use alternative Haskell packages, you can specify them on the # command line: # # $ nix-build .nix-helpers/termonad-with-packages.nix \ # --arg extraHaskellPackages 'haskellPackages: [ haskellPackages.colour haskellPackages.lens haskellPackages.pipes ]' # # This will make sure you can also use the Haskell packages `lens`, `colour`, and `pipes` in your # `~/.config/termonad/termonad.hs` file. (Actually, if Termonad transitively # depends on a library, you should be able to use it without having to specify # it. Although you shouldn't depend on this.) # # 2. install with `nix-env` # # `nix-env` can be used to install Termonad into your environment: # # $ nix-env --file .nix-helpers/termonad-with-packages.nix --install # # If you want to specify extra Haskell packages that you can use in your # `~/.config/termonad/termonad.hs` file, you can call `nix-env` like the # following: # # $ nix-env --file .nix-helpers/termonad-with-packages.nix --install \ # --arg extraHaskellPackages 'haskellPackages: [ haskellPackages.colour haskellPackages.lens haskellPackages.pipes ]' # # 3. an overlay for your user # # Nix makes it easy to create overlays. First, create the following file at # `~/.config/nixpkgs/overlays/termonad.nix`: # # ```nix # self: super: # let extraHaskellPackages = hp: [ hp.colour hp.lens hp.MonadRandom ]; in # { termonad = import /some/path/to/termonad/.nix-helpers/termonad-with-packages.nix { inherit extraHaskellPackages; }; # } # ``` # # Now Termonad can be installed through Nix's standard methods, including `nix-env`: # # $ nix-env --file '' --install --attr termonad # # 4. system-wide in /etc/nixos/configuration.nix # # You can set the `nixpkgs.overlays` attribute in your # `/etc/nixos/configuration.nix` file just like in (3) above. # # ```nix # { config, pkgs, ... }: # { # nixpkgs.overlays = [ (import /home/youruser/.config/nixpkgs/overlays/termonad.nix) ]; # } # ``` # # You can also add Termonad directly to your system packages: # # ```nix # { config, pkgs, ... }: # { # environment.systemPackages = with pkgs; [ # ... # (import /some/path/to/termonad/.nix-helpers/termonad-with-packages.nix { }) # ... # ]; # } # ``` let # Default Haskell packages that you can use in your Termonad configuration. # This is only used if the user doesn't specify the extraHaskellPackages # option. defaultPackages = hpkgs: with hpkgs; [ colour lens ]; in { extraHaskellPackages ? defaultPackages , nixpkgs ? null , additionalOverlays ? [] , compiler ? null , buildExamples ? false }@args: with (import ./nixpkgs.nix { inherit compiler nixpkgs additionalOverlays buildExamples; }); let # GHC environment that has termonad available, as well as the packages # specified above in extraHaskellPackages. env = termonadKnownWorkingHaskellPkgSet.ghcWithPackages (hpkgs: [ hpkgs.termonad ] ++ extraHaskellPackages hpkgs); in stdenv.mkDerivation { name = "termonad-with-packages-${env.version}"; buildInputs = [ gdk_pixbuf gnome3.adwaita-icon-theme hicolor-icon-theme ]; nativeBuildInputs = [ wrapGAppsHook ]; dontBuild = true; unpackPhase = ":"; # Using installPhase instead of buildCommand was recommended here: # https://github.com/cdepillabout/termonad/pull/109 installPhase = '' runHook preInstall mkdir -p $out/bin ln -sf ${env}/bin/termonad $out/bin/termonad gappsWrapperArgs+=( --set NIX_GHC "${env}/bin/ghc" ) runHook postInstall ''; preferLocalBuild = true; allowSubstitutes = false; } termonad-4.0.0.1/CHANGELOG.md0000755000000000000000000002164007346545000013473 0ustar0000000000000000## 4.0.0.1 * Update Termonad to be able to be built with the latest versions of the haskell-gi libraries. This shouldn't affect most users building with `stack`. It is only used [currently](https://github.com/NixOS/nixpkgs/pull/95434) for building Termonad with packages from Nixpkgs. ## 4.0.0.0 * Remove the dependently typed code for specifying terminal colors. [#161](https://github.com/cdepillabout/termonad/pull/161). Thanks [@ssbothwell](https://github.com/ssbothwell)! The `Palette` data type has been updated to not used length-indexed lists, but instead just newtype wrappers around normal lists. In prevous versions, the `Palette` data type looked like this: ```haskell data Palette c = NoPalette | BasicPalette !(Vec N8 c) | ExtendedPalette !(Vec N8 c) !(Vec N8 c) | ColourCubePalette !(Vec N8 c) !(Vec N8 c) !(Matrix '[N6, N6, N6] c) | FullPalette !(Vec N8 c) !(Vec N8 c) !(Matrix '[N6, N6, N6] c) !(Vec N24 c) ``` In 4.0.0.0, `Palette` has been changed to the following: ```haskell data Palette c = NoPalette | BasicPalette !(List8 c) | ExtendedPalette !(List8 c) !(List8 c) | ColourCubePalette !(List8 c) !(List8 c) !(Matrix c) | FullPalette !(List8 c) !(List8 c) !(Matrix c) !(List24 c) ``` Instead of using types like `Vec N8 c`, you will use types like `List8 c`. When setting the `palette` field of in a `ColourConfig`, you can now do it like the following. Note that there is both a `mkList8` function that returns `Maybe`, and an `unsafeMkList8` that throws a runtime error. Most users will probably want to use the `unsafeMkList8` function, since it is easy to use, and you can eyeball whether the list has the correct number of elements. If you're doing something more complicated, you may want to use the `mkList8` function: ```haskell myColourConfig :: ColourConfig (AlphaColour Double) myColourConfig = defaultColourConfig { palette = ExtendedPalette myStandardColours (maybe defaultLightColours id myLightColours) } where -- This is a an example of creating a linked-list of colours, -- This function uses an unsafe method for generating the list. -- An exception will be thrown if your list does not have exactly 8 elements. myStandardColours :: List8 (AlphaColour Double) myStandardColours = unsafeMkList8 [ createColour 40 30 20 -- dark brown (used as background colour) , createColour 180 30 20 -- red , createColour 40 160 20 -- green , createColour 180 160 20 -- dark yellow , createColour 40 30 120 -- dark purple , createColour 180 30 120 -- bright pink , createColour 40 160 120 -- teal , createColour 180 160 120 -- light brown ] -- This is an example of creating a linked-list of colours with a type -- safe method. mkList8 produces a Maybe value which must be handled explicitely. myLightColours :: Maybe (List8 (AlphaColour Double)) myLightColours = mkList8 [ createColour 70 60 50 -- brown , createColour 220 30 20 -- light red , createColour 40 210 20 -- light green , createColour 220 200 20 -- yellow , createColour 40 30 180 -- purple , createColour 140 30 80 -- dark pink , createColour 50 200 160 -- light teal , createColour 220 200 150 -- light brown ] ``` Also see the functions `setAtList8`, `overAtList8`, `setAtList24`, `overAtList24`, etc. ## 3.1.0.1 * Correct the solarized colours [#148](https://github.com/cdepillabout/termonad/pull/148). Thanks [@craigem](https://github.com/craigem)! * Add an example showing Gruvbox colours [#149](https://github.com/cdepillabout/termonad/pull/149). Thanks again [@craigem](https://github.com/craigem)! * Set an upperbound on `base` so we make sure that only GHC-8.8 is used. Some of the dependencies of Termonad don't support GHC-8.10 yet. ## 3.1.0.0 * Fix up deprecated functions used in Setup.hs. This should allow Termonad to be compiled with Cabal-3.0.0.0 (which is used by default in GHC-8.8). [#144](https://github.com/cdepillabout/termonad/pull/144) Thanks [mdorman](https://github.com/mdorman)! * Fully update to LTS-15 and GHC-8.8. Termonad now requires GHC-8.8 in order to be compiled. [#145](https://github.com/cdepillabout/termonad/pull/145). ## 3.0.0.0 * Remove the one-pixel white border around the `GtkNotebook` (the GTK widget thing that contains the tabs). [#138](https://github.com/cdepillabout/termonad/pull/138) * Add a right-click menu for the terminal. It currently allows copy and paste. [#136](https://github.com/cdepillabout/termonad/pull/136) Thanks @jecaro! * Add a preferences file that settings will be saved to and read from at `~/.config/termonad/termonad.yaml`. You can change settings with the Preferences dialog. **The settings will only be used from this file if you do not have a `~/.config/termonad/termonad.hs` file**. [#140](https://github.com/cdepillabout/termonad/pull/140) Thanks again @jecaro! ## 2.1.0.0 * Add a menu option to set preferences for a running Termonad session. The preferences you have set are lost when you end the Termonad session. [#130](https://github.com/cdepillabout/termonad/pull/130) Thanks @jecaro! ## 2.0.0.0 * Added menu option to search for a regex within the terminal output. This removes support for versions of VTE-2.91 older than 0.46. This means that compiling on older versions of Debian and Ubuntu may no longer work. [#118](https://github.com/cdepillabout/termonad/pull/118) ## 1.3.0.0 * Change all uses of [`Colour`](http://hackage.haskell.org/package/colour-2.3.5/docs/Data-Colour.html#t:Colour) to [`AlphaColour`](http://hackage.haskell.org/package/colour-2.3.5/docs/Data-Colour.html#t:AlphaColour) in `Termonad.Config.Colour`. Users should now use `AlphaColour` instead of `Colour`. Also, all uses of `sRGB24` should be replaced with `createColour`. This change is mechanical and should not affect how Termonad works at all. Thanks to @jecaro and @amir! [#116](https://github.com/cdepillabout/termonad/pull/116) ## 1.2.0.0 * Got the code for setting the backgroud color of the terminal actually working. Thanks @dakotaclemenceplaza. [#111](https://github.com/cdepillabout/termonad/pull/111) * This changes the type of `ColourConfig` to make the foreground and background colors of the terminal optional. * Various cleanup in the nix files. ## 1.1.0.0 * Added an [example](https://github.com/cdepillabout/termonad/blob/0cd741d51958806092418b55abdf1c1dc078841c/example-config/ExampleSolarizedColourExtension.hs) of how to setup a solarized color scheme. Thanks @craigem. [#90](https://github.com/cdepillabout/termonad/pull/90) and [#103](https://github.com/cdepillabout/termonad/pull/103) * Various fixes in the nix files. Make sure Termonad can see the GTK icons. [#91](https://github.com/cdepillabout/termonad/pull/91) and [#92](https://github.com/cdepillabout/termonad/pull/92) * Add a menu option to change the font size at runtime. You should be able to do this with the `Ctrl-+` and `Ctrl--` keys. [#95](https://github.com/cdepillabout/termonad/pull/95) * Get building with GHC 8.6. Thanks @clinty. [#98](https://github.com/cdepillabout/termonad/pull/98) ## 1.0.1.0 * Stop using the `widgetSetFocusOnClick` function, which is not supported on older versions of GTK. This lets Termonad be compiled with older versions of GTK. [#87](https://github.com/cdepillabout/termonad/pull/87). * Add CI. [#87](https://github.com/cdepillabout/termonad/pull/87). * Support versions of VTE-2.91 older than 0.44. [#87](https://github.com/cdepillabout/termonad/pull/87). * Add some functions for converting from a list to a `Vec` in `Termonad.Config.Vec`: `fromListVec` and `fromListVec_`. Commit 883eb98b5f. * Fix the paste hotkey. [#86](https://github.com/cdepillabout/termonad/pull/86). ## 1.0.0.0 * The API for configuring Termonad is now completely different. Many, many changes have gone into this version. You should approach it as a completely different application. The CHANGELOG will be kept up-to-date for future releases. ## 0.2.1.0 * Make sure the window title is set to "Termonad". * Relabel tabs when termonad is started. ## 0.2.0.0 * Open dialog asking if you want to quit when you try to use your WM to quit. * Termonad will attempt to open up a new terminal in the working directory of the current terminal. * Make sure termonad won't crash if dyre can't find GHC. * Add a few more ways to compile on NixOS. * Add an icon for termonad. ## 0.1.0.0 * Initial release. termonad-4.0.0.1/LICENSE0000644000000000000000000000276707346545000012675 0ustar0000000000000000Copyright Dennis Gosnell (c) 2018 All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Author name here nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. termonad-4.0.0.1/README.md0000755000000000000000000004477207346545000013154 0ustar0000000000000000 Termonad ========= [![Build Status](https://secure.travis-ci.org/cdepillabout/termonad.svg)](http://travis-ci.org/cdepillabout/termonad) [![Hackage](https://img.shields.io/hackage/v/termonad.svg)](https://hackage.haskell.org/package/termonad) [![Stackage LTS](http://stackage.org/package/termonad/badge/lts)](http://stackage.org/lts/package/termonad) [![Stackage Nightly](http://stackage.org/package/termonad/badge/nightly)](http://stackage.org/nightly/package/termonad) [![BSD3 license](https://img.shields.io/badge/license-BSD3-blue.svg)](./LICENSE) [![Join the chat at https://gitter.im/termonad/Lobby](https://badges.gitter.im/termonad/Lobby.svg)](https://gitter.im/termonad/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat in #termonad on irc.freenode.net](https://img.shields.io/badge/%23termonad-irc.freenode.net-brightgreen.svg)](https://webchat.freenode.net/) Termonad is a terminal emulator configurable in Haskell. It is extremely customizable and provides hooks to modify the default behavior. It can be thought of as the "XMonad" of terminal emulators. ![image of Termonad](./img/termonad.png) Termonad was [featured on an episode](https://www.youtube.com/watch?v=TLNr_gBv5HY) of [DistroTube](https://www.youtube.com/channel/UCVls1GmFKf6WlTraIb_IaJg). This video gives a short overview of Termonad. **Table of Contents** - [Termonad](#termonad) - [Installation](#installation) - [Arch Linux](#arch-linux) - [Ubuntu / Debian](#ubuntu--debian) - [Nix](#nix) - [Mac OS X](#mac-os-x) - [Installing with just `stack`](#installing-with-just-stack) - [Installing with just `nix`](#installing-with-just-nix) - [Installing with `stack` using `nix`](#installing-with-stack-using-nix) - [Windows](#windows) - [How to use Termonad](#how-to-use-termonad) - [Default Key Bindings](#default-key-bindings) - [Configuring Termonad](#configuring-termonad) - [Compiling Local Settings](#compiling-local-settings) - [Running with `stack`](#running-with-stack) - [Running with `nix`](#running-with-nix) - [Goals](#goals) - [Where to get help](#where-to-get-help) - [Contributions](#contributions) - [Maintainers](#maintainers) ## Installation Termonad can be installed on any system as long as the necessary GTK libraries are available. The following are instructions for installing Termonad on a few different distributions and systems. If the given steps don't work for you, or you want to add instructions for an additional system, please send a pull request. The following steps use the [`stack`](https://docs.haskellstack.org/en/stable/README/) build tool to build Termonad, but [`cabal`](https://www.haskell.org/cabal/) can be used as well. Steps for installing `stack` can be found on [this page](https://docs.haskellstack.org/en/stable/install_and_upgrade/). ### Arch Linux First, you must install the required GTK system libraries: ```sh $ pacman -S vte3 gobject-introspection ``` In order to install Termonad, clone this repository and run `stack install`. This will install the `termonad` binary to `~/.local/bin/`: ```sh $ git clone https://github.com/cdepillabout/termonad $ cd termonad/ $ stack install ``` ### Ubuntu / Debian First, you must install the required GTK system libraries: ```sh $ apt-get install gobject-introspection libgirepository1.0-dev libgtk-3-dev libvte-2.91-dev libpcre2-dev ``` In order to install Termonad, clone this repository and run `stack install`. This will install the `termonad` binary to `~/.local/bin/`: ```sh $ git clone https://github.com/cdepillabout/termonad $ cd termonad/ $ stack install ``` ### Nix If you have `nix` installed, you should be able to use it to build Termonad. This means that it will work on NixOS, or with `nix` on another distro. There are two different ways to use `nix` to build Termonad: The first is using `stack`. The following commands install `stack` for your user, clone this repository, and install the `termonad` binary to `~/.local/bin/`: ```sh $ nix-env -i stack $ git clone https://github.com/cdepillabout/termonad $ cd termonad/ $ stack --nix install ``` (_edit_: Building with `stack` using Nix-integration does not currently work. See [#99](https://github.com/cdepillabout/termonad/issues/99).) The second is using the normal `nix-build` machinery. The following commands clone this repository and build the `termonad` binary at `./result/bin/`: ```sh $ git clone https://github.com/cdepillabout/termonad $ cd termonad/ $ nix-build ``` ### Mac OS X Building and installing Termonad on Mac OS X should be possible with any of the following three methods: - Install the required system libraries (like GTK and VTE) by hand, then use `stack` to build Termonad. This is probably the easiest method. You don't have to understand anything about `nix`. However, it is slightly annoying to have to install GTK and VTE by hand. - Use `nix` to install both the required system libraries and Termonad itself. If you are a nix user and want an easy way to install Termonad, this is the recommended method. - Use `nix` to install install the required system libraries, and `stack` to build Termonad. If you are a nix user, but want to use `stack` to actually do development on Termonad, using `stack` may be easier than using `cabal`. The following sections describe each method. #### Installing with just `stack` (*currently no instructions available. please send a PR adding instructions if you get termonad to build using this method.*) #### Installing with just `nix` `nix` can be used to install Termonad with the following steps, assuming you have `nix` [installed](https://nixos.org/nix/download.html). These commands clone this repository and build the `termonad` binary at `./result/bin/`: ```sh $ git clone https://github.com/cdepillabout/termonad $ cd termonad/ $ nix-build ``` #### Installing with `stack` using `nix` `stack` can be used in conjunction with `nix` to install Termonad. `nix` will handle installing system dependencies (like GTK and VTE), while `stack` will handle compiling and installing Haskell packages. You must have `nix` [installed](https://nixos.org/nix/download.html). You will also need `stack` installed. You can do that with the following command: ```sh $ nix-env -i stack ``` After `stack` is installed, you will need to clone Termonad and build it: ``` $ git clone https://github.com/cdepillabout/termonad $ cd termonad/ $ stack --nix install ``` This will install the `termonad` binary to `~/.local/bin/`. ### Windows To run Termonad on Windows, you'll need: * any X server app, for example **Vcxsrv** * any WSL, for example **Ubuntu** I'm using both Vcxsrv and Ubuntu WSL. Configure both Vcxsrv and WSL. For Vcxsrv go with default settings everywhere, it will be fine. Configure your WSL as you want (choose your name etc.). After you set up the user, you'll have to update your OS, run: ```console $ sudo apt-get update $ sudo apt-get upgrade -y $ sudo apt-get dist-upgrade -y $ sudo apt-get autoremove -y ``` Configure the `DISPLAY` environment variable for the X server, and load the changes in bash: For WSL1: ```console $ echo "export DISPLAY=localhost:0.0" >> ~/.bashrc $ source ~/.bashrc ``` For WSL2: ```console $ echo export DISPLAY=$(awk '/nameserver / {print $2; exit}' /etc/resolv.conf 2>/dev/null):0 >> ~/.bashrc $ echo export LIBGL_ALWAYS_INDIRECT=1 >> ~/.bashrc $ source ~/.bashrc ``` If you're using WSL2, you have to create a separate **inbound rule** for TCP port 6000, to allow WSL access to the X server. If you're using mentioned earlier **Vcxsrv** you can enable public access for your X server by disabling Access Control on the Extra Settings. You can also use `-ac` flag in the Additional parameters for VcXsrv section. Your X server should now be configured. Execute following command to install the necessary GTK system libraries: ```console $ apt-get install gobject-introspection libgirepository1.0-dev libgtk-3-dev libvte-2.91-dev libpcre2-dev ``` The required GTK system libraries should now be installed. Clone the Termonad repo: ```sh $ git clone https://github.com/cdepillabout/termonad $ cd termonad/ $ stack build $ stack run ``` After `stack run`, you should see a new window with your Termonad running. ## How to use Termonad Termonad is similar to XMonad. The above steps will install a `termonad` binary somewhere on your system. If you have installed Termonad using `stack`, the `termonad` binary will be in `~/.local/bin/`. This binary is a version of Termonad configured with default settings. You can try running it to get an idea of what Termonad is like: ```sh $ ~/.local/bin/termonad ``` The following section describes the default key bindings. If you would like to configure Termonad with your own settings, first you will need to create a Haskell file called `~/.config/termonad/termonad.hs`. A following section gives an example configuration file. If this configuration file exists, when the `~/.local/bin/termonad` binary launches, it will try to use GHC to compile the configuration file. If GHC is able to successfully compile the configuration file, a separate binary will be created called something like `~/.cache/termonad/termonad-linux-x86_64`. This binary file can be thought of as your own personal Termonad, configured with all your own settings. When you run `~/.local/bin/termonad`, it will re-exec `~/.cache/termonad/termonad-linux-x86_64` if it exists. However, there is one difficulty with this setup. In order for the `~/.local/bin/termonad` binary to be able to compile your `~/.config/termonad/termonad.hs` configuration file, Termonad needs to know where GHC is, as well as where all your Haskell packages live. This presents some difficulties that will be discussed in a following section. ### Default Key Bindings Termonad provides the following default key bindings. | Key binding | Action | |------------|--------| | Ctrl Shift t | Open new tab. | | Ctrl Shift w | Close tab. | | Ctrl Shift f | Open Find dialog for searching for a regex. | | Ctrl Shift p | Find the regex **above** the current position. | | Ctrl Shift i | Find the regex **below** the current position. | | Ctrl + | Increase font size. | | Ctrl - | Decrease font size. | | Alt (number key) | Switch to tab `number`. For example, Alt 2 switches to tab 2. | ### Configuring Termonad Termonad has two different ways to be configured. The first way is to use the built-in Preferences editor. You can find this in the `Preferences` menu under `Edit` in the menubar. When opening Termonad for the first time, it will create a preferences file at `~/.config/termonad/termonad.yaml`. When you change a setting in the Preferences editor, Termonad will update the setting in the preferences file. When running Termonad, it will load settings from the preferences file. Do not edit the preferences file by hand, because it will be overwritten when updating settings in the Preferences editor. This method is perfect for users who only want to make small changes to the Termonad settings, like the default font size. The second way to configure Termonad is to use a Haskell-based settings file, called `~/.config/termonad/termonad.hs` by default. This method allows you to make large, sweeping changes to Termonad. This method is recommended for power users. **WARNING: If you have a `~/.config/termonad/termonad.hs` file, then all settings from `~/.config/termonad/termonad.yaml` will be ignored. If you want to set *ANY* settings in `~/.config/termonad/termonad.hs`, then you must set *ALL* settings in `~/.config/termonad/termonad.hs`.** The following is an example Termonad configuration file. You should save this to `~/.config/termonad/termonad.hs`. You can find more information on the available configuration options within the [`Termonad.Config`](https://hackage.haskell.org/package/termonad/docs/Termonad-Config.html) module. ```haskell {-# LANGUAGE OverloadedStrings #-} module Main where import Termonad.App (defaultMain) import Termonad.Config ( FontConfig, FontSize(FontSizePoints), Option(Set) , ShowScrollbar(ShowScrollbarAlways), defaultConfigOptions, defaultFontConfig , defaultTMConfig, fontConfig, fontFamily, fontSize, options, showScrollbar ) import Termonad.Config.Colour ( AlphaColour, ColourConfig, addColourExtension, createColour , createColourExtension, cursorBgColour, defaultColourConfig ) -- | This sets the color of the cursor in the terminal. -- -- This uses the "Data.Colour" module to define a dark-red color. -- There are many default colors defined in "Data.Colour.Names". cursBgColour :: AlphaColour Double cursBgColour = createColour 204 0 0 -- | This sets the colors used for the terminal. We only specify the background -- color of the cursor. colConf :: ColourConfig (AlphaColour Double) colConf = defaultColourConfig { cursorBgColour = Set cursBgColour } -- | This defines the font for the terminal. fontConf :: FontConfig fontConf = defaultFontConfig { fontFamily = "DejaVu Sans Mono" , fontSize = FontSizePoints 13 } main :: IO () main = do colExt <- createColourExtension colConf let termonadConf = defaultTMConfig { options = defaultConfigOptions { fontConfig = fontConf -- Make sure the scrollbar is always visible. , showScrollbar = ShowScrollbarAlways } } `addColourExtension` colExt defaultMain termonadConf ``` There are other example configuration files in the [example-config/](./example-config) directory. If you want to test what all the colors look like, you may find it convenient to use the [`print-console-colors`](http://hackage.haskell.org/package/print-console-colors) package, which provides an executable called `print-console-colors` that prints all of the colors for your terminal. ### Compiling Local Settings If you launch Termonad by calling `~/.local/bin/termonad`, it will try to compile the `~/.config/termonad/termonad.hs` file if it exists. The problem is that `~/.local/bin/termonad` needs to be able to see GHC and the required Haskell libraries to be able to compile `~/.config/termonad/termonad.hs`. There are a couple solutions to this problem, listed in the sections below. (These steps are definitely confusing. I would love to figure out a better way to do this. Please submit an issue or PR if you have a good idea about how to fix this.) #### Running with `stack` If you originally compiled Termonad with `stack`, you can use `stack` to execute Termonad. First, you must change to the directory with the Termonad source code. From there, you can run `stack exec`: ```sh $ cd termonad/ # change to the termonad source code directory $ stack exec -- termonad ``` `stack` will pick up the correct GHC version and libraries from the `stack.yaml` and `termonad.cabal` file. `termonad` will be run in an environment with GHC available. `termonad` will use this GHC and libraries to compile your `~/.config/termonad/termonad.hs` file. It if succeeds, it should create a `~/.cache/termonad/termonad-linux-x86_64` binary. If you need extra Haskell libraries available when compiling your `~/.config/termonad/termonad.hs` file, you can specify them to `stack exec`: ```sh $ stack exec --package lens --package conduit -- termonad ``` The problem with this is that `stack exec` changes quite a few of your environment variables. It is not recommended to actually run Termonad from within `stack exec`. After you run `stack exec -- termonad` and let it recompile your `~/.config/termonad/termonad.hs` file, exit Termonad. Re-run Termonad by calling it directly. Termonad will notice that `~/.config/termonad/termonad.hs` hasn't changed since `~/.cache/termonad/termonad-linux-x86_64` has been recompiled, so it will directly execute `~/.cache/termonad/termonad-linux-x86_64`. #### Running with `nix` Building Termonad with `nix` (by running `nix-build` in the top directory) sets it up so that Termonad can see GHC. Termonad should be able to compile the `~/.config/termonad/termonad.hs` file by default. If you're interested in how this works, or want to change which Haskell packages are available from your `~/.config/termonad/termonad.hs` file, please see the documentation in the [`.nix-helpers/termonad-with-packages.nix`](./.nix-helpers/termonad-with-packages.nix) file. ## Goals Termonad has the following goals: * fully configurable in Haskell There are already [many](https://gnometerminator.blogspot.com/p/introduction.html) [good](https://www.enlightenment.org/about-terminology.md) [terminal](http://software.schmorp.de/pkg/rxvt-unicode.html) [emulators](https://launchpad.net/sakura). However, there are no terminal emulators fully configurable in Haskell. Termonad fills this niche. * flexible Most people only need a terminal emulator that lets you change the font-size, cursor color, etc. They don't need tons of configuration options. Termonad should be for people that like lots of configuration options. Termonad should provide many hooks to allow the user full control over its behavior. * stable Termonad should be able to be used everyday as your main terminal emulator. It should not crash for any reason. If you experience a crash, please file an issue or a pull request! * good documentation The [documentation](https://hackage.haskell.org/package/termonad) for Termonad on Hackage should be good. You shouldn't have to guess at what certain data types or functions do. If you have a hard time understanding anything in the documentation, please submit an issue or PR. ## Where to get help If you find a bug in Termonad, please either [send a PR](https://github.com/cdepillabout/termonad/pulls) fixing it or create an [issue](https://github.com/cdepillabout/termonad/issues) explaining it. If you just need help with configuring Termonad, you can either join the [Gitter room](https://gitter.im/termonad/Lobby) or [#termonad on irc.freenode.net](https://webchat.freenode.net/). ## Contributions Contributions are highly appreciated. Termonad is currently missing many helpful configuration options and behavior hooks. If there is something you would like to add, please submit an issue or PR. ## Maintainers - [cdepillabout](https://github.com/cdepillabout) - [LSLeary](https://github.com/LSLeary) termonad-4.0.0.1/Setup.hs0000644000000000000000000001214107346545000013307 0ustar0000000000000000-- This file comes from cabal-doctest: -- https://github.com/phadej/cabal-doctest/blob/master/simple-example -- -- It is needed so that doctest can be run with the same options as the modules -- are compiled with. {-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedLists #-} {-# OPTIONS_GHC -Wall #-} module Main (main) where import Data.Maybe (catMaybes) import Distribution.PackageDescription (HookedBuildInfo, cppOptions, emptyBuildInfo) import Distribution.Simple (UserHooks, defaultMainWithHooks, preBuild, preRepl, simpleUserHooks) import Distribution.Simple.Program (configureProgram, defaultProgramDb, getDbProgramOutput, pkgConfigProgram) import Distribution.Text (simpleParse) import Distribution.Verbosity (normal) import Distribution.Version (Version, mkVersion) #ifndef MIN_VERSION_cabal_doctest #define MIN_VERSION_cabal_doctest(x,y,z) 0 #endif #if MIN_VERSION_cabal_doctest(1,0,0) import Distribution.Extra.Doctest (addDoctestsUserHook) main :: IO () main = do cppOpts <- getTermonadCPPOpts defaultMainWithHooks . addPkgConfigTermonadUserHook cppOpts $ addDoctestsUserHook "doctests" simpleUserHooks #else #ifdef MIN_VERSION_Cabal -- If the macro is defined, we have new cabal-install, -- but for some reason we don't have cabal-doctest in package-db -- -- Probably we are running cabal sdist, when otherwise using new-build -- workflow #warning You are configuring this package without cabal-doctest installed. \ The doctests test-suite will not work as a result. \ To fix this, install cabal-doctest before configuring. #endif main :: IO () main = do cppOpts <- getTermonadCPPOpts defaultMainWithHooks $ addPkgConfigTermonadUserHook cppOpts simpleUserHooks #endif -- | Add CPP macros representing the version of the GTK and VTE system -- libraries. addPkgConfigTermonadUserHook :: [String] -> UserHooks -> UserHooks addPkgConfigTermonadUserHook cppOpts oldUserHooks = do oldUserHooks { preBuild = pkgConfigTermonadHook cppOpts $ preBuild oldUserHooks , preRepl = pkgConfigTermonadHook cppOpts (preRepl oldUserHooks) } pkgConfigTermonadHook :: [String] -> (args -> flags -> IO HookedBuildInfo) -> args -> flags -> IO HookedBuildInfo pkgConfigTermonadHook cppOpts oldFunc args flags = do (maybeOldLibHookedInfo, oldExesHookedInfo) <- oldFunc args flags case maybeOldLibHookedInfo of Just oldLibHookedInfo -> do let newLibHookedInfo = oldLibHookedInfo { cppOptions = cppOptions oldLibHookedInfo <> cppOpts } pure (Just newLibHookedInfo, oldExesHookedInfo) Nothing -> do let newLibHookedInfo = emptyBuildInfo { cppOptions = cppOpts } pure (Just newLibHookedInfo, oldExesHookedInfo) getTermonadCPPOpts :: IO [String] getTermonadCPPOpts = do gtk <- getGtkVersionCPPOpts vte <- getVteVersionCPPOpts pure $ gtk <> vte getVteVersionCPPOpts :: IO [String] getVteVersionCPPOpts = do maybeVers <- getPkgConfigVersionFor "vte-2.91" case maybeVers of Nothing -> pure [] Just vers -> pure $ createVteVersionCPPOpts vers getGtkVersionCPPOpts :: IO [String] getGtkVersionCPPOpts = do maybeVers <- getPkgConfigVersionFor "gtk+-3.0" case maybeVers of Nothing -> pure [] Just vers -> pure $ createGtkVersionCPPOpts vers -- | Just like 'createGtkVersionCPPOpts' but for VTE instead of GTK. createVteVersionCPPOpts :: Version -> [String] createVteVersionCPPOpts vers = catMaybes $ [ if vers >= mkVersion [0,44] then Just "-DVTE_VERSION_GEQ_0_44" else Nothing ] -- | Based on the version of the GTK3 library as reported by @pkg-config@, return -- a list of CPP macros that contain the GTK version. These can be used in the -- Haskell code to work around differences in the gi-gtk library Haskell -- library when compiled against different versions of the GTK system library. -- -- This list may need to be added to. createGtkVersionCPPOpts :: Version -- ^ 'Version' of the GTK3 library as reported by @pkg-config@. -> [String] -- ^ A list of CPP macros to show the GTK version. createGtkVersionCPPOpts gtkVersion = catMaybes $ [ if gtkVersion >= mkVersion [3,22] then Just "-DGTK_VERSION_GEQ_3_22" else Nothing ] getPkgConfigVersionFor :: String -> IO (Maybe Version) getPkgConfigVersionFor program = do pkgDb <- configureProgram normal pkgConfigProgram defaultProgramDb pkgConfigOutput <- getDbProgramOutput normal pkgConfigProgram pkgDb ["--modversion", program] -- Drop the newline on the end of the pkgConfigOutput. -- This should give us a version number like @3.22.11@. let rawProgramVersion = reverse $ drop 1 $ reverse pkgConfigOutput let maybeProgramVersion = simpleParse rawProgramVersion case maybeProgramVersion of Nothing -> do putStrLn $ "In Setup.hs, in getPkgConfigVersionFor, could not parse " <> program <> " version: " <> show pkgConfigOutput putStrLn $ "\nNot defining any CPP macros based on the version of the system " <> program <> " library." putStrLn "\nCompilation of termonad may fail." pure Nothing Just programVersion -> pure $ Just programVersion termonad-4.0.0.1/app/0000755000000000000000000000000007346545000012434 5ustar0000000000000000termonad-4.0.0.1/app/Main.hs0000644000000000000000000000025107346545000013652 0ustar0000000000000000 module Main where import Termonad (defaultMain) import Termonad.Config (tmConfigFromPreferencesFile) main :: IO () main = defaultMain =<< tmConfigFromPreferencesFile termonad-4.0.0.1/default.nix0000755000000000000000000000171307346545000014025 0ustar0000000000000000# This is the main nix file for Termonad. It will build the `termonad` # executable. It will place it in a "wrapped" environment so that Termonad can # find GHC and a few Haskell libraries. This wrapped Termonad will be able to # rebuild its configuration file (which should be located at # `~/.config/termonad/termonad.hs`). # # Termonad can be built with the command `nix-build` in the main top # directory. # # The `termonad` executable will be created at `result/bin/termonad`. With # this default setup, you will be able to use the haskell packages `lens` and # `colour` in your `~/.config/termonad/termonad.hs` file, as well as the # Termonad package itself. # # If you want to install termonad into your environment, you can use `nix-env` # from the main top directory: # # $ nix-env --file default.nix --install { nixpkgs ? null , additionalOverlays ? [] , compiler ? null , buildExamples ? false }@args: import .nix-helpers/termonad-with-packages.nix args termonad-4.0.0.1/example-config/0000755000000000000000000000000007346545000014552 5ustar0000000000000000termonad-4.0.0.1/example-config/ExampleColourExtension.hs0000644000000000000000000000670407346545000021571 0ustar0000000000000000-- | This is an example Termonad configuration that shows how to use the -- 'ColourExtension' to set colours for your terminal. See the project-wide -- README.md for how to use Termonad Haskell configuration scripts. module Main where import Termonad ( CursorBlinkMode(CursorBlinkModeOff), Option(Set) , ShowScrollbar(ShowScrollbarNever), TMConfig, confirmExit, cursorBlinkMode , defaultConfigOptions, defaultTMConfig, options, showMenu, showScrollbar , start ) import Termonad.Config.Colour ( AlphaColour, ColourConfig, List8, Palette(ExtendedPalette), addColourExtension , createColour, createColourExtension, cursorBgColour, defaultColourConfig , defaultLightColours, foregroundColour, palette, mkList8, unsafeMkList8 ) -- This is our main 'TMConfig'. It holds all of the non-colour settings -- for Termonad. -- -- This shows how a few settings can be changed. myTMConfig :: TMConfig myTMConfig = defaultTMConfig { options = defaultConfigOptions { showScrollbar = ShowScrollbarNever , confirmExit = False , showMenu = False , cursorBlinkMode = CursorBlinkModeOff } } -- This is our 'ColourConfig'. It holds all of our colour-related settings. myColourConfig :: ColourConfig (AlphaColour Double) myColourConfig = defaultColourConfig -- Set the cursor background colour. This is the normal colour of the -- cursor. { cursorBgColour = Set (createColour 120 80 110) -- purple -- Set the default foreground colour of text of the terminal. , foregroundColour = Set (createColour 220 180 210) -- light pink -- Set the extended palette that has 8 colours standard colors and then 8 -- light colors. , palette = ExtendedPalette myStandardColours (maybe defaultLightColours id myLightColours) } where -- This is a an example of creating a linked-list of colours, -- This function uses an unsafe method for generating the list. -- An exception will be thrown if your list does not have 8 elements. myStandardColours :: List8 (AlphaColour Double) myStandardColours = unsafeMkList8 [ createColour 40 30 20 -- dark brown (used as background colour) , createColour 180 30 20 -- red , createColour 40 160 20 -- green , createColour 180 160 20 -- dark yellow , createColour 40 30 120 -- dark purple , createColour 180 30 120 -- bright pink , createColour 40 160 120 -- teal , createColour 180 160 120 -- light brown ] -- This is an example of creating a linked-list of colours with a type -- safe method. mkList8 produces a Maybe value which must be handled explicitely. myLightColours :: Maybe (List8 (AlphaColour Double)) myLightColours = mkList8 [ createColour 70 60 50 -- brown , createColour 220 30 20 -- light red , createColour 40 210 20 -- light green , createColour 220 200 20 -- yellow , createColour 40 30 180 -- purple , createColour 140 30 80 -- dark pink , createColour 50 200 160 -- light teal , createColour 220 200 150 -- light brown ] main :: IO () main = do -- First, create the colour extension based on 'myColourConfig'. myColourExt <- createColourExtension myColourConfig -- Update 'myTMConfig' with our colour extension. let newTMConfig = addColourExtension myTMConfig myColourExt -- Start Termonad with our updated 'TMConfig'. start newTMConfig termonad-4.0.0.1/example-config/ExampleGruvboxColourExtension.hs0000644000000000000000000001113507346545000023140 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} -- | This is an example Termonad configuration that shows how to use the -- Gruvbox colour scheme https://github.com/morhetz/gruvbox module Main where import Termonad ( CursorBlinkMode(CursorBlinkModeOn) , Option(Set) , ShowScrollbar(ShowScrollbarNever) , TMConfig , confirmExit , cursorBlinkMode , defaultConfigOptions , defaultTMConfig , options , showMenu , showScrollbar , start , FontConfig , FontSize(FontSizePoints) , defaultFontConfig , fontConfig , fontFamily , fontSize ) import Termonad.Config.Colour ( AlphaColour , ColourConfig , Palette(ExtendedPalette) , addColourExtension , createColour , createColourExtension , defaultColourConfig , defaultStandardColours , defaultLightColours , backgroundColour , foregroundColour , palette , List8 , mkList8 ) -- This is our main 'TMConfig'. It holds all of the non-colour settings -- for Termonad. -- -- This shows how a few settings can be changed. myTMConfig :: TMConfig myTMConfig = defaultTMConfig { options = defaultConfigOptions { showScrollbar = ShowScrollbarNever , confirmExit = False , showMenu = False , cursorBlinkMode = CursorBlinkModeOn , fontConfig = fontConf } } -- This is our Gruvbox dark 'ColourConfig'. It holds all of our dark-related settings. gruvboxDark :: ColourConfig (AlphaColour Double) gruvboxDark = defaultColourConfig -- Set the default foreground colour of text of the terminal. { foregroundColour = Set (createColour 213 196 161) -- fg2 , backgroundColour = Set (createColour 40 40 40) -- bg0 -- Set the extended palette that has 2 Vecs of 8 Gruvbox palette colours , palette = ExtendedPalette gruvboxDark1 gruvboxDark2 } where gruvboxDark1 :: List8 (AlphaColour Double) gruvboxDark1 = maybe defaultStandardColours id $ mkList8 [ createColour 40 40 40 -- bg0 , createColour 204 36 29 -- red.1 , createColour 152 151 26 -- green.2 , createColour 215 153 33 -- yellow.3 , createColour 69 133 136 -- blue.4 , createColour 177 98 134 -- purple.5 , createColour 104 157 106 -- aqua.6 , createColour 189 174 147 -- fg3 ] gruvboxDark2 :: List8 (AlphaColour Double) gruvboxDark2 = maybe defaultStandardColours id $ mkList8 [ createColour 124 111 100 -- bg4 , createColour 251 71 52 -- red.9 , createColour 184 187 38 -- green.10 , createColour 250 189 47 -- yellow.11 , createColour 131 165 152 -- blue.12 , createColour 211 134 155 -- purple.13 , createColour 142 192 124 -- aqua.14 , createColour 235 219 178 -- fg1 ] -- This is our Gruvbox light 'ColourConfig'. It holds all of our dark-related settings. gruvboxLight :: ColourConfig (AlphaColour Double) gruvboxLight = defaultColourConfig -- Set the default foreground colour of text of the terminal. { foregroundColour = Set (createColour 60 56 54) -- fg1 , backgroundColour = Set (createColour 251 241 199) -- bg0 -- Set the extended palette that has 2 Vecs of 8 Gruvbox palette colours , palette = ExtendedPalette gruvboxLight1 gruvboxLight2 } where gruvboxLight1 :: List8 (AlphaColour Double) gruvboxLight1 = maybe defaultLightColours id $ mkList8 [ createColour 251 241 199 -- bg0 , createColour 204 36 29 -- red.1 , createColour 152 151 26 -- green.2 , createColour 215 153 33 -- yellow.3 , createColour 69 133 136 -- blue.4 , createColour 177 98 134 -- purple.5 , createColour 104 157 106 -- aqua.6 , createColour 102 82 84 -- fg3 ] gruvboxLight2 :: List8 (AlphaColour Double) gruvboxLight2 = maybe defaultLightColours id $ mkList8 [ createColour 168 153 132 -- bg4 , createColour 157 0 6 -- red.9 , createColour 121 116 14 -- green.10 , createColour 181 118 20 -- yellow.11 , createColour 7 102 120 -- blue.12 , createColour 143 63 113 -- purple.13 , createColour 66 123 88 -- aqua.14 , createColour 60 56 54 -- fg1 ] -- This defines the font for the terminal. fontConf :: FontConfig fontConf = defaultFontConfig { fontFamily = "Monospace" , fontSize = FontSizePoints 12 } main :: IO () main = do -- First, create the colour extension based on either Gruvboxmodules. myColourExt <- createColourExtension gruvboxDark -- Update 'myTMConfig' with our colour extension. let newTMConfig = addColourExtension myTMConfig myColourExt -- Start Termonad with our updated 'TMConfig'. start newTMConfig termonad-4.0.0.1/example-config/ExampleSolarizedColourExtension.hs0000644000000000000000000001120207346545000023433 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} -- | This is an example Termonad configuration that shows how to use the -- Solarized colour scheme https://ethanschoonover.com/solarized/ module Main where import Termonad ( CursorBlinkMode(CursorBlinkModeOn) , Option(Set) , ShowScrollbar(ShowScrollbarNever) , TMConfig , confirmExit , cursorBlinkMode , defaultConfigOptions , defaultTMConfig , options , showMenu , showScrollbar , start , FontConfig , FontSize(FontSizePoints) , defaultFontConfig , fontConfig , fontFamily , fontSize ) import Termonad.Config.Colour ( AlphaColour , ColourConfig , Palette(ExtendedPalette) , addColourExtension , createColour , createColourExtension , defaultColourConfig , defaultStandardColours , defaultLightColours , backgroundColour , foregroundColour , palette , List8 , mkList8 ) -- This is our main 'TMConfig'. It holds all of the non-colour settings -- for Termonad. -- -- This shows how a few settings can be changed. myTMConfig :: TMConfig myTMConfig = defaultTMConfig { options = defaultConfigOptions { showScrollbar = ShowScrollbarNever , confirmExit = False , showMenu = False , cursorBlinkMode = CursorBlinkModeOn , fontConfig = fontConf } } -- This is our Solarized dark 'ColourConfig'. It holds all of our dark-related settings. solarizedDark :: ColourConfig (AlphaColour Double) solarizedDark = defaultColourConfig -- Set the default foreground colour of text of the terminal. { foregroundColour = Set (createColour 131 148 150) -- base0 , backgroundColour = Set (createColour 0 43 54) -- base03 -- Set the extended palette that has 2 Vecs of 8 Solarized palette colours , palette = ExtendedPalette solarizedDark1 solarizedDark2 } where solarizedDark1 :: List8 (AlphaColour Double) solarizedDark1 = maybe defaultStandardColours id $ mkList8 [ createColour 7 54 66 -- base02 , createColour 220 50 47 -- red , createColour 133 153 0 -- green , createColour 181 137 0 -- yellow , createColour 38 139 210 -- blue , createColour 211 54 130 -- magenta , createColour 42 161 152 -- cyan , createColour 238 232 213 -- base2 ] solarizedDark2 :: List8 (AlphaColour Double) solarizedDark2 = maybe defaultStandardColours id $ mkList8 [ createColour 0 43 54 -- base03 , createColour 203 75 22 -- orange , createColour 88 110 117 -- base01 , createColour 101 123 131 -- base00 , createColour 131 148 150 -- base0 , createColour 108 113 196 -- violet , createColour 147 161 161 -- base1 , createColour 253 246 227 -- base3 ] -- This is our Solarized light 'ColourConfig'. It holds all of our light-related settings. solarizedLight :: ColourConfig (AlphaColour Double) solarizedLight = defaultColourConfig -- Set the default foreground colour of text of the terminal. { foregroundColour = Set (createColour 101 123 131) -- base00 , backgroundColour = Set (createColour 253 246 227) -- base3 -- Set the extended palette that has 2 Vecs of 8 Solarized palette colours , palette = ExtendedPalette solarizedLight1 solarizedLight2 } where solarizedLight1 :: List8 (AlphaColour Double) solarizedLight1 = maybe defaultLightColours id $ mkList8 [ createColour 7 54 66 -- base02 , createColour 220 50 47 -- red , createColour 133 153 0 -- green , createColour 181 137 0 -- yellow , createColour 38 139 210 -- blue , createColour 211 54 130 -- magenta , createColour 42 161 152 -- cyan , createColour 238 232 213 -- base2 ] solarizedLight2 :: List8 (AlphaColour Double) solarizedLight2 = maybe defaultLightColours id $ mkList8 [ createColour 0 43 54 -- base03 , createColour 203 75 22 -- orange , createColour 88 110 117 -- base01 , createColour 101 123 131 -- base00 , createColour 131 148 150 -- base0 , createColour 108 113 196 -- violet , createColour 147 161 161 -- base1 , createColour 253 246 227 -- base3 ] -- This defines the font for the terminal. fontConf :: FontConfig fontConf = defaultFontConfig { fontFamily = "Monospace" , fontSize = FontSizePoints 12 } main :: IO () main = do -- First, create the colour extension based on either Solarized modules. myColourExt <- createColourExtension solarizedDark -- Update 'myTMConfig' with our colour extension. let newTMConfig = addColourExtension myTMConfig myColourExt -- Start Termonad with our updated 'TMConfig'. start newTMConfig termonad-4.0.0.1/glade/0000755000000000000000000000000007346545000012730 5ustar0000000000000000termonad-4.0.0.1/glade/README.md0000755000000000000000000000120707346545000014212 0ustar0000000000000000 # Termonad Glade Files This directory contains [Glade](https://wiki.gnome.org/Apps/Glade) files used by Termonad to define how the GTK interfaces should look. Glade is an visual interface designer. The Glade files are just XML files. They can be edited in the Glade program, or directly by hand. If you're new to Glade, you may be interested in going through a [tutorial](https://wiki.gnome.org/Apps/Glade/Tutorials). If you use the [shell.nix](../shell.nix) file, it should pull in `glade` for you, so you can just run it directly after entering into the Nix shell. Or, you can just install `glade` through your package-manager-of-choice. termonad-4.0.0.1/glade/preferences.glade0000755000000000000000000003132107346545000016232 0ustar0000000000000000 False Termonad Preferences center-on-parent dialog False 8 8 8 8 True vertical 10 False end gtk-ok True True True True True True 1 False False 0 True False True 0 6 6 True True True Font to use in the terminal. True False Sans 12 en-us 1 0 True False end Font: 0 0 True True Number of lines to keep in the output of the terminal. 1 2 True False end Scrollback length: 0 2 Confirm exit True True False Whether or not to pop-up a dialog asking if you are sure you want to exit when you close a tab or exit out of Termonad. center True 0 3 2 True False end Word char exceptions: 0 4 True True When double-clicking on text in the terminal with the mouse, Termonad will use these characters to determine what to highlight. Characters in this list will be counted as part of a word. This makes it easy to highlight things like URLs or file paths. 1 4 Show menu True True False Whether or not to show the menubar. (This is the bar at the top that has the "File", "Edit", "View", etc buttons.) center True 0 5 2 True False end Cursor blink mode: 0 7 True False end Show tabbar: 0 6 True False end Show scrollbar: 0 1 True False Whether or not to show a scrollbar on the terminal. "If Needed" only shows the scrollbar if the text on the terminal goes off the screen. 1 1 True False Whether or not to show the tab bar. "If Needed" only shows the tab bar when you have multiple tabs. 1 6 True False Set whether the cursor in the terminal should blink. "System" sets this to the system-level GTK setting. 1 7 True False 5 5 5 5 Warning: these settings will be used for current session only. To make them permanent, set them in ~/.config/termonad/termonad.hs fill True 30 0 8 2 True True 3 1 ok termonad-4.0.0.1/img/0000755000000000000000000000000007346545000012430 5ustar0000000000000000termonad-4.0.0.1/img/termonad-lambda.png0000644000000000000000000002741707346545000016200 0ustar0000000000000000PNG  IHDR\rfzTXtRaw profile type exifxڭiv8cw `<-~.Srے(H{dML斳_lΛjϫ^ߎϷkxuUxqA)yk4޿Ϋg^e9_??XpJ;`5KPCݿ !}g~ c^lj9U]_}9T6E},ā+ý,SM/SY$bh~qy}\tuwܾML~«Ӈ{⛟7(Q?B`!F>L8?mqwv目2r)ρQ:g맯+1C_" |7K`L͕v;!Fr?r+8K6+5.b1.]H.;[/J|: .%9&Lp>0B RȡJ`Řȟ+9SHѤr*z9s.YK(K)kkڛoK-bZmΤ;Ww}FiQFmI8̳:˯(W1vTqwuv‰'|ʩWTߣ~_Gͽ{^51$ŌxQHhbbf('j.)8)bD0nqI_.rF9н"kܾ귣 U|j8akWOϯA6拡)ú^ng>Wڢ>NcE.[grN\d,5{,s0~wwטrwטrwט?]5Oq%}\ctIט?]57ymI$ȼ̷uO75)0:,}̹:>-i-|2=qk\QtiMP}xMe DU,v|vۓdx&k4&-`E|ĩD828ޞgS<Ԕ8*\ٴ]MZU$ɰ_k?A#O4vYXbNqꗰ[  e33n?b,DX SDnqi۩]$!bV~ʌR!ߝ7kFG6~>9r6]cl٧"!a_~' 8ff AΕ7gP폇6nZ cTNrmBch$↓my2Iؠ{6 %d>U=/B Wx,gRS'}@تx1;&|S_3uVXzSi0zC&,}3Rj+y4Y5Kfw:4XNlVZNԨMA%61 :~XCN/g*Gݥh$a//Tqsna=Pm2z'ĶZ\!Ro&\qzzM(e6/@17Q&F7Et,8_ / L},/sZ<^ 9De<of}"} T3KU^T/p%ShUHާ+C7&5z( xri{%*߃Ǐ?`LmV` PD8PzFIb!o,To8Dbyoc]I:d>;56x88KPR_bFfqWh[!9)ED(RҨ;r;u<.v?V9wi6囥ϐ}^n? @Jnaqt|S4\l9ZyXA{CU8"2q+xyrO' WL A`TM *.Coy_G9T*o&0RoDVepkE~fsy^kR>Bw&Ps3ljt~- $9jgA(*v,wYqN=4\J?_z:{ad|&i#:W=DnؐIK⺺ڻ+1r}Yvmڡ` &s! liFD(ĢJtk^p>2L }%fRKY~`z`ڼp䰝?{ * 95z n 4㾊ԩ(Ƃcax 95ؓvw_ȂvLqEԀ>xnoʳ )GЋƕSC`4˶O({S@^ f`IŽRK9-2Fߦ@VǟI:i]vja u~G~f|l J[^ePУ/g{s)xœ7W@"_&k$u՗/l%dKt\\XA)D@{$V3Q(A922ij:+Q`^mBHaR AvAaT!Ƿћ/|ìKjIo5AAX7G0.;5%b!1H]PA`>(߰Mm[\*9>N= SAc G}bW֭Ri!XZYDlzVpצcuVIpf˜T"IqNv 8I3ޭw\cEQ΁',@ TITB Ʌ(@2s.eGP>!GX%>:7]0 ݑJC9['mG HKܜaoA3 [*qj"؍l%у!u,<:8T1 y0Q$='0iR8I$NtQl$~8ATD: $+o&ol&y:7X"vE"3ϳnIvK BAUNa:d{HH#G:%y-:# |%&#` +&q6sI8#WS!8m`r"A]¶LtS֨H4_YJѲT.^*yD\!09 č#A/W3Xj݈;D RLu+b[8T ̔æ{(@fX*uDGˠ!G=K;O2vm`6 32NF):Gʇ]g/cf[Vا ߐ/iY%Dy5rjҜgkQR10۩$CI+1'(rp˴3y .K4"ʑjAzQ,.'mr~ak|lH2EK:Z7c>㕕͂z0wѦ+I"#-;@AU Ax*im6zB[M6| 3YZah-0:;IQ"9+b:7g؈l6`GH jckj3F;j*w5(4xL@% MXi1u-6SiE@dh,nQ%`"+;и9;1Sզ>@¼4f&7_gAtsHg4eO6 ňt9z<Gmf4G *!.0>s25*;;?N{?]fw{'> x}JWj3еA !O~7w!=pcZIs m4JO2crѰ*^t mUT%3ӧ?Pb~>0/]`scΩ.FFБnz@u[v (dĢ~E_eA]'=& cζj!"6Svd9/} s\z3M)6en5s) g =_~= 7p*ׁAд UzBw mW=ą'U@5\ws^ bأ6<A3I)wuABVnY=ꌰoO|}5R_*km8 5x0Ig$TC0葓8Je'mY7eh݄ |V6n>1lqzB{-=&݈Vbrբ|WíJY u[S6,!u]OzD=Ktc}!2{S!'5zGz }=2Bϊz^@4\|=:,L[r]'FqY} !ǵ8R=s '@=σwjwI5io:+FST>g޿4Brн3ujWQ_ryl1v jr d߽߽+~*WOxDY`PL9I$84q? @,y5/vǷbKGD pHYs.#.#x?vtIME  )ZjIDATxYtZJnlPƱ62^Y 1L , !` @2g3'\̜\d&'C 0F,򆅑myQi_{_j.ٖmI]ݪ}bR߿꫿""""""""""""""""""""""""""""""""""""""" B4 "NdY(EQͥt8`AXFw!X+fF$yȲ\? WF5~/ f7yHT?a`F7x@hDQD>?; qD'&?O1_; =Fxh#.Wɧd<&@4]0*J+Cq+A)~u~r9~L/GehzDQTl>ڮFV'11뫠7S Dӛ٬?&ק<]Ktu$!}}|1']L4龂`%=h4]Mt嬯[{Ũp Y?i?SK&_DˉrL/Z;`9``2xf|^<>C\T|C,QF6{0R/"lv|?ͤG&+} b/PţTS ȤI,g4bաNj aڵX,|;r9ͬX% edf?vr[,Vj 7 V_L꣱d;QQgLfWr $0>hZ3J< b@e[{ I(rgNcAg}6Ydl1Ƞ>o1Ȁd@l1Ƞ>| 2 6dYM>q'k3Nk4K69W:|( Y}ش:kFZM chFzM Kx޾d8 ¾գ ֨><?s4PE M> Ax1h=bAK5 rըp[{ U} R׈M>cclsYfTE}*g EвLN s]#Qy1b7-)y`Q/Xa2 Ԩ3J#l?j\vz0o.Keqp\5*>lP3ÓU8Ba_D3*:bǁN3& f̅]sTΟ͒/ _ЇoޒMFxtE -lIDg\ԧqxQܻ" 7mlnK\k Z:PZp͟>?I rɼ {8HΗC=S$ftۡ^k Y@=n v7~}_\MV4[Z>D/cvMAx/gSo0 vuTaOe hY(^#@0ّn`x! AE@`[=!5!P(:u"Q#AdBO-B jܳ_0ru|,DozS׌w&l2 ԅZz0/OcsMNڛj%ƩAV"j4)v `0.q~̂?u#ˉ/,WU:RhQ;A0FxXDʁ/10ބ=@E%¾$\0e$ൎWS^1ӕ|q3>8Q't"Oj nMos-j))R)ŻGpג.=B<xyn)%\2~p(^ #U\Ƽx56vAnOKcI 6v+HPsx1wEx+cEmAPʯ8e=1ܿbҋZOPtҢ :P-u<*Y^qxxy>GO{SJ*ožli䞨Qf5ֆA<֘D@K>n{3 r֏:wE xdy~] r#/Mv)EW"UTEc;Z.2* .;~@2^7Ĺ.% \̓>~d\_vʀ {mG`!e֯t~< PK:v[C^QNd0TԀUq`<3pjWK:z؃B B+,k4kn4G8UVrܮv:[~ g>Q^aGG`ӝCh̲FՂpfa ;fحYtj|+ר"Lf*|:_~_BMwb ը:Bx/Z8U^d|ߖVo핢?x* Mi 9d<&{#W'&ÇGxg }-'pRzl >hk-fpMk#y9}87zz)-5J > a> sM/hOSkGn\ pw4:ά_w}h(ǂxR\(gPnߥXJ+?vUN+MGfUX6Ww-zeThݻuŶ9ro6}Rh^a\9pto4p Ge6jA7*IŶ;Ouxe}I5ﯟǓkGjrM=q{FOeb1O5&q*s!|t4W^;jnY,U)|m Q V-|/|$/'V 㶨M=Y6c3[BYZЧhSoF~o9ySybZ9Mlގz9[,x`Mlކaߏ=xHn錢KBlfKPX5J=_stifUk;OWUS.ӝά[{v%k4XkK.%fC-8y1w-AK:W#3N^aۡ9eHO,}ɏ#}Z~uku0~ׇC=G:{xg:Yї2]|p/i,ӽ/w/?ƫ\KzhűxUߟFGχ@;:l aA7e]/mrao%$pJGrL%zjI !Zp.)e{wsֿ?o PrkOs&`n11y>*~Y0yGd`ȀYȠ&M>"g}"|D 6  l4#2`N9 &>0Z@Ph@E#2ZȲ\.}"#|D 6  l42 +Ad@}$쁨k@.!*%ǚbI'g@ɱX#e.&*%YJ HĆJ($ CgJ@gWt,K&IcKl4McCHlZmK񁳙${ #bs A6,ppyXܛDӽH^Q !Y ̽LT8͗%HD~@;kDKcS yO %1@/^Dؾebmn{Y7"GA~6\7b~}YC"͉ ?H$+ֱDs \o"ȣ@#kKZmMmaLD<`DTDsz`7 MH$R/^}mXJ/t3W~A@/Xj)B'+<vJ(9QSbh25e, X(n=KЊ,L,vJD w5<P{X֖`\\R: #Ԭ-t&jL Uz%(Yg+%3T{}\ldjCl%P)s.VVnpw35\;+Qv?a jp{^`x 0gZh>(˲՘qwRPv]=KE1J&Yfa}=~0ӒP jB{e9U' `WgW .AWsO{Y ?C b~/XP>R\Kפ*;&! `ƃ? ]>̗ `ڃ {0{p}%4g'la%t[ YIp^XPa)x 09u-<0Yc^@?+a3x@D "be_Kt>N;Y r:iٜg%.k /pK}0Peн/}W fD """""""""""""""""""""""""""""""""""""""""E?1BGEIENDB`termonad-4.0.0.1/img/termonad.png0000755000000000000000000003553207346545000014762 0ustar0000000000000000PNG  IHDRU!bKGD3'| pHYs  tIME/m IDATxw|e}f8;mރY-Cč(Sp?WEDDE"Q Zҕi4{GA[$hfs]:>1,=kVѣGp80 9YE(ꫫV^gƮ0SOY^z)P41MS&l66 O\yU0SOY_|1@@ trUWܹss}ݚ`0ꈈ.c_=z*""""2”ax^n""""#L(p8T tb,K,¦2\ """" """"0("""" """"222"Wq˫|ԧ;&|'9բ[wBpYEDaPDF xB`6dsӘsrRDDv;fL< x;:wx/~KaPDv >ݽpW g"oT~>7l5ap-g[YyC~\A9^xQ}@WjnQ0/&`RjaƒtЭbΙ7QI ON610Uߧ*7ۺmW<z~ VV6ʠIo sMw\%l_^7އ0rޥ\V,0+K`A>)+hV6=swL&˃{3.Q[ ח2;u6fXD@Lx"pUDHe~!s5="VIt8Kj1|_Sݴw׶z\yMA *guYwggcl_˚uYohE3etuuY*ȤAAQAQAQA>OaGUaӫ*0믨 """"#Q__g """"#M,"""0("""" """"0("""" """"0("""" """"2àns}_BYxWS[ tsQN u8̿kr*c>ŵp+}ь/ y!҅77K89:K?-,$"{}\TCEDD~/X^/v:7Q m|p~22waw \xg䢯<=g&r]spK?@53Xv]u|9 kOȁ xe0i!_dtWe2QZK[w؀".DuR6_3Y邼h#W8G_')_e}S/SI' f,3/笉}]}-ŷ:N(S"""20 z9!QSb돯-8a\:XuTFqg_Ӭodᬙ= cye{=& """1a88{03$I  sȚ 7vfLT=cU|hZ6Fwٴx5?m>FhUPDDD>0傶Vafp6< On'loڲ ? 8$'oZV>]D:;2>5 Ds )~O<2sMTdN8kx廹7~! [rL.8|A@o̰Ş]OlGӿnyx̖37gG3ąk"ӈԆ&5۷7vdQzAc/ql."""d#@("""eAQE `ZQР*0X[]Ua ##S:;;S!DdDlvALaPDDDDaPDDDDEDDDDaPDDDDEDDDDaPDDDDEDDDDaPDDDDEDDDDaPd,½]tLBDFޖrs|*1 eGY9A2a\1b7:jhgd111xR3J¾oØn?SIR4t'Uv,@ .L;YsGp$M9ˆ`6-~I8CN1ILHĊиi Yt[Ĥd˄'`JƞƐO2p8݄ٴx\{י u(T?xֱs˟Yx ?b )w;~^0hhYIie#A >Hf4q&Cl`O-lk6҈+)e)D[FAL =Ope,l ٠ nYRl-eڔ̜1C?[j6g"ϛ$*[ÓoOaQ2e.}&13w[N\~Ʃ.j(ݸg?XWpsIwf'*Fq=7qQ~dD&GsdIOnmoc[kX8#uO;}& -ڎ%.m+@㖍lnqY4qi^l wnc;9{gf'1ڽX6qN})Wqi'~F2o<>X:N=u b_ʝEqd$0s',ei!mzYX.c8;/jbsaPb*w^4)\yd zATbvʑà;5BIJOclL4:ƐM3-]x2PMUN;@vA!uVRl6i.SEWVTI8ɱZQAgX Z15FOC۶7ĥ0j\.'XNvURօ/ ƓIAA6]LGMնˌiDVQh<ȯo!eP{5Nٚ{8wm5&|_>s 1idQw"v)ZCȖ -K]Wמē/gk5;pvm m'L ;1N ZԕQ酨HNʋY6+(;f*J*h HNNLOk˞@йaݖH"+)a׭gkC/ r]+Y޽;KYWь'Έ" PD :1bU|-Vְ|Y1.r҈ {ilL""HcLv"|v >vl(fCu+~g)J7Py8bѳ6좱b mCiۏ];Y6o$F0륾":9ʷP`Y97k,RSRYcOWO'- ddd1uԣ?a {25[O^)4\լxW|/RN&/=b^|:c,nz)- `=Y#^x/e/'̂əD9#4 |'7]tNi"\^-7/ш!:%d:Yj:p1I5?{y?XT˜Rȟ)IIL=s\tD<Co1zX`5(=t-_AL}X${+_\ CӬ/emm+[7>j(-Bٱ[-2FgյU/K k69`;\76ȿŝxl^̲\76UeJs{;]V"q6;Md!^fO&8bIJ''ņk48l @9 eeS8YQ}Il$:CaQSʶfH-K&lqk ErNZnrw_38v4 fN$0Z.jk5&"2ߞbwC(\{~&3m|4D2Hv35?4K}s7c?CXk׬cΜYzZ235cto&'3/q`U2O;w~ 7YI/i_o+eigPȺRvEw&{t&G}ū5cCgϊz7WUS."d%kpjLAr3۸nAFߩzJCd4,[7Lf|oX)DG8 6?$.]zɧc i2xW7p޼[W^ťw}:H<NΣ}m6Prv^Ғm`<[?g6M46L.fk)335;ju{&H„$XaB!0\ݿ pt Adm0ڎA8Cq'8ć~´wbb$8PwК `|Σq&+W}@O<˖4y"kV}ou?j3ӿ~%Xg3ÔX_BIiee]=)<)|w`J>(^5kY*:o g:$s57Wd؇g&7=^z|`Ĺw^,7iu[NWR2 oۃ8? e|]hEx4\ĥdQP4iy1 ⊍]#cѿӅ۰aذzZo:jhyHqZgcb;t>*aBw|mt[m|xL&7|C3@Gc;[{^Vm!xxCoC{ٕA_#e5,uY`hݾwr.#ذN+4Ni 6Zh$Nܽ,mHU5>~z3QNbrio2ǺgV֭ A3i>bǓoQʥn}%˩A\J&X!h iξuRex2Sj;r6ÓCnL3*V$)2@[sa¬X{otbodSs{qٝXKpt#9D?*K  N&'7pf$c1{!dʾIDATy,[su>coo1Ӧo"1bM;8RyGolG--/Qe&SOWӈN֮_)3&[1OwPZ6F [ٺ.t.>7rmǎ'vxgx!v7G\FۼO4 ZNwӻJ`wK=ĸ3KR\A3VgxR<-3MOv{:N$wl<6FQ^ښv2pDƓ=nl@S⪪mK4 Zd"57ul.gFA3hbv0 pDD9JOaoխM4-2SqCY7&$leUԶ"ʓ؂TwLˬF4Sfǽn~XfMٳi̯\BỏqXV>.`P5Yc#*t:tθ{Ŀ77ֿ6E~QnA9c<6}~,߻ n{&5;ؒD@Sqυ2>ÿ6a"Yd%C#fq ˼y3k[bIx:W\9fذHRƑV@Lܩv&eqCiLȶ"c :6mt1m|a;Dt G\F<_222NJގ3c[jk:0b3FblQNvwHQߦoaܼd#o:;;;lV/I+;#x.8{r2H3i6o<ǧ!{nDۈi7׏Y]N#Yt5l$&yۉ˟WSgЕ4H4o8al1EǓ2FP;Ĺ.Ad{,N]g͘sn䎬kKyK$~K%M;!y/9+k m#G\_hhő kg&J Jh\ O߯Y,w 'p2ڿfsh!v"PMUlKxmlD^˨H n` 'cYJew _':6O+q[_"+POl)igg-sV28ϣp&__Άǂ} laaK$^pLJlM,O imk4 KH8hm\#_ϤI=oкG85䳣quf*KEL{...z "Oɠ)>@*dqwvѱ.aOH=vTB}cۙf;Ha7zt~7޾G mԋwi= 'оqL{!غC""Io%9vUAX2%ǯ ("2LàaV^ Xj)9n+H"@wO _H&'|½4=FxM9>mMl:c-r=;DrNei*O\,w4:@;>n+fS<MCll*tF? ɎZJ+a0v]! )^: 0F+u: b1SHSRHkk K mXV 1#F+U=9( 0I? iD;쳓Qˎ{bV6r$'v[*B;d9#7<B4ry`ꄯjOՄ{;V"yp{i{,,]ͬgQuz 0Ň>s? bmn~i.'ƑzYxlES tJ)7b>7s3!w[xK 8q=D01v`W0߳;_ھAGbݿ?N6>W&v=\{$/Lc9.$vە-7t^> [F;aH޾9t% S!8h4_d[i*;M)x #*b zbGͯԮ ֯^CFHKXg*ў7ۚ3;EDg<îD/i%dbYHN 4<ճgmY}_?t~)yjn$x. q^7gZXa7HWKbvTpyU͙vtx-Y kiL"'}8}ھAGkbwN55o>˅IK6z[|&]t i8p=O1aj .l9 ğ!ΗGmmt4MGf-_B}Y]WyQ'4;ʵ|K 4I h>rB}{pgqD;,y6VSsm9V_?hmmqߒ۵k%N#v8Vz|1gkf5y!8`j3{kiE I`d'H ,|&:V~يrF7ohJ\$;P/MZZ;;{*xoy `bQ}g؈8=i=>2شV쑺레01 vR^R´=ѴO^'cL{>=sG3v7=T!'z:X=mli+I&pqD@ Ty25pdMk_(Y21cq+ g+|tmwLey>!6䰶o0ڟ4T"20\8"w]E,3#Hw4}&h]S /H҅Ә~8"B|wpТjjIdI<6 cgp^=Ń=̿%5r1kA+~ >2ɄE])`mu+zC1>}ˤ^&BVf|*.AtUC;whWHNPh_H緰b{dk/6'^vA,wA¨ga5|zBZz{JZne]}"lQi'о?[ʺWӴ3ʉgظ`+̓ o l^ld19$xؾZADSN'9w%pDh8Ϳh:QA?i Ď#xEkR1X͗eY'+i%ovlNrZDd0^_H,.vV~݄q1c'?NzI5Z;*1x>GLwX }ʂ1N%} ŵ*0 ᄞ+WǏ$fz:۶cVs@KH9 q]V`"oֻEDAIĹ-۽켵훎.4h2X_hu"" wT9oKl* (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  ( (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  ( (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  ( (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  ( (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  ( (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  (  ( oj@ eg8U̴ |+/d&bIENDB`termonad-4.0.0.1/shell.nix0000755000000000000000000000217507346545000013513 0ustar0000000000000000# Running `nix-shell` in the same directory as this file will enter an # environment in which you can easily hack on or build termonad. It provides: # * Termonad's system dependencies. # * GHC with all of termonad's haskell dependencies. # * Local haddocks for those dependencies. # * Hoogle with an index generated from those haddocks. # * Cabal. # * ghcid. # * open-haddock. # # In this environment you should be able to e.g. # * Build termonad: # $ cabal new-build # * Open a repl with access to the termonad libraries: # $ cabal new-repl # * Use `ghcid`: # $ ghcid --command='cabal new-repl' # * Open local haddocks for e.g. GI.Vte.Objects.Terminal: # $ open-haddock GI.Vte.Objects.Terminal # or for the gi-gtk package: # $ open-haddock gi-gtk # # If you pass nix-shell the arguments `--arg indexTermonad true`, then hoogle # will also index the Termonad libraries, however this will mean the environment # will need to be rebuilt every time the termonad source changes. { compiler ? null, indexTermonad ? false, nixpkgs ? null, additionalOverlays ? [] }@args: (import .nix-helpers/nixpkgs.nix args).termonadShell termonad-4.0.0.1/src/0000755000000000000000000000000007346545000012443 5ustar0000000000000000termonad-4.0.0.1/src/Termonad.hs0000644000000000000000000000075107346545000014553 0ustar0000000000000000-- | Module : Termonad -- Description : Termonad -- Copyright : (c) Dennis Gosnell, 2018 -- License : BSD3 -- Stability : experimental -- Portability : POSIX -- -- This module exposes termonad's basic configuration options, as well as 'defaultMain'. -- -- If you want to configure Termonad, please take a look at "Termonad.Config". module Termonad ( defaultMain , start , module Config ) where import Termonad.App (defaultMain, start) import Termonad.Config as Config termonad-4.0.0.1/src/Termonad/0000755000000000000000000000000007346545000014214 5ustar0000000000000000termonad-4.0.0.1/src/Termonad/App.hs0000644000000000000000000010350507346545000015274 0ustar0000000000000000{-# LANGUAGE ForeignFunctionInterface #-} module Termonad.App where import Termonad.Prelude import Config.Dyre (defaultParams, projectName, realMain, showError, wrapMain) import Control.Lens ((.~), (^.), (^..), over, set, view) import Control.Monad.Fail (fail) import Data.FocusList (focusList, moveFromToFL, updateFocusFL) import Data.Sequence (findIndexR) import GI.Gdk (castTo, managedForeignPtr, screenGetDefault) import GI.Gio ( ApplicationFlags(ApplicationFlagsFlagsNone) , MenuModel(MenuModel) , actionMapAddAction , applicationQuit , applicationRun , onApplicationActivate , onApplicationStartup , onSimpleActionActivate , simpleActionNew ) import GI.Gtk ( Application , ApplicationWindow(ApplicationWindow) , Box(Box) , CheckButton(CheckButton) , ComboBoxText(ComboBoxText) , Dialog(Dialog) , Entry(Entry) , FontButton(FontButton) , Label(Label) , PolicyType(PolicyTypeAutomatic) , PositionType(PositionTypeRight) , ResponseType(ResponseTypeAccept, ResponseTypeNo, ResponseTypeYes) , ScrolledWindow(ScrolledWindow) , SpinButton(SpinButton) , pattern STYLE_PROVIDER_PRIORITY_APPLICATION , aboutDialogNew , adjustmentNew , applicationAddWindow , applicationGetActiveWindow , applicationSetAccelsForAction , applicationSetMenubar , applicationWindowSetShowMenubar , boxPackStart , builderNewFromString , builderSetApplication , comboBoxGetActiveId , comboBoxSetActiveId , comboBoxTextAppend , containerAdd , cssProviderLoadFromData , cssProviderNew , dialogAddButton , dialogGetContentArea , dialogNew , dialogResponse , dialogRun , entryBufferGetText , entryBufferSetText , entryGetText , entryNew , fontChooserSetFontDesc , fontChooserGetFontDesc , getEntryBuffer , gridAttachNextTo , gridNew , labelNew , notebookGetNPages , notebookNew , notebookSetShowBorder , onEntryActivate , onNotebookPageRemoved , onNotebookPageReordered , onNotebookSwitchPage , onWidgetDeleteEvent , scrolledWindowSetPolicy , setWidgetMargin , spinButtonGetValueAsInt , spinButtonSetAdjustment , spinButtonSetValue , styleContextAddProviderForScreen , toggleButtonGetActive , toggleButtonSetActive , widgetDestroy , widgetGrabFocus , widgetSetCanFocus , widgetSetVisible , widgetShow , widgetShowAll , windowPresent , windowSetDefaultIconFromFile , windowSetTitle , windowSetTransientFor ) import qualified GI.Gtk as Gtk import GI.Pango ( FontDescription , pattern SCALE , fontDescriptionGetFamily , fontDescriptionGetSize , fontDescriptionGetSizeIsAbsolute , fontDescriptionNew , fontDescriptionSetFamily , fontDescriptionSetSize , fontDescriptionSetAbsoluteSize ) import GI.Vte ( CursorBlinkMode(..) , catchRegexError , regexNewForSearch , terminalCopyClipboard , terminalPasteClipboard , terminalSearchFindNext , terminalSearchFindPrevious , terminalSearchSetRegex , terminalSearchSetWrapAround , terminalSetCursorBlinkMode , terminalSetFont , terminalSetScrollbackLines , terminalSetWordCharExceptions ) import System.Environment (getExecutablePath) import System.FilePath (takeFileName) import Paths_termonad (getDataFileName) import Termonad.Gtk (appNew, objFromBuildUnsafe) import Termonad.Keys (handleKeyPress) import Termonad.Lenses ( lensConfirmExit , lensCursorBlinkMode , lensFontConfig , lensOptions , lensShowMenu , lensShowScrollbar , lensShowTabBar , lensScrollbackLen , lensTMNotebook , lensTMNotebookTabTermContainer , lensTMNotebookTabs , lensTMNotebookTabTerm , lensTMStateApp , lensTMStateAppWin , lensTMStateConfig , lensTMStateFontDesc , lensTMStateNotebook , lensTerm , lensWordCharExceptions ) import Termonad.PreferencesFile (saveToPreferencesFile) import Termonad.Term ( createTerm , relabelTabs , termExitFocused , setShowTabs , showScrollbarToPolicy ) import Termonad.Types ( FontConfig(..) , FontSize(FontSizePoints, FontSizeUnits) , ShowScrollbar(..) , ShowTabBar(..) , TMConfig , TMNotebookTab , TMState , TMState'(TMState) , getFocusedTermFromState , modFontSize , newEmptyTMState , tmNotebookTabTermContainer , tmNotebookTabs , tmStateApp , tmStateNotebook ) import Termonad.XML (interfaceText, menuText, preferencesText) setupScreenStyle :: IO () setupScreenStyle = do maybeScreen <- screenGetDefault case maybeScreen of Nothing -> pure () Just screen -> do cssProvider <- cssProviderNew let (textLines :: [Text]) = [ "scrollbar {" -- , " -GtkRange-slider-width: 200px;" -- , " -GtkRange-stepper-size: 200px;" -- , " border-width: 200px;" , " background-color: #aaaaaa;" -- , " color: #ff0000;" -- , " min-width: 4px;" , "}" -- , "scrollbar trough {" -- , " -GtkRange-slider-width: 200px;" -- , " -GtkRange-stepper-size: 200px;" -- , " border-width: 200px;" -- , " background-color: #00ff00;" -- , " color: #00ff00;" -- , " min-width: 50px;" -- , "}" -- , "scrollbar slider {" -- , " -GtkRange-slider-width: 200px;" -- , " -GtkRange-stepper-size: 200px;" -- , " border-width: 200px;" -- , " background-color: #0000ff;" -- , " color: #0000ff;" -- , " min-width: 50px;" -- , "}" , "tab {" , " background-color: transparent;" , "}" ] let styleData = encodeUtf8 (unlines textLines :: Text) cssProviderLoadFromData cssProvider styleData styleContextAddProviderForScreen screen cssProvider (fromIntegral STYLE_PROVIDER_PRIORITY_APPLICATION) createFontDescFromConfig :: TMConfig -> IO FontDescription createFontDescFromConfig tmConfig = do let fontConf = tmConfig ^. lensOptions . lensFontConfig createFontDesc (fontSize fontConf) (fontFamily fontConf) createFontDesc :: FontSize -> Text -> IO FontDescription createFontDesc fontSz fontFam = do fontDesc <- fontDescriptionNew fontDescriptionSetFamily fontDesc fontFam setFontDescSize fontDesc fontSz pure fontDesc setFontDescSize :: FontDescription -> FontSize -> IO () setFontDescSize fontDesc (FontSizePoints points) = fontDescriptionSetSize fontDesc $ fromIntegral (points * fromIntegral SCALE) setFontDescSize fontDesc (FontSizeUnits units) = fontDescriptionSetAbsoluteSize fontDesc $ units * fromIntegral SCALE adjustFontDescSize :: (FontSize -> FontSize) -> FontDescription -> IO () adjustFontDescSize f fontDesc = do currFontSz <- fontSizeFromFontDescription fontDesc let newFontSz = f currFontSz setFontDescSize fontDesc newFontSz modifyFontSizeForAllTerms :: (FontSize -> FontSize) -> TMState -> IO () modifyFontSizeForAllTerms modFontSizeFunc mvarTMState = do tmState <- readMVar mvarTMState let fontDesc = tmState ^. lensTMStateFontDesc adjustFontDescSize modFontSizeFunc fontDesc let terms = tmState ^.. lensTMStateNotebook . lensTMNotebookTabs . traverse . lensTMNotebookTabTerm . lensTerm foldMap (\vteTerm -> terminalSetFont vteTerm (Just fontDesc)) terms fontSizeFromFontDescription :: FontDescription -> IO FontSize fontSizeFromFontDescription fontDesc = do currSize <- fontDescriptionGetSize fontDesc currAbsolute <- fontDescriptionGetSizeIsAbsolute fontDesc return $ if currAbsolute then FontSizeUnits $ fromIntegral currSize / fromIntegral SCALE else let fontRatio :: Double = fromIntegral currSize / fromIntegral SCALE in FontSizePoints $ round fontRatio fontConfigFromFontDescription :: FontDescription -> IO (Maybe FontConfig) fontConfigFromFontDescription fontDescription = do fontSize <- fontSizeFromFontDescription fontDescription maybeFontFamily <- fontDescriptionGetFamily fontDescription return $ (`FontConfig` fontSize) <$> maybeFontFamily compareScrolledWinAndTab :: ScrolledWindow -> TMNotebookTab -> Bool compareScrolledWinAndTab scrollWin flTab = let ScrolledWindow managedPtrFLTab = tmNotebookTabTermContainer flTab foreignPtrFLTab = managedForeignPtr managedPtrFLTab ScrolledWindow managedPtrScrollWin = scrollWin foreignPtrScrollWin = managedForeignPtr managedPtrScrollWin in foreignPtrFLTab == foreignPtrScrollWin updateFLTabPos :: TMState -> Int -> Int -> IO () updateFLTabPos mvarTMState oldPos newPos = modifyMVar_ mvarTMState $ \tmState -> do let tabs = tmState ^. lensTMStateNotebook . lensTMNotebookTabs maybeNewTabs = moveFromToFL oldPos newPos tabs case maybeNewTabs of Nothing -> do putStrLn $ "in updateFLTabPos, Strange error: couldn't move tabs.\n" <> "old pos: " <> tshow oldPos <> "\n" <> "new pos: " <> tshow newPos <> "\n" <> "tabs: " <> tshow tabs <> "\n" <> "maybeNewTabs: " <> tshow maybeNewTabs <> "\n" <> "tmState: " <> tshow tmState pure tmState Just newTabs -> pure $ tmState & lensTMStateNotebook . lensTMNotebookTabs .~ newTabs -- | Try to figure out whether Termonad should exit. This also used to figure -- out if Termonad should close a given terminal. -- -- This reads the 'confirmExit' setting from 'ConfigOptions' to check whether -- the user wants to be notified when either Termonad or a given terminal is -- about to be closed. -- -- If 'confirmExit' is 'True', then a dialog is presented to the user asking -- them if they really want to exit or close the terminal. Their response is -- sent back as a 'ResponseType'. -- -- If 'confirmExit' is 'False', then this function always returns -- 'ResponseTypeYes'. askShouldExit :: TMState -> IO ResponseType askShouldExit mvarTMState = do tmState <- readMVar mvarTMState let confirm = tmState ^. lensTMStateConfig . lensOptions . lensConfirmExit if confirm then confirmationDialogForExit tmState else pure ResponseTypeYes where -- Show the user a dialog telling them there are still terminals running and -- asking if they really want to exit. -- -- Return the user's resposne as a 'ResponseType'. confirmationDialogForExit :: TMState' -> IO ResponseType confirmationDialogForExit tmState = do let app = tmState ^. lensTMStateApp win <- applicationGetActiveWindow app dialog <- dialogNew box <- dialogGetContentArea dialog label <- labelNew $ Just "There are still terminals running. Are you sure you want to exit?" containerAdd box label widgetShow label setWidgetMargin label 10 void $ dialogAddButton dialog "No, do NOT exit" (fromIntegral (fromEnum ResponseTypeNo)) void $ dialogAddButton dialog "Yes, exit" (fromIntegral (fromEnum ResponseTypeYes)) windowSetTransientFor dialog win res <- dialogRun dialog widgetDestroy dialog pure $ toEnum (fromIntegral res) -- | Force Termonad to exit without asking the user whether or not to do so. forceQuit :: TMState -> IO () forceQuit mvarTMState = do tmState <- readMVar mvarTMState let app = tmState ^. lensTMStateApp applicationQuit app setupTermonad :: TMConfig -> Application -> ApplicationWindow -> Gtk.Builder -> IO () setupTermonad tmConfig app win builder = do termonadIconPath <- getDataFileName "img/termonad-lambda.png" windowSetDefaultIconFromFile termonadIconPath setupScreenStyle box <- objFromBuildUnsafe builder "content_box" Box fontDesc <- createFontDescFromConfig tmConfig note <- notebookNew widgetSetCanFocus note False -- If this is not set to False, then there will be a one pixel white border -- shown around the notebook. notebookSetShowBorder note False boxPackStart box note True True 0 mvarTMState <- newEmptyTMState tmConfig app win note fontDesc terminal <- createTerm handleKeyPress mvarTMState void $ onNotebookPageRemoved note $ \_ _ -> do pages <- notebookGetNPages note if pages == 0 then forceQuit mvarTMState else setShowTabs tmConfig note void $ onNotebookSwitchPage note $ \_ pageNum -> do modifyMVar_ mvarTMState $ \tmState -> do let notebook = tmStateNotebook tmState tabs = tmNotebookTabs notebook maybeNewTabs = updateFocusFL (fromIntegral pageNum) tabs case maybeNewTabs of Nothing -> pure tmState Just (tab, newTabs) -> do widgetGrabFocus $ tab ^. lensTMNotebookTabTerm . lensTerm pure $ tmState & lensTMStateNotebook . lensTMNotebookTabs .~ newTabs void $ onNotebookPageReordered note $ \childWidg pageNum -> do maybeScrollWin <- castTo ScrolledWindow childWidg case maybeScrollWin of Nothing -> fail $ "In setupTermonad, in callback for onNotebookPageReordered, " <> "child widget is not a ScrolledWindow.\n" <> "Don't know how to continue.\n" Just scrollWin -> do TMState{tmStateNotebook} <- readMVar mvarTMState let fl = tmStateNotebook ^. lensTMNotebookTabs let maybeOldPosition = findIndexR (compareScrolledWinAndTab scrollWin) (focusList fl) case maybeOldPosition of Nothing -> fail $ "In setupTermonad, in callback for onNotebookPageReordered, " <> "the ScrolledWindow is not already in the FocusList.\n" <> "Don't know how to continue.\n" Just oldPos -> do updateFLTabPos mvarTMState oldPos (fromIntegral pageNum) relabelTabs mvarTMState newTabAction <- simpleActionNew "newtab" Nothing void $ onSimpleActionActivate newTabAction $ \_ -> void $ createTerm handleKeyPress mvarTMState actionMapAddAction app newTabAction applicationSetAccelsForAction app "app.newtab" ["T"] closeTabAction <- simpleActionNew "closetab" Nothing void $ onSimpleActionActivate closeTabAction $ \_ -> termExitFocused mvarTMState actionMapAddAction app closeTabAction applicationSetAccelsForAction app "app.closetab" ["W"] quitAction <- simpleActionNew "quit" Nothing void $ onSimpleActionActivate quitAction $ \_ -> do shouldExit <- askShouldExit mvarTMState when (shouldExit == ResponseTypeYes) $ forceQuit mvarTMState actionMapAddAction app quitAction applicationSetAccelsForAction app "app.quit" ["Q"] copyAction <- simpleActionNew "copy" Nothing void $ onSimpleActionActivate copyAction $ \_ -> do maybeTerm <- getFocusedTermFromState mvarTMState maybe (pure ()) terminalCopyClipboard maybeTerm actionMapAddAction app copyAction applicationSetAccelsForAction app "app.copy" ["C"] pasteAction <- simpleActionNew "paste" Nothing void $ onSimpleActionActivate pasteAction $ \_ -> do maybeTerm <- getFocusedTermFromState mvarTMState maybe (pure ()) terminalPasteClipboard maybeTerm actionMapAddAction app pasteAction applicationSetAccelsForAction app "app.paste" ["V"] preferencesAction <- simpleActionNew "preferences" Nothing void $ onSimpleActionActivate preferencesAction (const $ showPreferencesDialog mvarTMState) actionMapAddAction app preferencesAction enlargeFontAction <- simpleActionNew "enlargefont" Nothing void $ onSimpleActionActivate enlargeFontAction $ \_ -> modifyFontSizeForAllTerms (modFontSize 1) mvarTMState actionMapAddAction app enlargeFontAction applicationSetAccelsForAction app "app.enlargefont" ["plus"] reduceFontAction <- simpleActionNew "reducefont" Nothing void $ onSimpleActionActivate reduceFontAction $ \_ -> modifyFontSizeForAllTerms (modFontSize (-1)) mvarTMState actionMapAddAction app reduceFontAction applicationSetAccelsForAction app "app.reducefont" ["minus"] findAction <- simpleActionNew "find" Nothing void $ onSimpleActionActivate findAction $ \_ -> doFind mvarTMState actionMapAddAction app findAction applicationSetAccelsForAction app "app.find" ["F"] findAboveAction <- simpleActionNew "findabove" Nothing void $ onSimpleActionActivate findAboveAction $ \_ -> findAbove mvarTMState actionMapAddAction app findAboveAction applicationSetAccelsForAction app "app.findabove" ["P"] findBelowAction <- simpleActionNew "findbelow" Nothing void $ onSimpleActionActivate findBelowAction $ \_ -> findBelow mvarTMState actionMapAddAction app findBelowAction applicationSetAccelsForAction app "app.findbelow" ["I"] aboutAction <- simpleActionNew "about" Nothing void $ onSimpleActionActivate aboutAction $ \_ -> showAboutDialog app actionMapAddAction app aboutAction menuBuilder <- builderNewFromString menuText $ fromIntegral (length menuText) menuModel <- objFromBuildUnsafe menuBuilder "menubar" MenuModel applicationSetMenubar app (Just menuModel) let showMenu = tmConfig ^. lensOptions . lensShowMenu applicationWindowSetShowMenubar win showMenu windowSetTitle win "Termonad" -- This event will happen if the user requests that the top-level Termonad -- window be closed through their window manager. It will also happen -- normally when the user tries to close Termonad through normal methods, -- like clicking "Quit" or closing the last open terminal. -- -- If you return 'True' from this callback, then Termonad will not exit. -- If you return 'False' from this callback, then Termonad will continue to -- exit. void $ onWidgetDeleteEvent win $ \_ -> do shouldExit <- askShouldExit mvarTMState pure $ case shouldExit of ResponseTypeYes -> False _ -> True widgetShowAll win widgetGrabFocus $ terminal ^. lensTerm appActivate :: TMConfig -> Application -> IO () appActivate tmConfig app = do uiBuilder <- builderNewFromString interfaceText $ fromIntegral (length interfaceText) builderSetApplication uiBuilder app appWin <- objFromBuildUnsafe uiBuilder "appWin" ApplicationWindow applicationAddWindow app appWin setupTermonad tmConfig app appWin uiBuilder windowPresent appWin showAboutDialog :: Application -> IO () showAboutDialog app = do win <- applicationGetActiveWindow app aboutDialog <- aboutDialogNew windowSetTransientFor aboutDialog win void $ dialogRun aboutDialog widgetDestroy aboutDialog showFindDialog :: Application -> IO (Maybe Text) showFindDialog app = do win <- applicationGetActiveWindow app dialog <- dialogNew box <- dialogGetContentArea dialog grid <- gridNew searchForLabel <- labelNew (Just "Search for regex:") containerAdd grid searchForLabel widgetShow searchForLabel setWidgetMargin searchForLabel 10 searchEntry <- entryNew gridAttachNextTo grid searchEntry (Just searchForLabel) PositionTypeRight 1 1 widgetShow searchEntry setWidgetMargin searchEntry 10 -- setWidgetMarginBottom searchEntry 20 void $ onEntryActivate searchEntry $ dialogResponse dialog (fromIntegral (fromEnum ResponseTypeYes)) void $ dialogAddButton dialog "Close" (fromIntegral (fromEnum ResponseTypeNo)) void $ dialogAddButton dialog "Find" (fromIntegral (fromEnum ResponseTypeYes)) containerAdd box grid widgetShow grid windowSetTransientFor dialog win res <- dialogRun dialog searchString <- entryGetText searchEntry let maybeSearchString = case toEnum (fromIntegral res) of ResponseTypeYes -> Just searchString _ -> Nothing widgetDestroy dialog pure maybeSearchString doFind :: TMState -> IO () doFind mvarTMState = do tmState <- readMVar mvarTMState let app = tmStateApp tmState maybeSearchString <- showFindDialog app -- putStrLn $ "trying to find: " <> tshow maybeSearchString maybeTerminal <- getFocusedTermFromState mvarTMState case (maybeSearchString, maybeTerminal) of (Just searchString, Just terminal) -> do -- TODO: Figure out how to import the correct pcre flags. -- -- If you don't pass the pcre2Multiline flag, VTE gives -- the following warning: -- -- (termonad-linux-x86_64:18792): Vte-WARNING **: -- 21:56:31.193: (vtegtk.cc:2269):void -- vte_terminal_search_set_regex(VteTerminal*, -- VteRegex*, guint32): runtime check failed: -- (regex == nullptr || -- _vte_regex_get_compile_flags(regex) & PCRE2_MULTILINE) -- -- However, if you do add the pcre2Multiline flag, -- the terminalSearchSetRegex appears to just completely -- not work. let pcreFlags = 0 let newRegex = regexNewForSearch searchString (fromIntegral $ length searchString) pcreFlags eitherRegex <- catchRegexError (fmap Right newRegex) (\_ errMsg -> pure (Left errMsg)) case eitherRegex of Left errMsg -> do let msg = "error when creating regex: " <> errMsg hPutStrLn stderr msg Right regex -> do terminalSearchSetRegex terminal (Just regex) pcreFlags terminalSearchSetWrapAround terminal True _matchFound <- terminalSearchFindPrevious terminal -- TODO: Setup an actual logging framework to show these -- kinds of log messages. Also make a similar change in -- findAbove and findBelow. -- putStrLn $ "was match found: " <> tshow matchFound pure () _ -> pure () findAbove :: TMState -> IO () findAbove mvarTMState = do maybeTerminal <- getFocusedTermFromState mvarTMState case maybeTerminal of Nothing -> pure () Just terminal -> do _matchFound <- terminalSearchFindPrevious terminal -- putStrLn $ "was match found: " <> tshow matchFound pure () findBelow :: TMState -> IO () findBelow mvarTMState = do maybeTerminal <- getFocusedTermFromState mvarTMState case maybeTerminal of Nothing -> pure () Just terminal -> do _matchFound <- terminalSearchFindNext terminal -- putStrLn $ "was match found: " <> tshow matchFound pure () setShowMenuBar :: Application -> Bool -> IO () setShowMenuBar app visible = do void $ runMaybeT $ do win <- MaybeT $ applicationGetActiveWindow app appWin <- MaybeT $ castTo ApplicationWindow win lift $ applicationWindowSetShowMenubar appWin visible -- | Fill a combo box with ids and labels -- -- The ids are stored in the combobox as 'Text', so their type should be an -- instance of the 'Show' type class. comboBoxFill :: forall a. Show a => ComboBoxText -> [(a, Text)] -> IO () comboBoxFill comboBox = mapM_ go where go :: (a, Text) -> IO () go (value, textId) = comboBoxTextAppend comboBox (Just $ tshow value) textId -- | Set the current active item in a combobox given an input id. comboBoxSetActive :: Show a => ComboBoxText -> a -> IO () comboBoxSetActive cb item = void $ comboBoxSetActiveId cb (Just $ tshow item) -- | Get the current active item in a combobox -- -- The list of values to be searched in the combobox must be given as a -- parameter. These values are converted to Text then compared to the current -- id. comboBoxGetActive :: forall a. (Show a, Enum a) => ComboBoxText -> [a] -> IO (Maybe a) comboBoxGetActive cb values = findEnumFromMaybeId <$> comboBoxGetActiveId cb where findEnumFromMaybeId :: Maybe Text -> Maybe a findEnumFromMaybeId maybeId = maybeId >>= findEnumFromId findEnumFromId :: Text -> Maybe a findEnumFromId label = find (\x -> tshow x == label) values applyNewPreferences :: TMState -> IO () applyNewPreferences mvarTMState = do tmState <- readMVar mvarTMState let appWin = tmState ^. lensTMStateAppWin config = tmState ^. lensTMStateConfig notebook = tmState ^. lensTMStateNotebook ^. lensTMNotebook tabFocusList = tmState ^. lensTMStateNotebook ^. lensTMNotebookTabs showMenu = config ^. lensOptions ^. lensShowMenu applicationWindowSetShowMenubar appWin showMenu setShowTabs config notebook -- Sets the remaining preferences to each tab foldMap (applyNewPreferencesToTab mvarTMState) tabFocusList applyNewPreferencesToTab :: TMState -> TMNotebookTab -> IO () applyNewPreferencesToTab mvarTMState tab = do tmState <- readMVar mvarTMState let fontDesc = tmState ^. lensTMStateFontDesc term = tab ^. lensTMNotebookTabTerm ^. lensTerm scrolledWin = tab ^. lensTMNotebookTabTermContainer options = tmState ^. lensTMStateConfig ^. lensOptions terminalSetFont term (Just fontDesc) terminalSetCursorBlinkMode term (options ^. lensCursorBlinkMode) terminalSetWordCharExceptions term (options ^. lensWordCharExceptions) terminalSetScrollbackLines term (fromIntegral (options ^. lensScrollbackLen)) let vScrollbarPolicy = showScrollbarToPolicy (options ^. lensShowScrollbar) scrolledWindowSetPolicy scrolledWin PolicyTypeAutomatic vScrollbarPolicy -- | Show the preferences dialog. -- -- When the user clicks on the Ok button, it copies the new settings to TMState. -- Then apply them to the current terminals. showPreferencesDialog :: TMState -> IO () showPreferencesDialog mvarTMState = do -- Get app out of mvar tmState <- readMVar mvarTMState let app = tmState ^. lensTMStateApp -- Create the preference dialog and get some widgets preferencesBuilder <- builderNewFromString preferencesText $ fromIntegral (length preferencesText) preferencesDialog <- objFromBuildUnsafe preferencesBuilder "preferences" Dialog confirmExitCheckButton <- objFromBuildUnsafe preferencesBuilder "confirmExit" CheckButton showMenuCheckButton <- objFromBuildUnsafe preferencesBuilder "showMenu" CheckButton wordCharExceptionsEntryBuffer <- objFromBuildUnsafe preferencesBuilder "wordCharExceptions" Entry >>= getEntryBuffer fontButton <- objFromBuildUnsafe preferencesBuilder "font" FontButton showScrollbarComboBoxText <- objFromBuildUnsafe preferencesBuilder "showScrollbar" ComboBoxText comboBoxFill showScrollbarComboBoxText [ (ShowScrollbarNever, "Never") , (ShowScrollbarAlways, "Always") , (ShowScrollbarIfNeeded, "If needed") ] showTabBarComboBoxText <- objFromBuildUnsafe preferencesBuilder "showTabBar" ComboBoxText comboBoxFill showTabBarComboBoxText [ (ShowTabBarNever, "Never") , (ShowTabBarAlways, "Always") , (ShowTabBarIfNeeded, "If needed") ] cursorBlinkModeComboBoxText <- objFromBuildUnsafe preferencesBuilder "cursorBlinkMode" ComboBoxText comboBoxFill cursorBlinkModeComboBoxText [ (CursorBlinkModeSystem, "System") , (CursorBlinkModeOn, "On") , (CursorBlinkModeOff, "Off") ] scrollbackLenSpinButton <- objFromBuildUnsafe preferencesBuilder "scrollbackLen" SpinButton adjustmentNew 0 0 (fromIntegral (maxBound :: Int)) 1 10 0 >>= spinButtonSetAdjustment scrollbackLenSpinButton warningLabel <- objFromBuildUnsafe preferencesBuilder "warning" Label -- We show the warning label only if the user has launched termonad with a -- termonad.hs file executablePath <- getExecutablePath let hasTermonadHs = takeFileName executablePath == "termonad-linux-x86_64" widgetSetVisible warningLabel hasTermonadHs -- Make the dialog modal maybeWin <- applicationGetActiveWindow app windowSetTransientFor preferencesDialog maybeWin -- Init with current state fontChooserSetFontDesc fontButton (tmState ^. lensTMStateFontDesc) let options = tmState ^. lensTMStateConfig . lensOptions comboBoxSetActive showScrollbarComboBoxText $ options ^. lensShowScrollbar comboBoxSetActive showTabBarComboBoxText $ options ^. lensShowTabBar comboBoxSetActive cursorBlinkModeComboBoxText $ options ^. lensCursorBlinkMode spinButtonSetValue scrollbackLenSpinButton (fromIntegral $ options ^. lensScrollbackLen) toggleButtonSetActive confirmExitCheckButton $ options ^. lensConfirmExit toggleButtonSetActive showMenuCheckButton $ options ^. lensShowMenu entryBufferSetText wordCharExceptionsEntryBuffer (options ^. lensWordCharExceptions) (-1) -- Run dialog then close res <- dialogRun preferencesDialog -- When closing the dialog get the new settings when (toEnum (fromIntegral res) == ResponseTypeAccept) $ do maybeFontDesc <- fontChooserGetFontDesc fontButton maybeFontConfig <- liftM join $ mapM fontConfigFromFontDescription maybeFontDesc maybeShowScrollbar <- comboBoxGetActive showScrollbarComboBoxText [ShowScrollbarNever ..] maybeShowTabBar <- comboBoxGetActive showTabBarComboBoxText [ShowTabBarNever ..] maybeCursorBlinkMode <- comboBoxGetActive cursorBlinkModeComboBoxText [CursorBlinkModeSystem ..] scrollbackLen <- fromIntegral <$> spinButtonGetValueAsInt scrollbackLenSpinButton confirmExit <- toggleButtonGetActive confirmExitCheckButton showMenu <- toggleButtonGetActive showMenuCheckButton wordCharExceptions <- entryBufferGetText wordCharExceptionsEntryBuffer -- Apply the changes to mvarTMState modifyMVar_ mvarTMState $ pure . over lensTMStateFontDesc (`fromMaybe` maybeFontDesc) . over (lensTMStateConfig . lensOptions) ( set lensConfirmExit confirmExit . set lensShowMenu showMenu . set lensWordCharExceptions wordCharExceptions . over lensFontConfig (`fromMaybe` maybeFontConfig) . set lensScrollbackLen scrollbackLen . over lensShowScrollbar (`fromMaybe` maybeShowScrollbar) . over lensShowTabBar (`fromMaybe` maybeShowTabBar) . over lensCursorBlinkMode (`fromMaybe` maybeCursorBlinkMode) ) -- Save the changes to the preferences files withMVar mvarTMState $ saveToPreferencesFile . view lensTMStateConfig -- Update the app with new settings applyNewPreferences mvarTMState widgetDestroy preferencesDialog appStartup :: Application -> IO () appStartup _app = pure () -- | Run Termonad with the given 'TMConfig'. -- -- Do not perform any of the recompilation operations that the 'defaultMain' -- function does. start :: TMConfig -> IO () start tmConfig = do -- app <- appNew (Just "haskell.termonad") [ApplicationFlagsFlagsNone] -- Make sure the application is not unique, so we can open multiple copies of it. app <- appNew Nothing [ApplicationFlagsFlagsNone] void $ onApplicationStartup app (appStartup app) void $ onApplicationActivate app (appActivate tmConfig app) void $ applicationRun app Nothing -- | Run Termonad with the given 'TMConfig'. -- -- This function will check if there is a @~\/.config\/termonad\/termonad.hs@ file -- and a @~\/.cache\/termonad\/termonad-linux-x86_64@ binary. Termonad will -- perform different actions based on whether or not these two files exist. -- -- Here are the four different possible actions based on the existence of these -- two files. -- -- - @~\/.config\/termonad\/termonad.hs@ exists, @~\/.cache\/termonad\/termonad-linux-x86_64@ exists -- -- The timestamps of these two files are checked. If the -- @~\/.config\/termonad\/termonad.hs@ file has been modified after the -- @~\/.cache\/termonad\/termonad-linux-x86_64@ binary, then Termonad will use -- GHC to recompile the @~\/.config\/termonad\/termonad.hs@ file, producing a -- new binary at @~\/.cache\/termonad\/termonad-linux-x86_64@. This new binary -- will be re-executed. The 'TMConfig' passed to this 'defaultMain' will be -- effectively thrown away. -- -- If GHC fails to recompile the @~\/.config\/termonad\/termonad.hs@ file, then -- Termonad will just execute 'start' with the 'TMConfig' passed in. -- -- If the @~\/.cache\/termonad\/termonad-linux-x86_64@ binary has been modified -- after the @~\/.config\/termonad\/termonad.hs@ file, then Termonad will -- re-exec the @~\/.cache\/termonad\/termonad-linux-x86_64@ binary. The -- 'TMConfig' passed to this 'defaultMain' will be effectively thrown away. -- -- - @~\/.config\/termonad\/termonad.hs@ exists, @~\/.cache\/termonad\/termonad-linux-x86_64@ does not exist -- -- Termonad will use GHC to recompile the @~\/.config\/termonad\/termonad.hs@ -- file, producing a new binary at @~\/.cache\/termonad\/termonad-linux-x86_64@. -- This new binary will be re-executed. The 'TMConfig' passed to this -- 'defaultMain' will be effectively thrown away. -- -- If GHC fails to recompile the @~\/.config\/termonad\/termonad.hs@ file, then -- Termonad will just execute 'start' with the 'TMConfig' passed in. -- -- - @~\/.config\/termonad\/termonad.hs@ does not exist, @~\/.cache\/termonad\/termonad-linux-x86_64@ exists -- -- Termonad will ignore the @~\/.cache\/termonad\/termonad-linux-x86_64@ binary -- and just run 'start' with the 'TMConfig' passed to this function. -- -- - @~\/.config\/termonad\/termonad.hs@ does not exist, @~\/.cache\/termonad\/termonad-linux-x86_64@ does not exist -- -- Termonad will run 'start' with the 'TMConfig' passed to this function. -- -- Other notes: -- -- 1. That the locations of @~\/.config\/termonad\/termonad.hs@ and -- @~\/.cache\/termonad\/termonad-linux-x86_64@ may differ depending on your -- system. -- -- 2. In your own @~\/.config\/termonad\/termonad.hs@ file, you can use either -- 'defaultMain' or 'start'. As long as you always execute the system-wide -- @termonad@ binary (instead of the binary produced as -- @~\/.cache\/termonad\/termonad-linux-x86_64@), the effect should be the same. defaultMain :: TMConfig -> IO () defaultMain tmConfig = do let params = defaultParams { projectName = "termonad" , showError = \(cfg, oldErrs) newErr -> (cfg, oldErrs <> "\n" <> newErr) , realMain = \(cfg, errs) -> putStrLn (pack errs) *> start cfg } eitherRes <- tryIOError $ wrapMain params (tmConfig, "") case eitherRes of Left ioErr | ioeGetErrorType ioErr == doesNotExistErrorType && ioeGetFileName ioErr == Just "ghc" -> do putStrLn $ "Could not find ghc on your PATH. Ignoring your termonad.hs " <> "configuration file and running termonad with default settings." start tmConfig | otherwise -> do putStrLn $ "IO error occurred when trying to run termonad:" print ioErr putStrLn "Don't know how to recover. Exiting." Right _ -> pure () termonad-4.0.0.1/src/Termonad/Config.hs0000644000000000000000000000341207346545000015755 0ustar0000000000000000-- | Module : Termonad.Config -- Description : Termonad Configuration Options -- Copyright : (c) Dennis Gosnell, 2018 -- License : BSD3 -- Stability : experimental -- Portability : POSIX -- -- This module exposes termonad's basic configuration options. To set these -- options in your config, first ensure you've imported "Termonad". -- -- Then for your main, apply 'Termonad.start' or 'Termonad.defaultMain' to a 'TMConfig' value. -- We suggest you build such values by performing record updates on the -- 'defaultTMConfig' rather than using the 'TMConfig' constructor, as the latter -- is much more likely to break when there are changes to the 'TMConfig' type. -- -- E.g. -- -- @ -- -- Re-exports this module. -- import "Termonad" -- -- main :: IO () -- main = 'start' $ 'defaultTMConfig' -- { 'showScrollbar' = 'ShowScrollbarNever' -- , 'confirmExit' = False -- , 'showMenu' = False -- , 'cursorBlinkMode' = 'CursorBlinkModeOff' -- } -- @ -- -- Additional options can be found in the following modules. -- -- * "Termonad.Config.Colour" -- -- If you want to see an example configuration file, as well as an explanation -- for how to use Termonad, see the Termonad -- . module Termonad.Config ( -- * Main Config Data Type TMConfig(..) , defaultTMConfig , ConfigOptions(..) , defaultConfigOptions , ConfigHooks(..) , defaultConfigHooks -- * Fonts , FontSize(..) , defaultFontSize , FontConfig(..) , defaultFontConfig -- * Misc , Option(..) , ShowScrollbar(..) , ShowTabBar(..) , CursorBlinkMode(..) , tmConfigFromPreferencesFile ) where import GI.Vte (CursorBlinkMode(..)) import Termonad.PreferencesFile (tmConfigFromPreferencesFile) import Termonad.Types termonad-4.0.0.1/src/Termonad/Config/0000755000000000000000000000000007346545000015421 5ustar0000000000000000termonad-4.0.0.1/src/Termonad/Config/Colour.hs0000644000000000000000000007563407346545000017237 0ustar0000000000000000{-# LANGUAGE CPP #-} {-# LANGUAGE TemplateHaskell #-} -- | Module : Termonad.Config.Colour -- Description : Termonad Configuration Colour Options -- Copyright : (c) Dennis Gosnell, 2018 -- License : BSD3 -- Stability : experimental -- Portability : POSIX -- -- To use this config extension in your @~\/.config\/termonad\/termonad.hs@, first -- import this module. Create a new 'ColourExtension' with the 'createColourExtension' function. -- Then add the 'ColourExtension' to your 'TMConfig' with the 'addColourExtension' function. -- -- See -- -- for a simple example. -- -- When setting colors, you may find it convenient to use the -- -- package, which provides an executable called @print-console-colors@ that prints -- all of the colors for your terminal. module Termonad.Config.Colour ( -- * Colour Config ColourConfig(..) , defaultColourConfig , List8 , List6 , List24 , Matrix , mkList8 , unsafeMkList8 , setAtList8 , overAtList8 , mkList6 , unsafeMkList6 , setAtList6 , overAtList6 , mkList24 , unsafeMkList24 , setAtList24 , overAtList24 , mkMatrix , unsafeMkMatrix , setAtMatrix , overAtMatrix -- ** Colour Config Lenses , lensCursorFgColour , lensCursorBgColour , lensForegroundColour , lensBackgroundColour , lensPalette -- * Colour Extension , ColourExtension(..) , createColourExtension , createDefColourExtension , addColourExtension , addColourConfig , colourHook , addColourHook -- * Palette , Palette(..) , defaultStandardColours , defaultLightColours , defaultColourCube , defaultGreyscale -- * Colour -- | Check out the "Data.Colour" module for more info about 'AlphaColour'. , AlphaColour , createColour , sRGB32 , sRGB32show , opaque , transparent -- * Debugging and Internal Methods , showColourVec , showColourCube , paletteToList , coloursFromBits , cube , setAt , overAt -- * Doctest setup -- $setup ) where import Termonad.Prelude hiding ((\\), index) import Control.Lens ((%~), makeLensesFor) import Data.Colour ( AlphaColour , Colour , affineCombo , alphaChannel , black , darken , opaque , over , transparent , withOpacity ) import Data.Colour.SRGB (RGB(RGB), toSRGB, toSRGB24, sRGB24) import qualified Data.Foldable import GI.Gdk ( RGBA , newZeroRGBA , setRGBAAlpha , setRGBABlue , setRGBAGreen , setRGBARed ) import GI.Vte ( Terminal , terminalSetColors , terminalSetColorCursor #ifdef VTE_VERSION_GEQ_0_44 , terminalSetColorCursorForeground #endif , terminalSetColorBackground , terminalSetColorForeground ) import Text.Printf (printf) import Text.Show (showString) import Termonad.Lenses (lensCreateTermHook, lensHooks) import Termonad.Types ( Option(Unset) , TMConfig , TMState , whenSet ) -- $setup -- >>> import Data.Colour.Names (green, red) -- >>> import Data.Colour.SRGB (sRGB24show) ------------------- -- Colour Config -- ------------------- -- | This newtype is for length 8 lists. Construct it with 'mkList8' or 'unsafeMkList8' newtype List8 a = List8 { getList8 :: [a] } deriving (Show, Eq, Foldable, Functor) -- | Typesafe smart constructor for length 8 lists. mkList8 :: [a] -> Maybe (List8 a) mkList8 xs = if length xs == 8 then Just (List8 xs) else Nothing -- | Unsafe smart constructor for length 8 lists. unsafeMkList8 :: [a] -> List8 a unsafeMkList8 xs = case mkList8 xs of Just xs' -> xs' Nothing -> error $ "unsafeMkList8: input list contains " <> show (length xs) <> " elements. Must contain exactly 8 elements." -- | Set a given value in a list. -- -- >>> setAt 2 "hello" ["a","b","c","d"] -- ["a","b","hello","d"] -- -- You can set the first and last values in the list as well: -- -- >>> setAt 0 "hello" ["a","b","c","d"] -- ["hello","b","c","d"] -- >>> setAt 3 "hello" ["a","b","c","d"] -- ["a","b","c","hello"] -- -- If you try to set a value outside of the list, you'll get back the same -- list: -- -- >>> setAt (-10) "hello" ["a","b","c","d"] -- ["a","b","c","d"] -- >>> setAt 100 "hello" ["a","b","c","d"] -- ["a","b","c","d"] setAt :: forall a. Int -> a -> [a] -> [a] setAt n newVal = overAt n (\_ -> newVal) -- | Update a given value in a list. -- -- >>> overAt 2 (\x -> x ++ x) ["a","b","c","d"] -- ["a","b","cc","d"] -- -- You can update the first and last values in the list as well: -- -- >>> overAt 0 (\x -> "bye") ["a","b","c","d"] -- ["bye","b","c","d"] -- >>> overAt 3 (\x -> "") ["a","b","c","d"] -- ["a","b","c",""] -- -- If you try to set a value outside of the list, you'll get back the same -- list: -- -- >>> overAt (-10) (\_ -> "foobar") ["a","b","c","d"] -- ["a","b","c","d"] -- >>> overAt 100 (\_ -> "baz") ["a","b","c","d"] -- ["a","b","c","d"] overAt :: forall a. Int -> (a -> a) -> [a] -> [a] overAt n f = foldr go [] . zip [0..] where go :: (Int, a) -> [a] -> [a] go (i, a) next | i == n = f a : next | otherwise = a : next -- | Set a given value in a 'List8'. -- -- Internally uses 'setAt'. See documentation on 'setAt' for some examples. setAtList8 :: Int -> a -> List8 a -> List8 a setAtList8 n a (List8 l) = List8 (setAt n a l) -- | Set a given value in a 'List8'. -- -- Internally uses 'overAt'. See documentation on 'overAt' for some examples. overAtList8 :: Int -> (a -> a) -> List8 a -> List8 a overAtList8 n f (List8 l) = List8 (overAt n f l) -- | This newtype is for length 6 lists. Construct it with 'mkList6' or 'unsafeMkList6' newtype List6 a = List6 { getList6 :: [a] } deriving (Show, Eq, Foldable, Functor) -- | Typesafe smart constructor for length 6 lists. mkList6 :: [a] -> Maybe (List6 a) mkList6 xs = if length xs == 6 then Just (List6 xs) else Nothing -- | Unsafe smart constructor for length 6 lists. unsafeMkList6 :: [a] -> List6 a unsafeMkList6 xs = case mkList6 xs of Just xs' -> xs' Nothing -> error $ "unsafeMkList6: input list contains " <> show (length xs) <> " elements. Must contain exactly 6 elements." -- | Set a given value in a 'List6'. -- -- Internally uses 'setAt'. See documentation on 'setAt' for some examples. setAtList6 :: Int -> a -> List6 a -> List6 a setAtList6 n a (List6 l) = List6 (setAt n a l) -- | Set a given value in a 'List6'. -- -- Internally uses 'overAt'. See documentation on 'overAt' for some examples. overAtList6 :: Int -> (a -> a) -> List6 a -> List6 a overAtList6 n f (List6 l) = List6 (overAt n f l) -- | This newtype is for length 24 lists. Construct it with 'mkList24' or 'unsafeMkList24' newtype List24 a = List24 { getList24 :: [a] } deriving (Show, Eq, Foldable, Functor) -- | Typesafe smart constructor for length 24 lists. mkList24 :: [a] -> Maybe (List24 a) mkList24 xs = if length xs == 24 then Just (List24 xs) else Nothing -- | Unsafe smart constructor for length 24 lists. unsafeMkList24 :: [a] -> List24 a unsafeMkList24 xs = case mkList24 xs of Just xs' -> xs' Nothing -> error "List must contain 24 elements" -- | Set a given value in a 'List24'. -- -- Internally uses 'setAt'. See documentation on 'setAt' for some examples. setAtList24 :: Int -> a -> List24 a -> List24 a setAtList24 n a (List24 l) = List24 (setAt n a l) -- | Set a given value in a 'List24'. -- -- Internally uses 'overAt'. See documentation on 'overAt' for some examples. overAtList24 :: Int -> (a -> a) -> List24 a -> List24 a overAtList24 n f (List24 l) = List24 (overAt n f l) -- | This newtype is for 6x6x6 matrices.. Construct it with 'mkMatrix' or 'unsafeMkMatrix' newtype Matrix a = Matrix (List6 (List6 (List6 a))) deriving (Show, Eq, Functor, Foldable) getMatrix :: Matrix a -> [[[a]]] getMatrix (Matrix (List6 m)) = fmap getList6 $ (fmap . fmap) getList6 m -- | Unsafe smart constructor for 6x6x6 Matrices. mkMatrix :: [[[a]]] -> Maybe (Matrix a) mkMatrix xs = if length xs == 6 && all (\x -> length x == 6) xs && all (all (\x -> length x == 6)) xs then Just $ Matrix $ List6 (fmap List6 ((fmap . fmap) List6 xs)) else Nothing -- | Unsafe smart constructor for 6x6x6 Matrices. unsafeMkMatrix :: [[[a]]] -> Matrix a unsafeMkMatrix xs = case mkMatrix xs of Just xs' -> xs' Nothing -> error $ "unsafeMkMatrix: input list must be exactly 6x6x6" -- | Set a given value in a 'Matrix'. -- -- Internally uses 'setAt'. See documentation on 'setAt' for some examples. setAtMatrix :: Int -> Int -> Int -> a -> Matrix a -> Matrix a setAtMatrix x y z a m = overAtMatrix x y z (\_ -> a) m -- | Set a given value in a 'Matrix'. -- -- Internally uses 'overAt'. See documentation on 'overAt' for some examples. overAtMatrix :: Int -> Int -> Int -> (a -> a) -> Matrix a -> Matrix a overAtMatrix x y z f (Matrix l6) = Matrix (overAtList6 x (overAtList6 y (overAtList6 z f)) l6) -- | This is the color palette to use for the terminal. Each data constructor -- lets you set progressively more colors. These colors are used by the -- terminal to render -- . -- -- There are 256 total terminal colors. 'BasicPalette' lets you set the first 8, -- 'ExtendedPalette' lets you set the first 16, 'ColourCubePalette' lets you set -- the first 232, and 'FullPalette' lets you set all 256. -- -- The first 8 colors codes are the standard colors. The next 8 are the -- extended (light) colors. The next 216 are a full color cube. The last 24 are a -- grey scale. -- -- The following image gives an idea of what each individual color looks like: -- -- <> -- -- This picture does not exactly match up with Termonad's default colors, but it gives an -- idea of what each block of colors represents. -- -- You can use 'defaultStandardColours', 'defaultLightColours', -- 'defaultColourCube', and 'defaultGreyscale' as a starting point to -- customize the colors. The only time you'd need to use a constructor other -- than 'NoPalette' is when you want to customize the default colors. -- That is to say, using 'FullPalette' with all the defaults should give you the -- same result as using 'NoPalette'. data Palette c = NoPalette -- ^ Don't set any colors and just use the default from VTE. This is a black -- background with light grey text. | BasicPalette !(List8 c) -- ^ Set the colors from the standard colors. | ExtendedPalette !(List8 c) !(List8 c) -- ^ Set the colors from the extended (light) colors (as well as standard colors). | ColourCubePalette !(List8 c) !(List8 c) !(Matrix c) -- ^ Set the colors from the color cube (as well as the standard colors and -- extended colors). | FullPalette !(List8 c) !(List8 c) !(Matrix c) !(List24 c) -- ^ Set the colors from the grey scale (as well as the standard colors, -- extended colors, and color cube). deriving (Eq, Show, Functor, Foldable) -- | Convert a 'Palette' to a list of colors. This is helpful for debugging. paletteToList :: Palette c -> [c] paletteToList = Data.Foldable.toList -- | Create a vector of colors based on input bits. -- -- This is used to derive 'defaultStandardColours' and 'defaultLightColours'. -- -- >>> coloursFromBits 192 0 == defaultStandardColours -- True -- -- >>> coloursFromBits 192 63 == defaultLightColours -- True -- -- In general, as an end-user, you shouldn't need to use this. coloursFromBits :: forall b. (Ord b, Floating b) => Word8 -> Word8 -> List8 (AlphaColour b) coloursFromBits scale offset = genList createElem where createElem :: Int -> AlphaColour b createElem i = let red = cmp 0 i green = cmp 1 i blue = cmp 2 i color = opaque $ sRGB24 red green blue in color cmp :: Int -> Int -> Word8 cmp i = (offset +) . (scale *) . fromIntegral . bit i bit :: Int -> Int -> Int bit m i = i `div` (2 ^ m) `mod` 2 genList :: (Int -> a) -> List8 a genList f = unsafeMkList8 [ f x | x <- [0..7]] -- | A 'Vec' of standard colors. Default value for 'BasicPalette'. -- -- >>> showColourVec defaultStandardColours -- ["#000000ff","#c00000ff","#00c000ff","#c0c000ff","#0000c0ff","#c000c0ff","#00c0c0ff","#c0c0c0ff"] defaultStandardColours :: (Ord b, Floating b) => List8 (AlphaColour b) defaultStandardColours = coloursFromBits 192 0 -- | A 'Vec' of extended (light) colors. Default value for 'ExtendedPalette'. -- -- >>> showColourVec defaultLightColours -- ["#3f3f3fff","#ff3f3fff","#3fff3fff","#ffff3fff","#3f3fffff","#ff3fffff","#3fffffff","#ffffffff"] defaultLightColours :: (Ord b, Floating b) => List8 (AlphaColour b) defaultLightColours = coloursFromBits 192 63 -- | Convert an 'AlphaColour' to a 'Colour'. -- -- >>> sRGB24show $ pureColour (opaque green) -- "#008000" -- >>> sRGB24show $ pureColour (sRGB32 0x30 0x40 0x50 0x80) -- "#304050" -- -- We assume that black is the pure color for a fully transparent -- 'AlphaColour'. -- -- >>> sRGB24show $ pureColour transparent -- "#000000" -- -- This function has been taken from: -- https://wiki.haskell.org/Colour#Getting_semi-transparent_coordinates pureColour :: AlphaColour Double -> Colour Double pureColour alaphaColour | a > 0 = darken (recip a) (alaphaColour `over` black) | otherwise = black where a :: Double a = alphaChannel alaphaColour -- | 'round's and then clamps the input between 0 and 'maxBound'. -- -- Rounds the input: -- -- >>> quantize (100.2 :: Double) :: Word8 -- 100 -- -- Clamps to 'minBound' if input is too low: -- -- >>> quantize (-3 :: Double) :: Word8 -- 0 -- -- Clamps to 'maxBound' if input is too high: -- >>> quantize (1000 :: Double) :: Word8 -- 255 -- -- Function used to quantize the alpha channel in the same way as the 'RGB' -- components. It has been copied from "Data.Colour.Internal". quantize :: forall a b. (RealFrac a, Integral b, Bounded b) => a -> b quantize x | x <= fromIntegral l = l | fromIntegral h <= x = h | otherwise = round x where l :: b l = minBound h :: b h = maxBound -- | Show an 'AlphaColour' in hex. -- -- >>> sRGB32show (opaque red) -- "#ff0000ff" -- -- Similar to 'Data.Colour.SRGB.sRGB24show'. sRGB32show :: AlphaColour Double -> String sRGB32show c = printf "#%02x%02x%02x%02x" r g b a where r, g, b :: Word8 RGB r g b = toSRGB24 $ pureColour c -- This about the same code as in Data.Colour.SRGB.toSRGBBounded a :: Word8 a = quantize (255 * alphaChannel c) -- | Create an 'AlphaColour' from a four 'Word8's. -- -- >>> sRGB32show $ sRGB32 64 96 128 255 -- "#406080ff" -- >>> sRGB32show $ sRGB32 0x08 0x10 0x20 0x01 -- "#08102001" -- -- Note that if you specify the alpha as 0 (which means completely -- translucent), all the color channels will be set to 0 as well. -- -- >>> sRGB32show $ sRGB32 100 150 200 0 -- "#00000000" -- -- Similar to 'sRGB24' but also includes an alpha channel. Most users will -- probably want to use 'createColour' instead. sRGB32 :: Word8 -- ^ red channel -> Word8 -- ^ green channel -> Word8 -- ^ blue channel -> Word8 -- ^ alpha channel -> AlphaColour Double sRGB32 r g b 255 = withOpacity (sRGB24 r g b) 1 sRGB32 r g b a = let aDouble = fromIntegral a / 255 in (withOpacity (sRGB24 r g b) aDouble) -- | Create an 'AlphaColour' that is fully 'opaque'. -- -- >>> sRGB32show $ createColour 64 96 128 -- "#406080ff" -- >>> sRGB32show $ createColour 0 0 0 -- "#000000ff" -- -- Similar to 'sRGB24' but for 'AlphaColour'. createColour :: Word8 -- ^ red channel -> Word8 -- ^ green channel -> Word8 -- ^ blue channel -> AlphaColour Double createColour r g b = sRGB32 r g b 255 -- | A helper function for showing all the colors in 'Vec' of colors. showColourVec :: List8 (AlphaColour Double) -> [String] showColourVec (List8 xs) = fmap sRGB32show xs genMatrix :: (Int -> Int -> Int -> a) -> [a] genMatrix f = [ f x y z | x <- [0..5], y <- [0..5], z <- [0..5] ] build :: ((a -> [a] -> [a]) -> [a] -> [a]) -> [a] build g = g (:) [] chunksOf :: Int -> [e] -> [[e]] chunksOf i ls = map (take i) (build (splitter ls)) where splitter :: [e] -> ([e] -> a -> a) -> a -> a splitter [] _ n = n splitter l c n = l `c` splitter (drop i l) c n -- | Specify a colour cube with one colour vector for its displacement and three -- colour vectors for its edges. Produces a uniform 6x6x6 grid bounded by -- and orthognal to the faces. cube :: forall b. Fractional b => AlphaColour b -> AlphaColour b -> AlphaColour b -> AlphaColour b -> Matrix (AlphaColour b) cube d i j k = let xs = genMatrix $ \x y z -> affineCombo [(1, d), (coef x, i), (coef y, j), (coef z, k)] $ opaque black in unsafeMkMatrix $ chunksOf 6 $ chunksOf 6 xs where coef :: Int -> b coef x = fromIntegral x / 5 -- | A matrix of a 6 x 6 x 6 color cube. Default value for 'ColourCubePalette'. -- -- >>> putStrLn $ pack $ showColourCube defaultColourCube -- [ [ #000000ff, #00005fff, #000087ff, #0000afff, #0000d7ff, #0000ffff -- , #005f00ff, #005f5fff, #005f87ff, #005fafff, #005fd7ff, #005fffff -- , #008700ff, #00875fff, #008787ff, #0087afff, #0087d7ff, #0087ffff -- , #00af00ff, #00af5fff, #00af87ff, #00afafff, #00afd7ff, #00afffff -- , #00d700ff, #00d75fff, #00d787ff, #00d7afff, #00d7d7ff, #00d7ffff -- , #00ff00ff, #00ff5fff, #00ff87ff, #00ffafff, #00ffd7ff, #00ffffff -- ] -- , [ #5f0000ff, #5f005fff, #5f0087ff, #5f00afff, #5f00d7ff, #5f00ffff -- , #5f5f00ff, #5f5f5fff, #5f5f87ff, #5f5fafff, #5f5fd7ff, #5f5fffff -- , #5f8700ff, #5f875fff, #5f8787ff, #5f87afff, #5f87d7ff, #5f87ffff -- , #5faf00ff, #5faf5fff, #5faf87ff, #5fafafff, #5fafd7ff, #5fafffff -- , #5fd700ff, #5fd75fff, #5fd787ff, #5fd7afff, #5fd7d7ff, #5fd7ffff -- , #5fff00ff, #5fff5fff, #5fff87ff, #5fffafff, #5fffd7ff, #5fffffff -- ] -- , [ #870000ff, #87005fff, #870087ff, #8700afff, #8700d7ff, #8700ffff -- , #875f00ff, #875f5fff, #875f87ff, #875fafff, #875fd7ff, #875fffff -- , #878700ff, #87875fff, #878787ff, #8787afff, #8787d7ff, #8787ffff -- , #87af00ff, #87af5fff, #87af87ff, #87afafff, #87afd7ff, #87afffff -- , #87d700ff, #87d75fff, #87d787ff, #87d7afff, #87d7d7ff, #87d7ffff -- , #87ff00ff, #87ff5fff, #87ff87ff, #87ffafff, #87ffd7ff, #87ffffff -- ] -- , [ #af0000ff, #af005fff, #af0087ff, #af00afff, #af00d7ff, #af00ffff -- , #af5f00ff, #af5f5fff, #af5f87ff, #af5fafff, #af5fd7ff, #af5fffff -- , #af8700ff, #af875fff, #af8787ff, #af87afff, #af87d7ff, #af87ffff -- , #afaf00ff, #afaf5fff, #afaf87ff, #afafafff, #afafd7ff, #afafffff -- , #afd700ff, #afd75fff, #afd787ff, #afd7afff, #afd7d7ff, #afd7ffff -- , #afff00ff, #afff5fff, #afff87ff, #afffafff, #afffd7ff, #afffffff -- ] -- , [ #d70000ff, #d7005fff, #d70087ff, #d700afff, #d700d7ff, #d700ffff -- , #d75f00ff, #d75f5fff, #d75f87ff, #d75fafff, #d75fd7ff, #d75fffff -- , #d78700ff, #d7875fff, #d78787ff, #d787afff, #d787d7ff, #d787ffff -- , #d7af00ff, #d7af5fff, #d7af87ff, #d7afafff, #d7afd7ff, #d7afffff -- , #d7d700ff, #d7d75fff, #d7d787ff, #d7d7afff, #d7d7d7ff, #d7d7ffff -- , #d7ff00ff, #d7ff5fff, #d7ff87ff, #d7ffafff, #d7ffd7ff, #d7ffffff -- ] -- , [ #ff0000ff, #ff005fff, #ff0087ff, #ff00afff, #ff00d7ff, #ff00ffff -- , #ff5f00ff, #ff5f5fff, #ff5f87ff, #ff5fafff, #ff5fd7ff, #ff5fffff -- , #ff8700ff, #ff875fff, #ff8787ff, #ff87afff, #ff87d7ff, #ff87ffff -- , #ffaf00ff, #ffaf5fff, #ffaf87ff, #ffafafff, #ffafd7ff, #ffafffff -- , #ffd700ff, #ffd75fff, #ffd787ff, #ffd7afff, #ffd7d7ff, #ffd7ffff -- , #ffff00ff, #ffff5fff, #ffff87ff, #ffffafff, #ffffd7ff, #ffffffff -- ] -- ] defaultColourCube :: (Ord b, Floating b) => Matrix (AlphaColour b) defaultColourCube = let xs = genMatrix $ \x y z -> opaque $ sRGB24 (cmp x) (cmp y) (cmp z) in unsafeMkMatrix $ chunksOf 6 $ chunksOf 6 xs where cmp :: Int -> Word8 cmp i = let i' = fromIntegral i in signum i' * 55 + 40 * i' -- | Helper function for showing all the colors in a color cube. This is used -- for debugging. showColourCube :: Matrix (AlphaColour Double) -> String showColourCube matrix = let itemList = (mconcat . mconcat) $ getMatrix matrix in showSColourCube itemList "" where showSColourCube :: [AlphaColour Double] -> String -> String showSColourCube itemList = showString "[ " . showSquare 0 itemList . showString ", " . showSquare 1 itemList . showString ", " . showSquare 2 itemList . showString ", " . showSquare 3 itemList . showString ", " . showSquare 4 itemList . showString ", " . showSquare 5 itemList . showString "]" showSquare :: Int -> [AlphaColour Double] -> String -> String showSquare i colours = showString "[ " . showRow i 0 colours . showString ", " . showRow i 1 colours . showString ", " . showRow i 2 colours . showString ", " . showRow i 3 colours . showString ", " . showRow i 4 colours . showString ", " . showRow i 5 colours . showString "]\n" showRow :: Int -> Int -> [AlphaColour Double] -> String -> String showRow i j colours = showCol (headEx $ drop (i * 36 + j * 6 + 0) colours) . showString ", " . showCol (headEx $ drop (i * 36 + j * 6 + 1) colours) . showString ", " . showCol (headEx $ drop (i * 36 + j * 6 + 2) colours) . showString ", " . showCol (headEx $ drop (i * 36 + j * 6 + 3) colours) . showString ", " . showCol (headEx $ drop (i * 36 + j * 6 + 4) colours) . showString ", " . showCol (headEx $ drop (i * 36 + j * 6 + 5) colours) . showString "\n " showCol :: AlphaColour Double -> String -> String showCol col str = sRGB32show col <> str -- | A List of a grey scale. Default value for 'FullPalette'. -- -- >>> fmap sRGB32show defaultGreyscale -- List24 {getList24 = ["#080808ff","#121212ff","#1c1c1cff","#262626ff","#303030ff","#3a3a3aff","#444444ff","#4e4e4eff","#585858ff","#626262ff","#6c6c6cff","#767676ff","#808080ff","#8a8a8aff","#949494ff","#9e9e9eff","#a8a8a8ff","#b2b2b2ff","#bcbcbcff","#c6c6c6ff","#d0d0d0ff","#dadadaff","#e4e4e4ff","#eeeeeeff"]} defaultGreyscale :: (Ord b, Floating b) => List24 (AlphaColour b) defaultGreyscale = unsafeMkList24 $ do n <- [0..23] let l = 8 + 10 * n pure $ opaque $ sRGB24 l l l -- | The configuration for the colors used by Termonad. -- -- 'foregroundColour' and 'backgroundColour' allow you to set the color of the -- foreground text and background of the terminal. -- -- 'palette' allows you to set the full color palette used by the terminal. -- See 'Palette' for more information. -- -- If you don't set 'foregroundColour', 'backgroundColour', or 'palette', the -- defaults from VTE are used. -- -- If you want to use a terminal with a white (or light) background and a black -- foreground, it may be a good idea to change some of the colors in the -- 'Palette' as well. -- -- VTE works as follows: if you don't explicitly set a background or foreground color, -- it takes the 0th colour from the 'palette' to be the background color, and the 7th -- colour from the 'palette' to be the foreground color. If you notice oddities with -- colouring in certain applications, it may be helpful to make sure that these -- 'palette' colours match up with the 'backgroundColour' and 'foregroundColour' you -- have set.) -- -- 'cursorFgColour' and 'cursorBgColour' allow you to set the foreground color -- of the text under the cursor, as well as the color of the cursor itself. -- -- Termonad will behave differently depending on the combination -- 'cursorFgColour' and 'cursorBgColour' being 'Set' vs. 'Unset'. -- Here is the summary of the different possibilities: -- -- * 'cursorFgColour' is 'Set' and 'cursorBgColour' is 'Set' -- -- The foreground and background colors of the cursor are as you have set. -- -- * 'cursorFgColour' is 'Set' and 'cursorBgColour' is 'Unset' -- -- The cursor background color turns completely black so that it is not -- visible. The foreground color of the cursor is the color that you have -- 'Set'. This ends up being mostly unusable, so you are recommended to -- always 'Set' 'cursorBgColour' when you have 'Set' 'cursorFgColour'. -- -- * 'cursorFgColour' is 'Unset' and 'cursorBgColour' is 'Set' -- -- The cursor background color becomes the color you 'Set', while the cursor -- foreground color doesn't change from the letter it is over. For instance, -- imagine there is a letter on the screen with a black background and a -- green foreground. If you bring the cursor overtop of it, the cursor -- background will be the color you have 'Set', while the cursor foreground -- will be green. -- -- This is completely usable, but is slightly annoying if you place the cursor -- over a letter with the same foreground color as the cursor's background -- color, because the letter will not be readable. For instance, imagine you -- have set your cursor background color to red, and somewhere on the screen -- there is a letter with a black background and a red foreground. If you move -- your cursor over the letter, the background of the cursor will be red (as -- you have set), and the cursor foreground will be red (to match the original -- foreground color of the letter). This will make it so you can't -- actually read the letter, because the foreground and background are both -- red. -- -- * 'cursorFgColour' is 'Unset' and 'cursorBgColour' is 'Unset' -- -- This combination makes the cursor inverse of whatever text it is over. -- If your cursor is over red text with a black background, the cursor -- background will be red and the cursor foreground will be black. -- -- This is the default. -- -- 'cursorFgColour' is not supported in @vte-2.91@ versions older than 0.44. -- (This is somewhat confusing. Note that @vte-2.91@ is the name of the system -- library, and @0.44@ is its version number.) -- -- See 'defaultColourConfig' for the defaults for 'ColourConfig' used in Termonad. data ColourConfig c = ColourConfig { cursorFgColour :: !(Option c) -- ^ Foreground color of the cursor. This is the color of the text that -- the cursor is over. This is not supported on older versions of VTE. , cursorBgColour :: !(Option c) -- ^ Background color of the cursor. This is the color of the cursor -- itself. , foregroundColour :: !(Option c) -- ^ Color of the default foreground text in the terminal. , backgroundColour :: !(Option c) -- ^ Background color for the terminal , palette :: !(Palette c) -- ^ Color palette for the terminal. See 'Palette'. } deriving (Eq, Show, Functor) -- | Default setting for a 'ColourConfig'. The cursor colors, font foreground -- color, background color, and color palette are all left at the defaults set -- by VTE. -- -- >>> defaultColourConfig -- ColourConfig {cursorFgColour = Unset, cursorBgColour = Unset, foregroundColour = Unset, backgroundColour = Unset, palette = NoPalette} defaultColourConfig :: ColourConfig (AlphaColour Double) defaultColourConfig = ColourConfig { cursorFgColour = Unset , cursorBgColour = Unset , foregroundColour = Unset , backgroundColour = Unset , palette = NoPalette } $(makeLensesFor [ ("cursorFgColour", "lensCursorFgColour") , ("cursorBgColour", "lensCursorBgColour") , ("foregroundColour", "lensForegroundColour") , ("backgroundColour", "lensBackgroundColour") , ("palette", "lensPalette") ] ''ColourConfig ) ------------------------------ -- ConfigExtension Instance -- ------------------------------ -- | Extension that allows setting colors for terminals in Termonad. data ColourExtension = ColourExtension { colourExtConf :: MVar (ColourConfig (AlphaColour Double)) -- ^ 'MVar' holding the current 'ColourConfig'. This could potentially be -- passed to other extensions or user code. This would allow changing the -- colors for new terminals in realtime. , colourExtCreateTermHook :: TMState -> Terminal -> IO () -- ^ The 'createTermHook' used by the 'ColourExtension'. This sets the -- colors for a new terminal based on the 'ColourConfig' in 'colourExtConf'. } -- | The default 'createTermHook' for 'colourExtCreateTermHook'. Set the colors -- for a terminal based on the given 'ColourConfig'. colourHook :: MVar (ColourConfig (AlphaColour Double)) -> TMState -> Terminal -> IO () colourHook mvarColourConf _ vteTerm = do colourConf <- readMVar mvarColourConf let paletteColourList = paletteToList $ palette colourConf rgbaPaletteColourList <- traverse colourToRgba paletteColourList terminalSetColors vteTerm Nothing Nothing (Just rgbaPaletteColourList) whenSet (backgroundColour colourConf) $ terminalSetColorBackground vteTerm <=< colourToRgba whenSet (foregroundColour colourConf) $ terminalSetColorForeground vteTerm <=< colourToRgba whenSet (cursorBgColour colourConf) $ terminalSetColorCursor vteTerm . Just <=< colourToRgba #ifdef VTE_VERSION_GEQ_0_44 whenSet (cursorFgColour colourConf) $ terminalSetColorCursorForeground vteTerm . Just <=< colourToRgba #endif colourToRgba :: AlphaColour Double -> IO RGBA colourToRgba colour = do let RGB red green blue = toSRGB $ pureColour colour alpha = alphaChannel colour rgba <- newZeroRGBA setRGBARed rgba red setRGBAGreen rgba green setRGBABlue rgba blue setRGBAAlpha rgba alpha pure rgba -- | Create a 'ColourExtension' based on a given 'ColourConfig'. -- -- Most users will want to use this. createColourExtension :: ColourConfig (AlphaColour Double) -> IO ColourExtension createColourExtension conf = do mvarConf <- newMVar conf pure $ ColourExtension { colourExtConf = mvarConf , colourExtCreateTermHook = colourHook mvarConf } -- | Create a 'ColourExtension' based on 'defaultColourConfig'. -- -- Note that this is not needed if you just want to use the default colors for -- Termonad. However, if you want to pass around the 'MVar' 'ColourConfig' for -- extensions to use, then you may need this function. createDefColourExtension :: IO ColourExtension createDefColourExtension = createColourExtension defaultColourConfig -- | Add a given 'ColourConfig' to a 'TMConfig'. This adds 'colourHook' to the -- 'createTermHook' in 'TMConfig'. addColourConfig :: TMConfig -> ColourConfig (AlphaColour Double) -> IO TMConfig addColourConfig tmConf colConf = do ColourExtension _ newHook <- createColourExtension colConf let newTMConf = tmConf & lensHooks . lensCreateTermHook %~ addColourHook newHook pure newTMConf -- | This is similar to 'addColourConfig', but can be used on a -- 'ColourExtension' created with 'createColourExtension'. addColourExtension :: TMConfig -> ColourExtension -> TMConfig addColourExtension tmConf (ColourExtension _ newHook) = tmConf & lensHooks . lensCreateTermHook %~ addColourHook newHook -- | This function shows how to combine 'createTermHook's. -- -- This first runs the old hook, followed by the new hook. -- -- This is used internally by 'addColourConfig' and 'addColourExtension'. addColourHook :: (TMState -> Terminal -> IO ()) -- ^ New hook -> (TMState -> Terminal -> IO ()) -- ^ Old hook -> TMState -> Terminal -> IO () addColourHook newHook oldHook tmState term = do oldHook tmState term newHook tmState term termonad-4.0.0.1/src/Termonad/Gtk.hs0000644000000000000000000000400307346545000015272 0ustar0000000000000000{-# LANGUAGE CPP #-} module Termonad.Gtk where import Termonad.Prelude import Control.Monad.Fail (MonadFail, fail) import Data.GI.Base (ManagedPtr, withManagedPtr) import GHC.Stack (HasCallStack) import GI.Gdk ( GObject , castTo ) import GI.Gio (ApplicationFlags) import GI.Gtk (Application, IsWidget, Widget(Widget), applicationNew, builderGetObject, toWidget) import qualified GI.Gtk as Gtk objFromBuildUnsafe :: GObject o => Gtk.Builder -> Text -> (ManagedPtr o -> o) -> IO o objFromBuildUnsafe builder name constructor = do maybePlainObj <- builderGetObject builder name case maybePlainObj of Nothing -> error $ "Couldn't get " <> unpack name <> " from builder!" Just plainObj -> do maybeNewObj <- castTo constructor plainObj case maybeNewObj of Nothing -> error $ "Got " <> unpack name <> " from builder, but couldn't convert to object!" Just obj -> pure obj -- | Unsafely creates a new 'Application'. This calls 'fail' if it cannot -- create the 'Application' for some reason. -- -- This can fail for different reasons, one of which being that application -- name does not have a period in it. appNew :: (HasCallStack, MonadIO m, MonadFail m) => Maybe Text -- ^ The application name. Must have a period in it if specified. If passed -- as 'Nothing', then no application name will be used. -> [ApplicationFlags] -> m Application appNew appName appFlags = do maybeApp <- applicationNew appName appFlags case maybeApp of Nothing -> fail "Could not create application for some reason!" Just app -> pure app -- | Tests to see if two GTK widgets point to the same thing. This should only -- happen if they are actually the same thing. widgetEq :: (MonadIO m, IsWidget a, IsWidget b) => a -> b -> m Bool widgetEq a b = do Widget managedPtrA <- toWidget a Widget managedPtrB <- toWidget b liftIO $ withManagedPtr managedPtrA $ \ptrA -> withManagedPtr managedPtrB $ \ptrB -> pure (ptrA == ptrB) termonad-4.0.0.1/src/Termonad/Keys.hs0000644000000000000000000000613507346545000015470 0ustar0000000000000000 module Termonad.Keys where import Termonad.Prelude import Control.Lens (imap) import GI.Gdk ( EventKey , pattern KEY_0 , pattern KEY_1 , pattern KEY_2 , pattern KEY_3 , pattern KEY_4 , pattern KEY_5 , pattern KEY_6 , pattern KEY_7 , pattern KEY_8 , pattern KEY_9 , ModifierType(..) , getEventKeyHardwareKeycode , getEventKeyIsModifier , getEventKeyKeyval , getEventKeyLength , getEventKeyState , getEventKeyString , getEventKeyType ) import Termonad.Term (altNumSwitchTerm) import Termonad.Types (TMState) showKeys :: EventKey -> IO Bool showKeys eventKey = do eventType <- getEventKeyType eventKey maybeString <- getEventKeyString eventKey modifiers <- getEventKeyState eventKey len <- getEventKeyLength eventKey keyval <- getEventKeyKeyval eventKey isMod <- getEventKeyIsModifier eventKey keycode <- getEventKeyHardwareKeycode eventKey putStrLn "key press event:" putStrLn $ " type = " <> tshow eventType putStrLn $ " str = " <> tshow maybeString putStrLn $ " mods = " <> tshow modifiers putStrLn $ " isMod = " <> tshow isMod putStrLn $ " len = " <> tshow len putStrLn $ " keyval = " <> tshow keyval putStrLn $ " keycode = " <> tshow keycode putStrLn "" pure True data Key = Key { keyVal :: Word32 , keyMods :: Set ModifierType } deriving (Eq, Ord, Show) toKey :: Word32 -> Set ModifierType -> Key toKey = Key keyMap :: Map Key (TMState -> IO Bool) keyMap = let numKeys = [ KEY_1 , KEY_2 , KEY_3 , KEY_4 , KEY_5 , KEY_6 , KEY_7 , KEY_8 , KEY_9 , KEY_0 ] altNumKeys = imap (\i k -> (toKey k [ModifierTypeMod1Mask], stopProp (altNumSwitchTerm i)) ) numKeys in mapFromList altNumKeys stopProp :: (TMState -> IO a) -> TMState -> IO Bool stopProp callback terState = callback terState $> True removeStrangeModifiers :: Key -> Key removeStrangeModifiers Key{keyVal, keyMods} = let reservedModifiers = [ ModifierTypeModifierReserved13Mask , ModifierTypeModifierReserved14Mask , ModifierTypeModifierReserved15Mask , ModifierTypeModifierReserved16Mask , ModifierTypeModifierReserved17Mask , ModifierTypeModifierReserved18Mask , ModifierTypeModifierReserved19Mask , ModifierTypeModifierReserved20Mask , ModifierTypeModifierReserved21Mask , ModifierTypeModifierReserved22Mask , ModifierTypeModifierReserved23Mask , ModifierTypeModifierReserved24Mask , ModifierTypeModifierReserved25Mask , ModifierTypeModifierReserved29Mask ] in Key keyVal (difference keyMods reservedModifiers) handleKeyPress :: TMState -> EventKey -> IO Bool handleKeyPress terState eventKey = do -- void $ showKeys eventKey keyval <- getEventKeyKeyval eventKey modifiers <- getEventKeyState eventKey let oldKey = toKey keyval (setFromList modifiers) newKey = removeStrangeModifiers oldKey maybeAction = lookup newKey keyMap case maybeAction of Just action -> action terState Nothing -> pure False termonad-4.0.0.1/src/Termonad/Lenses.hs0000644000000000000000000000325307346545000016004 0ustar0000000000000000{-# LANGUAGE TemplateHaskell #-} module Termonad.Lenses where import Control.Lens (makeLensesFor, makePrisms) import Termonad.Types $(makeLensesFor [ ("term", "lensTerm") , ("pid", "lensPid") , ("unique", "lensUnique") ] ''TMTerm ) $(makeLensesFor [ ("tmNotebookTabTermContainer", "lensTMNotebookTabTermContainer") , ("tmNotebookTabTerm", "lensTMNotebookTabTerm") , ("tmNotebookTabLabel", "lensTMNotebookTabLabel") ] ''TMNotebookTab ) $(makeLensesFor [ ("tmNotebook", "lensTMNotebook") , ("tmNotebookTabs", "lensTMNotebookTabs") ] ''TMNotebook ) $(makeLensesFor [ ("tmStateApp", "lensTMStateApp") , ("tmStateAppWin", "lensTMStateAppWin") , ("tmStateNotebook", "lensTMStateNotebook") , ("tmStateFontDesc", "lensTMStateFontDesc") , ("tmStateConfig", "lensTMStateConfig") , ("tmStateUserReqExit", "lensTMStateUserReqExit") ] ''TMState' ) $(makePrisms ''FontSize) $(makeLensesFor [ ("fontFamily", "lensFontFamily") , ("fontSize", "lensFontSize") ] ''FontConfig ) $(makeLensesFor [ ("fontConfig", "lensFontConfig") , ("showScrollbar", "lensShowScrollbar") , ("colourConfig", "lensColourConfig") , ("scrollbackLen", "lensScrollbackLen") , ("confirmExit", "lensConfirmExit") , ("wordCharExceptions", "lensWordCharExceptions") , ("showMenu", "lensShowMenu") , ("showTabBar", "lensShowTabBar") , ("cursorBlinkMode", "lensCursorBlinkMode") ] ''ConfigOptions ) $(makeLensesFor [ ("createTermHook", "lensCreateTermHook") ] ''ConfigHooks ) $(makeLensesFor [ ("options", "lensOptions") , ("hooks", "lensHooks") ] ''TMConfig ) termonad-4.0.0.1/src/Termonad/Pcre.hs0000644000000000000000000000046707346545000015450 0ustar0000000000000000{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} module Termonad.Pcre where import Foreign.C (CUInt) import qualified Language.C.Inline as C C.verbatim "#define PCRE2_CODE_UNIT_WIDTH 0" C.include "" pcre2Multiline :: CUInt pcre2Multiline = [C.pure| unsigned int { PCRE2_MULTILINE } |] termonad-4.0.0.1/src/Termonad/PreferencesFile.hs0000644000000000000000000000475707346545000017626 0ustar0000000000000000 module Termonad.PreferencesFile where import Termonad.Prelude import Data.Yaml (decodeFileEither, encode, prettyPrintParseException) import System.Directory ( XdgDirectory(XdgConfig) , createDirectoryIfMissing , doesFileExist , getXdgDirectory ) import Termonad.Types ( ConfigOptions , TMConfig(TMConfig, hooks, options) , defaultConfigHooks , defaultConfigOptions ) -- | Get the path to the preferences file @~\/.config\/termonad\/termonad.yaml@. getPreferencesFile :: IO FilePath getPreferencesFile = do -- Get the termonad config directory confDir <- getXdgDirectory XdgConfig "termonad" createDirectoryIfMissing True confDir pure $ confDir "termonad.yaml" -- | Read the configuration for the preferences file -- @~\/.config\/termonad\/termonad.yaml@. This file stores only the 'options' of -- 'TMConfig' so 'hooks' are initialized with 'defaultConfigHooks'. If the -- file doesn't exist, create it with the default values. tmConfigFromPreferencesFile :: IO TMConfig tmConfigFromPreferencesFile = do confFile <- getPreferencesFile -- If there is no preferences file we create it with the default values exists <- doesFileExist confFile unless exists $ writePreferencesFile confFile defaultConfigOptions -- Read the configuration file eitherOptions <- decodeFileEither confFile options <- case eitherOptions of Left err -> do hPutStrLn stderr $ "Error parsing file " <> pack confFile hPutStrLn stderr $ pack $ prettyPrintParseException err pure defaultConfigOptions Right options -> pure options pure $ TMConfig { options = options, hooks = defaultConfigHooks } writePreferencesFile :: FilePath -> ConfigOptions -> IO () writePreferencesFile confFile options = do let yaml = encode options yamlWithComment = "# DO NOT EDIT THIS FILE BY HAND!\n" <> "#\n" <> "# This file is generated automatically by the Preferences dialog\n" <> "# in Termonad. Please open the Preferences dialog if you wish to\n" <> "# modify this file.\n" <> "#\n" <> "# The settings in this file will be ignored if you have a\n" <> "# termonad.hs file in this same directory.\n\n" <> yaml writeFile confFile yamlWithComment -- | Save the configuration to the preferences file -- @~\/.config\/termonad\/termonad.yaml@ saveToPreferencesFile :: TMConfig -> IO () saveToPreferencesFile TMConfig { options = options } = do confFile <- getPreferencesFile writePreferencesFile confFile options termonad-4.0.0.1/src/Termonad/Prelude.hs0000644000000000000000000000066107346545000016153 0ustar0000000000000000module Termonad.Prelude ( module X , hPutStrLn , whenJust ) where import Control.Lens as X ((&)) import Control.Monad.Trans.Maybe as X (MaybeT(MaybeT), runMaybeT) import ClassyPrelude as X import Data.Proxy as X import qualified Data.Text.IO as TextIO whenJust :: Monoid m => Maybe a -> (a -> m) -> m whenJust = flip foldMap hPutStrLn :: MonadIO m => Handle -> Text -> m () hPutStrLn hndl = liftIO . TextIO.hPutStrLn hndl termonad-4.0.0.1/src/Termonad/Term.hs0000644000000000000000000003614507346545000015470 0ustar0000000000000000{-# LANGUAGE CPP #-} module Termonad.Term where import Termonad.Prelude import Control.Lens ((^.), (.~), set, to) import Data.Colour.SRGB (Colour, RGB(RGB), toSRGB) import Data.FocusList (appendFL, deleteFL, getFocusItemFL) import GI.Gdk ( EventButton , EventKey , RGBA , getEventButtonButton , newZeroRGBA , setRGBABlue , setRGBAGreen , setRGBARed ) import GI.Gdk.Constants (pattern BUTTON_SECONDARY) import GI.Gio ( Cancellable , menuAppend , menuNew ) import GI.GLib ( SpawnFlags(SpawnFlagsDefault) ) import GI.Gtk ( Adjustment , Align(AlignFill) , ApplicationWindow , Box , Button , IconSize(IconSizeMenu) , Label , Notebook , Orientation(OrientationHorizontal) , PolicyType(PolicyTypeAlways, PolicyTypeAutomatic, PolicyTypeNever) , ReliefStyle(ReliefStyleNone) , ResponseType(ResponseTypeNo, ResponseTypeYes) , ScrolledWindow , applicationGetActiveWindow , boxNew , buttonNewFromIconName , buttonSetRelief , containerAdd , dialogAddButton , dialogGetContentArea , dialogNew , dialogRun , labelNew , labelSetEllipsize , labelSetLabel , labelSetMaxWidthChars , menuAttachToWidget , menuNewFromModel , menuPopupAtPointer , notebookAppendPage , notebookDetachTab , notebookGetNPages , notebookPageNum , notebookSetCurrentPage , notebookSetShowTabs , notebookSetTabReorderable , onButtonClicked , onWidgetButtonPressEvent , onWidgetKeyPressEvent , scrolledWindowNew , scrolledWindowSetPolicy , setWidgetMargin , widgetDestroy , widgetGrabFocus , widgetSetCanFocus , widgetSetHalign , widgetSetHexpand , widgetShow , windowSetFocus , windowSetTransientFor ) import GI.Pango (EllipsizeMode(EllipsizeModeMiddle), FontDescription) import GI.Vte ( PtyFlags(PtyFlagsDefault) , Terminal , onTerminalChildExited , onTerminalWindowTitleChanged , terminalGetWindowTitle , terminalNew , terminalSetCursorBlinkMode , terminalSetFont , terminalSetScrollbackLines , terminalSpawnSync , terminalSetWordCharExceptions ) import System.Directory (getSymbolicLinkTarget) import System.Environment (lookupEnv) import Termonad.Lenses ( lensConfirmExit , lensOptions , lensShowScrollbar , lensShowTabBar , lensTMNotebookTabLabel , lensTMNotebookTabTerm , lensTMNotebookTabTermContainer , lensTMNotebookTabs , lensTMStateApp , lensTMStateConfig , lensTMStateNotebook , lensTerm ) import Termonad.Types ( ConfigHooks(createTermHook) , ConfigOptions(scrollbackLen, wordCharExceptions, cursorBlinkMode) , ShowScrollbar(..) , ShowTabBar(..) , TMConfig(hooks, options) , TMNotebook , TMNotebookTab , TMState , TMState'(TMState, tmStateAppWin, tmStateConfig, tmStateFontDesc, tmStateNotebook) , TMTerm , assertInvariantTMState , createTMNotebookTab , newTMTerm , pid , tmNotebook , tmNotebookTabTerm , tmNotebookTabTermContainer , tmNotebookTabs ) focusTerm :: Int -> TMState -> IO () focusTerm i mvarTMState = do note <- tmNotebook . tmStateNotebook <$> readMVar mvarTMState notebookSetCurrentPage note (fromIntegral i) altNumSwitchTerm :: Int -> TMState -> IO () altNumSwitchTerm = focusTerm termExitFocused :: TMState -> IO () termExitFocused mvarTMState = do tmState <- readMVar mvarTMState let maybeTab = tmState ^. lensTMStateNotebook . lensTMNotebookTabs . to getFocusItemFL case maybeTab of Nothing -> pure () Just tab -> termClose tab mvarTMState termClose :: TMNotebookTab -> TMState -> IO () termClose tab mvarTMState = do tmState <- readMVar mvarTMState let confirm = tmState ^. lensTMStateConfig . lensOptions . lensConfirmExit close = if confirm then termExitWithConfirmation else termExit close tab mvarTMState termExitWithConfirmation :: TMNotebookTab -> TMState -> IO () termExitWithConfirmation tab mvarTMState = do tmState <- readMVar mvarTMState let app = tmState ^. lensTMStateApp win <- applicationGetActiveWindow app dialog <- dialogNew box <- dialogGetContentArea dialog label <- labelNew (Just "Close tab?") containerAdd box label widgetShow label setWidgetMargin label 10 void $ dialogAddButton dialog "No, do NOT close tab" (fromIntegral (fromEnum ResponseTypeNo)) void $ dialogAddButton dialog "Yes, close tab" (fromIntegral (fromEnum ResponseTypeYes)) windowSetTransientFor dialog win res <- dialogRun dialog widgetDestroy dialog case toEnum (fromIntegral res) of ResponseTypeYes -> termExit tab mvarTMState _ -> pure () termExit :: TMNotebookTab -> TMState -> IO () termExit tab mvarTMState = do detachTabAction <- modifyMVar mvarTMState $ \tmState -> do let notebook = tmStateNotebook tmState detachTabAction = notebookDetachTab (tmNotebook notebook) (tmNotebookTabTermContainer tab) let newTabs = deleteFL tab (tmNotebookTabs notebook) let newTMState = set (lensTMStateNotebook . lensTMNotebookTabs) newTabs tmState pure (newTMState, detachTabAction) detachTabAction relabelTabs mvarTMState relabelTabs :: TMState -> IO () relabelTabs mvarTMState = do TMState{tmStateNotebook} <- readMVar mvarTMState let notebook = tmNotebook tmStateNotebook tabFocusList = tmNotebookTabs tmStateNotebook foldMap (go notebook) tabFocusList where go :: Notebook -> TMNotebookTab -> IO () go notebook tmNotebookTab = do let label = tmNotebookTab ^. lensTMNotebookTabLabel scrolledWin = tmNotebookTab ^. lensTMNotebookTabTermContainer term' = tmNotebookTab ^. lensTMNotebookTabTerm . lensTerm relabelTab notebook label scrolledWin term' -- | Compute the text for a 'Label' for a GTK Notebook tab. -- -- >>> computeTabLabel 0 (Just "me@machine:~") -- "1. me@machine:~" -- -- >>> computeTabLabel 5 (Just "bash process") -- "6. bash process" -- -- >>> computeTabLabel 2 Nothing -- "3. shell" computeTabLabel :: Int -- ^ Tab number. 0 is used for the first tab, 1 for the second, etc. -> Maybe Text -- ^ A possible title for a tab. If this is 'Nothing', then the string -- @shell@ will be used. -> Text computeTabLabel pageNum maybeTitle = let title = fromMaybe "shell" maybeTitle in tshow (pageNum + 1) <> ". " <> title -- | Update the given 'Label' for a GTK Notebook tab. -- -- The new text for the label is determined by the 'computeTabLabel' function. relabelTab :: Notebook -> Label -> ScrolledWindow -> Terminal -> IO () relabelTab notebook label scrolledWin term' = do tabNum <- notebookPageNum notebook scrolledWin maybeTitle <- terminalGetWindowTitle term' let labelText = computeTabLabel (fromIntegral tabNum) maybeTitle labelSetLabel label labelText showScrollbarToPolicy :: ShowScrollbar -> PolicyType showScrollbarToPolicy ShowScrollbarNever = PolicyTypeNever showScrollbarToPolicy ShowScrollbarIfNeeded = PolicyTypeAutomatic showScrollbarToPolicy ShowScrollbarAlways = PolicyTypeAlways createScrolledWin :: TMState -> IO ScrolledWindow createScrolledWin mvarTMState = do tmState <- readMVar mvarTMState let showScrollbarVal = tmState ^. lensTMStateConfig . lensOptions . lensShowScrollbar vScrollbarPolicy = showScrollbarToPolicy showScrollbarVal scrolledWin <- scrolledWindowNew (Nothing :: Maybe Adjustment) (Nothing :: Maybe Adjustment) widgetShow scrolledWin scrolledWindowSetPolicy scrolledWin PolicyTypeAutomatic vScrollbarPolicy pure scrolledWin createNotebookTabLabel :: IO (Box, Label, Button) createNotebookTabLabel = do box <- boxNew OrientationHorizontal 5 label <- labelNew (Just "") labelSetEllipsize label EllipsizeModeMiddle labelSetMaxWidthChars label 10 widgetSetHexpand label True widgetSetHalign label AlignFill button <- buttonNewFromIconName (Just "window-close") (fromIntegral (fromEnum IconSizeMenu)) buttonSetRelief button ReliefStyleNone containerAdd box label containerAdd box button widgetSetCanFocus button False widgetSetCanFocus label False widgetSetCanFocus box False widgetShow box widgetShow label widgetShow button pure (box, label, button) setShowTabs :: TMConfig -> Notebook -> IO () setShowTabs tmConfig note = do npages <- notebookGetNPages note let shouldShowTabs = case tmConfig ^. lensOptions . lensShowTabBar of ShowTabBarIfNeeded -> npages > 1 ShowTabBarAlways -> True ShowTabBarNever -> False notebookSetShowTabs note shouldShowTabs toRGBA :: Colour Double -> IO RGBA toRGBA colour = do let RGB red green blue = toSRGB colour rgba <- newZeroRGBA setRGBARed rgba red setRGBAGreen rgba green setRGBABlue rgba blue pure rgba -- | TODO: This should probably be implemented in an external package, -- since it is a generally useful utility. -- -- It should also be implemented for windows and osx. cwdOfPid :: Int -> IO (Maybe Text) cwdOfPid pd = do #ifdef mingw32_HOST_OS pure Nothing #else #ifdef darwin_HOST_OS pure Nothing #else let pidPath = "/proc" show pd "cwd" eitherLinkTarget <- try $ getSymbolicLinkTarget pidPath case eitherLinkTarget of Left (_ :: IOException) -> pure Nothing Right linkTarget -> pure $ Just $ pack linkTarget #endif #endif -- | Get the current working directory from the shell in the focused tab of a -- notebook. -- -- Returns 'Nothing' if there is no focused tab of the notebook, or the -- current working directory could not be detected for the shell. getCWDFromFocusedTab :: TMNotebook -> IO (Maybe Text) getCWDFromFocusedTab currNote = do let maybeFocusedTab = getFocusItemFL (tmNotebookTabs currNote) case maybeFocusedTab of Nothing -> pure Nothing Just focusedNotebookTab -> do let shellPid = pid (tmNotebookTabTerm focusedNotebookTab) cwdOfPid shellPid -- | Create the VTE 'Terminal', set the fonts and options createAndInitVteTerm :: FontDescription -> ConfigOptions -> IO Terminal createAndInitVteTerm tmStateFontDesc curOpts = do vteTerm <- terminalNew terminalSetFont vteTerm (Just tmStateFontDesc) terminalSetWordCharExceptions vteTerm $ wordCharExceptions curOpts terminalSetScrollbackLines vteTerm (fromIntegral (scrollbackLen curOpts)) terminalSetCursorBlinkMode vteTerm (cursorBlinkMode curOpts) widgetShow vteTerm pure vteTerm -- | Starts a shell in a terminal and return a new TMTerm launchShell :: Terminal -- ^ GTK 'Terminal' to spawn the shell in. -> Maybe Text -- ^ An optional path to the current working directory to start the -- shell in. If 'Nothing', use the current working directory of the -- termonad process. -> IO Int launchShell vteTerm maybeCurrDir = do -- Should probably use GI.Vte.Functions.getUserShell, but contrary to its -- documentation it raises an exception rather wrap in Maybe. mShell <- lookupEnv "SHELL" let argv = fromMaybe ["/usr/bin/env", "bash"] (pure <$> mShell) -- Launch the shell shellPid <- terminalSpawnSync vteTerm [PtyFlagsDefault] maybeCurrDir argv Nothing ([SpawnFlagsDefault] :: [SpawnFlags]) Nothing (Nothing :: Maybe Cancellable) pure (fromIntegral shellPid) -- | Add a page to the notebook and switch to it. addPage :: TMState -> TMNotebookTab -> Box -- ^ The GTK Object holding the label we want to show for the tab of the -- newly created page of the notebook. -> IO () addPage mvarTMState notebookTab tabLabelBox = do -- Append a new notebook page and update the TMState to reflect this. (note, pageIndex) <- modifyMVar mvarTMState appendNotebookPage -- Switch the current Notebook page to the the newly added page. notebookSetCurrentPage note pageIndex where appendNotebookPage :: TMState' -> IO (TMState', (Notebook, Int32)) appendNotebookPage tmState = do let notebook = tmStateNotebook tmState note = tmNotebook notebook tabs = tmNotebookTabs notebook scrolledWin = tmNotebookTabTermContainer notebookTab pageIndex <- notebookAppendPage note scrolledWin (Just tabLabelBox) notebookSetTabReorderable note scrolledWin True setShowTabs (tmState ^. lensTMStateConfig) note let newTabs = appendFL tabs notebookTab newTMState = tmState & lensTMStateNotebook . lensTMNotebookTabs .~ newTabs pure (newTMState, (note, pageIndex)) -- | Set the keyboard focus on a vte terminal setFocusOn :: ApplicationWindow -> Terminal -> IO() setFocusOn tmStateAppWin vteTerm = do widgetGrabFocus vteTerm windowSetFocus tmStateAppWin (Just vteTerm) -- | Create a new 'TMTerm', setting it up and adding it to the GTKNotebook. createTerm :: (TMState -> EventKey -> IO Bool) -- ^ Funtion for handling key presses on the terminal. -> TMState -> IO TMTerm createTerm handleKeyPress mvarTMState = do -- Check preconditions assertInvariantTMState mvarTMState -- Read needed data in TMVar TMState{tmStateAppWin, tmStateFontDesc, tmStateConfig, tmStateNotebook=currNote} <- readMVar mvarTMState -- Create a new terminal and launch a shell in it vteTerm <- createAndInitVteTerm tmStateFontDesc (options tmStateConfig) maybeCurrDir <- getCWDFromFocusedTab currNote termShellPid <- launchShell vteTerm maybeCurrDir tmTerm <- newTMTerm vteTerm termShellPid -- Create the container add the VTE term in it scrolledWin <- createScrolledWin mvarTMState containerAdd scrolledWin vteTerm -- Create the GTK widget for the Notebook tab (tabLabelBox, tabLabel, tabCloseButton) <- createNotebookTabLabel -- Create notebook state let notebookTab = createTMNotebookTab tabLabel scrolledWin tmTerm -- Add the new notebooktab to the notebook. addPage mvarTMState notebookTab tabLabelBox -- Setup the initial label for the notebook tab. This needs to happen -- after we add the new page to the notebook, so that the page can get labelled -- appropriately. relabelTab (tmNotebook currNote) tabLabel scrolledWin vteTerm -- Connect callbacks void $ onButtonClicked tabCloseButton $ termClose notebookTab mvarTMState void $ onTerminalWindowTitleChanged vteTerm $ do TMState{tmStateNotebook} <- readMVar mvarTMState let notebook = tmNotebook tmStateNotebook relabelTab notebook tabLabel scrolledWin vteTerm void $ onWidgetKeyPressEvent vteTerm $ handleKeyPress mvarTMState void $ onWidgetKeyPressEvent scrolledWin $ handleKeyPress mvarTMState void $ onWidgetButtonPressEvent vteTerm $ handleMousePress vteTerm void $ onTerminalChildExited vteTerm $ \_ -> termExit notebookTab mvarTMState -- Put the keyboard focus on the term setFocusOn tmStateAppWin vteTerm -- Make sure the state is still right assertInvariantTMState mvarTMState -- Run user-defined hooks for modifying the newly-created VTE Terminal. createTermHook (hooks tmStateConfig) mvarTMState vteTerm pure tmTerm -- | Popup the context menu on right click handleMousePress :: Terminal -> EventButton -> IO Bool handleMousePress vteTerm event = do button <- getEventButtonButton event let rightClick = button == fromIntegral BUTTON_SECONDARY when rightClick $ do menuModel <- menuNew menuAppend menuModel (Just "Copy") (Just "app.copy") menuAppend menuModel (Just "Paste") (Just "app.paste") menu <- menuNewFromModel menuModel menuAttachToWidget menu vteTerm Nothing menuPopupAtPointer menu Nothing pure rightClick termonad-4.0.0.1/src/Termonad/Types.hs0000644000000000000000000005172307346545000015664 0ustar0000000000000000{-# OPTIONS_GHC -fno-warn-orphans #-} module Termonad.Types where import Termonad.Prelude import Control.Monad.Fail (fail) import Data.FocusList (FocusList, emptyFL, singletonFL, getFocusItemFL, lengthFL) import Data.Unique (Unique, hashUnique, newUnique) import Data.Yaml ( FromJSON(parseJSON) , ToJSON(toJSON) , Value(String) , withText ) import GI.Gtk ( Application , ApplicationWindow , IsWidget , Label , Notebook , ScrolledWindow , Widget , notebookGetCurrentPage , notebookGetNthPage , notebookGetNPages ) import GI.Pango (FontDescription) import GI.Vte (Terminal, CursorBlinkMode(..)) import Text.Pretty.Simple (pPrint) import Text.Show (ShowS, showParen, showString) import Termonad.Gtk (widgetEq) -- | A wrapper around a VTE 'Terminal'. This also stores the process ID of the -- process running on this terminal, as well as a 'Unique' that can be used for -- comparing terminals. data TMTerm = TMTerm { term :: !Terminal -- ^ The actual 'Terminal'. , pid :: !Int -- ^ The process ID of the process running in 'term'. , unique :: !Unique -- ^ A 'Unique' for comparing different 'TMTerm' for uniqueness. } instance Show TMTerm where showsPrec :: Int -> TMTerm -> ShowS showsPrec d TMTerm{..} = showParen (d > 10) $ showString "TMTerm {" . showString "term = " . showString "(GI.GTK.Terminal)" . showString ", " . showString "pid = " . showsPrec (d + 1) pid . showString ", " . showString "unique = " . showsPrec (d + 1) (hashUnique unique) . showString "}" -- | A container that holds everything in a given terminal window. The 'term' -- in the 'TMTerm' is inside the 'tmNotebookTabTermContainer' 'ScrolledWindow'. -- The notebook tab 'Label' is also available. data TMNotebookTab = TMNotebookTab { tmNotebookTabTermContainer :: !ScrolledWindow -- ^ The 'ScrolledWindow' holding the VTE 'Terminal'. , tmNotebookTabTerm :: !TMTerm -- ^ The 'Terminal' insidie the 'ScrolledWindow'. , tmNotebookTabLabel :: !Label -- ^ The 'Label' holding the title of the 'Terminal' in the 'Notebook' tab. } instance Show TMNotebookTab where showsPrec :: Int -> TMNotebookTab -> ShowS showsPrec d TMNotebookTab{..} = showParen (d > 10) $ showString "TMNotebookTab {" . showString "tmNotebookTabTermContainer = " . showString "(GI.GTK.ScrolledWindow)" . showString ", " . showString "tmNotebookTabTerm = " . showsPrec (d + 1) tmNotebookTabTerm . showString ", " . showString "tmNotebookTabLabel = " . showString "(GI.GTK.Label)" . showString "}" -- | This holds the GTK 'Notebook' containing multiple tabs of 'Terminal's. We -- keep a separate list of terminals in 'tmNotebookTabs'. data TMNotebook = TMNotebook { tmNotebook :: !Notebook -- ^ This is the GTK 'Notebook' that holds multiple tabs of 'Terminal's. , tmNotebookTabs :: !(FocusList TMNotebookTab) -- ^ A 'FocusList' containing references to each individual 'TMNotebookTab'. } instance Show TMNotebook where showsPrec :: Int -> TMNotebook -> ShowS showsPrec d TMNotebook{..} = showParen (d > 10) $ showString "TMNotebook {" . showString "tmNotebook = " . showString "(GI.GTK.Notebook)" . showString ", " . showString "tmNotebookTabs = " . showsPrec (d + 1) tmNotebookTabs . showString "}" data TMState' = TMState { tmStateApp :: !Application , tmStateAppWin :: !ApplicationWindow , tmStateNotebook :: !TMNotebook , tmStateFontDesc :: !FontDescription , tmStateConfig :: !TMConfig } instance Show TMState' where showsPrec :: Int -> TMState' -> ShowS showsPrec d TMState{..} = showParen (d > 10) $ showString "TMState {" . showString "tmStateApp = " . showString "(GI.GTK.Application)" . showString ", " . showString "tmStateAppWin = " . showString "(GI.GTK.ApplicationWindow)" . showString ", " . showString "tmStateNotebook = " . showsPrec (d + 1) tmStateNotebook . showString ", " . showString "tmStateFontDesc = " . showString "(GI.Pango.FontDescription)" . showString ", " . showString "tmStateConfig = " . showsPrec (d + 1) tmStateConfig . showString "}" type TMState = MVar TMState' instance Eq TMTerm where (==) :: TMTerm -> TMTerm -> Bool (==) = (==) `on` (unique :: TMTerm -> Unique) instance Eq TMNotebookTab where (==) :: TMNotebookTab -> TMNotebookTab -> Bool (==) = (==) `on` tmNotebookTabTerm createTMTerm :: Terminal -> Int -> Unique -> TMTerm createTMTerm trm pd unq = TMTerm { term = trm , pid = pd , unique = unq } newTMTerm :: Terminal -> Int -> IO TMTerm newTMTerm trm pd = do unq <- newUnique pure $ createTMTerm trm pd unq getFocusedTermFromState :: TMState -> IO (Maybe Terminal) getFocusedTermFromState mvarTMState = withMVar mvarTMState go where go :: TMState' -> IO (Maybe Terminal) go tmState = do let maybeNotebookTab = getFocusItemFL $ tmNotebookTabs $ tmStateNotebook tmState pure $ fmap (term . tmNotebookTabTerm) maybeNotebookTab createTMNotebookTab :: Label -> ScrolledWindow -> TMTerm -> TMNotebookTab createTMNotebookTab tabLabel scrollWin trm = TMNotebookTab { tmNotebookTabTermContainer = scrollWin , tmNotebookTabTerm = trm , tmNotebookTabLabel = tabLabel } createTMNotebook :: Notebook -> FocusList TMNotebookTab -> TMNotebook createTMNotebook note tabs = TMNotebook { tmNotebook = note , tmNotebookTabs = tabs } createEmptyTMNotebook :: Notebook -> TMNotebook createEmptyTMNotebook notebook = createTMNotebook notebook emptyFL notebookToList :: Notebook -> IO [Widget] notebookToList notebook = unfoldHelper 0 [] where unfoldHelper :: Int32 -> [Widget] -> IO [Widget] unfoldHelper index32 acc = do notePage <- notebookGetNthPage notebook index32 case notePage of Nothing -> pure acc Just notePage' -> unfoldHelper (index32 + 1) (acc ++ [notePage']) newTMState :: TMConfig -> Application -> ApplicationWindow -> TMNotebook -> FontDescription -> IO TMState newTMState tmConfig app appWin note fontDesc = newMVar $ TMState { tmStateApp = app , tmStateAppWin = appWin , tmStateNotebook = note , tmStateFontDesc = fontDesc , tmStateConfig = tmConfig } newEmptyTMState :: TMConfig -> Application -> ApplicationWindow -> Notebook -> FontDescription -> IO TMState newEmptyTMState tmConfig app appWin note fontDesc = newMVar $ TMState { tmStateApp = app , tmStateAppWin = appWin , tmStateNotebook = createEmptyTMNotebook note , tmStateFontDesc = fontDesc , tmStateConfig = tmConfig } newTMStateSingleTerm :: TMConfig -> Application -> ApplicationWindow -> Notebook -> Label -> ScrolledWindow -> Terminal -> Int -> FontDescription -> IO TMState newTMStateSingleTerm tmConfig app appWin note label scrollWin trm pd fontDesc = do tmTerm <- newTMTerm trm pd let tmNoteTab = createTMNotebookTab label scrollWin tmTerm tabs = singletonFL tmNoteTab tmNote = createTMNotebook note tabs newTMState tmConfig app appWin tmNote fontDesc traceShowMTMState :: TMState -> IO () traceShowMTMState mvarTMState = do tmState <- readMVar mvarTMState print tmState ------------ -- Config -- ------------ -- | The font size for the Termonad terminal. There are two ways to set the -- fontsize, corresponding to the two different ways to set the font size in -- the Pango font rendering library. -- -- If you're not sure which to use, try 'FontSizePoints' first and see how it -- looks. It should generally correspond to font sizes you are used to from -- other applications. data FontSize = FontSizePoints Int -- ^ This sets the font size based on \"points\". The conversion between a -- point and an actual size depends on the system configuration and the -- output device. The function 'GI.Pango.fontDescriptionSetSize' is used -- to set the font size. See the documentation for that function for more -- info. | FontSizeUnits Double -- ^ This sets the font size based on \"device units\". In general, this -- can be thought of as one pixel. The function -- 'GI.Pango.fontDescriptionSetAbsoluteSize' is used to set the font size. -- See the documentation for that function for more info. deriving (Eq, FromJSON, Generic, Show, ToJSON) -- | The default 'FontSize' used if not specified. -- -- >>> defaultFontSize -- FontSizePoints 12 defaultFontSize :: FontSize defaultFontSize = FontSizePoints 12 -- | Modify a 'FontSize' by adding some value. -- -- >>> modFontSize 1 (FontSizePoints 13) -- FontSizePoints 14 -- >>> modFontSize 1 (FontSizeUnits 9.0) -- FontSizeUnits 10.0 -- -- You can reduce the font size by passing a negative value. -- -- >>> modFontSize (-2) (FontSizePoints 13) -- FontSizePoints 11 -- -- If you try to create a font size less than 1, then the old font size will be -- used. -- -- >>> modFontSize (-10) (FontSizePoints 5) -- FontSizePoints 5 -- >>> modFontSize (-1) (FontSizeUnits 1.0) -- FontSizeUnits 1.0 modFontSize :: Int -> FontSize -> FontSize modFontSize i (FontSizePoints oldPoints) = let newPoints = oldPoints + i in FontSizePoints $ if newPoints < 1 then oldPoints else newPoints modFontSize i (FontSizeUnits oldUnits) = let newUnits = oldUnits + fromIntegral i in FontSizeUnits $ if newUnits < 1 then oldUnits else newUnits -- | Settings for the font to be used in Termonad. data FontConfig = FontConfig { fontFamily :: !Text -- ^ The font family to use. Example: @"DejaVu Sans Mono"@ or @"Source Code Pro"@ , fontSize :: !FontSize -- ^ The font size. } deriving (Eq, FromJSON, Generic, Show, ToJSON) -- | The default 'FontConfig' to use if not specified. -- -- >>> defaultFontConfig == FontConfig {fontFamily = "Monospace", fontSize = defaultFontSize} -- True defaultFontConfig :: FontConfig defaultFontConfig = FontConfig { fontFamily = "Monospace" , fontSize = defaultFontSize } -- | This data type represents an option that can either be 'Set' or 'Unset'. -- -- This data type is used in situations where leaving an option unset results -- in a special state that is not representable by setting any specific value. -- -- Examples of this include the 'cursorFgColour' and 'cursorBgColour' options -- supplied by the 'ColourConfig' @ConfigExtension@. By default, -- 'cursorFgColour' and 'cursorBgColour' are both 'Unset'. However, when -- 'cursorBgColour' is 'Set', 'cursorFgColour' defaults to the color of the text -- underneath. There is no way to represent this by setting 'cursorFgColour'. data Option a = Unset | Set !a deriving (Show, Read, Eq, Ord, Functor, Foldable) -- | Run a function over the value contained in an 'Option'. Return 'mempty' -- when 'Option' is 'Unset'. -- -- >>> whenSet (Set [1,2,3]) (++ [4,5,6]) :: [Int] -- [1,2,3,4,5,6] -- >>> whenSet Unset (++ [4,5,6]) :: [Int] -- [] whenSet :: Monoid m => Option a -> (a -> m) -> m whenSet = \case Unset -> \_ -> mempty Set x -> \f -> f x -- | Whether or not to show the scroll bar in a terminal. data ShowScrollbar = ShowScrollbarNever -- ^ Never show the scroll bar, even if there are too -- many lines on the terminal to show all at once. You -- should still be able to scroll with the mouse wheel. | ShowScrollbarAlways -- ^ Always show the scrollbar, even if it is not -- needed. | ShowScrollbarIfNeeded -- ^ Only show the scrollbar if there are too many -- lines on the terminal to show all at once. deriving (Enum, Eq, Generic, FromJSON, Show, ToJSON) -- | Whether or not to show the tab bar for switching tabs. data ShowTabBar = ShowTabBarNever -- ^ Never show the tab bar, even if there are multiple tabs -- open. This may be confusing if you plan on using multiple tabs. | ShowTabBarAlways -- ^ Always show the tab bar, even if you only have one tab open. | ShowTabBarIfNeeded -- ^ Only show the tab bar if you have multiple tabs open. deriving (Enum, Eq, Generic, FromJSON, Show, ToJSON) -- | Configuration options for Termonad. -- -- See 'defaultConfigOptions' for the default values. data ConfigOptions = ConfigOptions { fontConfig :: !FontConfig -- ^ Specific options for fonts. , showScrollbar :: !ShowScrollbar -- ^ When to show the scroll bar. , scrollbackLen :: !Integer -- ^ The number of lines to keep in the scroll back history for each terminal. , confirmExit :: !Bool -- ^ Whether or not to ask you for confirmation when closing individual -- terminals or Termonad itself. It is generally safer to keep this as -- 'True'. , wordCharExceptions :: !Text -- ^ When double-clicking on text in the terminal with the mouse, Termonad -- will use this value to determine what to highlight. The individual -- characters in this list will be counted as part of a word. -- -- For instance if 'wordCharExceptions' is @""@, then when you double-click -- on the text @http://@, only the @http@ portion will be highlighted. If -- 'wordCharExceptions' is @":"@, then the @http:@ portion will be -- highlighted. , showMenu :: !Bool -- ^ Whether or not to show the @File@ @Edit@ etc menu. , showTabBar :: !ShowTabBar -- ^ When to show the tab bar. , cursorBlinkMode :: !CursorBlinkMode -- ^ How to handle cursor blink. } deriving (Eq, Generic, FromJSON, Show, ToJSON) instance FromJSON CursorBlinkMode where parseJSON = withText "CursorBlinkMode" $ \c -> do case (c :: Text) of "CursorBlinkModeSystem" -> pure CursorBlinkModeSystem "CursorBlinkModeOn" -> pure CursorBlinkModeOn "CursorBlinkModeOff" -> pure CursorBlinkModeOff _ -> fail "Wrong value for CursorBlinkMode" instance ToJSON CursorBlinkMode where toJSON CursorBlinkModeSystem = String "CursorBlinkModeSystem" toJSON CursorBlinkModeOn = String "CursorBlinkModeOn" toJSON CursorBlinkModeOff = String "CursorBlinkModeOff" -- Not supposed to happened fall back to system toJSON (AnotherCursorBlinkMode _) = String "CursorBlinkModeSystem" -- | The default 'ConfigOptions'. -- -- >>> :{ -- let defConfOpt = -- ConfigOptions -- { fontConfig = defaultFontConfig -- , showScrollbar = ShowScrollbarIfNeeded -- , scrollbackLen = 10000 -- , confirmExit = True -- , wordCharExceptions = "-#%&+,./=?@\\_~\183:" -- , showMenu = True -- , showTabBar = ShowTabBarIfNeeded -- , cursorBlinkMode = CursorBlinkModeOn -- } -- in defaultConfigOptions == defConfOpt -- :} -- True defaultConfigOptions :: ConfigOptions defaultConfigOptions = ConfigOptions { fontConfig = defaultFontConfig , showScrollbar = ShowScrollbarIfNeeded , scrollbackLen = 10000 , confirmExit = True , wordCharExceptions = "-#%&+,./=?@\\_~\183:" , showMenu = True , showTabBar = ShowTabBarIfNeeded , cursorBlinkMode = CursorBlinkModeOn } -- | The Termonad 'ConfigOptions' along with the 'ConfigHooks'. data TMConfig = TMConfig { options :: !ConfigOptions , hooks :: !ConfigHooks } deriving Show -- | The default 'TMConfig'. -- -- 'options' is 'defaultConfigOptions' and 'hooks' is 'defaultConfigHooks'. defaultTMConfig :: TMConfig defaultTMConfig = TMConfig { options = defaultConfigOptions , hooks = defaultConfigHooks } --------------------- -- ConfigHooks -- --------------------- -- | Hooks into certain termonad operations and VTE events. Used to modify -- termonad's behaviour in order to implement new functionality. Fields should -- have sane @Semigroup@ and @Monoid@ instances so that config extensions can -- be combined uniformly and new hooks can be added without incident. data ConfigHooks = ConfigHooks { -- | Produce an IO action to run on creation of new @Terminal@, given @TMState@ -- and the @Terminal@ in question. createTermHook :: TMState -> Terminal -> IO () } instance Show ConfigHooks where showsPrec :: Int -> ConfigHooks -> ShowS showsPrec _ _ = showString "ConfigHooks {" . showString "createTermHook = " . showString "}" -- | Default values for the 'ConfigHooks'. -- -- - The default function for 'createTermHook' is 'defaultCreateTermHook'. defaultConfigHooks :: ConfigHooks defaultConfigHooks = ConfigHooks { createTermHook = defaultCreateTermHook } -- | Default value for 'createTermHook'. Does nothing. defaultCreateTermHook :: TMState -> Terminal -> IO () defaultCreateTermHook _ _ = pure () ---------------- -- Invariants -- ---------------- data FocusNotSameErr = FocusListFocusExistsButNoNotebookTabWidget | NotebookTabWidgetDiffersFromFocusListFocus | NotebookTabWidgetExistsButNoFocusListFocus deriving Show data TabsDoNotMatch = TabLengthsDifferent Int Int -- ^ The first 'Int' is the number of tabs in the -- actual GTK 'Notebook'. The second 'Int' is -- the number of tabs in the 'FocusList'. | TabAtIndexDifferent Int -- ^ The tab at index 'Int' is different between -- the actual GTK 'Notebook' and the 'FocusList'. deriving (Show) data TMStateInvariantErr = FocusNotSame FocusNotSameErr Int | TabsDoNotMatch TabsDoNotMatch deriving Show -- | Gather up the invariants for 'TMState' and return them as a list. -- -- If no invariants have been violated, then this function should return an -- empty list. invariantTMState' :: TMState' -> IO [TMStateInvariantErr] invariantTMState' tmState = runInvariants [ invariantFocusSame , invariantTMTabLength , invariantTabsAllMatch ] where runInvariants :: [IO (Maybe TMStateInvariantErr)] -> IO [TMStateInvariantErr] runInvariants = fmap catMaybes . sequence invariantFocusSame :: IO (Maybe TMStateInvariantErr) invariantFocusSame = do let tmNote = tmNotebook $ tmStateNotebook tmState index32 <- notebookGetCurrentPage tmNote maybeWidgetFromNote <- notebookGetNthPage tmNote index32 let focusList = tmNotebookTabs $ tmStateNotebook tmState maybeScrollWinFromFL = fmap tmNotebookTabTermContainer $ getFocusItemFL $ focusList idx = fromIntegral index32 case (maybeWidgetFromNote, maybeScrollWinFromFL) of (Nothing, Nothing) -> pure Nothing (Just _, Nothing) -> pure $ Just $ FocusNotSame NotebookTabWidgetExistsButNoFocusListFocus idx (Nothing, Just _) -> pure $ Just $ FocusNotSame FocusListFocusExistsButNoNotebookTabWidget idx (Just widgetFromNote, Just scrollWinFromFL) -> do isEq <- widgetEq widgetFromNote scrollWinFromFL if isEq then pure Nothing else pure $ Just $ FocusNotSame NotebookTabWidgetDiffersFromFocusListFocus idx invariantTMTabLength :: IO (Maybe TMStateInvariantErr) invariantTMTabLength = do let tmNote = tmNotebook $ tmStateNotebook tmState noteLength32 <- notebookGetNPages tmNote let noteLength = fromIntegral noteLength32 focusListLength = lengthFL $ tmNotebookTabs $ tmStateNotebook tmState lengthEqual = focusListLength == noteLength if lengthEqual then pure Nothing else pure $ Just $ TabsDoNotMatch $ TabLengthsDifferent noteLength focusListLength -- Turns a FocusList and Notebook into two lists of widgets and compares each widget for equality invariantTabsAllMatch :: IO (Maybe TMStateInvariantErr) invariantTabsAllMatch = do let tmNote = tmNotebook $ tmStateNotebook tmState focusList = tmNotebookTabs $ tmStateNotebook tmState flList = fmap tmNotebookTabTermContainer $ toList focusList noteList <- notebookToList tmNote tabsMatch noteList flList where tabsMatch :: forall a b . (IsWidget a, IsWidget b) => [a] -> [b] -> IO (Maybe TMStateInvariantErr) tabsMatch xs ys = foldr go (pure Nothing) (zip3 xs ys [0..]) where go :: (a, b, Int) -> IO (Maybe TMStateInvariantErr) -> IO (Maybe TMStateInvariantErr) go (x, y, i) acc = do isEq <- widgetEq x y if isEq then acc else pure . Just $ TabsDoNotMatch (TabAtIndexDifferent i) -- | Check the invariants for 'TMState', and call 'fail' if we find that they -- have been violated. assertInvariantTMState :: TMState -> IO () assertInvariantTMState mvarTMState = do tmState <- readMVar mvarTMState assertValue <- invariantTMState' tmState case assertValue of [] x-> pure () errs@(_:_) -> do putStrLn "In assertInvariantTMState, some invariants for TMState are being violated." putStrLn "\nInvariants violated:" print errs putStrLn "\nTMState:" pPrint tmState putStrLn "" fail "Invariants violated for TMState" pPrintTMState :: TMState -> IO () pPrintTMState mvarTMState = do tmState <- readMVar mvarTMState pPrint tmState termonad-4.0.0.1/src/Termonad/XML.hs0000644000000000000000000002353407346545000015217 0ustar0000000000000000{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} module Termonad.XML where import Termonad.Prelude import Data.Default (def) import Data.FileEmbed (embedFile) import Text.XML (renderText) import Text.XML.QQ (Document, xmlRaw) -- TODO: A number of widgets have different places where a child can be added -- (e.g. tabs vs. page content in notebooks). This can be reflected in a UI -- definition by specifying the “type” attribute on a The possible -- values for the “type” attribute are described in the sections describing the -- widget-specific portions of UI definitions. interfaceDoc :: Document interfaceDoc = [xmlRaw| Example Application 600 400 True vertical True |] interfaceText :: Text interfaceText = toStrict $ renderText def interfaceDoc menuDoc :: Document menuDoc = [xmlRaw| File
New _Tab app.newtab
_Close Tab app.closetab _Quit app.quit
Edit _Copy app.copy _Paste app.paste _Preferences app.preferences View _Enlarge Font Size app.enlargefont _Reduce Font Size app.reducefont Search _Find... app.find Find Above app.findabove Find Below app.findbelow Help _About app.about
|] menuText :: Text menuText = toStrict $ renderText def menuDoc aboutDoc :: Document aboutDoc = [xmlRaw| About False True True 6 12 6 True _Font: True font 1 0 0 True 1 0 True _Transition: True transition 1 0 1 True None Fade Slide 1 1 |] aboutText :: Text aboutText = toStrict $ renderText def aboutDoc closeTabDoc :: Document closeTabDoc = [xmlRaw| Close Tab False True True 10 True True Close tab? 10 True True True 10 True True Yes, close tab True True GTK_RELIEF_NORMAL No, do NOT close tab True True GTK_RELIEF_NORMAL |] closeTabText :: Text closeTabText = toStrict $ renderText def closeTabDoc preferencesText :: Text preferencesText = decodeUtf8 $(embedFile "glade/preferences.glade") termonad-4.0.0.1/termonad.cabal0000644000000000000000000002162407346545000014456 0ustar0000000000000000name: termonad version: 4.0.0.1 synopsis: Terminal emulator configurable in Haskell description: Please see . homepage: https://github.com/cdepillabout/termonad license: BSD3 license-file: LICENSE author: Dennis Gosnell maintainer: cdep.illabout@gmail.com copyright: 2017 Dennis Gosnell category: Text build-type: Custom cabal-version: 1.12 extra-source-files: README.md , CHANGELOG.md , default.nix , glade/preferences.glade , glade/README.md , img/termonad.png , .nix-helpers/nixops.nix , .nix-helpers/nixpkgs.nix , .nix-helpers/stack-shell.nix , .nix-helpers/termonad-with-packages.nix , shell.nix data-files: img/termonad-lambda.png custom-setup setup-depends: base , Cabal , cabal-doctest >=1.0.2 && <1.1 -- This flag builds the example code in the example-config/ directory, as well -- as the example from the README.md file. It is only used for testing. It -- should be enabled for CI. flag buildexamples description: Build an executable from the examples in the example-config/ directory, as well as the example from the README.md file. This is normally only used for testing. default: False library hs-source-dirs: src exposed-modules: Termonad , Termonad.App , Termonad.Config , Termonad.Config.Colour , Termonad.Gtk , Termonad.Keys , Termonad.Lenses , Termonad.Pcre , Termonad.PreferencesFile , Termonad.Prelude , Termonad.Term , Termonad.Types , Termonad.XML other-modules: Paths_termonad -- Termonad only supports GHC-8.8 build-depends: base >= 4.13 && < 4.14 , adjunctions , classy-prelude , colour , constraints , containers , data-default , directory >= 1.3.1.0 , distributive , dyre , file-embed , filepath , focuslist , gi-gdk , gi-gio , gi-glib , gi-gtk >= 3.0.24 , gi-pango , gi-vte >= 2.91.19 , haskell-gi-base >= 0.21.2 , inline-c , lens , mono-traversable , pretty-simple , QuickCheck , text , transformers , yaml , xml-conduit , xml-html-qq default-language: Haskell2010 ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates default-extensions: DataKinds , DefaultSignatures , DeriveAnyClass , DeriveFoldable , DeriveFunctor , DeriveGeneric , DerivingStrategies , EmptyCase , ExistentialQuantification , FlexibleContexts , FlexibleInstances , GADTs , GeneralizedNewtypeDeriving , InstanceSigs , KindSignatures , LambdaCase , MultiParamTypeClasses , NamedFieldPuns , NoImplicitPrelude , OverloadedLabels , OverloadedLists , OverloadedStrings , PatternSynonyms , PolyKinds , RankNTypes , RecordWildCards , ScopedTypeVariables , StandaloneDeriving , TypeApplications , TypeFamilies , TypeOperators other-extensions: TemplateHaskell , UndecidableInstances pkgconfig-depends: gtk+-3.0 , libpcre2-8 , vte-2.91 >= 0.46 executable termonad main-is: Main.hs hs-source-dirs: app build-depends: base , termonad default-language: Haskell2010 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N test-suite doctests type: exitcode-stdio-1.0 main-is: DocTest.hs hs-source-dirs: test build-depends: base , doctest , QuickCheck , template-haskell default-language: Haskell2010 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N test-suite termonad-test type: exitcode-stdio-1.0 main-is: Test.hs hs-source-dirs: test build-depends: base , genvalidity-containers , genvalidity-hspec , hedgehog , lens , QuickCheck , termonad , tasty , tasty-hedgehog , tasty-hspec default-language: Haskell2010 ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -threaded -rtsopts -with-rtsopts=-N default-extensions: DataKinds , GADTs , GeneralizedNewtypeDeriving , InstanceSigs , KindSignatures , NamedFieldPuns , NoImplicitPrelude , OverloadedStrings , OverloadedLabels , OverloadedLists , PatternSynonyms , PolyKinds , RankNTypes , RecordWildCards , ScopedTypeVariables , TypeApplications , TypeFamilies , TypeOperators other-extensions: TemplateHaskell executable termonad-readme main-is: README.lhs hs-source-dirs: test/readme build-depends: base , markdown-unlit , termonad , colour ghc-options: -pgmL markdown-unlit default-language: Haskell2010 ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -threaded -rtsopts -with-rtsopts=-N if flag(buildexamples) buildable: True else buildable: False executable termonad-example-colour-extension main-is: example-config/ExampleColourExtension.hs build-depends: base , termonad , colour default-language: Haskell2010 ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -threaded -rtsopts -with-rtsopts=-N if flag(buildexamples) buildable: True else buildable: False executable termonad-example-colour-extension-gruvbox main-is: example-config/ExampleGruvboxColourExtension.hs build-depends: base , termonad , colour default-language: Haskell2010 ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -threaded -rtsopts -with-rtsopts=-N if flag(buildexamples) buildable: True else buildable: False executable termonad-example-colour-extension-solarized main-is: example-config/ExampleSolarizedColourExtension.hs build-depends: base , termonad , colour default-language: Haskell2010 ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -threaded -rtsopts -with-rtsopts=-N if flag(buildexamples) buildable: True else buildable: False source-repository head type: git location: https://github.com/cdepillabout/termonad/ -- benchmark termonad-bench -- type: exitcode-stdio-1.0 -- main-is: Bench.hs -- hs-source-dirs: bench -- build-depends: base -- , criterion -- , termonad -- default-language: Haskell2010 -- ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N source-repository head type: git location: git@github.com:cdepillabout/termonad.git termonad-4.0.0.1/test/0000755000000000000000000000000007346545000012633 5ustar0000000000000000termonad-4.0.0.1/test/DocTest.hs0000644000000000000000000000032607346545000014535 0ustar0000000000000000 module Main where import Build_doctests (flags, pkgs, module_sources) import Test.DocTest (doctest) main :: IO () main = do doctest args where args :: [String] args = flags ++ pkgs ++ module_sources termonad-4.0.0.1/test/Test.hs0000644000000000000000000000041607346545000014107 0ustar0000000000000000 module Main where import Termonad.Prelude import Test.Tasty (TestTree, defaultMain, testGroup) main :: IO () main = do tests <- testsIO defaultMain tests testsIO :: IO TestTree testsIO = do pure $ testGroup "tests" [ -- focusListTests ] termonad-4.0.0.1/test/readme/0000755000000000000000000000000007346545000014070 5ustar0000000000000000termonad-4.0.0.1/test/readme/README.lhs0000644000000000000000000004477207346545000015553 0ustar0000000000000000 Termonad ========= [![Build Status](https://secure.travis-ci.org/cdepillabout/termonad.svg)](http://travis-ci.org/cdepillabout/termonad) [![Hackage](https://img.shields.io/hackage/v/termonad.svg)](https://hackage.haskell.org/package/termonad) [![Stackage LTS](http://stackage.org/package/termonad/badge/lts)](http://stackage.org/lts/package/termonad) [![Stackage Nightly](http://stackage.org/package/termonad/badge/nightly)](http://stackage.org/nightly/package/termonad) [![BSD3 license](https://img.shields.io/badge/license-BSD3-blue.svg)](./LICENSE) [![Join the chat at https://gitter.im/termonad/Lobby](https://badges.gitter.im/termonad/Lobby.svg)](https://gitter.im/termonad/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat in #termonad on irc.freenode.net](https://img.shields.io/badge/%23termonad-irc.freenode.net-brightgreen.svg)](https://webchat.freenode.net/) Termonad is a terminal emulator configurable in Haskell. It is extremely customizable and provides hooks to modify the default behavior. It can be thought of as the "XMonad" of terminal emulators. ![image of Termonad](./img/termonad.png) Termonad was [featured on an episode](https://www.youtube.com/watch?v=TLNr_gBv5HY) of [DistroTube](https://www.youtube.com/channel/UCVls1GmFKf6WlTraIb_IaJg). This video gives a short overview of Termonad. **Table of Contents** - [Termonad](#termonad) - [Installation](#installation) - [Arch Linux](#arch-linux) - [Ubuntu / Debian](#ubuntu--debian) - [Nix](#nix) - [Mac OS X](#mac-os-x) - [Installing with just `stack`](#installing-with-just-stack) - [Installing with just `nix`](#installing-with-just-nix) - [Installing with `stack` using `nix`](#installing-with-stack-using-nix) - [Windows](#windows) - [How to use Termonad](#how-to-use-termonad) - [Default Key Bindings](#default-key-bindings) - [Configuring Termonad](#configuring-termonad) - [Compiling Local Settings](#compiling-local-settings) - [Running with `stack`](#running-with-stack) - [Running with `nix`](#running-with-nix) - [Goals](#goals) - [Where to get help](#where-to-get-help) - [Contributions](#contributions) - [Maintainers](#maintainers) ## Installation Termonad can be installed on any system as long as the necessary GTK libraries are available. The following are instructions for installing Termonad on a few different distributions and systems. If the given steps don't work for you, or you want to add instructions for an additional system, please send a pull request. The following steps use the [`stack`](https://docs.haskellstack.org/en/stable/README/) build tool to build Termonad, but [`cabal`](https://www.haskell.org/cabal/) can be used as well. Steps for installing `stack` can be found on [this page](https://docs.haskellstack.org/en/stable/install_and_upgrade/). ### Arch Linux First, you must install the required GTK system libraries: ```sh $ pacman -S vte3 gobject-introspection ``` In order to install Termonad, clone this repository and run `stack install`. This will install the `termonad` binary to `~/.local/bin/`: ```sh $ git clone https://github.com/cdepillabout/termonad $ cd termonad/ $ stack install ``` ### Ubuntu / Debian First, you must install the required GTK system libraries: ```sh $ apt-get install gobject-introspection libgirepository1.0-dev libgtk-3-dev libvte-2.91-dev libpcre2-dev ``` In order to install Termonad, clone this repository and run `stack install`. This will install the `termonad` binary to `~/.local/bin/`: ```sh $ git clone https://github.com/cdepillabout/termonad $ cd termonad/ $ stack install ``` ### Nix If you have `nix` installed, you should be able to use it to build Termonad. This means that it will work on NixOS, or with `nix` on another distro. There are two different ways to use `nix` to build Termonad: The first is using `stack`. The following commands install `stack` for your user, clone this repository, and install the `termonad` binary to `~/.local/bin/`: ```sh $ nix-env -i stack $ git clone https://github.com/cdepillabout/termonad $ cd termonad/ $ stack --nix install ``` (_edit_: Building with `stack` using Nix-integration does not currently work. See [#99](https://github.com/cdepillabout/termonad/issues/99).) The second is using the normal `nix-build` machinery. The following commands clone this repository and build the `termonad` binary at `./result/bin/`: ```sh $ git clone https://github.com/cdepillabout/termonad $ cd termonad/ $ nix-build ``` ### Mac OS X Building and installing Termonad on Mac OS X should be possible with any of the following three methods: - Install the required system libraries (like GTK and VTE) by hand, then use `stack` to build Termonad. This is probably the easiest method. You don't have to understand anything about `nix`. However, it is slightly annoying to have to install GTK and VTE by hand. - Use `nix` to install both the required system libraries and Termonad itself. If you are a nix user and want an easy way to install Termonad, this is the recommended method. - Use `nix` to install install the required system libraries, and `stack` to build Termonad. If you are a nix user, but want to use `stack` to actually do development on Termonad, using `stack` may be easier than using `cabal`. The following sections describe each method. #### Installing with just `stack` (*currently no instructions available. please send a PR adding instructions if you get termonad to build using this method.*) #### Installing with just `nix` `nix` can be used to install Termonad with the following steps, assuming you have `nix` [installed](https://nixos.org/nix/download.html). These commands clone this repository and build the `termonad` binary at `./result/bin/`: ```sh $ git clone https://github.com/cdepillabout/termonad $ cd termonad/ $ nix-build ``` #### Installing with `stack` using `nix` `stack` can be used in conjunction with `nix` to install Termonad. `nix` will handle installing system dependencies (like GTK and VTE), while `stack` will handle compiling and installing Haskell packages. You must have `nix` [installed](https://nixos.org/nix/download.html). You will also need `stack` installed. You can do that with the following command: ```sh $ nix-env -i stack ``` After `stack` is installed, you will need to clone Termonad and build it: ``` $ git clone https://github.com/cdepillabout/termonad $ cd termonad/ $ stack --nix install ``` This will install the `termonad` binary to `~/.local/bin/`. ### Windows To run Termonad on Windows, you'll need: * any X server app, for example **Vcxsrv** * any WSL, for example **Ubuntu** I'm using both Vcxsrv and Ubuntu WSL. Configure both Vcxsrv and WSL. For Vcxsrv go with default settings everywhere, it will be fine. Configure your WSL as you want (choose your name etc.). After you set up the user, you'll have to update your OS, run: ```console $ sudo apt-get update $ sudo apt-get upgrade -y $ sudo apt-get dist-upgrade -y $ sudo apt-get autoremove -y ``` Configure the `DISPLAY` environment variable for the X server, and load the changes in bash: For WSL1: ```console $ echo "export DISPLAY=localhost:0.0" >> ~/.bashrc $ source ~/.bashrc ``` For WSL2: ```console $ echo export DISPLAY=$(awk '/nameserver / {print $2; exit}' /etc/resolv.conf 2>/dev/null):0 >> ~/.bashrc $ echo export LIBGL_ALWAYS_INDIRECT=1 >> ~/.bashrc $ source ~/.bashrc ``` If you're using WSL2, you have to create a separate **inbound rule** for TCP port 6000, to allow WSL access to the X server. If you're using mentioned earlier **Vcxsrv** you can enable public access for your X server by disabling Access Control on the Extra Settings. You can also use `-ac` flag in the Additional parameters for VcXsrv section. Your X server should now be configured. Execute following command to install the necessary GTK system libraries: ```console $ apt-get install gobject-introspection libgirepository1.0-dev libgtk-3-dev libvte-2.91-dev libpcre2-dev ``` The required GTK system libraries should now be installed. Clone the Termonad repo: ```sh $ git clone https://github.com/cdepillabout/termonad $ cd termonad/ $ stack build $ stack run ``` After `stack run`, you should see a new window with your Termonad running. ## How to use Termonad Termonad is similar to XMonad. The above steps will install a `termonad` binary somewhere on your system. If you have installed Termonad using `stack`, the `termonad` binary will be in `~/.local/bin/`. This binary is a version of Termonad configured with default settings. You can try running it to get an idea of what Termonad is like: ```sh $ ~/.local/bin/termonad ``` The following section describes the default key bindings. If you would like to configure Termonad with your own settings, first you will need to create a Haskell file called `~/.config/termonad/termonad.hs`. A following section gives an example configuration file. If this configuration file exists, when the `~/.local/bin/termonad` binary launches, it will try to use GHC to compile the configuration file. If GHC is able to successfully compile the configuration file, a separate binary will be created called something like `~/.cache/termonad/termonad-linux-x86_64`. This binary file can be thought of as your own personal Termonad, configured with all your own settings. When you run `~/.local/bin/termonad`, it will re-exec `~/.cache/termonad/termonad-linux-x86_64` if it exists. However, there is one difficulty with this setup. In order for the `~/.local/bin/termonad` binary to be able to compile your `~/.config/termonad/termonad.hs` configuration file, Termonad needs to know where GHC is, as well as where all your Haskell packages live. This presents some difficulties that will be discussed in a following section. ### Default Key Bindings Termonad provides the following default key bindings. | Key binding | Action | |------------|--------| | Ctrl Shift t | Open new tab. | | Ctrl Shift w | Close tab. | | Ctrl Shift f | Open Find dialog for searching for a regex. | | Ctrl Shift p | Find the regex **above** the current position. | | Ctrl Shift i | Find the regex **below** the current position. | | Ctrl + | Increase font size. | | Ctrl - | Decrease font size. | | Alt (number key) | Switch to tab `number`. For example, Alt 2 switches to tab 2. | ### Configuring Termonad Termonad has two different ways to be configured. The first way is to use the built-in Preferences editor. You can find this in the `Preferences` menu under `Edit` in the menubar. When opening Termonad for the first time, it will create a preferences file at `~/.config/termonad/termonad.yaml`. When you change a setting in the Preferences editor, Termonad will update the setting in the preferences file. When running Termonad, it will load settings from the preferences file. Do not edit the preferences file by hand, because it will be overwritten when updating settings in the Preferences editor. This method is perfect for users who only want to make small changes to the Termonad settings, like the default font size. The second way to configure Termonad is to use a Haskell-based settings file, called `~/.config/termonad/termonad.hs` by default. This method allows you to make large, sweeping changes to Termonad. This method is recommended for power users. **WARNING: If you have a `~/.config/termonad/termonad.hs` file, then all settings from `~/.config/termonad/termonad.yaml` will be ignored. If you want to set *ANY* settings in `~/.config/termonad/termonad.hs`, then you must set *ALL* settings in `~/.config/termonad/termonad.hs`.** The following is an example Termonad configuration file. You should save this to `~/.config/termonad/termonad.hs`. You can find more information on the available configuration options within the [`Termonad.Config`](https://hackage.haskell.org/package/termonad/docs/Termonad-Config.html) module. ```haskell {-# LANGUAGE OverloadedStrings #-} module Main where import Termonad.App (defaultMain) import Termonad.Config ( FontConfig, FontSize(FontSizePoints), Option(Set) , ShowScrollbar(ShowScrollbarAlways), defaultConfigOptions, defaultFontConfig , defaultTMConfig, fontConfig, fontFamily, fontSize, options, showScrollbar ) import Termonad.Config.Colour ( AlphaColour, ColourConfig, addColourExtension, createColour , createColourExtension, cursorBgColour, defaultColourConfig ) -- | This sets the color of the cursor in the terminal. -- -- This uses the "Data.Colour" module to define a dark-red color. -- There are many default colors defined in "Data.Colour.Names". cursBgColour :: AlphaColour Double cursBgColour = createColour 204 0 0 -- | This sets the colors used for the terminal. We only specify the background -- color of the cursor. colConf :: ColourConfig (AlphaColour Double) colConf = defaultColourConfig { cursorBgColour = Set cursBgColour } -- | This defines the font for the terminal. fontConf :: FontConfig fontConf = defaultFontConfig { fontFamily = "DejaVu Sans Mono" , fontSize = FontSizePoints 13 } main :: IO () main = do colExt <- createColourExtension colConf let termonadConf = defaultTMConfig { options = defaultConfigOptions { fontConfig = fontConf -- Make sure the scrollbar is always visible. , showScrollbar = ShowScrollbarAlways } } `addColourExtension` colExt defaultMain termonadConf ``` There are other example configuration files in the [example-config/](./example-config) directory. If you want to test what all the colors look like, you may find it convenient to use the [`print-console-colors`](http://hackage.haskell.org/package/print-console-colors) package, which provides an executable called `print-console-colors` that prints all of the colors for your terminal. ### Compiling Local Settings If you launch Termonad by calling `~/.local/bin/termonad`, it will try to compile the `~/.config/termonad/termonad.hs` file if it exists. The problem is that `~/.local/bin/termonad` needs to be able to see GHC and the required Haskell libraries to be able to compile `~/.config/termonad/termonad.hs`. There are a couple solutions to this problem, listed in the sections below. (These steps are definitely confusing. I would love to figure out a better way to do this. Please submit an issue or PR if you have a good idea about how to fix this.) #### Running with `stack` If you originally compiled Termonad with `stack`, you can use `stack` to execute Termonad. First, you must change to the directory with the Termonad source code. From there, you can run `stack exec`: ```sh $ cd termonad/ # change to the termonad source code directory $ stack exec -- termonad ``` `stack` will pick up the correct GHC version and libraries from the `stack.yaml` and `termonad.cabal` file. `termonad` will be run in an environment with GHC available. `termonad` will use this GHC and libraries to compile your `~/.config/termonad/termonad.hs` file. It if succeeds, it should create a `~/.cache/termonad/termonad-linux-x86_64` binary. If you need extra Haskell libraries available when compiling your `~/.config/termonad/termonad.hs` file, you can specify them to `stack exec`: ```sh $ stack exec --package lens --package conduit -- termonad ``` The problem with this is that `stack exec` changes quite a few of your environment variables. It is not recommended to actually run Termonad from within `stack exec`. After you run `stack exec -- termonad` and let it recompile your `~/.config/termonad/termonad.hs` file, exit Termonad. Re-run Termonad by calling it directly. Termonad will notice that `~/.config/termonad/termonad.hs` hasn't changed since `~/.cache/termonad/termonad-linux-x86_64` has been recompiled, so it will directly execute `~/.cache/termonad/termonad-linux-x86_64`. #### Running with `nix` Building Termonad with `nix` (by running `nix-build` in the top directory) sets it up so that Termonad can see GHC. Termonad should be able to compile the `~/.config/termonad/termonad.hs` file by default. If you're interested in how this works, or want to change which Haskell packages are available from your `~/.config/termonad/termonad.hs` file, please see the documentation in the [`.nix-helpers/termonad-with-packages.nix`](./.nix-helpers/termonad-with-packages.nix) file. ## Goals Termonad has the following goals: * fully configurable in Haskell There are already [many](https://gnometerminator.blogspot.com/p/introduction.html) [good](https://www.enlightenment.org/about-terminology.md) [terminal](http://software.schmorp.de/pkg/rxvt-unicode.html) [emulators](https://launchpad.net/sakura). However, there are no terminal emulators fully configurable in Haskell. Termonad fills this niche. * flexible Most people only need a terminal emulator that lets you change the font-size, cursor color, etc. They don't need tons of configuration options. Termonad should be for people that like lots of configuration options. Termonad should provide many hooks to allow the user full control over its behavior. * stable Termonad should be able to be used everyday as your main terminal emulator. It should not crash for any reason. If you experience a crash, please file an issue or a pull request! * good documentation The [documentation](https://hackage.haskell.org/package/termonad) for Termonad on Hackage should be good. You shouldn't have to guess at what certain data types or functions do. If you have a hard time understanding anything in the documentation, please submit an issue or PR. ## Where to get help If you find a bug in Termonad, please either [send a PR](https://github.com/cdepillabout/termonad/pulls) fixing it or create an [issue](https://github.com/cdepillabout/termonad/issues) explaining it. If you just need help with configuring Termonad, you can either join the [Gitter room](https://gitter.im/termonad/Lobby) or [#termonad on irc.freenode.net](https://webchat.freenode.net/). ## Contributions Contributions are highly appreciated. Termonad is currently missing many helpful configuration options and behavior hooks. If there is something you would like to add, please submit an issue or PR. ## Maintainers - [cdepillabout](https://github.com/cdepillabout) - [LSLeary](https://github.com/LSLeary)