diff --git a/Tea Code.app/Contents/Info.plist b/Tea Code.app/Contents/Info.plist
new file mode 100644
index 0000000..72b54ba
--- /dev/null
+++ b/Tea Code.app/Contents/Info.plist
@@ -0,0 +1,33 @@
+
+
+
+
+ CFBundleExecutable
+ launcher
+
+ CFBundleIdentifier
+ com.blurrysquire.teacode
+
+ CFBundleName
+ teacode
+
+ CFBundleDisplayName
+ TeaCode
+
+ CFBundleVersion
+ 1.0
+
+ CFBundleShortVersionString
+ 1.0
+
+ CFBundlePackageType
+ APPL
+
+ LSMinimumSystemVersion
+ 14.0
+
+ CFBundleIconFile
+ AppIcon
+
+
diff --git a/Tea Code.app/Contents/MacOS/launcher b/Tea Code.app/Contents/MacOS/launcher
new file mode 100755
index 0000000..b2484d7
--- /dev/null
+++ b/Tea Code.app/Contents/MacOS/launcher
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+# Tea Code launcher
+
+#set current dir
+cd "$(dirname "$0")"
+./tea-code
diff --git a/Tea Code.app/Contents/MacOS/resources/modified-icon.png b/Tea Code.app/Contents/MacOS/resources/modified-icon.png
new file mode 100644
index 0000000..a93df45
Binary files /dev/null and b/Tea Code.app/Contents/MacOS/resources/modified-icon.png differ
diff --git a/Tea Code.app/Contents/Resources/AppIcon.icns b/Tea Code.app/Contents/Resources/AppIcon.icns
new file mode 100644
index 0000000..b9e20f6
Binary files /dev/null and b/Tea Code.app/Contents/Resources/AppIcon.icns differ
diff --git a/package/macos/info.plist b/package/macos/info.plist
new file mode 100644
index 0000000..72b54ba
--- /dev/null
+++ b/package/macos/info.plist
@@ -0,0 +1,33 @@
+
+
+
+
+ CFBundleExecutable
+ launcher
+
+ CFBundleIdentifier
+ com.blurrysquire.teacode
+
+ CFBundleName
+ teacode
+
+ CFBundleDisplayName
+ TeaCode
+
+ CFBundleVersion
+ 1.0
+
+ CFBundleShortVersionString
+ 1.0
+
+ CFBundlePackageType
+ APPL
+
+ LSMinimumSystemVersion
+ 14.0
+
+ CFBundleIconFile
+ AppIcon
+
+
diff --git a/package/macos/launcher b/package/macos/launcher
new file mode 100755
index 0000000..b2484d7
--- /dev/null
+++ b/package/macos/launcher
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+# Tea Code launcher
+
+#set current dir
+cd "$(dirname "$0")"
+./tea-code
diff --git a/scripts/make_macosbundle.sh b/scripts/make_macosbundle.sh
new file mode 100755
index 0000000..337c042
--- /dev/null
+++ b/scripts/make_macosbundle.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+
+APP_NAME="Tea Code"
+APP_BUNDLE="$ROOT_DIR/${APP_NAME}.app"
+
+rm -rf "$APP_BUNDLE" #if it exists
+
+mkdir -p "$APP_BUNDLE/Contents/"
+mkdir -p "$APP_BUNDLE/Contents/MacOS"
+mkdir -p "$APP_BUNDLE/Contents/MacOS/resources"
+mkdir -p "$APP_BUNDLE/Contents/Resources"
+
+cp "$ROOT_DIR/tea-code" "$APP_BUNDLE/Contents/MacOS/tea-code"
+cp "$ROOT_DIR/package/macos/launcher" "$APP_BUNDLE/Contents/MacOS/launcher"
+
+chmod +x "$APP_BUNDLE/Contents/MacOS/tea-code"
+chmod +x "$APP_BUNDLE/Contents/MacOS/launcher"
+
+cp "$ROOT_DIR/package/macos/settings.toml" "$APP_BUNDLE/Contents/MacOS/settings.toml"
+cp "$ROOT_DIR/branding/AppIcon.icns" "$APP_BUNDLE/Contents/Resources/AppIcon.icns"
+cp "$ROOT_DIR/package/macos/info.plist" "$APP_BUNDLE/Contents/Info.plist"
+cp "$ROOT_DIR/resources/modified-icon.png" "$APP_BUNDLE/Contents/MacOS/resources/modified-icon.png"
+
+echo "Created $APP_BUNDLE"
diff --git a/src/main.cpp b/src/main.cpp
index 8748b0c..82e1479 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -14,6 +14,7 @@ enum {
#include "settings.hpp"
#include "file_view.hpp"
#include "editor_tabs.hpp"
+#include "terminal.hpp"
class MainFrame : public wxFrame {
public:
@@ -48,10 +49,16 @@ class MainFrame : public wxFrame {
if (!startup_path.empty()) {
this->file_view->PopulateTree(startup_path);
}
+ this->workspace_path = startup_path;
+ this->editor_splitter = new wxSplitterWindow(this->splitter, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_LIVE_UPDATE);
+ this->editor_tabs = new EditorTabs(this->editor_splitter, wxID_ANY);
+ this->terminal = new Terminal(this->editor_splitter, wxID_ANY, this->workspace_path);
- this->editor_tabs = new EditorTabs(this->splitter, wxID_ANY);
+ this->editor_splitter->SplitHorizontally(this->editor_tabs, this->terminal, -200);
+ this->editor_splitter->SetMinimumPaneSize(50);
+ this->editor_splitter->SetSashGravity(1.0);
- this->splitter->SplitVertically(this->file_view, this->editor_tabs, 200);
+ this->splitter->SplitVertically(this->file_view, this->editor_splitter, 200);
this->splitter->SetMinimumPaneSize(50);
Bind(wxEVT_MENU, &MainFrame::OnNewFile, this, wxID_NEW);
@@ -74,9 +81,11 @@ class MainFrame : public wxFrame {
private:
wxSplitterWindow* splitter;
+ wxSplitterWindow* editor_splitter;
wxString workspace_path;
FileView* file_view;
EditorTabs* editor_tabs;
+ Terminal* terminal;
void OnNewFile(wxCommandEvent& event) {
this->editor_tabs->NewFile();
@@ -100,7 +109,9 @@ class MainFrame : public wxFrame {
if (dir_dialog.ShowModal() == wxID_CANCEL) return;
- this->file_view->PopulateTree(dir_dialog.GetPath());
+ this->workspace_path = dir_dialog.GetPath();
+ this->file_view->PopulateTree(this->workspace_path);
+ this->terminal->ChangeDirectory(this->workspace_path);
}
void OnFileViewActivated(wxCommandEvent& event) {
diff --git a/src/terminal.cpp b/src/terminal.cpp
new file mode 100644
index 0000000..500b744
--- /dev/null
+++ b/src/terminal.cpp
@@ -0,0 +1,219 @@
+#include "terminal.hpp"
+
+#include
+#include
+
+//TODO:
+// input is only after the prompt
+// use ptx instead of this emulation
+
+Terminal::Terminal(wxWindow* parent, wxWindowID id, const wxString& working_directory)
+ : wxPanel(parent, id), process(nullptr), pid(0), input_start(0),
+ working_directory(working_directory), applying_programmatic_change(false), poll_timer(this)
+{
+ this->console = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_RICH2 | wxBORDER_NONE);
+
+ // read from settings
+ wxFont mono_font(12, wxFONTFAMILY_TELETYPE, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
+ this->console->SetFont(mono_font);
+
+ wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
+ sizer->Add(this->console, 1, wxEXPAND);
+ this->SetSizer(sizer);
+
+ this->console->Bind(wxEVT_CHAR, &Terminal::OnChar, this);
+ this->console->Bind(wxEVT_LEFT_UP, &Terminal::OnConsoleClick, this);
+ this->console->Bind(wxEVT_TEXT, &Terminal::OnTextChanged, this);
+ Bind(wxEVT_TIMER, &Terminal::OnPollTimer, this, this->poll_timer.GetId());
+ Bind(wxEVT_END_PROCESS, &Terminal::OnProcessTerminated, this);
+
+ this->StartShell();
+
+ this->console->SetFocus();
+}
+
+Terminal::~Terminal() {
+ this->poll_timer.Stop();
+
+ if (this->process != nullptr) {
+ this->process->Detach();
+ wxKill(this->pid, wxSIGTERM);
+ }
+}
+
+void Terminal::StartShell() {
+ #ifdef __WXMSW__
+ wxString shell_command = "cmd.exe";
+ #else
+ wxString shell_command = "/bin/bash -i";
+ #endif
+
+ wxEnvVariableHashMap env;
+ wxGetEnvMap(&env);
+ env["TERM"] = "dumb";
+ env["PS1"] = "\\w $ ";
+
+ this->process = new wxProcess(this);
+ this->process->Redirect();
+
+ wxExecuteEnv execute_env;
+ execute_env.env = env;
+ if (!this->working_directory.empty()) {
+ execute_env.cwd = this->working_directory;
+ }
+
+ this->pid = wxExecute(shell_command, wxEXEC_ASYNC, this->process, &execute_env);
+
+ if (this->pid == 0) {
+ delete this->process;
+ this->process = nullptr;
+ this->WriteLine("failed to start shell");
+ return;
+ }
+
+ //this->SendCommand("stty -echo");
+
+ this->poll_timer.Start(100);
+}
+
+void Terminal::SendCommand(const wxString& command) {
+ if (this->process == nullptr) return;
+
+ wxOutputStream* out_stream = this->process-> GetOutputStream();
+ if (out_stream != nullptr) {
+ wxString line = command + "\n";
+
+ out_stream->Write(line.mb_str(), line.length());
+ }
+}
+
+void Terminal::ChangeDirectory(const wxString& path) {
+ this->working_directory = path;
+ this->SendCommand("cd " + path);
+}
+
+void Terminal::MarkLocked() {
+ this->input_start = this->console->GetLastPosition();
+ this->locked_prefix = this->console->GetValue();
+}
+
+void Terminal::OnChar(wxKeyEvent& event) {
+ long selection_start, selection_end;
+ this->console->GetSelection(&selection_start, &selection_end);
+
+ bool crosses_boundary = selection_start != selection_end && selection_start < this->input_start;
+
+ if (crosses_boundary) {
+ this->console->SetInsertionPointEnd();
+
+ if (event.GetKeyCode() != WXK_RETURN && event.GetKeyCode() != WXK_BACK && event.GetKeyCode() != WXK_DELETE) {
+ event.Skip();
+ }
+
+ return;
+ }
+
+ long caret = this->console->GetInsertionPoint();
+
+ if (event.GetKeyCode() == WXK_RETURN) {
+ wxString command = this->console-> GetRange(this->input_start, this->console->GetLastPosition());
+ this->console->AppendText("\n");
+ this->MarkLocked();
+
+ this->SendCommand(command);
+
+ return;
+ }
+
+ if ((event.GetKeyCode() == WXK_BACK || event.GetKeyCode() == WXK_DELETE) && caret <= this->input_start) {
+ return;
+ }
+
+ if (caret < this->input_start) {
+ this->console->SetInsertionPointEnd();
+ }
+
+ event.Skip();
+}
+
+void Terminal::OnConsoleClick(wxMouseEvent& event) {
+ event.Skip();
+ this->CallAfter(&Terminal::ClampInsertionPoint);
+}
+
+void Terminal::ClampInsertionPoint() {
+ if (this->console->GetInsertionPoint() < this->input_start) {
+ this->console->SetInsertionPointEnd();
+ }
+}
+
+void Terminal::OnTextChanged(wxCommandEvent& event) {
+ event.Skip();
+
+ if (this->applying_programmatic_change) return;
+
+ wxString current = this->console->GetValue();
+
+ bool prefix_intact = (long)current.length() >= this->input_start && current.Left(this->input_start) == this->locked_prefix;
+
+ if (!prefix_intact) {
+ this->applying_programmatic_change = true;
+ this->console->ChangeValue(this->locked_prefix);
+ this->console->SetInsertionPointEnd();
+ this->applying_programmatic_change = false;
+ }
+}
+
+void Terminal::ReadStream() {
+ if (this->process == nullptr) return;
+ wxInputStream* in_stream = this->process->GetInputStream();
+ wxInputStream* err_stream = this->process->GetErrorStream();
+
+ if (in_stream != nullptr) {
+ while (this->process->IsInputAvailable()) {
+ char buffer[4096];
+ in_stream->Read(buffer, sizeof(buffer )- 1);
+
+ size_t read_count = in_stream->LastRead();
+
+ buffer[read_count] = '\0';
+
+ if (read_count > 0) {
+ this->console-> AppendText(wxString(buffer, wxConvUTF8));
+ this->MarkLocked();
+ }
+ }
+ }
+ if (err_stream != nullptr) {
+ while (this->process->IsErrorAvailable()) {
+ char buffer[4096];
+ err_stream->Read(buffer, sizeof(buffer )- 1);
+
+ size_t read_count = err_stream->LastRead();
+
+ buffer[read_count] = '\0';
+
+ if (read_count > 0) {
+ this->console-> AppendText(wxString(buffer, wxConvUTF8));
+ this->MarkLocked();
+ }
+ }
+ }
+}
+
+void Terminal::OnPollTimer(wxTimerEvent& event) {
+ this->ReadStream();
+}
+
+void Terminal::OnProcessTerminated(wxProcessEvent& event) {
+ this->ReadStream();
+
+ this->poll_timer.Stop();
+ //this->WriteLine("ProcessTerminated");
+ this->process = nullptr;
+}
+
+void Terminal::WriteLine(const wxString& text) {
+ this->console->AppendText(text + "\n");
+ this->MarkLocked();
+}
diff --git a/src/terminal.hpp b/src/terminal.hpp
new file mode 100644
index 0000000..898fe0d
--- /dev/null
+++ b/src/terminal.hpp
@@ -0,0 +1,39 @@
+#ifndef HPP_TERMINAL
+#define HPP_TERMINAL
+
+#include
+#include
+#include
+
+class Terminal : public wxPanel {
+private:
+ wxTextCtrl* console;
+ wxProcess* process;
+ long pid;
+ long input_start;
+ wxString locked_prefix;
+ wxString working_directory;
+ bool applying_programmatic_change;
+ wxTimer poll_timer;
+
+ void StartShell();
+ void ReadStream();
+ void ClampInsertionPoint();
+ void SendCommand(const wxString& command);
+ void MarkLocked();
+
+ void OnChar(wxKeyEvent& event);
+ void OnConsoleClick(wxMouseEvent& event);
+ void OnTextChanged(wxCommandEvent& event);
+ void OnPollTimer(wxTimerEvent& event);
+ void OnProcessTerminated(wxProcessEvent& event);
+
+public:
+ Terminal(wxWindow* parent, wxWindowID id, const wxString& working_directory = wxEmptyString);
+ virtual ~Terminal();
+
+ void WriteLine(const wxString& text);
+ void ChangeDirectory(const wxString& path);
+};
+
+#endif